填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫Home

Angular CLI 建構器(Builder)

Angular CLI builders

很多 Angular CLI 命令都要在你的程式碼上執行一些複雜的處理,比如風格檢查(lint)建構或測試。這些命令會透過一個叫做建築師(Architect)的內部工具來執行 CLI 建構器,而這些建構器會運用一些第三方工具來完成目標任務。

A number of Angular CLI commands run a complex process on your code, such as linting, building, or testing. The commands use an internal tool called Architect to run CLI builders, which apply another tool to accomplish the desired task.

在 Angular 的版本 8 中,CLI 建構器的 API 是穩定的,想要透過新增或修改命令來自訂 Angular CLI 的開發人員可以使用它。例如,你可以提供一個建構器來執行全新的任務,或者更改一個現有命令所使用的第三方工具。

With Angular version 8, the CLI Builder API is stable and available to developers who want to customize the Angular CLI by adding or modifying commands. For example, you could supply a builder to perform an entirely new task, or to change which third-party tool is used by an existing command.

本文件介紹了 CLI 建構器是如何與工作區配置檔案整合的,還展示了如何建立你自己的建構器。

This document explains how CLI builders integrate with the workspace configuration file, and shows how you can create your own builder.

你可以在這個 GitHub 儲存庫中的例子中找到程式碼。

You can find the code from the examples used here in this GitHub repository.

CLI 建構器

CLI builders

內部建築師工具會把工作委託給名叫建構器的處理器函式。處理器函式接收兩個引數:一組 options 輸入(JSON 物件)和一個 contextBuilderContext 物件)。

The internal Architect tool delegates work to handler functions called builders. A builder handler function receives two arguments; a set of input options (a JSON object), and a context (a BuilderContext object).

這裡對關注點的分離和原理圖中是一樣的,它也適用於其它要接觸(touch)程式碼的 CLI 命令(例如 ng generate)。

The separation of concerns here is the same as with schematics, which are used for other CLI commands that touch your code (such as ng generate).

  • 選項由 CLI 使用者提供,上下文由 CLI 建構器提供,並提供對 CLI 建構器 API 的訪問,而開發人員提供了處理函式的行為。

    Options are given by the CLI user, context is provided by and provides access to the CLI Builder API, and the developer provides the behavior.

  • BuilderContext 物件提供了訪問排程方法 BuilderContext.scheduleTarget() 的途徑。排程器會用指定的目標配置來執行建構器處理函式。

    The BuilderContext object provides access to the scheduling method, BuilderContext.scheduleTarget(). The scheduler executes the builder handler function with a given target configuration.

這個建構器處理函式可以是同步的(返回一個值)或非同步的(返回一個 Promise),也可以監視並返回多個值(返回一個 Observable)。最終返回的值全都是 BuilderOutput 型別的。該物件包含一個邏輯欄位 success 和一個可以包含錯誤資訊的可選欄位 error

The builder handler function can be synchronous (return a value) or asynchronous (return a Promise), or it can watch and return multiple values (return an Observable). The return value or values must always be of type BuilderOutput. This object contains a Boolean success field and an optional error field that can contain an error message.

Angular 提供了一些建構器,供 CLI 命令使用,如 ng buildng testng lint 等。這些內建 CLI 建構器的預設目標配置可以在工作空間配置檔案 angular.jsonarchitect 部分找到(並進行自訂)。可以透過建立自己的建構器來擴充套件和自訂 Angular,你可以使用 ng run CLI 命令來執行你自己的建構器。

Angular provides some builders that are used by the CLI for commands such as ng build, ng test, and ng lint. Default target configurations for these and other built-in CLI builders can be found (and customized) in the "architect" section of the workspace configuration file, angular.json. You can also extend and customize Angular by creating your own builders, which you can run using the ng run CLI command.

建構器的專案結構

Builder project structure

建構器位於一個 project 資料夾中,該資料夾的結構類似於 Angular 工作區,包括位於最上層的全域性配置檔案,以及位於工作程式碼所在原始檔夾中的更具體的配置。例如,myBuilder 資料夾中可能包含如下檔案。

