很多 Angular CLI 命令都要在你的代碼上執(zhí)行一些復(fù)雜的處理,比如風(fēng)格檢查(lint)構(gòu)建或測試。這些命令會通過一個叫做建筑師(Architect)的內(nèi)部工具來運行 CLI 構(gòu)建器,而這些構(gòu)建器會運用一些第三方工具來完成目標(biāo)任務(wù)。
在 Angular 的版本 8 中,CLI 構(gòu)建器的 API 是穩(wěn)定的,想要通過添加或修改命令來自定義 Angular CLI 的開發(fā)人員可以使用它。比如,你可以提供一個構(gòu)建器來執(zhí)行全新的任務(wù),或者更改一個現(xiàn)有命令所使用的第三方工具。
本文檔介紹了 CLI 構(gòu)建器是如何與工作區(qū)配置文件集成的,還展示了如何創(chuàng)建你自己的構(gòu)建器。
可以在這個 GitHub 倉庫中的例子中找到代碼。
內(nèi)部建筑師工具會把工作委托給名叫構(gòu)建器的處理器函數(shù)。處理器函數(shù)接收兩個參數(shù):一組 ?options
?輸入(JSON 對象)和一個 ?context
?(?BuilderContext
?對象)。
這里對關(guān)注點的分離和原理圖中是一樣的,它也適用于其它要接觸(touch)代碼的 CLI 命令(比如 ?ng generate
?)。
options
?對象是由本 CLI 的用戶提供的,而 ?context
?對象則由 CLI 構(gòu)建器的 API 提供context
?對象(它是 ?BuilderContext
?的實例)還允許你訪問調(diào)度方法 ?context.scheduleTarget()
?。調(diào)度器會用指定的目標(biāo)配置來執(zhí)行構(gòu)建器處理函數(shù)。這個構(gòu)建器處理函數(shù)可以是同步的(返回一個值)或異步的(返回一個 Promise),也可以監(jiān)視并返回多個值(返回一個 Observable)。最終返回的值全都是 ?BuilderOutput
?類型的。該對象包含一個邏輯字段 ?success
?和一個可以包含錯誤信息的可選字段 ?error
?。
Angular 提供了一些構(gòu)建器,供 CLI 命令使用,如 ?ng build
? 和 ?ng test
? 等。這些內(nèi)置 CLI 構(gòu)建器的默認(rèn)目標(biāo)配置可以在工作區(qū)配置文件 ?angular.json
? 的 ?architect
?部分找到(并進(jìn)行自定義)。可以通過創(chuàng)建自己的構(gòu)建器來擴(kuò)展和自定義 Angular,你可以使用 ?ng run
? CLI 命令來運行你自己的構(gòu)建器。
構(gòu)建器位于一個 ?project
?文件夾中,該文件夾的結(jié)構(gòu)類似于 Angular 工作區(qū),包括位于頂層的全局配置文件,以及位于工作代碼所在源文件夾中的更具體的配置。比如,?myBuilder
?文件夾中可能包含如下文件。
文件 |
用途 |
---|---|
src/my-builder.ts
|
這個構(gòu)建器定義的主要源碼。 |
src/my-builder.spec.ts
|
測試的源碼。 |
src/schema.json
|
構(gòu)建器輸入選項的定義。 |
builders.json
|
測試配置。 |
package.json
|
|
tsconfig.json
|
將此構(gòu)建器發(fā)布到 ?npm
?。如果你將其發(fā)布為 ?@example/my-builder
?,請使用以下命令安裝它。
npm install @example/my-builder
舉個例子,讓我們創(chuàng)建一個用來復(fù)制文件的構(gòu)建器。要創(chuàng)建構(gòu)建器,請使用 CLI 構(gòu)建器函數(shù) ?createBuilder()
?,并返回一個 ?Promise<BuilderOutput>
? 對象。
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> {
}
現(xiàn)在,讓我們?yōu)樗砑右恍┻壿?。下列代碼會從用戶選項中獲取源文件和目標(biāo)文件的路徑,并且把源文件復(fù)制到目標(biāo)文件(使用 NodeJS 內(nèi)置函數(shù)copyFile()的 Promise 版本)。如果文件操作失敗了,它會返回一個帶有底層錯誤信息的 error 對象。
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.message,
};
}
return { success: true };
}
默認(rèn)情況下,?copyFile()
? 方法不會往標(biāo)準(zhǔn)輸出或標(biāo)準(zhǔn)錯誤中打印任何信息。如果發(fā)生了錯誤,可能很難理解構(gòu)建器到底做了什么。可以使用 ?Logger
?API 來記錄一些額外的信息,以提供額外的上下文。這樣還能讓構(gòu)建器本身可以在一個單獨的進(jìn)程中執(zhí)行,即使其標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯誤被停用了也無所謂(就像在 Electron 應(yīng)用中一樣)。
你可以從上下文中檢索一個 ?Logger
?實例。
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) {
context.logger.error('Failed to copy file.');
return {
success: false,
error: err.message,
};
}
return { success: true };
}
CLI 構(gòu)建器 API 包含一些進(jìn)度報告和狀態(tài)報告工具,可以為某些函數(shù)和接口提供提示信息。
要報告進(jìn)度,請使用 ?context.reportProgress()
? 方法,它接受一個當(dāng)前值(value)、一個(可選的)總值(total)和狀態(tài)(status)字符串作為參數(shù)??傊悼梢允侨我鈹?shù)字,比如,如果你知道有多少個文件需要處理,那么總值可能是這些文件的數(shù)量,而當(dāng)前值是已處理過的數(shù)量。除非傳入了新的字符串,否則這個狀態(tài)字符串不會改變。
你可以看看 ?tslint
?構(gòu)建器如何報告進(jìn)度的例子。
在我們的例子中,這種復(fù)制操作或者已完成或者正在執(zhí)行,所以不需要進(jìn)度報告,但是可以報告狀態(tài),以便調(diào)用此構(gòu)建器的父構(gòu)建器知道發(fā)生了什么??梢杂?nbsp;?context.reportStatus()
? 方法生成一個任意長度的狀態(tài)字符串。
注意:
無法保證長字符串會完全顯示出來,可以裁剪它以適應(yīng)界面顯示。
傳入一個空字符串可以移除狀態(tài)。
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> {
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.message,
};
}
context.reportStatus('Done.');
return { success: true };
}
你可以通過 CLI 命令間接調(diào)用一個構(gòu)建器,也可以直接用 Angular CLI 的 ?ng run
? 命令來調(diào)用它。無論哪種情況,你都必須提供所需的輸入,但是可以用特定目標(biāo)中預(yù)配置的值作為其默認(rèn)值,然后指定一個預(yù)定義的、指定的配置進(jìn)行覆蓋,最后在命令行中進(jìn)一步覆蓋這些選項的值。
你可以在該構(gòu)建器的相關(guān) JSON 模式中定義構(gòu)建器都有哪些輸入。建筑師工具會把解析后的輸入值收集到一個 ?options
?對象中,并在將其傳給構(gòu)建器函數(shù)之前先根據(jù)這個模式驗證它們的類型。(Schematics 庫也對用戶輸入做了同樣的驗證)。
對于這個范例構(gòu)建器,你希望 ?options
?的值是帶有兩個鍵的 ?JsonObject
?:一個是 ?source
?,一個是 ?destination
?,它們都是字符串。
你可以提供如下模式來對這些值的類型進(jìn)行驗證。
{
"$schema": "http://json-schema.org/schema",
"type": "object",
"properties": {
"source": {
"type": "string"
},
"destination": {
"type": "string"
}
}
}
這是一個非常簡單的例子,但這種模式驗證也可以非常強(qiáng)大。欲知詳情,參閱 JSON 模式網(wǎng)站。
要把構(gòu)建器的實現(xiàn)與它的模式和名稱關(guān)聯(lián)起來,你需要創(chuàng)建一個構(gòu)建器定義文件,可以在 ?package.json
? 中指向該文件。
創(chuàng)建一個名為 ?builders.json
? 文件,它看起來像這樣。
{
"builders": {
"copy": {
"implementation": "./dist/my-builder.js",
"schema": "./src/schema.json",
"description": "Copies a file."
}
}
}
在 ?package.json
? 文件中,添加一個 ?builders
?鍵,告訴建筑師工具可以在哪里找到這個構(gòu)建器定義文件。
{
"name": "@example/copy-file",
"version": "1.0.0",
"description": "Builder for copying files",
"builders": "builders.json",
"dependencies": {
"@angular-devkit/architect": "~0.1200.0",
"@angular-devkit/core": "^12.0.0"
}
}
現(xiàn)在,這個構(gòu)建器的正式名字是 ?@example/copy-file:copy
?。第一部分是包名(使用 node 方案進(jìn)行解析),第二部分是構(gòu)建器名稱(使用 ?builders.json
? 文件進(jìn)行解析)。
使用某個 ?options
?是非常簡單的。在上一節(jié),你就曾用過 ?options.source
? 和 ?options.destination
?。
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.message,
};
}
context.reportStatus('Done.');
return { success: true };
構(gòu)建器必須有一個已定義的目標(biāo),此目標(biāo)會把構(gòu)建器與特定的輸入配置和項目關(guān)聯(lián)起來。
目標(biāo)是在 CLI 配置文件 ?angular.json
? 中定義的。目標(biāo)用于指定要使用的構(gòu)建器、默認(rèn)的選項配置,以及指定的備用配置。建筑師工具使用目標(biāo)定義來為一次特定的執(zhí)行解析輸入選項。
?angular.json
? 文件中為每個項目都有一節(jié)配置,每個項目的 ?architect
?部分都會為 CLI 命令(比如 ?build
?、?test
?和 ?lint
?)配置構(gòu)建器目標(biāo)。默認(rèn)情況下,?build
?命令會運行 ?@angular-devkit/build-angular:browser
? 構(gòu)建器來執(zhí)行 ?build
?任務(wù),并傳入 ?angular.json
? 中為 ?build
?目標(biāo)指定的默認(rèn)選項值。
{
"myApp": {
…
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"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",
…
}
}
},
…
該命令會給構(gòu)建器傳遞 options
節(jié)中指定的一組默認(rèn)選項。如果你傳入了 --configuration=production
標(biāo)志,它就會使用 production
備用配置中指定的值進(jìn)行覆蓋??梢栽诿钚兄袉为氈付ㄆ渌x項進(jìn)行覆蓋,還可以為 build
目標(biāo)添加更多備用配置,以定義其它環(huán)境,比如 stage
或 qa
。
通用的 ?ng run
? CLI 命令將以下格式的目標(biāo)字符串作為其第一個參數(shù)。
project:target[:configuration]
詳情 |
|
---|---|
項目(project) |
與此目標(biāo)關(guān)聯(lián)的 Angular CLI 項目的名稱。 |
目標(biāo) |
|
配置(configuration) |
(可選)用于覆蓋指定目標(biāo)的具體配置名稱,如 |
如果你的構(gòu)建器調(diào)用另一個構(gòu)建器,它可能需要讀取一個傳入的目標(biāo)字符串??梢允褂?nbsp;?@angular-devkit/architect
? 中的工具函數(shù) ?targetFromTargetString()
? 把這個字符串解析成一個對象。
建筑師會異步運行構(gòu)建器。要調(diào)用某個構(gòu)建器,就要在所有配置解析完成之后安排一個要運行的任務(wù)。
在調(diào)度器返回 ?BuilderRun
?控件對象之前,不會執(zhí)行該構(gòu)建器函數(shù)。CLI 通常會通過調(diào)用 ?context.scheduleTarget()
? 函數(shù)來調(diào)度任務(wù),然后使用 ?angular.json
? 文件中的目標(biāo)定義來解析輸入選項。
建筑師會接受默認(rèn)的選項對象來解析指定目標(biāo)的輸入選項,然后覆蓋所用配置中的值(如果有的話),然后再從傳給 ?context.scheduleTarget()
? 的覆蓋對象中覆蓋這些值。對于 Angular CLI,覆蓋對象是從命令行參數(shù)中構(gòu)建的。
建筑師會根據(jù)構(gòu)建器的模式對生成的選項值進(jìn)行驗證。如果輸入有效,建筑師會創(chuàng)建上下文并執(zhí)行該構(gòu)建器。
你還可以通過調(diào)用 ?context.scheduleBuilder()
? 從另一個構(gòu)建器或測試中調(diào)用某個構(gòu)建器。你可以直接把 ?options
?對象傳給該方法,并且這些選項值會根據(jù)這個構(gòu)建器的模式進(jìn)行驗證,而無需進(jìn)一步調(diào)整。
只有 ?context.scheduleTarget()
? 方法來解析這些配置和并通過 ?angular.json
? 文件進(jìn)行覆蓋。
讓我們創(chuàng)建一個簡單的 ?angular.json
? 文件,它會把目標(biāo)配置放到上下文中。
你可以把這個構(gòu)建器發(fā)布到 npm,并使用如下命令來安裝它:
npm install @example/copy-file
如果用 ?ng new builder-test
? 創(chuàng)建一個新項目,那么生成的 ?angular.json
? 文件就是這樣的,它只有默認(rèn)的構(gòu)建器參數(shù)。
{
// …
"projects": {
// …
"builder-test": {
// …
"architect": {
// …
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
// … more options…
"outputPath": "dist/builder-test",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json"
},
"configurations": {
"production": {
// … more options…
"optimization": true,
"aot": true,
"buildOptimizer": true
}
}
}
}
}
}
// …
}
添加一個新的目標(biāo),來運行我們的構(gòu)建器以復(fù)制文件。該目標(biāo)告訴構(gòu)建器,復(fù)制 ?package.json
? 文件。
你需要更新 ?angular.json
? 文件,把這個構(gòu)建器的目標(biāo)添加到新項目的 ?architect
?部分。
architect
?對象添加一個新的目標(biāo)小節(jié)copy-package
? 的目標(biāo)使用了我們的構(gòu)建器,它發(fā)布到了 ?@example/copy-file
?。source
?(你要復(fù)制的現(xiàn)有文件)和 ?destination
?(你要復(fù)制到的路徑){
"projects": {
"builder-test": {
"architect": {
"copy-package": {
"builder": "@example/copy-file:copy",
"options": {
"source": "package.json",
"destination": "package-copy.json"
}
},
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/builder-test",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json"
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"aot": true,
"buildOptimizer": true
}
}
}
}
}
}
}
要想使用這個新目標(biāo)的默認(rèn)配置運行我們的構(gòu)建器,請使用以下 CLI 命令。
ng run builder-test:copy-package
這將把 ?package.json
? 文件復(fù)制成 ?package-copy.json
?。
你可以使用命令行參數(shù)來覆蓋已配置的默認(rèn)值。比如,要改用其它 ?destination
?值運行,請使用以下 CLI 命令。
ng run builder-test:copy-package --destination=package-other.json
這將把此文件復(fù)制為 ?package-other.json
? 而不再是 ?package-copy.json
?。因為我們沒有覆蓋 source 選項,所以它仍然會從 ?package.json
? 文件復(fù)制(提供給該目標(biāo)的默認(rèn)值)。
對構(gòu)建器進(jìn)行集成測試,以便你可以使用建筑師的調(diào)度器來創(chuàng)建一個上下文,就像這個例子中一樣。
my-builder.spec.ts
?。該代碼創(chuàng)建了 ?JsonSchemaRegistry
?(用于模式驗證)、?TestingArchitectHost
?(對 ?ArchitectHost
?的內(nèi)存實現(xiàn))和 ?Architect
?的新實例。package.json
? 文件添加了一個 ?builders.json
? 文件,并修改了 ?package.json
? 文件以指向它。下面是運行此復(fù)制文件構(gòu)建器的測試范例。該測試使用該構(gòu)建器來復(fù)制 ?package.json
? 文件,并驗證復(fù)制后的文件內(nèi)容與源文件相同。
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';
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('..');
});
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);
});
});
在你的倉庫中運行這個測試時,需要使用 ts-node 包。你可以把 ?
index.spec.ts
? 重命名為 ?index.spec.js
? 來回避它。
建筑師希望構(gòu)建器運行一次(默認(rèn)情況下)并返回。這種行為與那些需要監(jiān)視文件更改的構(gòu)建器(比如 Webpack)并不完全兼容。建筑師可以支持監(jiān)視模式,但要注意一些問題。
BuilderOutput
?對象。一旦它被執(zhí)行,就會進(jìn)入一個由外部事件觸發(fā)的監(jiān)視模式。如果一個事件導(dǎo)致它重啟,那么此構(gòu)建器應(yīng)該執(zhí)行 ?context.reportRunning()
? 函數(shù)來告訴建筑師再次運行它。如果調(diào)度器還計劃了另一次運行,就會阻止建筑師停掉這個構(gòu)建器。當(dāng)你的構(gòu)建器通過調(diào)用 ?BuilderRun.stop()
? 來退出監(jiān)視模式時,建筑師會從構(gòu)建器的 Observable 中取消訂閱,并調(diào)用構(gòu)建器的退出邏輯進(jìn)行清理。(這種行為也允許停止和清理運行時間過長的構(gòu)建。)
一般來說,如果你的構(gòu)建器正在監(jiān)視一個外部事件,你應(yīng)該把你的運行分成三個階段。
階段 |
詳情 |
---|---|
運行 |
比如 webpack 編譯。這會在 webpack 完成并且你的構(gòu)建器發(fā)出 |
監(jiān)視 |
在兩次運行之間監(jiān)視外部事件流。比如,webpack 會監(jiān)視文件系統(tǒng)是否發(fā)生了任何變化。這會在 webpack 重啟構(gòu)建時結(jié)束,并調(diào)用 |
完成 |
任務(wù)完全完成(比如,webpack 應(yīng)運行多次),或者構(gòu)建器停止運行(使用 |
CLI 構(gòu)建器 API 提供了一種通過構(gòu)建器執(zhí)行自定義邏輯,以改變 Angular CLI 行為的新方式。
angular.json
? 配置文件中指定了選項的默認(rèn)值,它可以被目標(biāo)的備用配置覆蓋,還可以進(jìn)一步被命令行標(biāo)志所覆蓋
更多建議: