天天看點

angular入口_NG-ZORRO 開發部落格:支援二級入口

Photo by Boxed Water Is Better on Unsplash

太長不讀版

從 7.3.0 版本開始,ng-zorro-antd 允許使用者從二級入口(secondary entry point)引入元件及其樣式,進而能夠減小打包體積并避免和其他元件庫的沖突。https://ng.ant.design/docs/getting-started/zh#%E5%8D%95%E7%8B%AC%E5%BC%95%E5%85%A5%E6%9F%90%E4%B8%AA%E7%BB%84%E4%BB%B6

Ant Design Of Angular​ng.ant.design

背景

在 7.3.0 版本之前,ng-zorro-antd 限制使用者統一引入

NgZorroAntdModule

而不支援引入元件對應的 module(比如

NzButtonModule

),即不支援二級入口,這樣的設計是出于以下幾點考慮:

  • 使用友善,使用者引入一個 module 就可以使用全部的元件
  • tree shake 可以有效的的控制包體積,ng-zorro-antd 從第一版起就完全使用 TypeScript 編寫,tree shake 有效的避免了打入多餘代碼,特别是對于使用了較多元件的大型項目而言

後來社群回報的幾個問題和一些工具鍊的變動促使我們重新考慮支援二級入口:

  • 僅僅使用了幾個元件就帶來了 180KB 左右的 js 代碼(對于一些小項目和 mobile 項目來說影響較大),以及體積 300KB 左右的 CSS 代碼(有一部分 base 代碼沒有被 tree shake 掉,參考延伸閱讀)
  • 在某些情況下和其他元件庫沖突,例如這個 issue
  • 使用 ng-packagr 作為打包工具後,支援二級入口變得較為容易

是以在 7.3.0 版本中,我們為大家帶來了二級入口的支援。

這篇文章會舉一個例子來向大家展示使用二級入口之後的收益,幫助大家判斷是否要使用(或者遷移)到二級入口,感興趣的同學還可以進一步了解到我們是如何實作這一功能的,以及為什麼使用二級入口能進一步減小打包體積。

例子

此處僅僅作為例子展示,更加詳細的使用方式請參考官網文檔。

angular入口_NG-ZORRO 開發部落格:支援二級入口

大家可以在這裡找到該例子,它僅僅在界面上渲染了一個 nz-button,template 如下:

<button nz-button nzType="primary">Hello Zorro</button>
           

我們分别看看使用舊的方式和使用新的二級入口應該如何編寫代碼。

使用舊方式,你需要在 module 中引入

NgZorroAntdModule

,并在 style.css 中引入樣式:

import { NgZorroAntdModule } from 'ng-zorro-antd';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, NgZorroAntdModule],
  bootstrap: [AppComponent]
})
export class AppModule {}

/* You can add global styles to this file, and also import other style files */
@import '~ng-zorro-antd/ng-zorro-antd.min.css';
           

使用二級入口,我們應該引入

NzButtonModule

而不是

NgZorroAntdModule

,并引入基礎樣式和 button 元件特有的樣式檔案:

import { NzButtonModule } from 'ng-zorro-antd';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, NzButtonModule],
  bootstrap: [AppComponent]
})
export class AppModule {}

/* You can add global styles to this file, and also import other style files */
@import '~ng-zorro-antd/style/index.min.css'; /* 基礎樣式 */
@import '~ng-zorro-antd/button/style/index.min.css';
           

運作 ng serve,你可以看到展示效果完全相同。

接下來我們比較一下打包後的檔案的體積和檔案内容。配置 angular.json 中

"sourceMap": true

,然後運作

ng build —prod

,可以看到打包日志。

使用舊方式的打包日志:

Date: 2019-04-17T03:55:41.321Z
Hash: d9350d83e93a6b110d6d
Time: 53455ms
chunk {0} runtime.26209474bfa8dc87a77c.js, runtime.26209474bfa8dc87a77c.js.map (runtime) 1.46 kB [entry] [rendered]
chunk {1} es2015-polyfills.18155782d41984482c9b.js, es2015-polyfills.18155782d41984482c9b.js.map (es2015-polyfills) 56.5 kB [initial] [rendered]
chunk {2} main.6c0b0c2ea9cc4e66e1bd.js, main.6c0b0c2ea9cc4e66e1bd.js.map (main) 404 kB [initial] [rendered]
chunk {3} polyfills.a531a96cecea302cf122.js, polyfills.a531a96cecea302cf122.js.map (polyfills) 41.1 kB [initial] [rendered]
chunk {4} styles.c6051f607c1062f3af9d.css, styles.c6051f607c1062f3af9d.css.map (styles) 410 kB [initial] [rendered]
           

使用二級入口的打包日志:

Date: 2019-04-17T03:53:15.246Z
Hash: f27d2968076d355865b4
Time: 35999ms
chunk {0} runtime.26209474bfa8dc87a77c.js, runtime.26209474bfa8dc87a77c.js.map (runtime) 1.46 kB [entry] [rendered]
chunk {1} es2015-polyfills.18155782d41984482c9b.js, es2015-polyfills.18155782d41984482c9b.js.map (es2015-polyfills) 56.5 kB [initial] [rendered]
chunk {2} main.14597b256a2017488623.js, main.14597b256a2017488623.js.map (main) 221 kB [initial] [rendered]
chunk {3} polyfills.a531a96cecea302cf122.js, polyfills.a531a96cecea302cf122.js.map (polyfills) 41.1 kB [initial] [rendered]
chunk {4} styles.6ed6021586bb21f08fe9.css, styles.6ed6021586bb21f08fe9.css.map (styles) 67.4 kB [initial] [rendered]
           

可以看到,使用二級入口之後有以下好處:

  • 打包速度變快,因為要處理的代碼減少了
  • main.js 體積減小了 183KB,styles.css 體積減小了 342.6 KB

我們使用 source-map-explorer 進一步對 main.js 的内容進行分析,運作

source-map-explorer main.xxxxxxx.js

指令,得到包内容的分解圖:

angular入口_NG-ZORRO 開發部落格:支援二級入口

可以看到使用了二級入口之後:

  • ng-zorro-antd 本身的大小降低了 92KB,其中一些元件的代碼被移除了
  • 一些 ng-zorro-antd 的依賴,例如 @angular/cdk 的體積大大降低,date-fns 直接被移除了

我是否應該使用二級入口?

我們先來對比分析一下舊方式和二級入口的優缺點。

舊方式
  • 心智負擔:

    NgZorroAntdModule

    一把梭
  • 打包體積:不徹底的 tree shake,多餘的樣式,較大
  • 打包速度:一般
  • 相容其他元件庫:大部分情況下不會發生沖突
二級入口
  • 心智負擔:需要按照使用的元件來引入 module,不過可以通過封裝一個 share module 來降低代碼量
  • 打包體積:徹底的 tree shake,沒有備援的樣式,較小
  • 打包體積:快一些
  • 相容其他元件庫:可以通過避免引入沖突的元件來解決沖突
知乎居然不支援表格……

可以看到除了需要根據使用的元件來引入 module 這一點比較麻煩之外,二級入口相比之前的方式都有不錯的改進,是以我們的建議是:

推薦使用二級入口

,除非你的項目龐大,用了很多元件,不那麼在乎打包體積,而且改造的成本比較高。

實作

Angular 關于元件庫的打包有一套稱為 Angular Package Format (APF) 的規範,其中規定了二級入口的檔案格式要求。

建立二級入口點

ng-zorro-antd 使用 ng-packagr 進行打包。基于這個打包工具,要支援 APF 所規定的二級入口是相對容易的,隻需要在每個元件底下都創一個名為 package.json 的檔案并輸入以下内容:

{
  "ngPackage": {
    "lib": {
      "entryFile": "public-api.ts"
    }
  }
}
           

ng-packagr 就會知道你想要在此處建立二級入口了。

修改元件之間的引用方式

ng-zorro-antd 的部分元件之間存在依賴,換用二級入口之後,各個元件之間會獨立打包,是以引用方式必須要修改,否則 ng-packagr 會告訴你所需的全部資源檔案不在目前目錄下,參考這個 issue。

以 popover 元件為例,該元件繼承了 tooltip 元件,是以要這樣修改其引用:

- import { NzToolTipComponent } from '../tooltip/nz-tooltip.component';
+ import { NzToolTipComponent } from 'ng-zorro-antd/tooltip';
           