A builder resides in a "project" folder that is similar in structure to an Angular workspace, with global configuration files at the top level, and more specific configuration in a source folder with the code files that define the behavior. For example, your myBuilder folder could contain the following files.

檔案

FILES

目的

PURPOSE

src/my-builder.ts

這個建構器定義的主要原始碼。

Main source file for the builder definition.

src/my-builder.spec.ts

測試的原始碼。

Source file for tests.

src/schema.json

建構器輸入選項的定義。

Definition of builder input options.

builders.json

測試配置。

Builders definition.

package.json

相依套件。參閱https://docs.npmjs.com/files/package.json

Dependencies. See https://docs.npmjs.com/files/package.json.

tsconfig.json

TypeScript 配置檔案

TypeScript configuration.

你可以把建構器發佈到 npm(請參閱發佈你的函式庫)。如果把它發佈成了 @example/my-builder,就可以使用下面的命令來安裝它。

You can publish the builder to npm (see Publishing your Library). If you publish it as @example/my-builder, you can install it using the following command.

npm install @example/my-builder
      
      npm install @example/my-builder
    

建立建構器

Creating a builder

舉個例子,讓我們建立一個用來執行 shell 命令的建構器。要建立建構器,請使用 CLI 建構器函式 createBuilder(),並返回一個 Promise<BuilderOutput> 物件。

As an example, let's create a builder that executes a shell command. To create a builder, use the createBuilder() CLI Builder function, and return a Promise<BuilderOutput> object.

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; import { JsonObject } from '@angular-devkit/core'; interface Options extends JsonObject { command: string; args: string[]; } export default createBuilder(commandBuilder); function commandBuilder( options: Options, context: BuilderContext, ): Promise<BuilderOutput> { }
src/my-builder.ts (builder skeleton)
      
      import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import { JsonObject } from '@angular-devkit/core';

interface Options extends JsonObject {
  command: string;
  args: string[];
}

export default createBuilder(commandBuilder);

function commandBuilder(
  options: Options,
  context: BuilderContext,
  ): Promise<BuilderOutput> {
}
    

現在,讓我們為它新增一些邏輯。下列程式碼會從使用者選項中檢索命令和引數、產生新程序,並等待該程序完成。如果程序成功(返回程式碼為 0),就會解析成返回的值。

Now let’s add some logic to it. The following code retrieves the command and arguments from the user options, spawns the new process, and waits for the process to finish. If the process is successful (returns a code of 0), it resolves the return value.

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; import { JsonObject } from '@angular-devkit/core'; import * as childProcess from 'child_process'; interface Options extends JsonObject { command: string; args: string[]; } export default createBuilder(commandBuilder); function commandBuilder( options: Options, context: BuilderContext, ): Promise<BuilderOutput> { const child = childProcess.spawn(options.command, options.args); return new Promise(resolve => { child.on('close', code => { resolve({ success: code === 0 }); }); }); }
src/my-builder.ts (builder)
      
      import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import { JsonObject } from '@angular-devkit/core';
import * as childProcess from 'child_process';

interface Options extends JsonObject {
  command: string;
  args: string[];
}

export default createBuilder(commandBuilder);

function commandBuilder(
  options: Options,
  context: BuilderContext,
  ): Promise<BuilderOutput> {
    const child = childProcess.spawn(options.command, options.args);
    return new Promise(resolve => {
      child.on('close', code => {
        resolve({ success: code === 0 });
      });
    });
}
    

處理輸出

Handling output

預設情況下,spawn() 方法會把所有內容輸出到程序標準輸出(stdout)和標準錯誤(stderr)中。為了便於測試和除錯,我們可以把輸出轉發給 CLI 建構器的 Logger。這樣還能讓建構器本身可以在一個單獨的程序中執行,即使其標準輸出和標準錯誤被停用了也無所謂(就像在 Electron 應用中一樣)。

By default, the spawn() method outputs everything to the process standard output and error. To make it easier to test and debug, we can forward the output to the CLI Builder logger instead. This also allows the builder itself to be executed in a separate process, even if the standard output and error are deactivated (as in an Electron app).

我們可以從上下文中檢索一個 Logger 實例。

