BookStore示例項目---菜單欄UI分析


部署

參照 ABP示例項目BookStore搭建部署

項目解構

1)、動態腳本代理

啟動項目時,默認會調用兩個接口

/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,它是嵌入式資源文件,以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

如圖:
image

由上面得知,開始調用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擴展點

在上面的代碼中,涉及到了兩個類:MainNavbarBrandViewComponentMainNavbarMenuViewComponent。如此這里便有兩個擴展點,首先就是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)、菜單欄多語言顯示

image

這是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模塊中,涉及的文件如下:

image

如此,BookStore項目的菜單欄UI便分析完了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM