Abp Vnext Blazor替換UI組件 集成BootstrapBlazor(詳細過程)
Abp Vnext自帶的blazor項目使用的是 Blazorise,但是試用後發現不支持多標籤。於是想替換為BootstrapBlazor。
過程比較複雜,本人已經把模塊寫好了只需要替換掉即可。
demo也在源碼裏面
創建一個Abp模塊
Q:為什麼不選擇應用程序?
因為模塊中包含Blazor的ssr和Wasm的host。可以直接使用,而創建應用程序的話只能從ssr或wasm的host中二選一,雖然可以創建兩次再把host複製合併但太麻煩了。
精簡模塊
刪除以下無用目錄:
- angular(前端)
- host/DemoApp.Web.Host (mvc使用)
- host/DemoApp.Web.Unified (mvc使用)
- host/DemoApp.Web (mvc使用)
項目結構與如何啟動項目
IdentityServer
應用程序是其他應用程序使用的身份驗證服務器,它有自己的appsettings.json
包含數據庫連接字符串和其他配置,需要初始化數據庫HttpApi.Host
託管模塊的HTTP API. 它有自己的appsettings.json
包含數據庫連接字符串和其他配置
先把項目跑起來Blazor.Host
Blazor WebAssembly模式的啟動程序,它有自己的appsettings.json
(位於wwwroot中)包含HTTP API服務器地址和IdentityServer等配置,前後端分離,需要先啟動前面兩個程序才能正常使用Blazor.Server.Host
Blazor Server模式的啟動程序,它有自己的appsettings.json
包含數據庫連接字符串和其他配置,但是它內部默認集成了IdentityServer和HttpApi.Host模塊,相當於前後端不分離,所以它可以直接用。
啟動項目(WebAssembly模式)
因為項目默認數據庫為MSSQLLocalDB所以不需要另外修改配置,直接初始化數據庫即可。
首先在控制台中切換到DemoApp.IdentityServer
項目所在目錄,執行
dotnet ef database update
按順序打開如下項目:
- DemoApp.IdentityServer
- DemoApp.HttpApi.Host
- DemoApp.Blazor.Host
打開//localhost:44307/正常載入wasm頁面,點擊右上角登錄會跳轉到identityServer認證中心(//localhost:44364/),輸入用戶名admin
密碼1q2w3E*
登錄完成跳轉回wasm
啟動項目(Server模式)
由於Server.Host默認集成了IdentityServer和HttpApi(需要改造,後文有)
初始化數據庫
首先在控制台中切換到DemoApp.Blazor.Server.Host
項目所在目錄,執行
dotnet ef database update
直接啟動後打開//localhost:44313/即可
可以看到登錄的時候也是//localhost:44313/,不像wasm一樣會跳到identityserver(因為它自己就集成了)。
替換模塊主題
DemoApp.Blazor
這是模塊的Blazor公共項目,一般在這裏面編寫相關頁面和組件
- 移除依賴
Volo.Abp.AspNetCore.Components.Web.Theming
,替換為Abp.AspNetCore.Blazor.Theme.Bootstrap
。 - 打開
DemoAppBlazorModule
2.1 把DependsOn中依賴的模塊名AbpAspNetCoreComponentsWebThemingModule
改為AbpAspNetCoreBlazorThemeBootstrapModule
2.2 引用Abp.AspNetCore.Blazor.Theme.Bootstrap
Abp.AspNetCore.Blazor.Theme
命名空間 - 打開
_Imports.razor
,刪除@using Volo.Abp.BlazoriseUI
@using Blazorise
@using Blazorise.DataGrid
,添加@using BootstrapBlazor.Components
@using Abp.AspNetCore.Blazor.Theme
DemoApp.Blazor.Server
這個是模塊的ssr模式下引用的類庫,這個簡單,只需要替換依賴就行。
- 移除依賴
Volo.Abp.AspNetCore.Components.Server.Theming
,替換為Abp.AspNetCore.Blazor.Theme.Bootstrap.Server
- 打開
DemoAppBlazorServerModule
2.1 把DependsOn中依賴的模塊名AbpAspNetCoreComponentsServerThemingModule
改為AbpAspNetCoreBlazorThemeBootstrapServerModule
2.2 引用Abp.AspNetCore.Blazor.Theme.Bootstrap
命名空間
DemoApp.Blazor.WebAssembly
這個是模塊的wasm模式下引用的類庫,由上。
- 移除依賴
Volo.Abp.AspNetCore.Components.WebAssembly.Theming
,替換為Abp.AspNetCore.Blazor.Theme.Bootstrap.WebAssembly
- 打開
DemoAppBlazorWebAssemblyModule
2.1 把DependsOn中依賴的模塊名AbpAspNetCoreComponentsWebAssemblyThemingModule
改為AbpAspNetCoreBlazorThemeBootstrapWebAssemblyModule
2.2 引用Abp.AspNetCore.Blazor.Theme.Bootstrap
命名空間
替換Host主題
Blazor.Host
首先我們替換WebAssembly Host的主題,它比Server集成更簡單一點
移除依賴
由於自帶的用戶管理、權限管理、租戶管理等UI模塊都是依賴了Blazorise的,所以需要從項目依賴中移除這幾項:
- Volo.Abp.Identity.Blazor.WebAssembly
- Volo.Abp.TenantManagement.Blazor.WebAssembly
- Volo.Abp.SettingManagement.Blazor.WebAssembly
- Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme(主題)
- Blazorise.Bootstrap
- Blazorise.Icons.FontAwesome
修改DemoAppBlazorHostModule
using System;
using System.Net.Http;
using Abp.AspNetCore.Blazor.Theme;
using Abp.AspNetCore.Blazor.Theme.Bootstrap;
using DemoApp.Blazor.WebAssembly;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Account;
using Volo.Abp.Autofac.WebAssembly;
using Volo.Abp.AutoMapper;
using Volo.Abp.Modularity;
using Volo.Abp.UI.Navigation;
namespace DemoApp.Blazor.Host
{
[DependsOn(
typeof(AbpAutofacWebAssemblyModule),
typeof(AbpAccountApplicationContractsModule),
typeof(DemoAppBlazorWebAssemblyModule)
)]
public class DemoAppBlazorHostModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var environment = context.Services.GetSingletonInstance<IWebAssemblyHostEnvironment>();
var builder = context.Services.GetSingletonInstance<WebAssemblyHostBuilder>();
ConfigureAuthentication(builder);
ConfigureHttpClient(context, environment);
ConfigureRouter(context);
ConfigureUI(builder);
ConfigureMenu(context);
ConfigureAutoMapper(context);
}
private void ConfigureRouter(ServiceConfigurationContext context)
{
Configure<AbpRouterOptions>(options =>
{
//options.AppAssembly = typeof(DemoAppBlazorHostModule).Assembly;這裡要注釋掉
options.AdditionalAssemblies.Add(this.GetType().Assembly);
});
}
private void ConfigureMenu(ServiceConfigurationContext context)
{
Configure<AbpNavigationOptions>(options =>
{
options.MenuContributors.Add(new DemoAppHostMenuContributor(context.Services.GetConfiguration()));
});
}
private static void ConfigureAuthentication(WebAssemblyHostBuilder builder)
{
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("AuthServer", options.ProviderOptions);
options.ProviderOptions.DefaultScopes.Add("DemoApp");
});
}
private static void ConfigureUI(WebAssemblyHostBuilder builder)
{
builder.RootComponents.Add<App>("#ApplicationContainer");
}
private static void ConfigureHttpClient(ServiceConfigurationContext context, IWebAssemblyHostEnvironment environment)
{
context.Services.AddTransient(sp => new HttpClient
{
BaseAddress = new Uri(environment.BaseAddress)
});
}
private void ConfigureAutoMapper(ServiceConfigurationContext context)
{
Configure<AbpAutoMapperOptions>(options =>
{
options.AddMaps<DemoAppBlazorHostModule>();
});
}
}
}
修改_Imports.razor
刪除
@using Blazorise
@using Blazorise.DataGrid
添加
@using BootstrapBlazor.Components
@using Abp.AspNetCore.Blazor.Theme
重新生成樣式
因為修改了主題需要重新bundle
先生成DemoApp.Blazor.Host項目,然後在控制台中轉到DemoApp.Blazor.Host所在目錄
執行:
abp bundle
如果顯示abp不是命令則需要安裝abp-cli
登錄後顯示 :
Blazor.Server.Host
1.移除與替換依賴
移除以下包
- Blazorise.Bootstrap
- Blazorise.Icons.FontAwesome
- Microsoft.EntityFrameworkCore.Tools
- Volo.Abp.EntityFrameworkCore.SqlServer
- Volo.Abp.AspNetCore.Authentication.JwtBearer
- Volo.Abp.AspNetCore.Components.Server.BasicTheme
- Volo.Abp.AuditLogging.EntityFrameworkCore
- Volo.Abp.Account.Web.IdentityServer
- Volo.Abp.Account.Application
- Volo.Abp.FeatureManagement.EntityFrameworkCore
- Volo.Abp.FeatureManagement.Application
- Volo.Abp.Identity.Blazor.Server
- Volo.Abp.Identity.EntityFrameworkCore
- Volo.Abp.Identity.Application
- Volo.Abp.TenantManagement.Blazor.Server
- Volo.Abp.TenantManagement.EntityFrameworkCore
- Volo.Abp.TenantManagement.Application
- Volo.Abp.SettingManagement.Blazor.Server
- Volo.Abp.SettingManagement.EntityFrameworkCore
- Volo.Abp.SettingManagement.Application
- Volo.Abp.PermissionManagement.Application
- Volo.Abp.PermissionManagement.EntityFrameworkCore
- DemoApp.EntityFrameworkCore\DemoApp.EntityFrameworkCore
- DemoApp.HttpApi
添加以下包
- Volo.Abp.AspNetCore.Authentication.OpenIdConnect
- Volo.Abp.AspNetCore.Mvc.Client
- Volo.Abp.AspNetCore.Authentication.OAuth
- Volo.Abp.Http.Client.IdentityModel.Web
- Volo.Abp.PermissionManagement.HttpApi.Client
- Volo.Abp.Identity.HttpApi.Client
- Volo.Abp.TenantManagement.HttpApi.Client
- Volo.Abp.FeatureManagement.HttpApi.Client
- DemoApp.HttpApi.Client
2.修改Module.cs
1.刪除DependsOn中已移除的模塊
還要刪除
-
DemoAppEntityFrameworkCoreModule(因為不需要直接讀取數據庫了)
-
DemoAppApplicationModule
-
DemoAppHttpApiModule
添加以下模塊 -
AbpAspNetCoreMvcClientModule
-
AbpAspNetCoreAuthenticationOAuthModule
-
AbpAspNetCoreAuthenticationOpenIdConnectModule
-
AbpHttpClientIdentityModelWebModule
-
AbpAspNetCoreMvcUiBasicThemeModule
-
AbpAspNetCoreSerilogModule
-
AbpIdentityHttpApiClientModule
-
AbpFeatureManagementHttpApiClientModule
-
AbpTenantManagementHttpApiClientModule
-
AbpPermissionManagementHttpApiClientModule
2.ConfigureServices
public override void ConfigureServices(ServiceConfigurationContext context)
{
var hostingEnvironment = context.Services.GetHostingEnvironment();
var configuration = context.Services.GetConfiguration();
Configure<AbpBundlingOptions>(options =>
{
// MVC UI
options.StyleBundles.Configure(
BasicThemeBundles.Styles.Global,
bundle =>
{
bundle.AddFiles("/global-styles.css");
}
);
//BLAZOR UI
options.StyleBundles.Configure(
BlazorBootstrapThemeBundles.Styles.Global,
bundle =>
{
bundle.AddFiles("/blazor-global-styles.css");
//You can remove the following line if you don't use Blazor CSS isolation for components
bundle.AddFiles("/DemoApp.Blazor.Server.Host.styles.css");
}
);
});
context.Services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options => { options.ExpireTimeSpan = TimeSpan.FromDays(365); })
.AddAbpOpenIdConnect("oidc", options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.ClientId = configuration["AuthServer:ClientId"];
options.ClientSecret = configuration["AuthServer:ClientSecret"];
options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("role");
options.Scope.Add("email");
options.Scope.Add("phone");
options.Scope.Add("DemoApp");
});
if(hostingEnvironment.IsDevelopment())
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.ReplaceEmbeddedByPhysical<DemoAppDomainSharedModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}src{0}DemoApp.Domain.Shared", Path.DirectorySeparatorChar)));
options.FileSets.ReplaceEmbeddedByPhysical<DemoAppDomainModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}src{0}DemoApp.Domain", Path.DirectorySeparatorChar)));
options.FileSets.ReplaceEmbeddedByPhysical<DemoAppApplicationContractsModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}src{0}DemoApp.Application.Contracts", Path.DirectorySeparatorChar)));
options.FileSets.ReplaceEmbeddedByPhysical<DemoAppApplicationModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}src{0}DemoApp.Application", Path.DirectorySeparatorChar)));
options.FileSets.ReplaceEmbeddedByPhysical<DemoAppBlazorHostModule>(hostingEnvironment.ContentRootPath);
});
}
context.Services.AddAbpSwaggerGen(
options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "DemoApp API", Version = "v1" });
options.DocInclusionPredicate((docName, description) => true);
options.CustomSchemaIds(type => type.FullName);
});
Configure<AbpLocalizationOptions>(options =>
{
options.Languages.Add(new LanguageInfo("cs", "cs", "Čeština"));
options.Languages.Add(new LanguageInfo("en", "en", "English"));
options.Languages.Add(new LanguageInfo("en-GB", "en-GB", "English (UK)"));
options.Languages.Add(new LanguageInfo("fi", "fi", "Finnish"));
options.Languages.Add(new LanguageInfo("fr", "fr", "Français"));
options.Languages.Add(new LanguageInfo("hi", "hi", "Hindi", "in"));
options.Languages.Add(new LanguageInfo("it", "it", "Italian", "it"));
options.Languages.Add(new LanguageInfo("hu", "hu", "Magyar"));
options.Languages.Add(new LanguageInfo("pt-BR", "pt-BR", "Português (Brasil)"));
options.Languages.Add(new LanguageInfo("ru", "ru", "Русский"));
options.Languages.Add(new LanguageInfo("sk", "sk", "Slovak"));
options.Languages.Add(new LanguageInfo("tr", "tr", "Türkçe"));
options.Languages.Add(new LanguageInfo("zh-Hans", "zh-Hans", "簡體中文"));
options.Languages.Add(new LanguageInfo("zh-Hant", "zh-Hant", "繁體中文"));
});
Configure<AbpMultiTenancyOptions>(options =>
{
options.IsEnabled = MultiTenancyConsts.IsEnabled;
});
context.Services.AddTransient(sp => new HttpClient
{
BaseAddress = new Uri("/")
});
Configure<AbpNavigationOptions>(options =>
{
options.MenuContributors.Add(new DemoAppMenuContributor());
});
// Configure<AbpRouterOptions>(options => { options.AppAssembly = typeof(DemoAppBlazorHostModule).Assembly; });
Configure<AbpRouterOptions>(options => { options.AdditionalAssemblies .Add(typeof(DemoAppBlazorHostModule).Assembly); });//要改成這個
}
3.OnApplicationInitialization
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var env = context.GetEnvironment();
var app = context.GetApplicationBuilder();
app.UseAbpRequestLocalization();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseCorrelationId();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
//app.UseJwtTokenMiddleware();
if (MultiTenancyConsts.IsEnabled)
{
app.UseMultiTenancy();
}
// app.UseUnitOfWork();
//app.UseIdentityServer();
app.UseAuthorization();
app.UseSwagger();
app.UseAbpSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "DemoApp API"); });
app.UseConfiguredEndpoints();
using (var scope = context.ServiceProvider.CreateScope())
{
AsyncHelper.RunSync(async () =>
{
await scope.ServiceProvider
.GetRequiredService<IDataSeeder>()
.SeedAsync();
});
}
}
3.修改_Imports.razor
刪除
@using Blazorise
@using Blazorise.DataGrid
@using Volo.Abp.BlazoriseUI
@using Volo.Abp.BlazoriseUI.Components
添加
@using BootstrapBlazor.Components
@using Abp.AspNetCore.Blazor.Theme
4.刪除EntityFrameworkCore和Migrations目錄
因為我們直接調用httpApi獲取數據所以不需要host去讀取數據庫,所以把這兩個目錄刪除
5._Host.cshtml
@page "/"
@namespace DemoApp.Blazor.Server.Host.Pages
@using System.Globalization
@using Abp.AspNetCore.Blazor.Theme.Bootstrap
@using Abp.AspNetCore.Blazor.Theme.Server
@using Volo.Abp.Localization
@{
Layout = null;
var rtl = CultureHelper.IsRtl ? "rtl" : string.Empty;
}
<!DOCTYPE html>
<html lang="@CultureInfo.CurrentCulture.Name" dir="@rtl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DemoApp.Blazor.Server</title>
<base href="~/" />
<abp-style-bundle name="@BlazorBootstrapThemeBundles.Styles.Global" />
</head>
<body class="abp-application-layout bg-light @rtl">
<component type="typeof(App)" render-mode="Server" />
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<abp-script-bundle name="@BlazorBootstrapThemeBundles.Scripts.Global" />
</body>
</html>
6.DemoAppMenuContributor
注釋ConfigureMainMenuAsync方法體,因為我們沒有那幾個模塊了
7.修改appsettings.json配置
刪除ConnectionStrings節點
修改AuthServer為:
"AuthServer": {
"Authority": "//localhost:44364",
"RequireHttpsMetadata": "true",
"ClientId": "DemoApp_Blazor_Server",
"ClientSecret": "1q2w3e*"
}
其中Authority配置項為IdentityServer的uri,ClientId需要記住,等會還要用到
添加:
"RemoteServices": {
"Default": {
"BaseUrl": "//localhost:44396/"
}
}
這裡配置的是httpapi的uri
5.添加登錄控制器
創建Controllers目錄,添加AccountController
public class AccountController : ChallengeAccountController
{
}
6.添加identityServer配置
打開DemoApp.IdentityServer
項目
1.修改appsettings.json
在IdentityServer的Clients中添加
"DemoApp_Blazor_Server": {
"ClientId": "DemoApp_Blazor_Server",
"RootUrl": "//localhost:44313/"
"ClientSecret": "1q2w3e*",
}
定位到IdentityServer/IdentityServerDataSeedContributor.cs
,添加IdentityServer配置。
修改CreateClientsAsync方法,添加
var blazorServerTieredClientId = configurationSection["DemoApp_Blazor_Server:ClientId"];
if (!blazorServerTieredClientId.IsNullOrWhiteSpace())
{
var blazorServerTieredClientRootUrl = configurationSection["DemoApp_Blazor_Server:RootUrl"].EnsureEndsWith('/');
/* Admin_BlazorServerTiered client is only needed if you created a tiered blazor server
* solution. Otherwise, you can delete this client. */
await CreateClientAsync(
name: blazorServerTieredClientId,
scopes: commonScopes,
grantTypes: new[] { "hybrid" },
secret: (configurationSection["DemoApp_Blazor_Server:ClientSecret"] ?? "1q2w3e*").Sha256(),
redirectUri: $"{blazorServerTieredClientRootUrl}signin-oidc",
postLogoutRedirectUri: $"{blazorServerTieredClientRootUrl}signout-callback-oidc",
frontChannelLogoutUri: $"{blazorServerTieredClientRootUrl}Account/FrontChannelLogout",
corsOrigins: new[] { blazorServerTieredClientRootUrl.RemovePostFix("/") }
);
}
修改完成後需要重新打開IdentityServer配置即可生效。
7.修改菜單
定位到Menus>DemoAppMenuContributor.cs
using System.Threading.Tasks;
using DemoApp.MultiTenancy;
using Volo.Abp.UI.Navigation;
namespace DemoApp.Blazor.Server.Host.Menus
{
public class DemoAppMenuContributor : IMenuContributor
{
public async Task ConfigureMenuAsync(MenuConfigurationContext context)
{
if (context.Menu.Name == StandardMenus.Main)
{
await ConfigureMainMenuAsync(context);
}
}
private Task ConfigureMainMenuAsync(MenuConfigurationContext context)
{
var administration = context.Menu.GetAdministration();
context.Menu.Items.Insert(0,
new ApplicationMenuItem("Index", displayName: "Index", "/", icon: "fa fa-home"));
// if (MultiTenancyConsts.IsEnabled)
// {
// administration.SetSubItemOrder(TenantManagementMenuNames.GroupName, 1);
// }
// else
// {
// administration.TryRemoveMenuItem(TenantManagementMenuNames.GroupName);
// }
//
// administration.SetSubItemOrder(IdentityMenuNames.GroupName, 2);
// administration.SetSubItemOrder(SettingManagementMenus.GroupName, 3);
return Task.CompletedTask;
}
}
}
未完成的
由於移除了abp中的幾個頁面模塊,所以需要重寫用戶管理、角色管理、租戶管理等頁面,這些模塊我完善之後會放出來。還有identityServer的登錄頁面也應該重寫。