We can retrieve a Logger instance from the context.

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; import { JsonObject } from '@angular-devkit/core'; import * as childProcess from 'child_process'; interface Options extends JsonObject { command: string; args: string[]; } export default createBuilder(commandBuilder); function commandBuilder( options: Options, context: BuilderContext, ): Promise<BuilderOutput> { const child = childProcess.spawn(options.command, options.args); child.stdout.on('data', data => { context.logger.info(data.toString()); }); child.stderr.on('data', data => { context.logger.error(data.toString()); }); return new Promise(resolve => { child.on('close', code => { resolve({ success: code === 0 }); }); }); }
src/my-builder.ts (handling output)
      
      import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import { JsonObject } from '@angular-devkit/core';
import * as childProcess from 'child_process';

interface Options extends JsonObject {
  command: string;
  args: string[];
}

export default createBuilder(commandBuilder);

function commandBuilder(
  options: Options,
  context: BuilderContext,
  ): Promise<BuilderOutput> {
    const child = childProcess.spawn(options.command, options.args);

    child.stdout.on('data', data => {
      context.logger.info(data.toString());
    });
    child.stderr.on('data', data => {
      context.logger.error(data.toString());
    });

    return new Promise(resolve => {
      child.on('close', code => {
        resolve({ success: code === 0 });
      });
    });
}
    

進度和狀態報告

Progress and status reporting

CLI 建構器 API 包含一些進度報告和狀態報告工具,可以為某些函式和介面提供提示資訊。

The CLI Builder API includes progress and status reporting tools, which can provide hints for certain functions and interfaces.

要報告進度,請使用 BuilderContext.reportProgress() 方法,它接受一個當前值(value)、一個(可選的)總值(total)和狀態(status)字串作為引數。總值可以是任意數字,例如,如果你知道有多少個檔案需要處理,那麼總值可能是這些檔案的數量,而當前值是已處理過的數量。除非傳入了新的字串,否則這個狀態字串不會改變。

To report progress, use the BuilderContext.reportProgress() method, which takes a current value, (optional) total, and status string as arguments. The total can be any number; for example, if you know how many files you have to process, the total could be the number of files, and current should be the number processed so far. The status string is unmodified unless you pass in a new string value.

你可以看看 tslint 建構器如何報告進度的例子

You can see an example of how the tslint builder reports progress.

在我們的例子中,shell 命令或者已完成或者正在執行,所以不需要進度報告,但是可以報告狀態,以便呼叫此建構器的父建構器知道發生了什麼。可以用 BuilderContext.reportStatus() 方法產生一個任意長度的狀態字串。(注意,無法保證長字串會完全顯示出來,可以裁剪它以適應介面顯示。)傳入一個空字串可以移除狀態。

In our example, the shell command either finishes or is still executing, so there’s no need for a progress report, but we can report status so that a parent builder that called our builder would know what’s going on. Use the BuilderContext.reportStatus() method to generate a status string of any length. (Note that there’s no guarantee that a long string will be shown entirely; it could be cut to fit the UI that displays it.) Pass an empty string to remove the status.

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; import { JsonObject } from '@angular-devkit/core'; import * as childProcess from 'child_process'; interface Options extends JsonObject { command: string; args: string[]; } export default createBuilder(commandBuilder); function commandBuilder( options: Options, context: BuilderContext, ): Promise<BuilderOutput> { context.reportStatus(`Executing "${options.command}"...`); const child = childProcess.spawn(options.command, options.args); child.stdout.on('data', data => { context.logger.info(data.toString()); }); child.stderr.on('data', data => { context.logger.error(data.toString()); }); return new Promise(resolve => { context.reportStatus(`Done.`); child.on('close', code => { resolve({ success: code === 0 }); }); }); }
src/my-builder.ts (progess reporting)
      
      import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import { JsonObject } from '@angular-devkit/core';
import * as childProcess from 'child_process';

interface Options extends JsonObject {
  command: string;
  args: string[];
}

export default createBuilder(commandBuilder);