對于 core 檔案夾底下的一些可複用的代碼也是如此,都需要修改成從 ng-zorro-antd/core 引入。

import { isNotNil, zoomBigMotion, NzNoAnimationDirective } from 'ng-zorro-antd/core';
           

配置 tsconfig

修改了引用方式後,需要修改 tsconfig.json 的

paths

字段,讓 TypeScript 能正确定位想要引入的檔案。

修改元件庫的 tsconfig:

{
+ "paths": {
+   "ng-zorro-antd": ["./ng-zorro-antd.module"],
+   "ng-zorro-antd/*": ["./*"]
+ }
}
           

官網的 tsconfig 也需要進行修改:

{
  "paths": {
    "ng-zorro-antd": [ "../components/ng-zorro-antd.module.ts" ],
+   "ng-zorro-antd/*": [ "../components/*" ],
  }
}
           
ng-zorro-antd 有一段腳本會在打包時替換官網的 tsconfig,使得我們可以在開發時引用元件的源碼以享受 hot reload,在建構官網時使用打包後的元件庫確定元件庫确實可用。

解除元件之間的循環依賴

有些元件之間由于之間抽象不夠充分,在不支援單獨入口時,打包并不會暴露這個問題,但是在支援二級入口之後,TypeScript 就會因為循環依賴而出錯。

menu 和 dropdown,tree 和 tree-select 這兩對元件之間存在循環依賴,而出現循環依賴的原因是相同的,都是

可被嵌套在 A 中的 B 元件想要知道它是否被嵌套在 A 元件裡,在嵌套或不嵌套的兩種情況下,B 元件應該被注入不同的服務。

以 tree 和 tree-select 為例,當 tree 被嵌套在 tree-select 中時,它應該被注入

NzTreeSelectService

,否則應該被注入

NzTreeService

。解決方法是将兩個 service 的邏輯抽象為一個名為

NzTreeBaseService

的類,放到 core/tre 目錄底下,然後建立這樣一個令牌:

export const NzTreeHigherOrderServiceToken = new InjectionToken<NzTreeBaseService>('NzTreeHigherOrder');
           

在向 NzTreeComponent 提供 service 時,我們提供的是傳回一個 NzTreeBaseService 的工廠方法:

{
  provide: NzTreeBaseService,
  useFactory: NzTreeServiceFactory,
  deps: [[new SkipSelf(), new Optional(), NzTreeHigherOrderServiceToken], NzTreeService]
},

export function NzTreeServiceFactory(
  higherOrderService: NzTreeBaseService,
  treeService: NzTreeService
): NzTreeBaseService {
  return higherOrderService ? higherOrderService : treeService;
}
           

而在

NzTreeSelectComponent

裡,我們通過ef="https://github.com/NG-ZORRO/ng-zorro-antd/blob/53724be9bd0f8f1e33a45c927c408f3f3a45dc05/components/tree-select/nz-tree-select.component.ts#L55-L60">提供一個 NzTreeHigherOrderServiceToken 來讓

NzTreeComponent

知道它被封裝在了 tree-select 裡:

@Component({
  selector: 'nz-tree-select',
  animations: [slideMotion, zoomMotion],
  templateUrl: './nz-tree-select.component.html',
  providers: [
    NzTreeSelectService,
    {
      provide: NzTreeHigherOrderServiceToken,
      useFactory: higherOrderServiceFactory,
      deps: [[new Self(), Injector]]
    },
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => NzTreeSelectComponent),
      multi: true
    }
  ]
})
export class NzTreeSelectComponent {}
           

重新導出二級入口的内容

在確定二級入口能夠正确打包之後,我們還必須確定一級入口能夠重新導出(re-export)所有二級入口的内容,以保持對舊的引入方式的相容。方法是修改 ng-zorro-antd.module.ts 中對元件的導出方式:

+ export * from 'ng-zorro-antd/affix';
- export * from './affix/';
           

然而 ng-packagr 的一個 bug 會造成導出同名變量的沖突。原因是,ngc 要求導出一個 module 下所有的 directive component service 和子 module,如果開發者沒有在入口檔案中顯式的導出它們,ngc 就會隐式地導出,例如 date picker 元件的入口檔案 ng-zorro-antd-date-picker.d.ts 裡就有一些隐式導出的 component 和 module:

