許多 Angular CLI 指令會在你的程式碼上執行複雜的程序,例如建置、測試或提供應用程式服務。 這些指令使用一個名為 Architect 的內部工具來執行 CLI 建置器,建置器會呼叫另一個工具(打包器、測試執行器、伺服器)來完成預定的工作。 自訂建置器可以執行全新的工作,或變更既有指令所使用的第三方工具。
本文件說明 CLI 建置器如何與工作區設定檔整合,並示範如何建立你自己的建置器。
HELPFUL: 本文範例使用的程式碼可在這個 GitHub 儲存庫中找到。
CLI 建置器
內部的 Architect 工具將工作委派給稱為_建置器_的處理函式。 建置器處理函式接收兩個引數:
| 引數 | 類型 |
|---|---|
options |
JSONObject |
context |
BuilderContext |
這裡的關注點分離與原理圖相同;原理圖用於其他會碰觸你的程式碼的 CLI 指令(例如 ng generate)。
options物件由 CLI 使用者的選項與設定提供,而context物件則由 CLI Builder API 自動提供。- 除了情境資訊外,
context物件還提供了一個排程方法context.scheduleTarget()。 排程器會以指定的目標設定執行建置器處理函式。
建置器處理函式可以是同步的(回傳一個值)、非同步的(回傳 Promise),或以監看模式回傳多個值(回傳 Observable)。
回傳值必須永遠是 BuilderOutput 類型。
這個物件包含一個布林值 success 欄位,以及一個可選的 error 欄位,可放入錯誤訊息。
Angular 提供了一些建置器,供 CLI 用於 ng build 與 ng test 等指令。
這些與其他內建 CLI 建置器的預設目標設定,可在工作區設定檔 angular.json 的 "architect" 區段中找到並設定。
此外,你也可以建立自己的建置器來擴充與自訂 Angular,並使用 ng run CLI 指令直接執行它們。
建置器專案結構
建置器位於一個「專案」資料夾中,其結構與 Angular 工作區相似:最上層放全域設定檔,原始碼資料夾中則放更具體的設定與定義行為的程式碼檔案。
例如,你的 myBuilder 資料夾可能包含下列檔案。
| 檔案 | 用途 |
|---|---|
src/my-builder.ts |
建置器定義的主要原始碼檔案。 |
src/my-builder.spec.ts |
測試用的原始碼檔案。 |
src/schema.json |
建置器輸入選項的定義。 |
builders.json |
建置器定義。 |
package.json |
依賴。請參閱 https://docs.npmjs.com/files/package.json。 |
tsconfig.json |
TypeScript 設定。 |
建置器可以發佈到 npm,請參閱發佈你的程式庫。
建立建置器
以範例來說,我們來建立一個將檔案複製到新位置的建置器。
若要建立建置器,使用 createBuilder() CLI Builder 函式,並回傳一個 Promise<BuilderOutput> 物件。
src/my-builder.ts (建置器骨架)
import {BuilderContext, BuilderOutput, createBuilder} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
interface Options extends JsonObject {
source: string;
destination: string;
}
export default createBuilder(copyFileBuilder);
async function copyFileBuilder(options: Options, context: BuilderContext): Promise<BuilderOutput> {
}
現在為它加入一些邏輯。
下列程式碼從使用者選項中取得來源與目的地檔案路徑,並將檔案從來源複製到目的地(使用 Promise 版本的 Node.js 內建 copyFile() 函式)。
若複製操作失敗,它會回傳一個帶有底層問題訊息的錯誤。
src/my-builder.ts (建置器)
import {BuilderContext, BuilderOutput, createBuilder} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
import {promises as fs} from 'fs';
interface Options extends JsonObject {
source: string;
destination: string;
}
export default createBuilder(copyFileBuilder);
async function copyFileBuilder(options: Options, context: BuilderContext): Promise<BuilderOutput> {
try {
await fs.copyFile(options.source, options.destination);
} catch (err) {
return {
success: false,
error: (err as Error).message,
};
}
return {success: true};
}
處理輸出
預設情況下,copyFile() 不會將任何內容輸出到程序的標準輸出或標準錯誤。
若發生錯誤,可能很難理解問題發生時建置器到底在做什麼。
使用 Logger API 記錄額外資訊,來補上更多情境。
這也能讓建置器本身在獨立的程序中執行,即使標準輸出與錯誤被停用也沒問題。
你可以從 context 取得一個 Logger 實例。
src/my-builder.ts (處理輸出)
try {
await fs.copyFile(options.source, options.destination);
} catch (err) {
context.logger.error('Failed to copy file.');
return {
success: false,
error: (err as Error).message,
};
}
進度與狀態回報
CLI Builder API 內含進度與狀態回報工具,可為特定函式與介面提供提示。
若要回報進度,使用 context.reportProgress() 方法,它接收目前值、可選的總數與狀態字串作為引數。
總數可以是任何數字。例如,若你知道需要處理多少檔案,總數可以是檔案數量,目前值則是已處理的數量。
狀態字串在你傳入新字串值之前不會被修改。
在我們的範例中,複製操作不是完成就是仍在執行,因此不需要進度回報,但你可以回報狀態,讓呼叫我們建置器的父建置器知道目前狀況。
使用 context.reportStatus() 方法來產生任意長度的狀態字串。
HELPFUL: 不保證長字串會完整顯示;它可能被截斷以符合顯示它的 UI。
傳入空字串即可移除狀態。
src/my-builder.ts (進度回報)
context.reportStatus(`Copying ${options.source} to ${options.destination}.`);
try {
await fs.copyFile(options.source, options.destination);
} catch (err) {
context.logger.error('Failed to copy file.');
return {
success: false,
error: (err as Error).message,
};
}
context.reportStatus('Done.');
return {success: true};
建置器輸入
你可以透過 ng build 之類的 CLI 指令間接呼叫建置器,或用 Angular CLI 的 ng run 指令直接呼叫。
無論哪種情況,你都必須提供必要的輸入,但其他輸入可以預設為針對特定_目標_預先設定的值、以設定指定的值,或在命令列上設定的值。
輸入驗證
你在與建置器相關聯的 JSON schema 中定義建置器輸入。
與原理圖類似,Architect 工具會將解析後的輸入值收集成一個 options 物件,並在傳給建置器函式之前,依 schema 驗證它們的類型。
以我們的範例建置器來說,options 應該是一個有兩個鍵的 JsonObject:
source 與 destination,兩者都是字串。
你可以提供下列 schema 來驗證這些值的類型。
schema.json
{
"$schema": "https://json-schema.org/schema",
"type": "object",
"properties": {
"source": {
"type": "string"
},
"destination": {
"type": "string"
}
}
}
HELPFUL: 這是最小範例,但用 schema 做驗證可以非常強大。 更多資訊請參閱 JSON schemas 網站。
若要將建置器實作與其 schema 及名稱連結起來,你需要建立一個_建置器定義_檔案,並在 package.json 中指向它。
建立一個名為 builders.json 的檔案,內容如下:
builders.json
{
"builders": {
"copy": {
"implementation": "./dist/my-builder.js",
"schema": "./src/schema.json",
"description": "Copies a file."
}
}
}
在 package.json 檔案中,加入一個 builders 鍵,告訴 Architect 工具去哪裡找我們的建置器定義檔。
package.json
{
"name": "@example/copy-file",
"version": "1.0.0",
"description": "Builder for copying files",
"builders": "builders.json",
"dependencies": {
"@angular/build": "^21.2.0"
}
}
我們的建置器正式名稱現在是 @example/copy-file:copy。
第一部分是套件名稱,第二部分是 builders.json 檔案中指定的建置器名稱。
這些值可透過 options.source 與 options.destination 存取。
src/my-builder.ts (回報狀態)
await fs.copyFile(options.source, options.destination);
目標設定
建置器必須有一個已定義的目標,將它與特定的輸入設定及專案相關聯。
目標定義在 angular.json CLI 設定檔中。
目標會指定要使用的建置器、它的預設選項設定,以及具名的替代設定。
Angular CLI 中的 Architect 會使用目標定義,來解析某次執行的輸入選項。
angular.json 檔案為每個專案設有一區段,每個專案設定的 "architect" 區段則設定了供 CLI 指令(如 'build'、'test' 與 'serve')使用的建置器目標。
例如,預設情況下,ng build 指令會執行建置器 @angular/build:application 來執行建置工作,並傳入 angular.json 中為 build 目標指定的預設選項值。
angular.json
{
"myApp": {
"...": "...",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/myApp",
"index": "src/index.html",
"...": "..."
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"...": "..."
}
}
},
"...": "..."
}
}
}
指令會將 "options" 區段中指定的預設選項集合傳給建置器。
若你傳入 --configuration=production 旗標,它會使用 production 設定中指定的覆寫值。
你也可以在命令列上個別指定更多的選項覆寫。
目標字串
通用的 ng run CLI 指令將目標字串作為第一個引數,其形式如下。
project:target[:configuration]
| 詳細資訊 | |
|---|---|
| project | 與該目標相關聯的 Angular CLI 專案名稱。 |
| target | angular.json 檔案 "architect" 區段中具名的建置器設定。 |
| configuration | (可選)該目標的特定設定覆寫名稱,定義於 angular.json 檔案中。 |
若你的建置器呼叫另一個建置器,它可能需要讀取傳入的目標字串。
使用 @angular-devkit/architect 的 targetFromTargetString() 工具函式,將這個字串解析成物件。
排程與執行
Architect 以非同步方式執行建置器。 若要呼叫建置器,你排程一個工作,待所有設定解析完成後執行。
在排程器回傳 BuilderRun 控制物件之前,建置器函式不會被執行。
CLI 通常會呼叫 context.scheduleTarget() 函式來排程工作,然後使用 angular.json 檔案中的目標定義來解析輸入選項。
Architect 解析指定目標的輸入選項時,會先取預設選項物件,接著以設定的值覆寫,再以傳給 context.scheduleTarget() 的覆寫物件的值進一步覆寫。
對 Angular CLI 而言,覆寫物件是由命令列引數建立的。
Architect 會依建置器的 schema 驗證解析後的選項值。 若輸入有效,Architect 會建立 context 並執行建置器。
更多資訊請參閱工作區設定。
HELPFUL: 你也可以透過呼叫 context.scheduleBuilder(),直接從另一個建置器或測試中呼叫建置器。
你直接將 options 物件傳給該方法,這些選項值會依建置器的 schema 驗證,不做進一步調整。
只有 context.scheduleTarget() 方法會透過 angular.json 檔案解析設定與覆寫。
預設 Architect 設定
我們來建立一個簡單的 angular.json 檔案,將目標設定放入情境中。
你可以將建置器發佈到 npm(請參閱發佈你的程式庫),並使用下列指令安裝它:
npm install @example/copy-file
若你用 ng new builder-test 建立新專案,產生的 angular.json 檔案大致如下,只有預設的建置器設定。
angular.json
{
"projects": {
"builder-test": {
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/builder-test",
"index": "src/index.html",
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json"
},
"configurations": {
"production": {
"optimization": true,
"aot": true
}
}
}
}
}
}
}
新增目標
新增一個會執行我們建置器來複製檔案的目標。
這個目標告訴建置器複製 package.json 檔案。
- 我們會在專案的
architect物件中新增一段目標區段 - 名為
copy-package的目標使用我們的建置器,你已將它發佈為@example/copy-file。 - options 物件為你定義的兩個輸入提供預設值。
source:你要複製的既有檔案。destination:你要複製到的路徑。
angular.json
{
"projects": {
"builder-test": {
"architect": {
"copy-package": {
"builder": "@example/copy-file:copy",
"options": {
"source": "package.json",
"destination": "package-copy.json"
}
}
// Existing targets...
}
}
}
}
執行建置器
若要以新目標的預設設定執行我們的建置器,使用下列 CLI 指令。
ng run builder-test:copy-package
這會將 package.json 檔案複製為 package-copy.json。
使用命令列引數來覆寫已設定的預設值。
例如,若要以不同的 destination 值執行,使用下列 CLI 指令。
ng run builder-test:copy-package --destination=package-other.json
這會改將檔案複製為 package-other.json,而不是 package-copy.json。
因為你沒有覆寫 source 選項,它仍然會從預設的 package.json 檔案複製。
測試建置器
對你的建置器使用整合測試,這樣你就能使用 Architect 排程器建立 context,如這個範例所示。
在建置器原始碼目錄中,建立一個新的測試檔案 my-builder.spec.ts。這個測試會建立 JsonSchemaRegistry(用於 schema 驗證)、TestingArchitectHost(ArchitectHost 的記憶體內實作)與 Architect 的新實例。
以下是一個執行複製檔案建置器的測試範例。
這個測試使用建置器複製 package.json 檔案,並驗證複製後檔案的內容與來源相同。
src/my-builder.spec.ts
import {Architect} from '@angular-devkit/architect';
import {TestingArchitectHost} from '@angular-devkit/architect/testing';
import {schema} from '@angular-devkit/core';
import {promises as fs} from 'fs';
import {join} from 'path';
describe('Copy File Builder', () => {
let architect: Architect;
let architectHost: TestingArchitectHost;
beforeEach(async () => {
const registry = new schema.CoreSchemaRegistry();
registry.addPostTransform(schema.transforms.addUndefinedDefaults);
// TestingArchitectHost() takes workspace and current directories.
// Since we don't use those, both are the same in this case.
architectHost = new TestingArchitectHost(__dirname, __dirname);
architect = new Architect(architectHost, registry);
// This will either take a Node package name, or a path to the directory
// for the package.json file.
await architectHost.addBuilderFromPackage(join(__dirname, '..'));
});
it('can copy files', async () => {
// A "run" can have multiple outputs, and contains progress information.
const run = await architect.scheduleBuilder('@example/copy-file:copy', {
source: 'package.json',
destination: 'package-copy.json',
});
// The "result" member (of type BuilderOutput) is the next output.
const output = await run.result;
// Stop the builder from running. This stops Architect from keeping
// the builder-associated states in memory, since builders keep waiting
// to be scheduled.
await run.stop();
// Expect that the copied file is the same as its source.
const sourceContent = await fs.readFile('package.json', 'utf8');
const destinationContent = await fs.readFile('package-copy.json', 'utf8');
expect(destinationContent).toBe(sourceContent);
});
});
HELPFUL: 在你的儲存庫中執行這個測試時,你需要 ts-node 套件。
你可以將 my-builder.spec.ts 重新命名為 my-builder.spec.js 來避免這個需求。
監看模式
大多數建置器執行一次後就回傳。然而,這種行為與會監看變更的建置器(例如開發伺服器)並不完全相容。 Architect 可以支援監看模式,但有一些需要注意的地方。
若要在監看模式下使用,建置器處理函式應回傳一個
Observable。 Architect 會訂閱這個Observable直到它完成,若建置器以相同引數再次被排程,可能會重用它。建置器應在每次執行後永遠發出一個
BuilderOutput物件。 一旦執行過,它就可以進入監看模式,等待外部事件觸發。 若某個事件觸發它重新啟動,建置器應執行context.reportRunning()函式,告訴 Architect 它正在再次執行。 這可避免在另一次執行被排程時,Architect 停止這個建置器。
當你的建置器呼叫 BuilderRun.stop() 退出監看模式時,Architect 會取消訂閱建置器的 Observable,並呼叫建置器的 teardown 邏輯進行清理。
這種行為也讓長時間執行的建置可以被停止與清理。
一般而言,若你的建置器正在監看外部事件,你應將執行分成三個階段。
| 階段 | 詳細資訊 |
|---|---|
| 執行中 | 正在執行的工作,例如呼叫編譯器。當編譯器完成且你的建置器發出 BuilderOutput 物件時,此階段結束。 |
| 監看中 | 在兩次執行之間,監看外部事件串流。例如,監看檔案系統的任何變更。當編譯器重新啟動、且 context.reportRunning() 被呼叫時,此階段結束。 |
| 完成 | 任務已完全完成(例如某個需要執行多次的編譯器),或建置器執行已被停止(使用 BuilderRun.stop())。Architect 會執行 teardown 邏輯,並取消訂閱你的建置器的 Observable。 |
摘要
CLI Builder API 提供了一種方式,透過建置器執行自訂邏輯,來改變 Angular CLI 的行為。
- 建置器可以是同步或非同步的,執行一次或監看外部事件,並能排程其他建置器或目標。
- 建置器的選項預設值在
angular.json設定檔中指定,可被目標的替代設定覆寫,再被命令列旗標進一步覆寫 - Angular 團隊建議你使用整合測試來測試 Architect 建置器。使用單元測試來驗證建置器執行的邏輯。
- 若你的建置器回傳
Observable,它應在該Observable的 teardown 邏輯中清理建置器。