function commandBuilder(
  options: Options,
  context: BuilderContext,
  ): Promise<BuilderOutput> {
    context.reportStatus(`Executing "${options.command}"...`);
    const child = childProcess.spawn(options.command, options.args);

    child.stdout.on('data', data => {
      context.logger.info(data.toString());
    });
    child.stderr.on('data', data => {
      context.logger.error(data.toString());
    });

    return new Promise(resolve => {
      context.reportStatus(`Done.`);
      child.on('close', code => {
        resolve({ success: code === 0 });
      });
    });
}
    

建構器的輸入

Builder input

你可以透過 CLI 命令間接呼叫一個建構器,也可以直接用 Angular CLI 的 ng run 命令來呼叫它。無論哪種情況,你都必須提供所需的輸入,但是可以用特定目標中預配置的值作為其預設值,然後指定一個預定義的、指定的配置進行覆蓋,最後在命令列中進一步覆蓋這些選項的值。

You can invoke a builder indirectly through a CLI command, or directly with the Angular CLI ng run command. In either case, you must provide required inputs, but can allow other inputs to default to values that are pre-configured for a specific target, provide a pre-defined, named override configuration, and provide further override option values on the command line.

對輸入的驗證

Input validation

你可以在該建構器的相關 JSON 模式中定義建構器都有哪些輸入。建築師工具會把解析後的輸入值收集到一個 options 物件中,並在將其傳給建構器函式之前先根據這個模式驗證它們的型別。(Schematics 函式庫也對使用者輸入做了同樣的驗證)。

You define builder inputs in a JSON schema associated with that builder. The Architect tool collects the resolved input values into an options object, and validates their types against the schema before passing them to the builder function. (The Schematics library does the same kind of validation of user input).

對於這個範例建構器,我們希望 options 值是帶有兩個鍵的 JsonObject:一個是字串型的 command,一個是字串陣列型的 args

For our example builder, we expect the options value to be a JsonObject with two keys: a command that is a string, and an args array of string values.

我們可以提供如下模式來對這些值的型別進行驗證。

We can provide the following schema for type validation of these values.

{ "$schema": "http://json-schema.org/schema", "type": "object", "properties": { "command": { "type": "string" }, "args": { "type": "array", "items": { "type": "string" } } } }
command/schema.json
      
      {
  "$schema": "http://json-schema.org/schema",
  "type": "object",
  "properties": {
    "command": {
      "type": "string"
    },
    "args": {
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  }
}
    

這是一個非常簡單的例子,但這種模式驗證也可以非常強大。要了解更多資訊,請參閱 JSON 模式網站

This is a very simple example, but the use of a schema for validation can be very powerful. For more information, see the JSON schemas website.

要把建構器的實現與它的模式和名稱關聯起來,我們需要建立一個建構器定義檔案,可以在 package.json 中指向該檔案。

To link our builder implementation with its schema and name, we need to create a builder definition file, which we can point to in package.json.

建立一個名為 builders.json 檔案,它看起來像這樣。

Create a file named builders.json file that looks like this.

{ "builders": { "command": { "implementation": "./command", "schema": "./command/schema.json", "description": "Runs any command line in the operating system." } } }
builders.json
      
      {
  "builders": {
    "command": {
      "implementation": "./command",
      "schema": "./command/schema.json",
      "description": "Runs any command line in the operating system."
    }
  }
}
    

package.json 檔案中,新增一個 builders 鍵,告訴建築師工具可以在哪裡找到這個建構器定義檔案。

In the package.json file, add a builders key that tells the Architect tool where to find our builder definition file.

{ "name": "@example/command-runner", "version": "1.0.0", "description": "Builder for Command Runner", "builders": "builders.json", "devDependencies": { "@angular-devkit/architect": "^1.0.0" } }
package.json
      
      {
  "name": "@example/command-runner",
  "version": "1.0.0",
  "description": "Builder for Command Runner",
  "builders": "builders.json",
  "devDependencies": {
    "@angular-devkit/architect": "^1.0.0"
  }
}
    

現在,這個建構器的正式名字是 @example/command-runner:command。第一部分是套件名稱(使用 node 方案進行解析),第二部分是建構器名稱(使用 builders.json 檔案進行解析)。

The official name of our builder is now @example/command-runner:command. The first part of this is the package name (resolved using node resolution), and the second part is the builder name (resolved using the builders.json file).

使用某個 options 是非常簡單的。在上一節,我們就曾訪問過 options.command

Using one of our options is very straightforward, we did this in the previous section when we accessed options.command.

context.reportStatus(`Executing "${options.command}"...`); const child = childProcess.spawn(options.command, options.args);
src/my-builder.ts (report status)
      
      context.reportStatus(`Executing "${options.command}"...`);
const child = childProcess.spawn(options.command, options.args);
    

目標配置

Target configuration

建構器必須有一個已定義的目標,此目標會把建構器與特定的輸入配置和專案關聯起來。

A builder must have a defined target that associates it with a specific input configuration and project.

目標是在 CLI 配置檔案 angular.json 中定義的。目標用於指定要使用的建構器、預設的選項配置,以及指定的備用配置。建築師工具使用目標定義來為一次特定的執行解析輸入選項。

Targets are defined in the angular.json CLI configuration file. A target specifies the builder to use, its default options configuration, and named alternative configurations. The Architect tool uses the target definition to resolve input options for a given run.

angular.json 檔案中為每個專案都有一節配置,每個專案的 architect 部分都會為 CLI 命令(例如 buildtestlint)配置建構器目標。預設情況下,build 命令會執行 @angular-devkit/build-angular:browser 建構器來執行 build 任務,並傳入 angular.json 中為 build 目標指定的預設選項值。

The angular.json file has a section for each project, and the "architect" section of each project configures targets for builders used by CLI commands such as 'build', 'test', and 'lint'. By default, for example, the build command runs the builder @angular-devkit/build-angular:browser to perform the build task, and passes in default option values as specified for the build target in angular.json.

{ "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", ... } } }, ...
angular.json
      
      {
  "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",
            ...
          }
        }
      },
      ...
    

