The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

認識 Angular Library 函式庫專案並學會自製 Angular 表單驗證器模組

當 Angular 越用越熟,你將會發現其開發效率極高無比,除了極佳的工具支援外,優異的模組化技術更是不在話下。上週在教 Angular 7 開發實戰:進階開發篇 的時候,有學員許願,說希望我能分享一些 Angular 函式庫的作法,說著說著就這麼願望成真了! 😆 當越來越多專案都用 Angular 開發時,你一定會發現有越來越多的共用元件出現。本篇文章我將設計出一款同時讓 Reactive Forms 與 Template-driven Forms 皆可使用的表單驗證器,並完整介紹如何用 Angular CLI 從頭到尾建立並發行一個自己的 Angular Library 函式庫!


Photo by Janko Ferlič on Unsplash

簡介 Angular CLI 建立專案過程

Angular CLI 從 v6 版本開始,使用 ng new 建立專案的時候,預設採用多專案架構進行管理,你可以從 angular.json 中看見一個 projects 節點,這裡就是定義多專案的設定。在這樣的架構下,我們可以很容易的採用 monorepo 來管理專案,讓多個專案在同一個 Git Repo 下進行開發。

不過,使用 ng new 建立專案時就會建立一個預設的 Angular 應用程式專案,目錄置於 src/e2e/ 目錄下。

如果我們只想要針對特定目的來撰寫一個 Angular Library (函式庫),那麼你很有可能不想在建立專案時也建立預設 Angular 專案。此時,你可以使用 Angular CLI v7 新增的 --create-application=false 參數來建立新專案,這個建立的過程,就不會直接建立 Angular 應用程式,專案架構比較乾淨。

以下是建立 ng-compare-equal-validator 專案的命令:

ng new ng-compare-equal-validator --create-application=false --routing false --style css

建立完成後,你可以看到 angular.json 的內容非常乾淨:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {}
}

此時如果才想要加入專案進去,就可以用以下命令建立應用程式:

ng g application demo1 --routing

※ 建立應用程式時,專案預設會建立在 ./projects 目錄下!

建立 Angular Library 函式庫專案

接下來我們就來一步步建立本次文章所需建立的 Angular 表單驗證器專案!

  1. 建立專案骨架 (Monorepo)

    ng new ng-validators --create-application=false --routing false --style css
    cd ng-validators
    code .
    
  2. 建立 Angular Library 函式庫專案

    對 Angular 應用程式來說,通常所有 Comeponts 與 Directives 都會以 app- 作為前置詞(prefix),但開發給任意專案共用的函式庫,通常不會這樣命名。例如我公司的英文名稱為 Duotify,我就可以將前置詞改為 dt- 為主,那麼這個時候你可以這樣執行:

    ng g library dt-compare-equal-validator --prefix=dt
    
  3. 建立 Angular 應用程式專案 (用來測試我們撰寫的函式庫元件之用)

    ng g application demo1 --routing --style css --skip-tests
    

使用函式庫與啟動 Angular 應用程式

目前為止,我們建立了三個專案:

  1. dt-compare-equal-validator 函式庫專案
  2. demo1 這個用來測試函式庫的 Angular 應用程式專案
  3. demo1-e2e 這個 Angular 應用程式附帶的 E2E 測試專案

此時你可以測試看看,使用 npm run buildng build 就會自動建置 dt-compare-equal-validator 專案,那是因為在 angular.json 檔案中有個 "defaultProject": "dt-compare-equal-validator" 設定,因此預設會建置這個專案。

請注意:從 Angular CLI v6.1 開始,建置 Angular Library 預設就是採用 --prod 生產環境建置!

如果你想要建置 demo1 專案的話,可以試試 ng build demo1ng build --project demo1 這個命令,就會自動對 demo1 專案進行建置!

這裡有個蠻值得一提的地方,就是專案根目錄下的 tsconfig.json 檔案,裡面有個 "paths" 區段設定,替 TypeScript 的編譯器指出 import 路徑的別名:

"paths": {
  "dt-compare-equal-validator": [
    "dist/dt-compare-equal-validator"
  ],
  "dt-compare-equal-validator/*": [
    "dist/dt-compare-equal-validator/*"
  ]
}

也就是說,在這個 monorepo 專案中,任何一個子專案都可以透過 'dt-compare-equal-validator' 來 import 函式庫中的任何模組或服務元件:

import { DtCompareEqualValidatorModule } from 'dt-compare-equal-validator';

