當 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 表單驗證器專案!
-
建立專案骨架 (Monorepo)
ng new ng-validators --create-application=false --routing false --style css
cd ng-validators
code .
-
建立 Angular Library 函式庫專案
對 Angular 應用程式來說,通常所有 Comeponts 與 Directives 都會以 app-
作為前置詞(prefix),但開發給任意專案共用的函式庫,通常不會這樣命名。例如我公司的英文名稱為 Duotify,我就可以將前置詞改為 dt-
為主,那麼這個時候你可以這樣執行:
ng g library dt-compare-equal-validator --prefix=dt
-
建立 Angular 應用程式專案 (用來測試我們撰寫的函式庫元件之用)
ng g application demo1 --routing --style css --skip-tests
使用函式庫與啟動 Angular 應用程式
目前為止,我們建立了三個專案:
dt-compare-equal-validator
函式庫專案
demo1
這個用來測試函式庫的 Angular 應用程式專案
demo1-e2e
這個 Angular 應用程式附帶的 E2E 測試專案
此時你可以測試看看,使用 npm run build
或 ng build
就會自動建置 dt-compare-equal-validator
專案,那是因為在 angular.json
檔案中有個 "defaultProject": "dt-compare-equal-validator"
設定,因此預設會建置這個專案。
請注意:從 Angular CLI v6.1 開始,建置 Angular Library 預設就是採用 --prod
生產環境建置!
如果你想要建置 demo1
專案的話,可以試試 ng build demo1
或 ng 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 架構,並設定好模組匯入。
最後,我們需要確認專案可以正常建置,有幾件事需要提醒各位:
-
所有的 Angular Library 都需要建置後才能被 Angular 應用程式開發時參考
你可以用 ng build dt-compare-equal-validator --watch
命令自動建置專案,該命令會自動建置任意檔案變更,並自動重新建置此函式庫。
-
執行 Angular 應用程式
ng serve demo1
-
如果要跑單元測試
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 共用模組範例,所有需要對外公開的元件都必須註冊在這裡。如果未來有新增元件,也都需要註冊在這個檔案內 @NgModule
的 declarations
與 export
屬性。
自製 Angular 表單驗證器 (Reactive Forms)
我先寫出 Reactive Forms 可用的表單驗證器,並透過以下步驟進行設計:
-
修改 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 {}
-
寫出一個 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 };
}
};
}
-
修改 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';
-
我們來測試一下是否可以正常匯入 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';
-
最後我們將 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 元件的範本使用。
以下是設計的過程:
-
先在 dt-compare-equal-validator
專案下建立一個名為 CompareEqualDirective
的元件
ng g directive compare-equal --project dt-compare-equal-validator
該元件預設的選取器將會是 [dtCompareEqual]
-
然後將該元件類別註冊到 src\lib\dt-compare-equal-validator.module.ts
的 exports
,同時也將沒用到的 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 {}
-
接著將驗證器元件完成
這裡必須實作 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);
}
}
-
然後將該 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';
-
最後我們將 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 通常需要注意以下事項:
-
決定套件名稱
請開啟 projects\dt-compare-equal-validator\package.json
檔案,設定 name
與 version
屬性。這裡的 name
屬性必須是全世界唯一的名稱,不能跟現有的 npm 套件衝突。
如果可以的話,請在 projects\dt-compare-equal-validator\package.json
額外填上 license
、repository
、description
、keywords
與 homepage
資訊!詳細說明文件參見:npm-package.json | npm Documentation
-
決定套件版本
由於每次發佈版本時,版本號都不能重複,因次每次發行都應該要決定你想用的版本編號。
這邊可以用 npm version patch
、npm version minor
或 npm version major
來自動變更版號,詳情請見 npm-version 說明。
-
決定授權條款
你可以在 projects\dt-compare-equal-validator\
目錄下建立一個 LICENSE
檔案,並將你想要的授權條款放進去。例如我這個函式庫,打算以 MIT License 進行授權,就可以將授權內容複製進去。
你可以到 Choose a License 網站選擇一個適合的授權條款。
-
撰寫 README.md 說明文件
-
封裝 npm 套件
ng build dt-compare-equal-validator
cd dist/dt-compare-equal-validator && npm pack
注意:新版的 Angular CLI 已經會自動將 LICENSE 與 README.md 複製到發行目錄。
-
發行 npm 套件
其實任何人都可以發佈 npm 套件,只要到 npm 網站註冊會員即可。
註冊好之後,可以用 npm login
命令進行登入,如果已經登入過,則可以用 npm whoami
查詢目前登入的身分。
接著就只要進入 dist/dt-compare-equal-validator
目錄執行 npm publish
即可發佈套件!
線上測試
如果想在線上看看 dt-compare-equal-validator 套件的使用方式,可以連到 StackBlitz 網站,進入以下網址直接看測試結果即可:
開放原始碼
本文章的所有版控紀錄,也已經放上 GitHub Repo 給大家參考。
相關連結