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.HostBlazor WebAssembly模式的启动程序,它有自己的appsettings.json(位于wwwroot中)包含HTTP API服务器地址和IdentityServer等配置,前后端分离,需要先启动前面两个程序才能正常使用
  • Blazor.Server.HostBlazor Server模式的启动程序,它有自己的appsettings.json包含数据库连接字符串和其他配置,但是它内部默认集成了IdentityServer和HttpApi.Host模块,相当于前后端不分离,所以它可以直接用。

启动项目(WebAssembly模式)

因为项目默认数据库为MSSQLLocalDB所以不需要另外修改配置,直接初始化数据库即可。

首先在控制台中切换到DemoApp.IdentityServer项目所在目录,执行

dotnet ef database update

按顺序打开如下项目:

  • DemoApp.IdentityServer
  • DemoApp.HttpApi.Host
  • DemoApp.Blazor.Host

打开https://localhost:44307/正常载入wasm页面,点击右上角登录会跳转到identityServer认证中心(https://localhost:44364/),输入用户名admin密码1q2w3E*登录完成跳转回wasm

启动项目(Server模式)

由于Server.Host默认集成了IdentityServer和HttpApi(需要改造,后文有)
初始化数据库
首先在控制台中切换到DemoApp.Blazor.Server.Host项目所在目录,执行

dotnet ef database update

直接启动后打开https://localhost:44313/即可

可以看到登录的时候也是https://localhost:44313/,不像wasm一样会跳到identityserver(因为它自己就集成了)。

替换模块主题

DemoApp.Blazor

这是模块的Blazor公共项目,一般在这里面编写相关页面和组件

  1. 移除依赖Volo.Abp.AspNetCore.Components.Web.Theming,替换为Abp.AspNetCore.Blazor.Theme.Bootstrap
  2. 打开DemoAppBlazorModule
    2.1 把DependsOn中依赖的模块名AbpAspNetCoreComponentsWebThemingModule改为AbpAspNetCoreBlazorThemeBootstrapModule
    2.2 引用Abp.AspNetCore.Blazor.Theme.Bootstrap Abp.AspNetCore.Blazor.Theme命名空间
  3. 打开_Imports.razor,删除@using Volo.Abp.BlazoriseUI @using Blazorise @using Blazorise.DataGrid,添加@using BootstrapBlazor.Components @using Abp.AspNetCore.Blazor.Theme

DemoApp.Blazor.Server

这个是模块的ssr模式下引用的类库,这个简单,只需要替换依赖就行。

  1. 移除依赖Volo.Abp.AspNetCore.Components.Server.Theming,替换为Abp.AspNetCore.Blazor.Theme.Bootstrap.Server
  2. 打开DemoAppBlazorServerModule
    2.1 把DependsOn中依赖的模块名AbpAspNetCoreComponentsServerThemingModule改为AbpAspNetCoreBlazorThemeBootstrapServerModule
    2.2 引用Abp.AspNetCore.Blazor.Theme.Bootstrap命名空间

DemoApp.Blazor.WebAssembly

这个是模块的wasm模式下引用的类库,由上。

  1. 移除依赖Volo.Abp.AspNetCore.Components.WebAssembly.Theming,替换为Abp.AspNetCore.Blazor.Theme.Bootstrap.WebAssembly
  2. 打开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();
            var builder = context.Services.GetSingletonInstance();
            ConfigureAuthentication(builder);
            ConfigureHttpClient(context, environment);
            ConfigureRouter(context);
            ConfigureUI(builder);
            ConfigureMenu(context);
            ConfigureAutoMapper(context);
        }

        private void ConfigureRouter(ServiceConfigurationContext context)
        {
            Configure(options =>
            {
                //options.AppAssembly = typeof(DemoAppBlazorHostModule).Assembly;这里要注释掉
                options.AdditionalAssemblies.Add(this.GetType().Assembly);
            });
        }

        private void ConfigureMenu(ServiceConfigurationContext context)
        {
            Configure(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("#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(options =>
            {
                options.AddMaps();
            });
        }
    }
}

修改_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(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(options =>
                {
                    options.FileSets.ReplaceEmbeddedByPhysical(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}src{0}DemoApp.Domain.Shared", Path.DirectorySeparatorChar)));
                    options.FileSets.ReplaceEmbeddedByPhysical(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}src{0}DemoApp.Domain", Path.DirectorySeparatorChar)));
                    options.FileSets.ReplaceEmbeddedByPhysical(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}src{0}DemoApp.Application.Contracts", Path.DirectorySeparatorChar)));
                    options.FileSets.ReplaceEmbeddedByPhysical(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}src{0}DemoApp.Application", Path.DirectorySeparatorChar)));
                    options.FileSets.ReplaceEmbeddedByPhysical(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(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(options =>
            {
                options.IsEnabled = MultiTenancyConsts.IsEnabled;
            });

            context.Services.AddTransient(sp => new HttpClient
            {
                BaseAddress = new Uri("/")
            });

          

            Configure(options =>
            {
                options.MenuContributors.Add(new DemoAppMenuContributor());
            });

// Configure(options => { options.AppAssembly = typeof(DemoAppBlazorHostModule).Assembly; });
            Configure(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()
                        .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;
}




    
    
    DemoApp.Blazor.Server
    

    


    

    
An error has occurred. This application may no longer respond until reloaded. An unhandled exception has occurred. See browser dev tools for details. Reload ??

6.DemoAppMenuContributor

注释ConfigureMainMenuAsync方法体,因为我们没有那几个模块了

7.修改appsettings.json配置

删除ConnectionStrings节点

修改AuthServer为:

 "AuthServer": {
    "Authority": "https://localhost:44364",
    "RequireHttpsMetadata": "true",
    "ClientId": "DemoApp_Blazor_Server",
    "ClientSecret": "1q2w3e*"
  }

其中Authority配置项为IdentityServer的uri,ClientId需要记住,等会还要用到

添加:

  "RemoteServices": {
    "Default": {
      "BaseUrl": "https://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": "https://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的登录页面也应该重写。