部署
参照 ABP示例项目BookStore搭建部署
项目解构
1)、动态脚本代理
启动项目时,默认会调用两个接口
代码语言:javascript复制/Abp/ApplicationConfigurationScript
/Abp/ServiceProxyScript
ServiceProxyScript会解析项目路由,动态生成api路径。此两个接口封装在了Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic
程序集中。一旦引用该程序集便会自动调用接口。
1.1)、虚拟文件系统
说到虚拟文件系统,先要了解 嵌入资源文件。简而言之,就是以程序调用的形式访问文件。对于虚拟文件系统的了解,可以参考:
基于ASP.NET Core的模块化设计: 虚拟文件系统
ABP虚拟文件系统(VirtualFileSystem)实例------定制菜单栏显示用户姓名
1.2)、小结
上面说到的动态脚本代理是如何调用的?在模块 Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared
中有一类cshtml,它是嵌入式资源文件,以PageAccount文件夹下_ViewStart.cshtml为例:
@using Volo.Abp.AspNetCore.Mvc.UI.Theming
@inject IThemeManager ThemeManager
@{
Layout = ThemeManager.CurrentTheme.GetApplicationLayout();
}
在这里调用Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic
中的GetApplicationLayout方法:
public virtual string GetLayout(string name, bool fallbackToDefault = true)
{
switch (name)
{
case StandardLayouts.Application:
return "~/Themes/Basic/Layouts/Application.cshtml";
case StandardLayouts.Account:
return "~/Themes/Basic/Layouts/Account.cshtml";
case StandardLayouts.Empty:
return "~/Themes/Basic/Layouts/Empty.cshtml";
default:
return fallbackToDefault ? "~/Themes/Basic/Layouts/Application.cshtml" : null;
}
}
而这三个cshtml视图文件都包含了这么一段脚本:
代码语言:javascript复制<script src="~/Abp/ApplicationConfigurationScript"></script>
<script src="~/Abp/ServiceProxyScript"></script>
如此便调用了后端方法生成动态脚本,同时我们可以改造这里的视图,用来定制网站的菜单栏等UI界面。
2)、UI界面菜单栏分析
2.1)、ABP UI界面单测项目分析
ABP简单菜单栏分析,项目源码:https://github.com/abpframework/abp/tree/dev/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo
开始调用layout下的视图文件,用以加载动态js代理,但是同时还会去渲染菜单导航栏。
代码语言:javascript复制<body class="abp-application-layout bg-light">
@await Component.InvokeLayoutHookAsync(LayoutHooks.Body.First, StandardLayouts.Application)
@(await Component.InvokeAsync<MainNavbarViewComponent>())
<div class="@containerClass">
@(await Component.InvokeAsync<PageAlertsViewComponent>())
<div id="AbpContentToolbar">
<div class="text-right mb-2">
@RenderSection("content_toolbar", false)
</div>
</div>
@RenderBody()
</div>
<abp-script-bundle name="@BasicThemeBundles.Scripts.Global" />
<script src="~/Abp/ApplicationConfigurationScript"></script>
<script src="~/Abp/ServiceProxyScript"></script>
@await Component.InvokeAsync(typeof(WidgetScriptsViewComponent))
@await RenderSectionAsync("scripts", false)
@await Component.InvokeLayoutHookAsync(LayoutHooks.Body.Last, StandardLayouts.Application)
</body>
MainNavbarViewComponent类会加载一个视图,此视图渲染整个导航栏。
代码语言:javascript复制<nav class="navbar navbar-expand-md navbar-dark bg-dark shadow-sm flex-column flex-md-row mb-4" id="main-navbar" style="min-height: 4rem;">
<div class="container">
@(await Component.InvokeAsync<MainNavbarBrandViewComponent>())
<button class="navbar-toggler" type="button" data-toggle="collapse"
data-target="#main-navbar-collapse" aria-controls="main-navbar-collapse"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="main-navbar-collapse">
<ul class="navbar-nav mx-auto">
@(await Component.InvokeAsync<MainNavbarMenuViewComponent>())
</ul>
<ul class="navbar-nav">
@(await Component.InvokeAsync<MainNavbarToolbarViewComponent>())
</ul>
</div>
</div>
</nav>
2.2)、BookStore示例项目应用的UI扩展点
在上面的代码中,涉及到了两个类:MainNavbarBrandViewComponent
、MainNavbarMenuViewComponent
。如此这里便有两个扩展点,首先就是IBrandingProvider
接口。在MainNavbarBrandViewComponent
源码中会这么调用该接口:
@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Components
@inject IBrandingProvider BrandingProvider
<a class="navbar-brand" href="~/">@BrandingProvider.AppName</a>
ABP源码有一个继承自该接口的默认类:
代码语言:javascript复制public class DefaultBrandingProvider : IBrandingProvider, ITransientDependency
{
public virtual string AppName => "MyApplication";
public virtual string LogoUrl => null;
}
BookStore项目中的扩展点:
代码语言:javascript复制namespace Acme.BookStore.Web
{
[Dependency(ReplaceServices = true)]
public class BookStoreBrandingProvider : DefaultBrandingProvider
{
public override string AppName => "BookStore";
}
}
MainNavbarMenuViewComponent
类源码中会调用一个视图:
@using Volo.Abp.UI.Navigation
@model ApplicationMenu
@foreach (var menuItem in Model.Items)
{
var elementId = string.IsNullOrEmpty(menuItem.ElementId) ? string.Empty : $"id="{menuItem.ElementId}"";
var cssClass = string.IsNullOrEmpty(menuItem.CssClass) ? string.Empty : menuItem.CssClass;
var disabled = menuItem.IsDisabled ? "disabled" : string.Empty;
if (menuItem.IsLeaf)
{
if (menuItem.Url != null)
{
<li class="nav-item @cssClass @disabled" @elementId>
<a class="nav-link" href="@(menuItem.Url ?? "#")">
@if (menuItem.Icon != null)
{
if (menuItem.Icon.StartsWith("fa"))
{
<i class="@menuItem.Icon"></i>
}
}
@menuItem.DisplayName
</a>
</li>
}
}
else
{
<li class="nav-item">
<div class="dropdown">
<a class="nav-link dropdown-toggle" href="#" id="Menu_@(menuItem.Name)" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@if (menuItem.Icon != null)
{
if (menuItem.Icon.StartsWith("fa"))
{
<i class="@menuItem.Icon"></i>
}
}
@menuItem.DisplayName
</a>
<div class="dropdown-menu border-0 shadow-sm" aria-labelledby="Menu_@(menuItem.Name)">
@foreach (var childMenuItem in menuItem.Items)
{
@await Html.PartialAsync("~/Themes/Basic/Components/Menu/_MenuItem.cshtml", childMenuItem)
}
</div>
</div>
</li>
}
}
在这里就会显示菜单栏及其子菜单。那么这么的扩展点在哪里呢?在模块类中有这么一个配置菜单的方法:
代码语言:javascript复制Configure<AbpNavigationOptions>(options =>
{
options.MenuContributors.Add(new DefaultMenuContributor());
});
如果我们可以参考DefaultMenuContributor
类的实现,扩展自己的菜单。
BookStore示例项目的扩展点:
代码语言:javascript复制public class BookStoreMenuContributor : IMenuContributor
{
public async Task ConfigureMenuAsync(MenuConfigurationContext context)
{
if (context.Menu.Name == StandardMenus.Main)
{
await ConfigureMainMenuAsync(context);
}
}
// 配置菜单栏的 显示
private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
{
if (!MultiTenancyConsts.IsEnabled)
{
var administration = context.Menu.GetAdministration();
administration.TryRemoveMenuItem(TenantManagementMenuNames.GroupName);
}
var l = context.ServiceProvider.GetRequiredService<IStringLocalizer<BookStoreResource>>();
context.Menu.Items.Insert(0, new ApplicationMenuItem("BookStore.Home", l["Menu:Home"], "/"));
context.Menu.AddItem(
new ApplicationMenuItem("BooksStore", l["Menu:BookStore"])
.AddItem(new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books"))
);
}
}
3)、菜单栏多语言显示
这是ABP示例项目BookStore的菜单栏,前面两个在上面已经有了描述,而多语言的显示是怎么渲染加载出来的呢?
在ABP的源码中,有多个模块专门处理UI界面。其中,有一个基础的模块,就是我们前面提到的
Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic
模块。在这里处理基本的一些UI主题界面,比如,菜单栏,工具栏等。
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic
{
[DependsOn(
typeof(AbpAspNetCoreMvcUiThemeSharedModule),
typeof(AbpAspNetCoreMvcUiMultiTenancyModule)
)]
public class AbpAspNetCoreMvcUiBasicThemeModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// 添加基础 主题
Configure<AbpThemingOptions>(options =>
{
options.Themes.Add<BasicTheme>();
if (options.DefaultThemeName == null)
{
options.DefaultThemeName = BasicTheme.Name;
}
});
// 添加嵌入资源文件
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpAspNetCoreMvcUiBasicThemeModule>("Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic");
});
// 添加工具栏 (多语言)
Configure<AbpToolbarOptions>(options =>
{
options.Contributors.Add(new BasicThemeMainTopToolbarContributor());
});
// 样式及脚本捆绑
Configure<AbpBundlingOptions>(options =>
{
options
.StyleBundles
.Add(BasicThemeBundles.Styles.Global, bundle =>
{
bundle
.AddBaseBundles(StandardBundles.Styles.Global)
.AddContributors(typeof(BasicThemeGlobalStyleContributor));
});
options
.ScriptBundles
.Add(BasicThemeBundles.Scripts.Global, bundle =>
{
bundle
.AddBaseBundles(StandardBundles.Scripts.Global)
.AddContributors(typeof(BasicThemeGlobalScriptContributor));
});
});
}
}
}
我们看看工具栏的处理类BasicThemeMainTopToolbarContributor
public class BasicThemeMainTopToolbarContributor : IToolbarContributor
{
public async Task ConfigureToolbarAsync(IToolbarConfigurationContext context)
{
if (context.Toolbar.Name != StandardToolbars.Main)
{
return;
}
if (!(context.Theme is BasicTheme))
{
return;
}
var languageProvider = context.ServiceProvider.GetService<ILanguageProvider>();
//TODO: This duplicates GetLanguages() usage. Can we eleminate this?
var languages = await languageProvider.GetLanguagesAsync();
if (languages.Count > 1)
{
context.Toolbar.Items.Add(new ToolbarItem(typeof(LanguageSwitchViewComponent)));
}
if (context.ServiceProvider.GetRequiredService<ICurrentUser>().IsAuthenticated)
{
context.Toolbar.Items.Add(new ToolbarItem(typeof(UserMenuViewComponent)));
}
}
}
在这里有一个处理语言转换视图组件LanguageSwitchViewComponent
和用户菜单视图组件UserMenuViewComponent
。ILanguageProvider接口有一个默认实现类:
public class DefaultLanguageProvider : ILanguageProvider, ITransientDependency
{
protected AbpLocalizationOptions Options { get; }
public DefaultLanguageProvider(IOptions<AbpLocalizationOptions> options)
{
Options = options.Value;
}
public Task<IReadOnlyList<LanguageInfo>> GetLanguagesAsync()
{
return Task.FromResult((IReadOnlyList<LanguageInfo>)Options.Languages);
}
}
这里的GetLanguagesAsync
方法直接返回选项类AbpLocalizationOptions的Languages属性。而ABP开放出来的多语言配置接口就是这个属性,我们将多语言添加到这个属性中,ABP就会加载出来所有的多语言。
BookStore项目的扩展:
代码语言:javascript复制Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Get<BookStoreResource>()
.AddBaseTypes(
typeof(AbpUiResource)
);
options.Languages.Add(new LanguageInfo("cs", "cs", "Čeština"));
options.Languages.Add(new LanguageInfo("en", "en", "English"));
options.Languages.Add(new LanguageInfo("pt-BR", "pt-BR", "Português"));
options.Languages.Add(new LanguageInfo("tr", "tr", "Türkçe"));
options.Languages.Add(new LanguageInfo("zh-Hans", "zh-Hans", "简体中文"));
});
ABP是如何加载渲染出来视图的呢?有这么一个类LanguageSwitchViewComponent
,这个类在上面也有调用,前提就是要在选项类中添加多语言。源码如下:
public class LanguageSwitchViewComponent : AbpViewComponent
{
private readonly ILanguageProvider _languageProvider;
public LanguageSwitchViewComponent(ILanguageProvider languageProvider)
{
_languageProvider = languageProvider;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var languages = await _languageProvider.GetLanguagesAsync();
var currentLanguage = languages.FindByCulture(
CultureInfo.CurrentCulture.Name,
CultureInfo.CurrentUICulture.Name
);
var model = new LanguageSwitchViewComponentModel
{
CurrentLanguage = currentLanguage,
OtherLanguages = languages.Where(l => l != currentLanguage).ToList()
};
return View("~/Themes/Basic/Components/Toolbar/LanguageSwitch/Default.cshtml", model);
}
}
Default.cshtml视图:
代码语言:javascript复制@using System.Linq
@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Themes.Basic.Components.Toolbar.LanguageSwitch
@model LanguageSwitchViewComponentModel
@if (Model.OtherLanguages.Any())
{
<div class="dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@Model.CurrentLanguage.DisplayName
</a>
<div class="dropdown-menu dropdown-menu-right border-0 shadow-sm" aria-labelledby="dropdownMenuLink">
@foreach (var language in Model.OtherLanguages)
{
<a class="dropdown-item" href="/Abp/Languages/Switch?culture=@(language.CultureName)&uiCulture=@(language.UiCultureName)&returnUrl=@Context.Request.Path">@language.DisplayName</a>
}
</div>
</div>
}
还有一个扩展点,也可以通过 扩展
IToolbarContributor
接口。可以参考BasicThemeMainTopToolbarContributor
类。
ABP中处理菜单栏视图主要是在Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic
模块中
如此,BookStore项目的菜单栏UI便分析完了。