基於ng-zorro的ASP.NET ZERO前端實現
Abp官方提供的企業版(ASP.NET ZERO)[以下簡稱Zero]模板中前端使用的是Metronic,本篇部落格介紹使用ng-zorro和ng-alain替換官方前端,以及使用官方生成器自動生成程式碼。
為什麼要重複造輪子?
ng-zorro與Abp結合其實已經有很多例子了,但好像它們都是使用了ng-alain的專案結構,導致無法使用Abp官方的程式碼生成器,因此必須造一輪子符合 ASP.NET Zero前端的程式碼結構。
關於52ABP
專案參考及使用了 ofollow,noindex" target="_blank">52Abp 的很多程式碼。52Abp早期程式碼使用abp-ng2-module以及delon的相關包,後來作者將這些包整合進自己的yoyo-moudle包並實現了一些自定義。考慮更新速度(我喜歡償新),我使用了官方abp-ng2-module和delon的package,一些比較特殊的功能通過擴充套件來實現(比如下面的MenuService)。
實現目標
- 全相容Zero服務端(拿來即用)
- 相容ABP官方程式碼生成器
- 使用ng-zorro最新版本
- 使用ng-alain 2.0+
開始
以下我只介紹實現過程中的幾個重要過程,具體可參看專案原始碼。
- 關於shared module: ng-alain官方有 shared module 的相關介紹。Zero模板中也有類似功能的module:
src\shared\common\common.module.ts
, 所以就沒有使用Shared module,而是把其中的功能都放到CommonModule裡面供其他moudle引入。
@NgModule({ imports: [ ngCommon.CommonModule, AbpModule, NgZorroAntdModule, DelonABCModule, AlainThemeModule.forChild() ], exports: [ AbpModule, NgZorroAntdModule, AlainThemeModule, DelonABCModule, ], providers: [ ModalHelper, ], })
- 使用ng-zorro的訊息提示:這一步使用了52Abp的程式碼,52Abp程式碼位於
AppConsts.ts
中,這邊我將其獨立到src\shared\helpers\MessageProviderHelper.ts
中(程式碼比較長就不貼了, 請自行檢視原始碼),並在src\root.component.ts
中呼叫:
export class RootComponent implements OnInit { constructor( private _modalService: NzModalService, private _messageService: NzMessageService, private _notifyService: NzNotificationService, ) {} ngOnInit(): void { MessageProviderHelper.useNgZorroMessage(this._messageService, this._modalService); MessageProviderHelper.useNgZorroNotify(this._notifyService); } }
- delon中有個Page-Header元件可以很方便的生成breadcrumb,但該功能只有在匹配到完整URL的時候才會自動生成。在Zero中有一個LanguageTexts的路由很特殊,它長這個樣
'languagetexts/:name/texts'
,在開啟該頁面的時候就無法完整匹配,也即無法生成Page-Header breadcrumb。解決方式其實很簡單,檢視Page-Header原始碼發現其依賴MenuService服務生成breadcrumb。那麼我們就可以通過繼承MenuService服務,override其中的getPathByUrl(url: string, recursive = false): Menu[]
方法,其實確切應該要overridegetHit
這個方法,但很不幸它是private的,無法override, 所以只能重寫getPathByUrl,完整程式碼如下:
import { Injectable } from '@angular/core'; import { MenuService, Menu } from '@delon/theme'; @Injectable() export class AbpMenuService extends MenuService { getPathByUrl(url: string, recursive = false): Menu[] { const ret: Menu[] = []; let item = this.getHitEx(url, recursive); if (!item) { return ret; } do { ret.splice(0, 0, item); item = item.__parent; } while (item); return ret; } private getHitEx(url: string, recursive = false, cb: (i: Menu) => void = null) { let item: Menu = null; while (!item && url) { this.visit(i => { if (cb) { cb(i); } // 或條件就是修改增加的 if ((i.link != null && i.link === url) || (!(i.children && i.children.length) && url.startsWith(i.link))) { item = i; } }); if (!recursive) { break; } url = url .split('/') .slice(0, -1) .join('/'); } return item; } }
接下去在 delon.module.ts
中將預設的MenuService替換成我們自己的:
export class DelonModule { constructor( @Optional() @SkipSelf() parentModule: DelonModule, ) { } static forRoot(): ModuleWithProviders { return { ngModule: DelonModule, providers: [ { provide: PageHeaderConfig, useFactory: pageHeaderConfig }, { provide: MenuService, useClass: AbpMenuService, multi: false } ], }; } }
- 使用delon
sidebar-nav
元件:Zero模板和delon都有自己的Menu定義,要想直接使用sidebar-nav
元件,必須將Zero的Menu定義轉成delon的Menu定義,並新增到delon 的MenuService中。我們可以在src\app\shared\layout\nav\app-navigation.service.ts
內增加一個輔助方法來完成該工作:
mapToNgAlainMenu() { let menu = this.getMenu(); let ngAlainRootMenu = <Menu>{ text: this._localizationService.l(menu.name), group: false, hideInBreadcrumb: true, children: [] }; this.generateNgAlainMenus(ngAlainRootMenu.children, menu.items); let ngAlainMenus = [ ngAlainRootMenu ]; this._ngAlainMenuService.add(ngAlainMenus); } generateNgAlainMenus(ngAlainMenus: Menu[], appMenuItems: AppMenuItem[]) { appMenuItems.forEach(item => { let ngAlainMenu: Menu; ngAlainMenu = { text: this._localizationService.l(item.name), link: item.route, icon: `${item.icon}`, hide: !this.showMenuItem(item) }; if (item.items && item.items.length > 0) { ngAlainMenu.children = []; this.generateNgAlainMenus(ngAlainMenu.children, item.items); } ngAlainMenus.push(ngAlainMenu); }); }
在 src\app\app.component.ts
建構函式中使用:
constructor( _appNavigationService: AppNavigationService ) { super(injector); _appNavigationService.mapToNgAlainMenu(); // ... }
- 關於國際化:ng-zorro國際化在文件中描述的使用方法在 這裡 ,無論是“全域性配置”還是“執行時修改”都需要在程式碼裡硬匯入相關語言模組。我們在結合AspNet zero使用的時候會遇到一個問題,Zero的多語言資訊是從服務端動態獲取到的,我們需要以一種動態的方式來設定多語言。其實在官方AspNet Zero前端模板裡已經有實現方式,程式碼如下:
function registerLocales(resolve: (value?: boolean | Promise<boolean>) => void, reject: any) { if (shouldLoadLocale()) { let angularLocale = convertAbpLocaleToAngularLocale(abp.localization.currentLanguage.name); import(`@angular/common/locales/${angularLocale}.js`) .then(module => { registerLocaleData(module.default); resolve(true); }, reject); } else { resolve(true); } }
這裡關鍵是運用WebPack的Dynamic Import Expression方式通過import語句動態匯入語言模組,然後進行設定。此處相關知識可參考這篇blog: Dynamic Import of Locales in Angular 。
接下來我們實現ng-zorro的國際化動態匯入:
首先,因為abp服務端的語言標誌字串和ng-zorro的有些許區別,所以必須建立一個Map來進行轉換,可以把這個map寫在appconfig.json中:
"ngZorroLocaleMappings": [ { "from": "en", "to": "en_US" }, { "from": "zh-Hans", "to": "zh_CN" } ]
以下程式碼位於 root.module.ts
中
建立一個方法進行轉換:
export function convertAbpLocaleToNgZorroLocale(locale: string): string { if (!AppConsts.ngZorroLocaleMappings) { return locale; } let localeMapings = _.filter(AppConsts.ngZorroLocaleMappings, { from: locale }); if (localeMapings && localeMapings.length) { return localeMapings[0]['to']; } return locale; }
最後實現動態配置:
function registerNgZorroLocales(injector: Injector) { if (shouldLoadLocale()) { let ngZorroLcale = convertAbpLocaleToNgZorroLocale(abp.localization.currentLanguage.name); import(`ng-zorro-antd/esm5/i18n/languages/${ngZorroLcale}.js`) .then(module => { let nzI18nService = injector.get(NzI18nService); nzI18nService.setLocale(module.default); }); } }
結束
平生第一次發文,不足之處請諒解。下一篇寫關於使用ABP生成器生成全部程式碼,敬請期待。