所以為了讓我們的 Angular Library 專案可以被匯入到 Angular 應用程式之中,你可以先修改 demo1 專案內的 app.module.ts 檔案,並將 DtCompareEqualValidatorModule 模組匯入到預設的 AppModule 之中。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { DtCompareEqualValidatorModule } from 'dt-compare-equal-validator';

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

如此一來,就完成了一個基本的 Monorepo 架構,並設定好模組匯入。

最後,我們需要確認專案可以正常建置,有幾件事需要提醒各位:

  1. 所有的 Angular Library 都需要建置後才能被 Angular 應用程式開發時參考

    你可以用 ng build dt-compare-equal-validator --watch 命令自動建置專案,該命令會自動建置任意檔案變更,並自動重新建置此函式庫。

  2. 執行 Angular 應用程式

    ng serve demo1
    
  3. 如果要跑單元測試

    ng test dt-compare-equal-validator
    ng test demo1
    

由於專案根目錄下的 tsconfig.json"paths" 屬性設定過 dt-compare-equal-validator 對應到 dist/dt-compare-equal-validator 目錄,因此所有函式庫都必須先建置過,才有可能讓其他專案參考到。但這個預設值設定,很容易造成問題,例如你可能會常遇到 dt-compare-equal-validator 建置失敗或 VS Code 運作異常等情況。此時建議你將 "paths" 調整如下,直接從 Angular 應用程式專案中參考 Library 目錄下的原始碼,如此一來就不用先建置 Library 專案就能跑了:

"paths": {
  "dt-compare-equal-validator": [
    "projects/dt-compare-equal-validator/src/public-api.ts"
  ],
  "dt-compare-equal-validator/*": [
    "projects/dt-compare-equal-validator/src/*"
  ]
}

這樣的設定最大的缺點,就是當你真的想要建置 Library 專案時,必須手動將設定改回來,這樣才能建置出可以發行到 npm registry 的 npm 套件。

認識 Angular Library 函式庫專案

我們剛剛建立的 dt-compare-equal-validator 函式庫專案,有幾個重要的檔案,分別介紹如下:

  • src/public-api.ts

    這個檔案定義了本函式庫對外公開的所有類別或變數。

  • src/lib/dt-compare-equal-validator.component.ts

    這是個 Angular 共用 UI 元件範例 (Component)

  • src/lib/dt-compare-equal-validator.service.ts

    這是個 Angular 共用服務元件範例 (Service)

  • src/lib/dt-compare-equal-validator.module.ts

    這是個 Angular 共用模組範例,所有需要對外公開的元件都必須註冊在這裡。如果未來有新增元件,也都需要註冊在這個檔案內 @NgModuledeclarationsexport 屬性。

自製 Angular 表單驗證器 (Reactive Forms)

我先寫出 Reactive Forms 可用的表單驗證器,並透過以下步驟進行設計:

  1. 修改 src/lib/dt-compare-equal-validator.module.ts 匯入 ReactiveFormsModule

    import { NgModule } from '@angular/core';
    import { DtCompareEqualValidatorComponent } from './dt-compare-equal-validator.component';
    import { ReactiveFormsModule } from '@angular/forms';
    
    @NgModule({
      declarations: [DtCompareEqualValidatorComponent],
      imports: [ReactiveFormsModule],
      exports: [DtCompareEqualValidatorComponent]
    })
    export class DtCompareEqualValidatorModule {}
    
  2. 寫出一個 Reactive Forms 使用的驗證器函式 (Function)

    建立 src/lib/compareEqual.ts 檔案,內容如下:

    import {
      ValidatorFn,
      AbstractControl,
      ValidationErrors
    } from '@angular/forms';
    
    export function compareEqual(controlName: string): ValidatorFn {
      return (control: AbstractControl): ValidationErrors | null => {
        if (
          !control.parent ||
          control.parent.get(controlName).value === control.value
        ) {
          return null;
        } else {
          return { compareEqual: true };
        }
      };
    }
    
  3. 修改 src/public-api.ts 公開這個 compareEqual 驗證器函式

    /*
     * Public API Surface of dt-compare-equal-validator
     */
    
    export * from './lib/dt-compare-equal-validator.service';
    export * from './lib/dt-compare-equal-validator.component';
    export * from './lib/dt-compare-equal-validator.module';
    export * from './lib/compareEqual';
    
  4. 我們來測試一下是否可以正常匯入 compareEqual 函式使用

    由於我們之前就先執行了 ng build dt-compare-equal-validator --watch 命令,修改程式碼的過程其實也一直在建置這個函式庫專案。

    有時候 ng build dt-compare-equal-validator 會執行失敗,只要關閉正在編輯的檔案,就可以順利進行建置。

    此時你可以直接開啟 demo1 專案的 app.component.ts 檔案,看能不能在 VSCode 中順利 import 這個 compareEqual 函式:

    import { compareEqual } from 'dt-compare-equal-validator';
    

  5. 最後我們將 AppComponent 的表單功能完成

    • app.component.ts

      import { Component, OnInit } from '@angular/core';
      import { compareEqual } from 'dt-compare-equal-validator';
      import { FormBuilder, FormGroup, Validators } from '@angular/forms';
      
      @Component({
        selector: 'app-root',
        templateUrl: './app.component.html',
        styleUrls: ['./app.component.css']
      })
      export class AppComponent implements OnInit {
        form: FormGroup;
        constructor(private fb: FormBuilder) {}
        ngOnInit(): void {
          this.form = this.fb.group({
            pw: ['', [Validators.required]],
            pw2: ['', [Validators.required, compareEqual('pw')]]
          });
        }
      }
      
    • app.component.html

      <form [formGroup]="form">
        <div>輸入密碼: <input type="text" formControlName="pw" /></div>
        <pre>{{ this.form.get('pw').errors | json }}</pre>
        <div>確認密碼: <input type="text" formControlName="pw2" /></div>
        <pre>{{ this.form.get('pw2').errors | json }}</pre>
      </form>
      

    程式撰寫完成後,很有可能會遇到以下錯誤:

    那是因為 Angular CLI 開發伺服器無法偵測到函式庫的新版本,這時只要重新啟動 Angular CLI 開發伺服器 (npm start) 即可解決。有時候 VSCode 也會發生問題,也需要重開才能解決。

自製 Angular 表單驗證器 (Template-driven Forms)

接著我們將 Reactive Forms 的表單驗證器進行 Directive 元件封裝,以便讓 Angular 元件的範本使用。

以下是設計的過程:

  1. 先在 dt-compare-equal-validator 專案下建立一個名為 CompareEqualDirective 的元件

    ng g directive compare-equal --project dt-compare-equal-validator
    

    該元件預設的選取器將會是 [dtCompareEqual]

  2. 然後將該元件類別註冊到 src\lib\dt-compare-equal-validator.module.tsexports,同時也將沒用到的 DtCompareEqualValidatorComponent 預設元件移除。

    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { CompareEqualDirective } from './compare-equal.directive';
    
    @NgModule({
      declarations: [CompareEqualDirective],
      imports: [FormsModule],
      exports: [CompareEqualDirective]
    })
    export class DtCompareEqualValidatorModule {}
    
  3. 接著將驗證器元件完成

    這裡必須實作 Validator 介面與其 validate(control: AbstractControl) 方法。

    還有,我們必須傳入要比對的欄位名稱,因此必須傳入一個 dtCompareEqual 屬性,我們透過 @Input() 宣告這個屬性是個傳入參數即可。

    import { Directive, forwardRef, Input } from '@angular/core';
    import {
      NG_VALIDATORS,
      Validator,
      AbstractControl,
      ValidationErrors
    } from '@angular/forms';
    import { compareEqual } from './compareEqual';
    
    const COMPARE_EQUAL_VALIDATOR: any = {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => CompareEqualDirective),
      multi: true
    };
    
    @Directive({
      selector: '[dtCompareEqual]',
      providers: [COMPARE_EQUAL_VALIDATOR]
    })
    export class CompareEqualDirective implements Validator {
      @Input() dtCompareEqual: string;
      constructor() {}
      validate(control: AbstractControl): ValidationErrors {
        return compareEqual(this.dtCompareEqual)(control);
      }
    }
    
  4. 然後將該 CompareEqualDirective 類別註冊到 src/public-api.ts

    /*
     * Public API Surface of dt-compare-equal-validator
     */
    
    export * from './lib/dt-compare-equal-validator.service';
    export * from './lib/dt-compare-equal-validator.component';
    export * from './lib/dt-compare-equal-validator.module';
    export * from './lib/compareEqual';
    export * from './lib/compare-equal.directive';
    
  5. 最後我們將 AppComponent 的表單功能完成

    • app.module.ts

      import { BrowserModule } from '@angular/platform-browser';
      import { NgModule } from '@angular/core';
      
      import { AppRoutingModule } from './app-routing.module';
      import { AppComponent } from './app.component';
      
      import { DtCompareEqualValidatorModule } from 'dt-compare-equal-validator';
      import { FormsModule } from '@angular/forms';
      
      @NgModule({
        declarations: [AppComponent],
        imports: [
          BrowserModule,
          FormsModule,
          AppRoutingModule,
          DtCompareEqualValidatorModule
        ],
        providers: [],
        bootstrap: [AppComponent]
      })
      export class AppModule {}
      
    • app.component.ts

      import { Component, OnInit } from '@angular/core';
      
      @Component({
        selector: 'app-root',
        templateUrl: './app.component.html',
        styleUrls: ['./app.component.css']
      })
      export class AppComponent implements OnInit {
        pw: string;
        pw2: string;
        constructor() {}
        ngOnInit(): void {}
      }
      
    • app.component.html

      <form>
        <div>
          輸入密碼:
          <input
            type="text"
            name="pw"
            #tPw="ngModel"
            [(ngModel)]="pw"
            required
          />
        </div>
        <pre>{{ tPw.errors | json }}</pre>
        <div>
          確認密碼:
          <input
            type="text"
            name="pw2"
            #tPw2="ngModel"
            [(ngModel)]="pw2"
            required
            dtCompareEqual="pw"
          />
        </div>
        <pre>{{ tPw2.errors | json }}</pre>
      </form>
      

將 Angular 函式庫封裝成 npm 套件

如果要將我們剛剛開發的 Angular 函式庫封裝成 npm 套件,這時就需要設定一下 npm scripts,方便我們日後可以方便的進行套件打包。

要將我們開發好的驗證器函式庫封裝,基本上需要進行以下兩個命令:

ng build dt-compare-equal-validator
cd dist/dt-compare-equal-validator && npm pack

這個過程會建立 dt-compare-equal-validator-0.0.1.tgz 壓縮檔,如果是在我們目前的 Monorepo 內進行開發,是完全不需要額外安裝的,原因是因為我們已經設定 tsconfig.json 裡面的路徑對應 ( "paths" )。但如果想到其他 Angular 專案使用這個函式庫,那就可以執行以下命令直接安裝起來:

npm install path/to/dt-compare-equal-validator-0.0.1.tgz

任何一個 Angular 專案,只要匯入 dt-compare-equal-validator 模組,就可以立即使用必要的函式與元件!

將 Angular 函式庫的 npm 套件發行至 npm registry

要發行 npm 套件到 npm registry 通常需要注意以下事項:

  1. 決定套件名稱

    請開啟 projects\dt-compare-equal-validator\package.json 檔案,設定 nameversion 屬性。這裡的 name 屬性必須是全世界唯一的名稱,不能跟現有的 npm 套件衝突。

    如果可以的話,請在 projects\dt-compare-equal-validator\package.json 額外填上 licenserepositorydescriptionkeywordshomepage 資訊!詳細說明文件參見:npm-package.json | npm Documentation

  2. 決定套件版本

    由於每次發佈版本時,版本號都不能重複,因次每次發行都應該要決定你想用的版本編號。

    這邊可以用 npm version patchnpm version minornpm version major 來自動變更版號,詳情請見 npm-version 說明。

  3. 決定授權條款

    你可以在 projects\dt-compare-equal-validator\ 目錄下建立一個 LICENSE 檔案,並將你想要的授權條款放進去。例如我這個函式庫,打算以 MIT License 進行授權,就可以將授權內容複製進去。

    你可以到 Choose a License 網站選擇一個適合的授權條款。

  4. 撰寫 README.md 說明文件

  5. 封裝 npm 套件

    ng build dt-compare-equal-validator
    cd dist/dt-compare-equal-validator && npm pack
    

    注意:新版的 Angular CLI 已經會自動將 LICENSE 與 README.md 複製到發行目錄。

  6. 發行 npm 套件

    其實任何人都可以發佈 npm 套件,只要到 npm 網站註冊會員即可。

    註冊好之後,可以用 npm login 命令進行登入,如果已經登入過,則可以用 npm whoami 查詢目前登入的身分。

    接著就只要進入 dist/dt-compare-equal-validator 目錄執行 npm publish 即可發佈套件!

線上測試

如果想在線上看看 dt-compare-equal-validator 套件的使用方式,可以連到 StackBlitz 網站,進入以下網址直接看測試結果即可:

開放原始碼

本文章的所有版控紀錄,也已經放上 GitHub Repo 給大家參考。

相關連結

留言評論