[译2019]构建器简介
在这篇博文中,我们将介绍 Angular CLI 中的新 API,它允许您添加 CLI 功能并增强现有功能。我们将讨论如何与此 API 交互,以及允许您向 CLI 添加附加功能的扩展点。
您可以在此 GitHub 存储库中找到以下示例中的代码。
历史
大约一年前,我们在 Angular CLI 中引入了工作区文件 ( angular.json ),并重新设计了如何实现其命令的许多核心概念。我们最终将命令放入框中:
- 原理图命令。到目前为止,您可能已经听说过 Schematics,这是 CLI 用于生成和修改代码的库。它是在版本 5 中引入的,现在用于大多数涉及代码的命令,例如
new、generate、add和update。 - 各种命令。这些是专门编码的命令,并不特定于某个项目;
help、version、config、doc、我们新添加的analytics和我们的复活节彩蛋(嘘!不要告诉任何人!)。 - 任务命令。这个类别本质上是“在人们的代码上运行一个进程”。构建是一个很好的例子,但 linting 和测试也是如此。
我们很久以前就开始设计这最后一个;最初开发它是为了允许人们替换他们的 webpack 配置或切换到不同的底层构建实现。我们最终起草了一个简单的初始任务运行系统,我们可以暂时保持实验性。我们将此 API 命名为“架构师”。
尽管它没有得到官方支持,但 Architect 在想要使用自定义构建的人们或想要自定义工作流程的第三方库的人们中取得了成功。 Nx 使用它来执行 Bazel 命令,Ionic 使用它通过 Jest 运行单元测试,用户可以使用ngx-build-plus等工具扩展其 webpack 配置。而这仅仅是开始。
在 Angular CLI 版本 8 中,该 API 的改进版本现已稳定并得到官方支持。
概念概述
Architect API 具有用于安排和协调任务的工具,CLI 使用这些工具来执行命令。它使用称为构建器的函数作为任务的实现(可以调度其他构建器),并使用工作区的angular.json将项目和目标解析为其构建器实现。
这是一个非常通用的系统,具有可塑性和前瞻性。它包含用于进度报告、日志记录和测试的 API,并且可以扩展新功能。
建设者(Builders)
构建器是实现可以替换命令的任务的逻辑和行为的函数,例如运行 linter。
构建器接收两个参数;输入(或选项)以及提供 CLI 和构建器之间通信的上下文。这里的关注点分离与 Schematics 相同;选项由 CLI 用户给出,上下文由 API 提供,并且您提供行为。它可以是同步的、异步的,也可以监视并输出多个值。输出应始终为BuilderOutput类型,其中包含success布尔字段和可选error字段(可以包含错误消息)。
工作区文件和目标
Architect 依赖angular.json工作区文件来解析目标及其选项。
angular.json将工作空间分为多个项目,每个项目都有多个目标。项目的一个示例是您的默认应用程序,它是在运行ng new时创建的。该项目的目标之一是build ,它在使用ng build时自动运行。该目标(默认情况下)具有三个键:
builder。运行此目标时要使用的构建器的名称,其格式为packageName:builderName。options。运行此目标时使用的默认选项集。configurations。使用特定配置运行此目标时要应用的选项的名称映射。
执行目标时解析选项的方式是采用默认options对象,然后覆盖使用的configuration中的值(如果有),然后覆盖传递给scheduleTarget() overrides对象中的值。对于 Angular CLI, overrides对象是根据命令行参数构建的。然后根据构建器的模式对其进行验证,只有这样,如果有效,才会创建上下文并且构建器本身将执行。
有关工作区的更多信息,请参阅https://angular.io/guide/workspace-config 。
创建自定义生成器
作为示例,让我们创建一个执行 shell 命令的构建器。要创建 Builder,请使用createBuilder工厂,并返回BuilderOutput对象:
// index.ts
import { BuilderOutput, createBuilder } from '@angular-devkit/architect';
export default createBuilder((options, context) => {
return new Promise<BuilderOutput>(resolve => {
resolve({ success: true });
});
});
现在让我们添加一些逻辑;我们想要使用用户选项来获取命令和参数,生成新进程,等待进程完成,如果进程成功(返回代码 0),我们将向 Architect 表明我们已成功:
// index.ts
import { BuilderOutput, createBuilder } from '@angular-devkit/architect';
import * as childProcess from 'child_process';
export default createBuilder((options, context) => {
const child = childProcess.spawn(options.command, options.args);
return new Promise<BuilderOutput>(resolve => {
child.on('close', code => {
resolve({ success: code === 0 });
});
});
});
处理输出
现在, spawn方法将所有内容输出到进程标准输出和错误。我们可能想将它们转发给记录器。我们这样做有两个原因;第一,它使得测试时更容易调试,第二,架构师本身可以在单独的进程中执行我们的构建器本身,或者停用标准输出和错误(例如在电子应用程序中)。
为此,我们可以使用上下文对象中可用的Logger实例,它允许我们转发进程的输出:
// index.ts
import { BuilderOutput, createBuilder } from '@angular-devkit/architect';
import * as childProcess from 'child_process';
export default createBuilder((options, context) => {
const child = childProcess.spawn(options.command, options.args, { stdio: 'pipe' });
child.stdout.on('data', (data) => {
context.logger.info(data.toString());
});
child.stderr.on('data', (data) => {
context.logger.error(data.toString());
});
return new Promise<BuilderOutput>(resolve => {
child.on('close', code => {
resolve({ success: code === 0 });
});
});
});
进展与现状
实现您自己的构建器时相关的最后一个 API 是进度和状态报告。
在我们的例子中,shell 命令要么完成,要么仍在执行,因此没有必要添加进度。但我们仍然可以报告状态,以便致电我们的父构建者知道发生了什么。
// index.ts
import { BuilderOutput, createBuilder } from '@angular-devkit/architect';
import * as childProcess from 'child_process';
export default createBuilder((options, context) => {
context.reportStatus(`Executing "${options.command}"...`);
const child = childProcess.spawn(options.command, options.args, { stdio: 'pipe' });
child.stdout.on('data', (data) => {
context.logger.info(data.toString());
});
child.stderr.on('data', (data) => {
context.logger.error(data.toString());
});
return new Promise<BuilderOutput>(resolve => {
context.reportStatus(`Done.`);
child.on('close', code => {
resolve({ success: code === 0 });
});
});
});
要报告进度,请使用reportProgress方法,该方法将当前值和(可选)总值作为参数。总数可以是任意数字;例如,如果您知道必须处理多少个文件,则total可能是文件数,而current应该是迄今为止处理的数量。这就是tslint构建器报告进度的方式。
验证输入
构建器接收的options对象也使用 JSON 架构进行验证。如果您使用过 Schematics,这是相同的过程。该文件应该与您的代码一起存在并发布,我们将在下面看到如何链接到它。
在我们的示例构建器中,我们期望选项是一个接收两个键的对象:一个字符串command和一个字符串args数组。因此,我们通过该验证创建模式:
// schema.json
{
"$schema": "http://json-schema.org/schema",
"type": "object",
"properties": {
"command": {
"type": "string"
},
"args": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
模式确实非常强大,可以应用大量的验证。有关 JSON Schemas 的更多信息,您可以参考JSON Schema 官方网站。
创建构建器包
需要为我们的自定义构建器包创建一个最终文件,并使其与 Angular CLI 兼容; builder.json文件,它将我们的构建器实现与其架构和名称链接起来。该文件如下所示:
// builders.json
{
"builders": {
"command": {
"implementation": "./command",
"schema": "./command/schema.json",
"description": "Runs any command line in the operating system."
}
}
}
然后在package.json文件中我们添加一个指向该文件的builders键:
// package.json
{
"name": "@example/command-runner",
"version": "1.0.0",
"description": "Builder for Architect",
"builders": "builders.json",
"devDependencies": {
"@angular-devkit/architect": "^1.0.0"
}
}
这将告诉架构师在哪里可以找到我们的构建器定义文件。
我们建造者的正式名称是 "@example/command-runner:command" 。 :之前的第一部分是包名称(使用节点解析解析),第二部分是构建器名称(使用builder.json解析)。
测试你的构建器
测试构建器本身的推荐方法是通过集成测试。那是因为你不能轻易地创建context ,你需要通过架构师调度器。
为了减少样板文件,我们创建了一种简单的方法来实例化 Architect;首先创建一个JsonSchemaRegistry (用于模式验证),然后创建一个TestingArchitectHost ,最后创建一个Architect实例。然后您可以添加builders.json文件。
以下是运行命令生成器的示例,该命令生成器运行ls ,然后验证它是否成功运行并列出了正确的文件。请记住,我们将命令的 STDOUT 转发给了记录器,因此我们将使用它:
// index_spec.ts
import { Architect, ArchitectHost } 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: ArchitectHost;
beforeEach(async () => {
const registry = new schema.CoreSchemaRegistry();
registry.addPostTransform(schema.transforms.addUndefinedDefaults);
// Arguments to TestingArchitectHost are 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('..');
});
// This might not work in Windows.
it('can run ls', 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 contain multiple outputs, and contains progress information.
const run = await architect.scheduleBuilder('@example/command-runner:command', {
command: 'ls',
args: [__dirname],
}, { logger }); // We pass the logger for checking later.
// The "result" member is the next output of the runner.
// This is of type BuilderOutput.
const output = await run.result;
// Stop the builder from running. This really stops Architect from keeping
// the builder associated states in memory, since builders keep waiting
// to be scheduled.
await run.stop();
// Expect that it succeeded.
expect(output.success).toBe(true);
// Expect that this file was listed. It should be since we're running
// `ls $__dirname`.
expect(logs).toContain('index_spec.ts');
});
});
要运行上面的代码片段,您应该使用ts-node包。如果您更喜欢使用 Node 运行测试,请将'index_spec.ts'重命名为'index_spec.js' 。
将构建器添加到项目中
因此,让我们创建一个简单的angular.json来显示我们迄今为止学到的所有内容。假设我们将构建器发布到@example/command-runner ,并且我们使用ng new builder-test创建了一个新应用程序,我们的angular.json可能如下所示(为简洁起见,删除了大部分部分):
// angular.json
{
// ... removed for brievity.
"projects": {
// ...
"builder-test": {
// ...
"architect": {
// ...
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
// ... more options in the original
"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 in the original
"optimization": true,
"aot": true,
"buildOptimizer": true
}
}
}
}
}
}
// ...
}
如果我们要添加一个新目标,以便使用新的构建器在文件上使用(例如) touch shell 命令(更新其修改日期),我们将 npm install @example/command-runner ,然后更新angular.json文件,如下所示:
// 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
}
}
}
}
}
}
}
Angular CLI 有一个名为run的命令,它是运行 Architect 构建器的通用命令。它采用以下形式的目标字符串作为其第一个参数 project:target[:configuration] 。要运行我们的目标,我们将使用以下命令:
ng run builder-test:touch
现在我们可能想要覆盖一些参数。不幸的是,我们还不能从命令行覆盖数组,但为了演示,我们可以更改命令本身:
ng run builder-test:touch --command=ls
观看模式
构建器预计会运行一次并默认返回,但它们也可以返回Observable来实现自己的监视模式(如webpack构建器)。构建器处理函数应该返回一个Observable 。架构师将订阅它,直到它完成或停止,并且如果使用相同的参数再次调度构建器,则可以重用它(但不能保证)。
- 构建器应始终在每次完成后返回
BuilderOutput对象。一旦完成,它就可以进入由外部事件触发的监视模式,如果它重新启动,它应该执行context.reportRunning()函数来告诉 Architect 构建器正在再次运行。如果有新的调度,这将防止 Architect 停止构建器。 - 此外,当构建器停止时(例如使用
run.stop()),Architect 将取消订阅Observable,并且将调用其拆卸逻辑。这允许您清理并停止当前正在运行的构建。
一般来说,如果您的构建器正在监视外部事件,则会分为 3 个阶段:
- running。例如,webpack 编译。当 webpack 完成并且您的构建器将
BuilderOutput对象发布到Observable时,此过程结束。 - watching。在两次运行之间,观察外部事件。例如,webpack 监视文件系统的任何更改。当 webpack 重新启动构建并调用
context.reportRunning()时,此过程结束。这又回到步骤 1。 - completes.任务要么完全完成(例如,webpack 应该运行多次),要么构建器运行被停止(使用
run.stop())。你的拆卸逻辑被执行,并且Observable被释放。
结论
以下是我们在这篇文章中学到的内容的摘要:
- 我们提供了一个新的 API,让开发人员可以更改 Angular CLI 命令的行为,或添加新命令,使用构建器执行自定义逻辑。
- 构建器可以是同步的、异步的,或者监视外部事件并运行多次,并且可以调度其他构建器或目标。
- 构建器在运行目标时收到的选项首先从
angular.json文件中读取,然后由配置(如果有)覆盖,然后由命令行标志(如果有)覆盖。 - 测试 Architect 构建器的推荐方法是使用集成测试。请记住,您可以单独对构建器执行的逻辑进行单元测试。
- 如果你的构建器返回一个
Observable,它应该在该Observable的拆卸逻辑中进行清理。
这些 API 将会有更多的使用。例如,Bazel 实现严重依赖它们来更改build和serve命令。
我们已经看到社区实现了其他构建器,例如允许 CLI 使用jest 、 cypress进行测试。天空确实是无限的,CLI 可以扩展和适应您的项目。
感谢您的阅读!
文章来源地址:https://blog.angular.dev/introducing-cli-builders-d012d4489f1b