基於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
項目參考及使用了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: srcsharedcommoncommon.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中,這邊我將其獨立到srcsharedhelpersMessageProviderHelper.ts中(代碼比較長就不貼了, 請自行查看源碼),並在src
oot.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[]方法,其實確切應該要override getHit這個方法,但很不幸它是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中。我們可以在srcappsharedlayout
avapp-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);
});
}
在srcappapp.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生成器生成全部代碼,敬請期待。
項目參考
- ng-zorro
- ng-alain及delon
- 52ABP
- 其它
One More thing
最最最重要的當然是上源碼啦:https://github.com/rqx110/abp-ng-zorro,因 ASP.NET Zero是商業項目,這邊只提供前端,不提供後端的代碼。
覺得還可以,請不要吝嗇你的Star。
TAG:程序員小新人學習 |