該命令會給建構器傳遞 options 節中指定的一組預設選項。如果你傳入了 --configuration=production 標誌,它就會使用 production 備用配置中指定的值進行覆蓋。你可以在命令列中單獨指定其它選項進行覆蓋,還可以為 build 目標新增更多備用配置,以定義其它環境,比如 stageqa

The command passes the builder the set of default options specified in the "options" section. If you pass the --configuration=production flag, it uses the override values specified in the production alternative configuration. You can specify further option overrides individually on the command line. You might also add more alternative configurations to the build target, to define other environments such as stage or qa.

目標字串

Target strings

通用的 ng run CLI 命令的第一個引數是形如 project:target[:configuration] 的目標字串。

The generic ng run CLI command takes as its first argument a target string of the form project:target[:configuration].

  • project:與此目標關聯的 Angular CLI 專案的名稱。

    project: The name of the Angular CLI project that the target is associated with.

  • targetangular.json 檔案 architect 下的指定建構器配置。

    target: A named builder configuration from the architect section of the angular.json file.

  • configuration:(可選)用於覆蓋指定目標的具體配置名稱,如 angular.json 檔案中的定義。

    configuration: (optional) The name of a specific configuration override for the given target, as defined in the angular.json file.

如果你的建構器呼叫另一個建構器,它可能需要讀取一個傳入的目標字串。你可以使用 @angular-devkit/architect 中的工具函式 targetFromTargetString() 把這個字串解析成一個物件。

If your builder calls another builder, it may need to read a passed target string. You can parse this string into an object by using the targetFromTargetString() utility function from @angular-devkit/architect.

排程並執行

Schedule and run

建築師會非同步執行建構器。要呼叫某個建構器,就要在所有配置解析完成之後安排一個要執行的任務。

Architect runs builders asynchronously. To invoke a builder, you schedule a task to be run when all configuration resolution is complete.

在排程器返回 BuilderRun 控制元件物件之前,不會執行該建構器函式。CLI 通常會透過呼叫 BuilderContext.scheduleTarget() 函式來排程任務,然後使用 angular.json 檔案中的目標定義來解析輸入選項。

The builder function is not executed until the scheduler returns a BuilderRun control object. The CLI typically schedules tasks by calling the BuilderContext.scheduleTarget() function, and then resolves input options using the target definition in the angular.json file.