export * from './public-api';
export { AbstractPickerComponent as ɵp } from './abstract-picker.component';
export { DateRangePickerComponent as ɵo } from './date-range-picker.component';
export { HeaderPickerComponent as ɵr } from './header-picker.component';
export { CalendarFooterComponent as ɵd } from './lib/calendar/calendar-footer.component';
export { CalendarHeaderComponent as ɵb } from './lib/calendar/calendar-header.component';
export { CalendarInputComponent as ɵc } from './lib/calendar/calendar-input.component';
export { OkButtonComponent as ɵe } from './lib/calendar/ok-button.component';
export { TimePickerButtonComponent as ɵf } from './lib/calendar/time-picker-button.component';
export { TodayButtonComponent as ɵg } from './lib/calendar/today-button.component';
export { DateTableComponent as ɵh } from './lib/date/date-table.component';
export { DecadePanelComponent as ɵl } from './lib/decade/decade-panel.component';
export { LibPackerModule as ɵa } from './lib/lib-packer.module';
export { MonthPanelComponent as ɵj } from './lib/month/month-panel.component';
export { MonthTableComponent as ɵk } from './lib/month/month-table.component';
export { DateRangePopupComponent as ɵn } from './lib/popups/date-range-popup.component';
export { InnerPopupComponent as ɵm } from './lib/popups/inner-popup.component';
export { YearPanelComponent as ɵi } from './lib/year/year-panel.component';
export { NzPickerComponent as ɵq } from './picker.component';
           

這在沒有二級入口的情況下不會觸發 bug,但是如果我們在一級入口重新導出,而且其他 module 也有這種隐式導出的話,就會發生導出重名變量的沖突。解決的辦法是顯式地導出這些變量。然而這帶來了另外一個問題,即一些内部實作的東西也暴露給使用者了。

比較合了解決方案是 ng-packagr 給 SEP 起一個有兩級結構的 alias,比如 alias

as θda

而不是

as θa

。我們會在以後嘗試修複掉這個瑕疵。

單獨打包 CSS

在實作了 js 部分的二級入口之後,我們還需要為各個元件打包 CSS 來實作樣式的按需引用。

各個元件之間的樣式也存在依賴關系,比如 form 元件内置了 grid 的功能(通過 nzSpan 使用者可以對表單内元素進行布局),是以它需要依賴 grid 的樣式。原來各個元件的 index.less 檔案不包含這些依賴資訊,不能作為打包樣式檔案或單獨引入的入口,是以我們在每個元件的 style 檔案夾底下建立了 entry.less 檔案用以記錄依賴關系。

修改建構腳本,去查詢這些 entry.less 檔案并打包成 CSS,除此之外,還需要為所有元件通用的基本樣式進行單獨打包。

到這裡,二級入口的實作就完成了。

延伸閱讀:為什麼二級入口帶來了更好的 tree shake?

我們在比較舊方法和二級入口的時候說二級入口帶來了更加徹底的 tree shake,這從何說起呢?

首先我們要了解 Angular tree shake 的原理,即了解什麼情況下 module directive component 和 service 會被 tree shake 掉,推薦閱讀這篇文章,簡單來說:

寫在 entryComponent 的不會被搖樹優化,而 declaration 裡的,隻要沒有被使用(在模闆中被聲明過至少一次),就會被優化 對于 service 來說,隻有

providedIn: 'root'

才能被搖樹優化,寫在 providers 裡的都不會被優化。 對于 imports 裡面的 submodule 來說,module 本身不會被優化,其 metadata 裡的資訊會被遞歸處理。

之前 ng-zorro-antd 所要求引用的

NgZorroAntdModule

的 imports 中有

NzModalModule

等,而

NzModalModule

又引用了來自 @angular/cdk 的

OverlayModule

,這就是為什麼 demo 中第一個包(即用舊方法的包)含有 @angular/cdk 的代碼。另外

NzModalModule

還 provide 了

NzModalService

,是以該 service 的代碼也會被打包進去。而使用二級入口之後,上面這些代碼就不會被打包了。

參考連結

  1. Angular Package Format
  2. Tree shaking
  3. Angular AOT