Angular小博客

分享让你更聪明

[译2019]构建器简介

浏览:1次 日期:2024年10月19日 22:48:43 作者:admin

在这篇博文中,我们将介绍 Angular CLI 中的新 API,它允许您添加 CLI 功能并增强现有功能。我们将讨论如何与此 API 交互,以及允许您向 CLI 添加附加功能的扩展点。

您可以在此 GitHub 存储库中找到以下示例中的代码。

历史

大约一年前,我们在 Angular CLI 中引入了工作区文件 ( angular.json ),并重新设计了如何实现其命令的许多核心概念。我们最终将命令放入框中:

我们很久以前就开始设计这最后一个;最初开发它是为了允许人们替换他们的 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时自动运行。该目标(默认情况下)具有三个键:

执行目标时解析选项的方式是采用默认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 。架构师将订阅它,直到它完成或停止,并且如果使用相同的参数再次调度构建器,则可以重用它(但不能保证)。

一般来说,如果您的构建器正在监视外部事件,则会分为 3 个阶段:

结论

以下是我们在这篇文章中学到的内容的摘要:

这些 API 将会有更多的使用。例如,Bazel 实现严重依赖它们来更改buildserve命令。

我们已经看到社区实现了其他构建器,例如允许 CLI 使用jest 、 cypress进行测试。天空确实是无限的,CLI 可以扩展和适应您的项目。

感谢您的阅读!

文章来源地址:https://blog.angular.dev/introducing-cli-builders-d012d4489f1b