建築師會接受預設的選項物件來解析指定目標的輸入選項,然後覆蓋所用配置中的值(如果有的話),然後再從傳給 BuilderContext.scheduleTarget() 的覆蓋物件中覆蓋這些值。對於 Angular CLI,覆蓋物件是從命令列引數中建構的。

Architect resolves input options for a given target by taking the default options object, then overwriting values from the configuration used (if any), then further overwriting values from the overrides object passed to BuilderContext.scheduleTarget(). For the Angular CLI, the overrides object is built from command line arguments.

建築師會根據建構器的模式對產生的選項值進行驗證。如果輸入有效,建築師會建立上下文並執行該建構器。

Architect validates the resulting options values against the schema of the builder. If inputs are valid, Architect creates the context and executes the builder.

欲知詳情,請參閱工作空間配置

For more information see Workspace Configuration.

你還可以透過呼叫 BuilderContext.scheduleBuilder() 從另一個建構器或測試中呼叫某個建構器。你可以直接把 options 物件傳給該方法,並且這些選項值會根據這個建構器的模式進行驗證,而無需進一步調整。

You can also invoke a builder directly from another builder or test by calling BuilderContext.scheduleBuilder(). You pass an options object directly to the method, and those option values are validated against the schema of the builder without further adjustment.

只有 BuilderContext.scheduleTarget() 方法來解析這些配置和並透過 angular.json 檔案進行覆蓋。

Only the BuilderContext.scheduleTarget() method resolves the configuration and overrides through the angular.json file.

預設建築師配置

Default architect configuration

讓我們建立一個簡單的 angular.json 檔案,它會把目標配置放到上下文中。

Let’s create a simple angular.json file that puts target configurations into context.

我們可以把這個建構器發佈到 npm(請參閱發佈你的函式庫),並使用如下命令來安裝它:

We can publish the builder to npm (see Publishing your Library), and install it using the following command:

npm install @example/command-runner
      
      npm install @example/command-runner
    

如果我們使用 ng new builder-test 建立一個新專案,那麼產生的 angular.json 檔案就是這樣的,它只有預設的建構器引數。

If we create a new project with ng new builder-test, the generated angular.json file looks something like this, with only default builder configurations.

{ // ... "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 } } } } } } // ... }
angular.json
      
      {
  // ...
  "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
            }
          }
        }
      }
    }
  }
  // ...
}
    

新增一個目標

Adding a target

讓我們新增一個新的目標來執行我們的建構器執行一個特定的命令。這個目標會告訴建構器在檔案上執行 touch,以便更新修改過的日期。

Let's add a new target that will run our builder to execute a particular command. This target will tell the builder to run touch on a file, in order to update its modified date.

我們需要更新 angular.json 檔案,把這個建構器的目標新增到新專案的 architect 部分。

