部署
項目解構
1)、動態腳本代理
啟動項目時,默認會調用兩個接口
/Abp/ApplicationConfigurationScript
/Abp/ServiceProxyScript
ServiceProxyScript會解析項目路由,動態生成api路徑。此兩個接口封裝在了Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic
程序集中。一旦引用該程序集便會自動調用接口。
1.1)、虛擬文件系統
說到虛擬文件系統,先要了解 嵌入資源文件。簡而言之,就是以程序調用的形式訪問文件。對於虛擬文件系統的了解,可以參考:
ABP虛擬文件系統(VirtualFileSystem)實例------定制菜單欄顯示用戶姓名
1.2)、小結
上面說到的動態腳本代理是如何調用的?在模塊 Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared
中有一類cshtml,它是嵌入式資源文件,以Page\Account文件夾下_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視圖文件都包含了這么一段腳本:
<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代理,但是同時還會去渲染菜單導航欄。
<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類會加載一個視圖,此視圖渲染整個導航欄。
<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源碼有一個繼承自該接口的默認類:
public class DefaultBrandingProvider : IBrandingProvider, ITransientDependency
{
public virtual string AppName => "MyApplication";
public virtual string LogoUrl => null;
}
BookStore項目中的擴展點:
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>
}
}
在這里就會顯示菜單欄及其子菜單。那么這么的擴展點在哪里呢?在模塊類中有這么一個配置菜單的方法:
Configure<AbpNavigationOptions>(options =>
{
options.MenuContributors.Add(new DefaultMenuContributor());
});
如果我們可以參考DefaultMenuContributor
類的實現,擴展自己的菜單。
BookStore示例項目的擴展點:
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項目的擴展:
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視圖:
@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便分析完了。