介绍CLI Builder

介绍CLI Builder

在本文中,我们将介绍新的Angular CLI API,它将允许您扩展现有的CLI功能并添加新功能。 我们将讨论如何使用此API,以及它的扩展要点是什么,这些点允许向CLI添加新功能。

故事


大约一年前,我们在Angular CLI中引入了工作空间文件( angular.json ),并重新考虑了实现其命令的许多基本原则。 我们发现我们将团队放在“盒子”中:

  1. 示意图命令-“示意图命令 到目前为止,您可能已经听说过Schematics,CLI使用该库来生成和修改代码。 它出现在版本5中,并且当前在大多数与代码有关的命令中使用,例如newgenerateaddupdate
  2. 杂项命令-“其他团队” 。 这些是与您的项目没有直接关系的命令: helpversionconfigdoc 。 最近, 分析方法以及复活节彩蛋也出现了(嘘!对任何人来说都不是一句话!)。
  3. 任务命令-“任务命令 总的来说,该类别是“发布在其他人的代码上执行的进程”。 -例如, 构建是项目构建, 棉绒是调试, 测试是测试。

我们很久以前就开始设计angular.json 。 最初,它被认为是Webpack配置的替代品。 另外,它应该允许开发人员独立选择项目程序集的实现。 结果,我们得到了一个基本的任务启动系统,该系统对于我们的实验仍然简单而方便。 我们将此API称为“建筑师”。

尽管没有正式支持Architect,但它在想要自定义项目程序集的开发人员以及需要控制其工作流的第三方库中很受欢迎。 Nx使用它执行Bazel命令,Ionic使用它在Jest上运行单元测试,用户可以使用ngx-build-plus之类的工具扩展Webpack配置。 那仅仅是开始。

Angular CLI版本8中使用了该API的官方支持,稳定和改进版本。

概念图


Architect API提供了用于安排和协调Angular CLI用于实现其命令的任务的工具。 它使用称为
“建造者”-可以充当其他收藏家的任务或计划者的“收藏家”。 另外,它使用angular.json作为收集器本身的一组指令。

这是一个非常通用的系统,旨在灵活且可扩展。 它包含用于报告,记录和测试的API。 如有必要,可以将系统扩展为新任务。

选择器


汇编器是实现可以代替CLI命令的任务的逻辑和行为的函数。 -例如,启动短绒棉。

收集器函数有两个参数:输入值(或选项)和提供CLI和收集器本身之间关系的上下文。 这里的责任划分与Schematics中的相同-CLI用户设置选项,API负责上下文,您(开发人员)设置必要的行为。 该行为可以同步,异步或简单地显示一定数量的值来实现。 输出必须为BuilderOutput类型,其中包含逻辑字段成功和可选字段error (其中包含错误消息)。

工作区文件和任务


Architect API依赖angular.json (一个用于存储任务及其设置的工作空间文件)。

angular.json将工作空间划分为多个项目,然后又将它们划分为多个任务。 例如,使用ng new命令创建的应用程序就是这样一个项目。 该项目中的任务之一是构建任务,可以使用ng build命令启动 。 默认情况下,此任务具有三个键:

  1. builder-用于完成任务的收集的名称,格式为PACKAGE_NAME:ASSEMBLY_NAME
  2. options-默认情况下启动任务时使用的设置。
  3. 配置 -使用指定配置启动任务时将应用的设置。

设置的应用如下:任务开始时,设置从选项块中获取,然后,如果已指定配置,则其设置将写入现有设置的顶部。 此后,如果将其他设置传递给scheduleTarget() - 覆盖块,则将最后写入它们。 使用Angular CLI时,命令行参数将传递给overrides 。 将所有设置转移到收集器之后,他将根据自己的方案检查它们,并且仅当设置与之相对应时,才会创建上下文,并且收集器将开始工作。

有关工作区的更多信息,请参见此处

创建自己的收藏家


作为示例,让我们创建一个收集器,该收集器将在命令行上运行命令。 要创建收集器,请使用createBuilder工厂并返回BuilderOutput对象:

import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; export default createBuilder((options, context) => { return new Promise<BuilderOutput>(resolve => { resolve({ success: true }); }); }); 

现在,让我们向收集器添加一些逻辑:我们要通过设置来控制收集器,创建新流程,等待流程完成,如果流程成功完成(即返回代码0),则将其发送给Architect:

 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方法将所有数据传递到流程的标准输出。 我们可能希望将它们转移到记录器 -记录器。 在这种情况下,首先,将促进测试过程中的调试,其次,Architect本身可以在单独的进程中运行我们的收集器,或者禁用进程的标准输出(例如,在Electron应用程序中)。

为此,我们可以使用上下文对象中可用的Logger ,它将允许我们重定向流程的输出:

 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的最后一部分是进度和当前状态报告。

在我们的例子中,命令已完成或已执行,因此添加进度报告没有任何意义。 但是,我们可以将我们的状态传达给父收集者,以便他了解正在发生的事情。

 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 ,然后将其数量转换为最新的 ,可以传输已处理文件的数量。 tslint收集器就是这样报告其进度的。

输入验证


使用JSON模式检查传递给收集器的options对象。 如果您知道原理图,这类似于原理图。

在我们的收集器示例中,我们希望参数将是一个接收两个键的对象: command-命令(字符串)和args-参数(字符串数组)。 我们的验证方案如下所示:

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

