.net core3.1 abp動態選單和動態許可權(思路) (二)
ps:本文需要先把abp的原始碼下載一份來下,跟著一起找實現,更容易懂
在abp中,對於許可權和選單使用靜態來管理,選單的載入是在登陸頁面的地方(具體是怎麼知道的,瀏覽器按F12,然後去sources中去找)
這個/AbpScripts/GetScripts是獲取需要初始化的script,源自AbpScriptsController,GetScripts方法包括
頁面載入時的連結是:http://localhost:62114/AbpScripts/GetScripts?v=637274153555501055
_multiTenancyScriptManager //當前租戶初始化 對應報文的 abp.multiTenancy
_sessionScriptManager //當前session初始化 對應報文的 abp.session
_localizationScriptManager //本地化的初始化 對應報文的 abp.localization
_featuresScriptManager //對應報文的 abp.features
_authorizationScriptManager //許可權初始化 對應報文的 abp.auth
_navigationScriptManager //導航選單初始化 對應報文的 abp.nav
_settingScriptManager //設定初始化 對應報文的 abp.setting
_timingScriptManager //對應報文的 abp.clock
_customConfigScriptManager //對應報文的 abp.custom
好了,現在基本算是找到選單和許可權js獲取的地方了,一般系統裡面,許可權是依賴於選單和選單按鈕的,所以我們先不管許可權,先把選單做成動態載入的
從await _navigationScriptManager.GetScriptAsync()開始,一路F12,大概流程是
(介面)INavigationScriptManager=>(介面實現)NavigationScriptManager=>(方法)GetScriptAsync=>(呼叫)await _userNavigationManager.GetMenusAsync=>
(介面)IUserNavigationManager=>(介面實現)UserNavigationManager=>(方法)GetMenuAsync=>(呼叫)navigationManager.Menus=>
(介面)INavigationManager=>(介面實現)NavigationManager=>(非靜態建構函式為Menus屬性賦值)NavigationManager
到這裡之後基本就到底了,我們看看NavigationManager的內容
internal class NavigationManager : INavigationManager, ISingletonDependency { public IDictionary<string, MenuDefinition> Menus { get; private set; } //屬性 public MenuDefinition MainMenu //屬性 { get { return Menus["MainMenu"]; } } private readonly IIocResolver _iocResolver; private readonly INavigationConfiguration _configuration; public NavigationManager(IIocResolver iocResolver, INavigationConfiguration configuration) //非靜態建構函式 { _iocResolver = iocResolver; _configuration = configuration; Menus = new Dictionary<string, MenuDefinition> { {"MainMenu", new MenuDefinition("MainMenu", new LocalizableString("MainMenu", AbpConsts.LocalizationSourceName))} }; } public void Initialize() //初始化方法 { var context = new NavigationProviderContext(this); foreach (var providerType in _configuration.Providers) { using (var provider = _iocResolver.ResolveAsDisposable<NavigationProvider>(providerType)) { provider.Object.SetNavigation(context); //中式英語翻譯一下,應該是設定導航 } } } }
這個類裡面就只有屬性、需要注入的介面宣告、非靜態建構函式、初始化方法,我們到這裡需要關注的是Menus這個屬性,這個屬性似乎將會包含我們需要生成的選單內容
Menus = new Dictionary<string, MenuDefinition> { {"MainMenu", new MenuDefinition("MainMenu", new LocalizableString("MainMenu", AbpConsts.LocalizationSourceName))} };
這裡是對Menus的賦值,例項化了一個Dictionary,前面的不用看,主要是看標紅的這句話,從new LocalizableString("MainMenu", AbpConsts.LocalizationSourceName)裡面獲取到值
好了現在基本找到地方了,我們不知道LocalizableString是什麼意思,但是我們可以百度一波
ILocalizableString/LocalizableString:封裝需要被本地化的string的資訊,並提供Localize方法(呼叫ILocalizationManager的GetString方法)返回本地化的string. SourceName指定其從那個本地化資源讀取本地化文字。
LocalizableString("Questions", "") 如果本地找不到資源,會報300
大概的意思是通過new LocalizableString,我們可以在本地化來源為AbpConsts.LocalizationSourceName的string裡面尋找到Key為MainMenu的value(理解不對請噴)
現在需要去找到那個地方對MainMenu進行了本地化操作,一般來說這個事情都是在程式載入的時候進行的,先對MainMenu進行讀取,儲存到本地,然後在_navigationScriptManager讀取,傳輸給前臺
似乎不好找了,但是我們發現有一個型別MenuDefinition,F12一下,可以發現寶藏
namespace Abp.Application.Navigation { /// <summary> /// Represents a navigation menu for an application. //表示應用程式的導航選單
/// </summary> public class MenuDefinition : IHasMenuItemDefinitions { /// <summary> /// Unique name of the menu in the application. Required. //應用程式中選單的唯一名稱。 必須 /// </summary> public string Name { get; private set; } /// <summary> /// Display name of the menu. Required. //選單顯示名稱 必須
/// </summary> public ILocalizableString DisplayName { get; set; } /// <summary> /// Can be used to store a custom object related to this menu. Optional. //可用於儲存與此選單相關的自定義物件
/// </summary> public object CustomData { get; set; } /// <summary> /// Menu items (first level). //選單項(第一級)
/// </summary> public List<MenuItemDefinition> Items { get; set; } /// <summary> /// Creates a new <see cref="MenuDefinition"/> object. /// </summary> /// <param name="name">Unique name of the menu</param> /// <param name="displayName">Display name of the menu</param> /// <param name="customData">Can be used to store a custom object related to this menu.</param> public MenuDefinition(string name, ILocalizableString displayName, object customData = null) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name", "Menu name can not be empty or null."); } if (displayName == null) { throw new ArgumentNullException("displayName", "Display name of the menu can not be null."); } Name = name; DisplayName = displayName; CustomData = customData; Items = new List<MenuItemDefinition>(); } /// <summary> /// Adds a <see cref="MenuItemDefinition"/> to <see cref="Items"/>. /// </summary> /// <param name="menuItem"><see cref="MenuItemDefinition"/> to be added</param> /// <returns>This <see cref="MenuDefinition"/> object</returns> public MenuDefinition AddItem(MenuItemDefinition menuItem) { Items.Add(menuItem); return this; } /// <summary> /// Remove menu item with given name /// </summary> /// <param name="name"></param> public void RemoveItem(string name) { Items.RemoveAll(m => m.Name == name); } } }
找到了選單的型別了,那麼我們去找儲存的地方就好找了,我們其實可以根據AddItem這個方法去找,去檢視哪個地方引用了
AddItem方法新增的是MenuItemDefinition型別的變數,那我們現在退出abp原始碼,去我們的AbpLearn專案中去全域性搜尋一下
看來是同一個AbpLearnNavigationProvider類裡面,雙擊過去看一下
/// <summary> /// This class defines menus for the application. /// </summary> public class AbpLearnNavigationProvider : NavigationProvider { public override void SetNavigation(INavigationProviderContext context) { context.Manager.MainMenu .AddItem( new MenuItemDefinition( PageNames.Home, L("HomePage"), url: "", icon: "fas fa-home", requiresAuthentication: true ) ).AddItem( new MenuItemDefinition( PageNames.Tenants, L("Tenants"), url: "Tenants", icon: "fas fa-building", permissionDependency: new SimplePermissionDependency(PermissionNames.Pages_Tenants) ) ).AddItem( new MenuItemDefinition( PageNames.Users, L("Users"), url: "Users", icon: "fas fa-users", permissionDependency: new SimplePermissionDependency(PermissionNames.Pages_Users) ) ).AddItem( new MenuItemDefinition( PageNames.Roles, L("Roles"), url: "Roles", icon: "fas fa-theater-masks", permissionDependency: new SimplePermissionDependency(PermissionNames.Pages_Roles) ) ) .AddItem( new MenuItemDefinition( PageNames.About, L("About"), url: "About", icon: "fas fa-info-circle" ) ).AddItem( // Menu items below is just for demonstration! new MenuItemDefinition( "MultiLevelMenu", L("MultiLevelMenu"), icon: "fas fa-circle" ).AddItem( new MenuItemDefinition( "AspNetBoilerplate", new FixedLocalizableString("ASP.NET Boilerplate"), icon: "far fa-circle" ).AddItem( new MenuItemDefinition( "AspNetBoilerplateHome", new FixedLocalizableString("Home"), url: "https://aspnetboilerplate.com?ref=abptmpl", icon: "far fa-dot-circle" ) ).AddItem( new MenuItemDefinition( "AspNetBoilerplateTemplates", new FixedLocalizableString("Templates"), url: "https://aspnetboilerplate.com/Templates?ref=abptmpl", icon: "far fa-dot-circle" ) ).AddItem( new MenuItemDefinition( "AspNetBoilerplateSamples", new FixedLocalizableString("Samples"), url: "https://aspnetboilerplate.com/Samples?ref=abptmpl", icon: "far fa-dot-circle" ) ).AddItem( new MenuItemDefinition( "AspNetBoilerplateDocuments", new FixedLocalizableString("Documents"), url: "https://aspnetboilerplate.com/Pages/Documents?ref=abptmpl", icon: "far fa-dot-circle" ) ) ).AddItem( new MenuItemDefinition( "AspNetZero", new FixedLocalizableString("ASP.NET Zero"), icon: "far fa-circle" ).AddItem( new MenuItemDefinition( "AspNetZeroHome", new FixedLocalizableString("Home"), url: "https://aspnetzero.com?ref=abptmpl", icon: "far fa-dot-circle" ) ).AddItem( new MenuItemDefinition( "AspNetZeroFeatures", new FixedLocalizableString("Features"), url: "https://aspnetzero.com/Features?ref=abptmpl", icon: "far fa-dot-circle" ) ).AddItem( new MenuItemDefinition( "AspNetZeroPricing", new FixedLocalizableString("Pricing"), url: "https://aspnetzero.com/Pricing?ref=abptmpl#pricing", icon: "far fa-dot-circle" ) ).AddItem( new MenuItemDefinition( "AspNetZeroFaq", new FixedLocalizableString("Faq"), url: "https://aspnetzero.com/Faq?ref=abptmpl", icon: "far fa-dot-circle" ) ).AddItem( new MenuItemDefinition( "AspNetZeroDocuments", new FixedLocalizableString("Documents"), url: "https://aspnetzero.com/Documents?ref=abptmpl", icon: "far fa-dot-circle" ) ) ) ); } private static ILocalizableString L(string name) { return new LocalizableString(name, AbpLearnConsts.LocalizationSourceName); } }
好了,現在我們找到選單定義的地方了,那麼我們如何去做動態選單哪?
首先我們想一下需要什麼樣的動態選單?
1.從資料庫載入,不從資料庫載入怎麼叫動態
2.可以根據不同Host(管理者)和Tenant(租戶)載入不同的選單,不可能管理者和租戶看到的選單全是一個樣子的吧!
3.可以根據不同的角色或者使用者載入不同的選單(這個就牽扯到許可權了,比如誰可以看到什麼,不可以看到什麼)
4.許可權、按鈕最好和選單相繫結,這樣便於控制
......
根據以上幾點,我們可以確定
1.必須要在使用者登入之後加載出來的選單才能符合條件
2.選單需要建一個表(因為abp預設沒有單獨的選單表),來進行存放
3.欄位需要包含:選單名,選單與許可權對應的名稱(用於動態許可權),選單對應的Url,Icon,級聯父Id,是否啟用,排序,租戶Id
4.需要對選單進行編輯時,因為牽扯到多租戶,我們需要對多租戶定義一個標準的選單,在新增租戶時,自動將標準選單複製儲存一份到新租戶中,所以我們需要對於選單的進行區分,一般來說Host對應的資料行TenantId(int)都為null,我們可以將標準選單的TenantId標為-1,已經分配儲存的選單TenantId為當前租戶Id,這樣便於區分和查詢
好了,讓我們開始寫動態選單吧