開發Angular庫的簡單指導(譯)


1. 最近工作上用到Angular,需要查閱一些英文資料,雖然英文非常爛,但是種種原因又不得不硬着頭皮上,只是每次看英文都很費力,因此決定將一些比較重要的特別是需要反復閱讀的資料翻譯一下,以節約再次閱讀的時間。
2. 另外一方面,如果只是看英文,不做筆記和記錄,通常會很浮躁,很多知識點都是一知半解,因此倒不如翻譯一下,以加深自己的理解(雖說是翻譯,但實際上只是按照自己的理解,復述一下,因此不敢擅用直譯、意譯了,更加不敢說什么信雅達了)。
3. 再強調一下,由於英文水平有限,錯誤在所難免(並非客套),如果因為這篇文章的錯誤而誤導,深表歉意!也因此在下面首先貼出原文鏈接。

原文鏈接

在這篇博客里面,主要講述向npm發布Angular組件所必需的相關知識,這些組件具備以下特性:

  • 平台中立的(比如可運行在瀏覽器和Web Workers環境)
  • 可以將所有文件打包在一起,也可以多個文件的形式發布
  • 可以使用Angular的預編譯
  • 使用TypeScript時允許IDE智能提示,可以進行編譯時類型檢查

這篇文章不會說明怎么開發一個npm模塊,如果想了解這方面的知識,可以訪問下面的鏈接:

下面,就以我最近發布的ngresizeable組件為例來說明,ngresizable是一個能夠調整DOM元素大小的簡單組件。

平台中立的組件

Angualr的一個突出優勢就是平台中立的,基本上所有需要交互的模塊都是通過一個抽象層來進行——Renderer,我們自己寫的組件也應該依賴於抽象層,而不是具體平台的API(依賴倒置原則),換句話說,如果你為Web創建一個類庫,不要直接操作DOM,因為這樣將不能在Web Workers和服務器環境中運行。

看下面的例子:

@Component({
  selector: 'my-zippy',
  template: `
    <section class="zippy">
      <header #header class="zippy-header">{{ title }}</header>
      <section class="zippy-content" id="zippy-content">
        <ng-content></ng-content>
      </section>
    </section>
  `
})
class ZippyComponent {
  @Input() title = '';
  @Input() toggle = true;
  @ViewChild('header') header: ElementRef;

  ngAfterViewInit() {
    this.header.nativeElement.addEventListener('click', () => {
      this.toggle = !this.toggle;
      document.querySelector('#zippy-content').hidden = !this.toggle;
      if (this.toggle) {
        this.header.nativeElement.classList.add('toggled');
      } else {
        this.header.nativeElement.classList.remove('toggled');
      }
    });
  }
}

這段代碼和底層平台耦合的很緊,包含很多“反模式”,例如:

  1. 直接訪問header元素的addEventListener方法
  2. 沒有清除添加在header上的事件監聽
  3. 直接訪問headerclassList屬性
  4. 訪問全局對象document,而document對象在其它平台可能是無效的

重構一下,以實現平台中立:


@Component({
  selector: 'my-zippy',
  template: `
    <section class="zippy">
      <header #header class="zippy-header">{{ title }}</header>
      <section #content class="zippy-content" id="zippy-content">
        <ng-content></ng-content>
      </section>
    </section>
  `
})
class ZippyComponent implements AfterViewInit, OnDestroy {
  @ViewChild('header') header: ElementRef;
  @ViewChild('content') content: ElementRef;
  @Input() title = '';
  @Input() toggle = true;
  
  private cleanCallback: any;

  constructor(private renderer: Renderer) {}

  ngAfterViewInit() {
    this.cleanCallback = this.renderer.listen(this.header.nativeElement, 'click', () => {
      this.toggle = !this.toggle;
      this.renderer.setElementProperty(this.content.nativeElement, 'hidden', !this.toggle);
      this.renderer.setElementClass(this.header.nativeElement, 'toggled', this.toggle);
    });
  }

  ngOnDestroy() {
    if (typeof this.cleanCallback === 'function')
      this.cleanCallback();
  }
}

使用Renderer代替直接操作DOM和全局對象訪問,上面的代碼看上去好多了,也可以在多個平台運行,但是手工操作還是太多,比如綁定和取消click事件,因此還可以如下優化:

@Component({
  selector: 'my-zippy',
  template: `
    <section class="zippy">
      <header (click)="toggleZippy()" [class.toggled]="toggle"
        class="zippy-header">{{ title }}</header>
      <section class="zippy-content" [hidden]="!toggle">
        <ng-content></ng-content>
      </section>
    </section>
  `
})
class ZippyComponent implements AfterViewInit, OnDestroy {
  @Input() title = '';
  @Input() toggle = true;

  toggleZippy() {
    this.toggle = !this.toggle;
  }
}

上面是一個最優實現,平台中立,並且容易測試(在toggleZippy方法中切換組件的可見性,從而更容易測試)。

發布組件

發布組件並不是不重要的事,甚至Angular都發布了好幾種不同結構的npm模塊。

一般來說,編寫我們自己的模塊包時,需要考慮下面這些事項:

  1. 支持搖樹優化。如果把系統當成一棵代碼樹,搖樹優化指的就是把不需要的代碼從系統發布包中移除,就想搖樹一樣,甩掉不需要的枝葉,搖樹優化在發布產品包時非常重要。
  2. 開發者在開發模式下應該盡可能方便的使用,實際上就是既要支持產品模式下的體積小,又要支持開發模式下的易於調用。
  3. 需要保持發布包盡可能小,從而節約帶寬和下載時間。