方案是真正强大的工具,可以执行大量检查。 有关JSON方案的更多信息,您可以参考JSON Schema官方网站

创建一个构建包


为了使它与Angular CLI兼容,我们需要为自己的收集器创建一个密钥文件-builders.json ,该文件负责收集器的实现,其名称和验证方案之间的关系。 文件本身如下所示:

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

然后,在package.json文件中,添加builders键,指向builders.json文件:

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

这将告诉Architect在哪里寻找收集器定义文件。

因此,我们的收集器的名称为“ @ example / command-runner:command” 。 名称的第一部分,冒号(:)之前是使用package.json定义的包名称。 第二部分是使用builders.json文件定义的收集器的名称。

测试您自己的构建器


推荐的测试组装程序的方法是通过集成测试。 这是因为创建上下文并不容易,因此您应该使用Architect的调度程序。

为了简化模式,我们想到了一种创建Architect实例的简单方法:首先创建一个JsonSchemaRegistry (以测试架构),然后创建TestingArchitectHost ,最后是一个Architect实例。 现在,您可以编译builders.json配置文件

这是运行收集器的示例,该收集器执行ls命令并验证命令是否成功完成。 请注意,我们将在logger中使用流程的标准输出。

 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); //  TestingArchitectHost –    . //     ,   . architectHost = new TestingArchitectHost(__dirname, __dirname); architect = new Architect(architectHost, registry); //      NPM-, //    package.json  . await architectHost.addBuilderFromPackage('..'); }); //      Windows it('can run ls', async () => { //  ,     . const logger = new logging.Logger(''); const logs = []; logger.subscribe(ev => logs.push(ev.message)); // "run"    ,       . const run = await architect.scheduleBuilder('@example/command-runner:command', { command: 'ls', args: [__dirname], }, { logger }); // "result" –    . //    "BuilderOutput". const output = await run.result; //  . Architect     //   ,    ,    . await run.stop(); //   . expect(output.success).toBe(true); // ,     . // `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文件可能看起来像这样(为简洁起见,某些内容已删除):

 { // ...   . "projects": { // ... "builder-test": { // ... "architect": { // ... "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": { // ...   "optimization": true, "aot": true, "buildOptimizer": true } } } } 

如果我们决定使用收集器添加新任务以将touch命令应用于(例如)文件(更新文件修改日期),我们将运行npm install example / command-runner ,然后对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具有运行命令,这是运行收集器的主要命令。 作为第一个参数,它采用格式为PROJECT:TASK [:CONFIGURATION]的字符串。 要运行我们的任务,我们可以使用ng run builder-test:touch命令。

现在我们可能要重新定义一些参数。 不幸的是,到目前为止,我们无法从命令行重新定义数组,但是我们可以更改命令本身进行演示: ng run builder-test:touch --command = ls 。 -这将输出src / main.ts文件

观看模式


默认情况下,假定收集器将被调用一次并终止,但是,它们可以返回Observable来实现自己的观察模式(就像Webpack收集器一样)。 如果使用相同的参数调用收集器(尽管不能保证),那么Architect将订阅Observable直到完成或停止为止,并且可以再次订阅收集器。

  1. 每次执行后,收集器应返回一个BuilderOutput对象。 完成后,它可以进入由外部事件引起的观察模式,如果再次启动,则必须调用context.reportRunning()函数来通知Architect收集器正在重新工作。 这样可以保护收集器,防止建筑师在新的呼叫中阻止收集器。
  2. 当收集器停止时(例如,使用run.stop()),使用拆解逻辑 (销毁算法),架构师自己从Observable退订。 如果此过程已在运行,这将允许您停止并清除程序集。

综上所述,如果您的收集者观看外部事件,则它可以分为三个阶段:

  1. 履行。 例如,Webpack的编译。 当Webpack完成构建并且您的收集器将BuilderOutput发送到Observable时,此步骤结束。
  2. 观察。 -在两次发射之间,将监视外部事件。 例如,Webpack监视文件系统是否有任何更改。 当Webpack恢复构建并调用context.reportRunning()时,此步骤结束。 此步骤之后,步骤1重新开始。
  3. 完成。 -任务已完全完成(例如,预期Webpack将启动一定次数)或停止了收集器的启动(使用run.stop() )。 在这种情况下,将执行Observable销毁算法并将其清除。

结论


以下是我们在该出版物中学到的摘要:

  1. 我们提供了一个新的API,使开发人员可以更改Angular CLI命令的行为,并使用实现必要逻辑的汇编程序添加新的API。
  2. 收集器可以是同步的,异步的,并且可以响应外部事件。 它们可以被其他收藏家多次调用。
  3. 首先从angular.json文件中读取收集器在任务启动时收到的参数,然后将其用配置中的参数(如果有)覆盖,然后如果添加了命令行标志,则将其覆盖。
  4. 推荐的测试收集器的方法是通过集成测试,但是您可以与收集器逻辑分开执行单元测试。
  5. 如果收集器返回一个Observable,则应在通过销毁算法后将其清除。

在不久的将来,使用这些API的频率将会增加。 例如,Bazel实现与它们紧密相关。

我们已经看到社区如何创建新的CLI收集器以供使用,例如jestcypress用于测试。

Source: https://habr.com/ru/post/zh-CN450746/


All Articles