We need to update the angular.json file to add a target for this builder to the "architect" section of our new project.

  • 我們會為專案的 architect 物件新增一個新的目標小節。

    We'll add a new target section to the "architect" object for our project.

  • 名為 touch 的目標使用了我們的建構器,它發佈到了 @example/command-runner。(參閱發佈你的函式庫

    The target named "touch" uses our builder, which we published to @example/command-runner. (See Publishing your Library)

  • 這個配置物件為我們定義的兩個輸入提供了預設值:command(要執行的 Unix 命令)和 args (包含要操作的檔案的陣列)。

    The options object provides default values for the two inputs that we defined; command, which is the Unix command to execute, and args, an array that contains the file to operate on.

  • 這些配置鍵都是可選的,但我們先不展開。

    The configurations key is optional, we'll leave it out for now.

{ "projects": { "builder-test": { "architect": { "touch": { "builder": "@example/command-runner:command", "options": { "command": "touch", "args": [ "src/main.ts" ] } }, "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 } } } } } } }
angular.json
      
      {
  "projects": {
    "builder-test": {
      "architect": {
        "touch": {
          "builder": "@example/command-runner:command",
          "options": {
            "command": "touch",
            "args": [
              "src/main.ts"
            ]
          }
        },
        "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
            }
          }
        }
      }
    }
  }
}
    

執行這個建構器

Running the builder

要想使用這個新目標的預設配置執行我們的建構器,請在 Linux shell 中使用以下 CLI 命令。

To run our builder with the new target's default configuration, use the following CLI command in a Linux shell.

ng run builder-test:touch
      
      ng run builder-test:touch
    

這將在 src/main.ts 檔案上執行 touch 命令。

This will run the touch command on the src/main.ts file.

你可以使用命令列引數來覆蓋已配置的預設值。例如,要改用其它 command 值執行,請使用以下 CLI 命令。

You can use command-line arguments to override the configured defaults. For example, to run with a different command value, use the following CLI command.

ng run builder-test:touch --command=ls
      
      ng run builder-test:touch --command=ls
    

這將呼叫 ls 命令而不是 touch 命令。因為我們沒有覆蓋 args 選項,所以它會列出 src/main.ts 檔案的資訊(提供給該目標的預設值)。

This will call the ls command instead of the touch command. Because we did not override the args option, it will list information about the src/main.ts file (the default value provided for the target).

測試一個建構器

Testing a builder

對建構器進行整合測試,以便你可以使用建築師的排程器來建立一個上下文,就像這個例子中一樣。

Use integration testing for your builder, so that you can use the Architect scheduler to create a context, as in this example.

  • 在建構器的原始碼目錄下,我們建立了一個新的測試檔案 index.spec.ts。該程式碼建立了 JsonSchemaRegistry(用於模式驗證)、TestingArchitectHost(對 ArchitectHost 的記憶體實現)和 Architect 的新實例。

    In the builder source directory, we have created a new test file my-builder.spec.ts. The code creates new instances of JsonSchemaRegistry (for schema validation), TestingArchitectHost (an in-memory implementation of ArchitectHost), and Architect.

  • 我們緊挨著這個建構器的 package.json檔案添加了一個 builders.json 檔案,並修改了 package.json 檔案以指向它。

    We've added a builders.json file next to the builder's package.json file, and modified the package file to point to it.

下面是執行該命令建構器的測試範例。該測試使用該建構器來執行 node --print 'foo' 命令,然後驗證 logger 中是否包含一條 foo 記錄。

Here’s an example of a test that runs the command builder. The test uses the builder to run the node --print 'foo' command, then validates that the logger contains an entry for foo.

import { Architect } from '@angular-devkit/architect'; import { TestingArchitectHost } from '@angular-devkit/architect/testing'; import { logging, schema } from '@angular-devkit/core'; describe('Command Runner 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 run node', async () => { // Create a logger that keeps an array of all messages that were logged. const logger = new logging.Logger(''); const logs = []; logger.subscribe(ev => logs.push(ev.message)); // A "run" can have multiple outputs, and contains progress information. const run = await architect.scheduleBuilder('@example/command-runner:command', { command: 'node', args: ['--print', '\'foo\''], }, { logger }); // We pass the logger for checking later. // 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 foo was logged expect(logs).toContain('foo'); }); });
src/my-builder.spec.ts
      
      import { Architect } from '@angular-devkit/architect';
import { TestingArchitectHost } from '@angular-devkit/architect/testing';
import { logging, schema } from '@angular-devkit/core';

describe('Command Runner 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 run node', async () => {
    // Create a logger that keeps an array of all messages that were logged.
    const logger = new logging.Logger('');
    const logs = [];
    logger.subscribe(ev => logs.push(ev.message));

    // A "run" can have multiple outputs, and contains progress information.
    const run = await architect.scheduleBuilder('@example/command-runner:command', {
      command: 'node',
      args: ['--print', '\'foo\''],
    }, { logger });  // We pass the logger for checking later.

    // 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 foo was logged
    expect(logs).toContain('foo');
  });
});
    

在你的儲存庫中執行這個測試時,需要使用 ts-node套件。你可以把 index.spec.ts 重新命名為 index.spec.js 來回避它。

When running this test in your repo, you need the ts-nodepackage. You can avoid this by renaming my-builder.spec.ts to my-builder.spec.js.

監視(watch)模式

Watch mode

建築師希望建構器執行一次(預設情況下)並返回。這種行為與那些需要監視檔案更改的建構器(例如 Webpack)並不完全相容。建築師可以支援監視模式,但要注意一些問題。

Architect expects builders to run once (by default) and return. This behavior is not entirely compatible with a builder that watches for changes (like Webpack, for example). Architect can support watch mode, but there are some things to look out for.

  • 要在監視模式下使用,建構器處理函式應返回一個 Observable。建築師會訂閱這個 Observable,直到這個 Observable 完成(complete)為止。此外,如果使用相同的引數再次排程這個建構器,建築師還能複用這個 Observable。

    To be used with watch mode, a builder handler function should return an Observable. Architect subscribes to the Observable until it completes and might reuse it if the builder is scheduled again with the same arguments.

  • 這個建構器應該總是在每次執行後發出一個 BuilderOutput 物件。一旦它被執行,就會進入一個由外部事件觸發的監視模式。如果一個事件導致它重啟,那麼此建構器應該執行 BuilderContext.reportRunning() 函式來告訴建築師再次執行它。如果排程器還計劃了另一次執行,就會阻止建築師停掉這個建構器。

    The builder should always emit a BuilderOutput object after each execution. Once it’s been executed, it can enter a watch mode, to be triggered by an external event. If an event triggers it to restart, the builder should execute the BuilderContext.reportRunning() function to tell Architect that it is running again. This prevents Architect from stopping the builder if another run is scheduled.

當你的建構器透過呼叫 BuilderRun.stop() 來退出監視模式時,建築師會從建構器的 Observable 中取消訂閱,並呼叫建構器的退出邏輯進行清理。(這種行為也允許停止和清理執行時間過長的建構。)

When your builder calls BuilderRun.stop() to exit watch mode, Architect unsubscribes from the builder’s Observable and calls the builder’s teardown logic to clean up. (This behavior also allows for long running builds to be stopped and cleaned up.)

一般來說,如果你的建構器正在監視一個外部事件,你應該把你的執行分成三個階段。

In general, if your builder is watching an external event, you should separate your run into three phases.

  1. 執行,例如 webpack 編譯。這會在 webpack 完成並且你的建構器發出 BuilderOutput 物件時結束。

    Running For example, webpack compiles. This ends when webpack finishes and your builder emits a BuilderOutput object.

  2. 監視,在兩次執行之間監視外部事件流。例如,webpack 會監視檔案系統是否發生了任何變化。這會在 webpack 重啟建構時結束,並呼叫 BuilderContext.reportRunning()。這樣就會再回到第 1 步。

    Watching Between two runs, watch an external event stream. For example, webpack watches the file system for any changes. This ends when webpack restarts building, and BuilderContext.reportRunning() is called. This goes back to step 1.

  3. 完成,任務完全完成(例如,webpack 應執行多次),或者建構器停止執行(使用 BuilderRun.stop())。你的退出邏輯被呼叫了,建築師也從你的建構器的 Observable 中取消了訂閱。

    Completion Either the task is fully completed (for example, webpack was supposed to run a number of times), or the builder run was stopped (using BuilderRun.stop()). Your teardown logic is executed, and Architect unsubscribes from your builder’s Observable.

總結

Summary

CLI 建構器 API 提供了一種透過建構器執行自訂邏輯,以改變 Angular CLI 行為的新方式。

The CLI Builder API provides a new way of changing the behavior of the Angular CLI by using builders to execute custom logic.

  • 建構器既可以是同步的,也可以是非同步的,它可以只執行一次也可以監視外部事件,還可以排程其它建構器或目標。

    Builders can be synchronous or asynchronous, execute once or watch for external events, and can schedule other builders or targets.

  • 建構器在 angular.json 配置檔案中指定了選項的預設值,它可以被目標的備用配置覆蓋,還可以進一步被命令列標誌所覆蓋。

    Builders have option defaults specified in the angular.json configuration file, which can be overwritten by an alternate configuration for the target, and further overwritten by command line flags.

  • 我們建議你使用整合測試來測試建築師的建構器。你還可以使用單元測試來驗證這個建構器的執行邏輯。

    We recommend that you use integration tests to test Architect builders. You can use unit tests to validate the logic that the builder executes.

  • 如果你的建構器返回一個 Observable,你應該在那個 Observable 的退出邏輯中進行清理。

    If your builder returns an Observable, it should clean up in the teardown logic of that Observable.