為了添加搖樹優化特性,我們需要使用ES2015的模塊化方式(也稱之為esm),從而可以讓諸如RollupWebpack之類的打包器能夠處理未使用的exports。為了實現這種特性,可以在tsconfig.json中如下配置(從ngresizable項目中復制過來):

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "sourceMap": true,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "outDir": "./dist",
    "lib": ["es2015", "dom"]
  },
  "files": [
    "./lib/ngresizable.module.ts"
  ]
}

如果想具有更廣泛的適用性,還應該提供ES5的版本,有兩種選擇方案:

  • 使用兩個目錄,分別存放esmES5兩個版本
    • esm:包括使用ES2015模塊化的部分
    • ES5:不使用ES2015語法的部分(比如使用CommonJS、System或UMD等模塊化方法來替代)
  • 在一個包中同時提供esmES5 UMD兩種版本

第二種方案有如下的優勢:

  • 不會有很多附加文件,只包含esm版的文件和一個包含所有功能的已經打好的單一包
  • 開發人員在使用SystemJS格式開發時,只需要一次request請求,否則的話,SystemJS會為每個文件發起一次請求

不管使用哪個工具,例如,ngresizable組件使用Google的rollup,都可以方便的生成ES5ES2015格式的模塊包,因此,后續將不需要任何額外步湊,就可以生成ES2015語法的組件包。

最后,因為沒有使得目錄結構更加復雜,我們可以在根路徑簡單的輸出兩種格式:esm格式的代碼和符合UMD規范的包。

包配置

因此,我們有兩種格式的包(esmUMD),那么問題來了,在package.json中應該配置哪個入口呢?我們想讓理解esm的打包器使用ES2015模塊化方案,而其它的則使用UMD模塊化方案。

為此,可以如下配置package.json

  • 設置main屬性指向ES5 UMD規范包
  • 設置module屬性指向esm版本的入口文件,module是諸如rollupwebpack等打包器所期望的ES2015模塊引用入口,但是一些舊版本的打包器使用jsnext:main屬性,因此我們可以同時設置modulejsnext:main

最終,package.json配置成:

{
  ...
  "main": "ngresizable.bundle.js",
  "module": "ngresizable.module.js",
  "jsnext:main": "ngresizable.module.js",
  ...
}

提供類型定義文件

因為類庫的使用者很可能使用TypeScript,因此需要提供類型定義文件,從而實現IDE智能提示和類型檢查,為此,需要打開tsconfig.json中的declaration標志,並且在package.json中設置types屬性:

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "sourceMap": true,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "declaration": true,
    "outDir": "./dist",
    "lib": ["es2015", "dom"]
  },
  "files": [
    "./lib/ngresizable.module.ts"
  ]
}
{
  ...
  "main": "ngresizable.bundle.js",
  "module": "ngresizable.module.js",
  "jsnext:main": "ngresizable.module.js",
  "types": "ngresizable.module.d.ts",
  ...
}

兼容Angular的預編譯AOT

AOT是一個很強大的特性,我們開發/發布的組件包最好也能夠兼容AOT特性。

如果我們發布一個不附加任何元數據的JavaScript模塊,依賴於這個模塊的Angular應用就不能AOT編譯,但是我們怎么向ngc提供元信息呢?可以包括組件包所使用的TypeScript版本,但這並不是唯一的方式,我們還可以通過使用ngc預編譯好組件包,同時啟用tsconfig.json中的angular編譯選項中的skipTemplateCodegen,最終,tsconfig.json如下所示:

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "sourceMap": true,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "declaration": true,
    "outDir": "./dist",
    "lib": ["es2015", "dom"]
  },
  "files": [
    "./lib/ngresizable.module.ts"
  ],
  "angularCompilerOptions": {
    "skipTemplateCodegen": true
  }
}

通過默認的ngc為組件和模塊生成ngfactories ,並通過skipTemplateCodegen 選項僅生成*.metadata.json文件。

扼要重述

應用上面的步驟后,ngresizable結構如下:

.
├── README.md
├── ngresizable.actions.d.ts
├── ngresizable.actions.js
├── ngresizable.actions.js.map
├── ngresizable.actions.metadata.json
├── ngresizable.bundle.js
├── ngresizable.component.d.ts
├── ngresizable.component.js
├── ngresizable.component.js.map
├── ngresizable.component.metadata.json
├── ngresizable.module.d.ts
├── ngresizable.module.js
├── ngresizable.module.js.map
├── ngresizable.module.metadata.json
├── ngresizable.reducer.d.ts
├── ngresizable.reducer.js
├── ngresizable.reducer.js.map
├── ngresizable.reducer.metadata.json
├── ngresizable.store.d.ts
├── ngresizable.store.js
├── ngresizable.store.js.map
├── ngresizable.store.metadata.json
├── ngresizable.utils.d.ts
├── ngresizable.utils.js
├── ngresizable.utils.js.map
├── ngresizable.utils.metadata.json
└── package.json

最終的包內容:

  • ngresizable.bundle.js - 組件的ES5 UMD發布包
  • esm - 可以進行搖樹優化的源碼
  • *.js.map - 便於調試的source map 文件
  • *.metadata.json - ngc編譯需要的元信息文件
  • *.d.ts - 允許TypeScript編譯器進行類型檢查和智能提示的類型定義文件

其它說明

非常重要的一個主題是就是代碼風格,模塊應該遵循最佳實踐,進一步的信息訪問angular.io 的格式指南。

  • 注意,不要使用ng作為組件的前綴,因為有可能和google官方的組件沖突。 *

結論

在這篇博客中,簡短的說明了發布Angular組件庫需要考慮的一些最重要的事項:怎么和底層平台解耦,怎么保持搖樹優化特性,怎么最小化組件包,怎么對AOT預編譯友好等等。


免責聲明!

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



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