像建筑师一样构建 Angular(第 2 部分)
在博客系列“像建筑师一样构建 Angular”的这一部分中,我们将研究如何使用 angular-devkit 优化生产构建,并通过弄清楚如何实现环境来完善我们的自定义构建。
回顾
在《像架构师一样构建 Angular(第一部分)》中,我们学习了如何使用最新的 Architect API。通过使用 Architect API 和 RxJS 编写 Builder,我们能够使用新的生产构建版本来扩展 Angular CLI,该构建版本通过 Closure Compiler 优化了 Angular。
我们最终得到了一个执行 RxJS Observable 的函数,如下所示:
export function executeClosure(
options: ClosureBuilderSchema,
context: BuilderContext
): Observable<BuilderOutput> {
return of(context).pipe(
concatMap( results => ngc(options, context) ),
concatMap( results => compileMain(options, context)),
concatMap( results => closure(options, context) ),
mapTo({ success: true }),
catchError(error => {
context.reportStatus('Error: ' + error);
return [{ success: false }];
}),
);
}
在本节的开始,让我们使用@angular-devkit 中的工具为生产包添加更多优化buildOptimizer
。
创建一个名为optimizeBuild的新方法,该方法返回一个RxJS Observable,并将该方法添加到pipe
in中executeClosure
。
return of(context).pipe(
concatMap( results => ngc(options, context) ),
concatMap( results => compileMain(options, context)),
concatMap( results => optimizeBuild(options, context)),
concatMap( results => closure(options, context) ),
@angular-devkit/build-optimizer
在 build_tools 目录中安装。
npm i @angular-devkit/build-optimizer --save-dev
buildOptimizer
像这样导入。
import { buildOptimizer } from '@angular-devkit/build-optimizer';
本质上,Angular 编译器运行后,out-tsc 中的每个 component.js 文件都需要使用 buildOptimizer 进行后处理。此工具会移除不必要的装饰器,避免文件包变得臃肿。
该脚本的算法如下:
- 列出 out-tsc 目录中所有扩展名为 .component.js 的文件
- 读取文件名数组中的每个文件
- 调用 buildOptimizer,传入每个文件的内容
- 使用 buildOptimizer 的输出将文件写入磁盘
让我们使用一个名为 glob 的方便的 npm 包来列出具有给定扩展名的所有文件。
在 build_tools 目录中安装 glob。
npm i glob --save-dev
将 glob 导入 src/closure/index.ts。
import { glob } from 'glob';
在optimizeBuild
方法中,声明一个新的const
并调用它files
。
const files = glob.sync(normalize('out-tsc/**/*.component.js'));
glob.sync
将会同步将所有匹配 glob 的文件格式化为一个字符串数组。在上面的例子中,files
等于一个字符串数组,其中包含所有扩展名为 的文件的路径.component.js
。

现在我们有了一个需要使用 进行后处理的文件名数组buildOptimizer
。我们的函数optimizeBuild
需要返回一个 Observable,但我们有一个文件名数组。
本质上来说optimizeBuild
,所有文件处理完毕后才应该发出数据,所以我们需要将文件映射到一个 Observable 数组,并使用一个 RxJS 方法forkJoin
等待所有 Observable 处理完毕。构建过程中的下一步是将应用程序与 Closure Compiler 捆绑在一起。该任务必须等待optimizeBuild
完成。
const optimizedFiles = files.map((file) => {
return new Observable((observer) => {
readFile(file, 'utf-8', (err, data) => {
if (err) {
observer.error(err);
}
writeFile(file, buildOptimizer({ content: data }).content, (error) => {
if (error) {
observer.error(error);
}
observer.next(file);
observer.complete();
});
});
});
});
return forkJoin(optimizedFiles);
每个文件都使用 从磁盘读取readFile
,使用 对文件内容进行后处理buildOptimizer
,并使用 将结果内容写入磁盘writeFile
。观察者调用next
和complete
来通知forkJoin
异步操作已执行。
如果在运行此优化之前查看 out-tsc 目录中的文件,这些文件将包含如下装饰器:
AppComponent.decorators = [
{ type: Component, args: [{
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
},] },
];
buildOptimizer
现在,装饰器已通过with you run删除architect build_repo:closure_build
。
让我们继续合并环境,以便我们可以从默认的 Angular CLI 构建中复制此功能。
处理环境
处理环境配置比前面的练习要简单得多。首先我们来看一下问题。
在 src/environments 中默认有两个文件。
- 环境.ts
- 环境.产品.ts
environment.prod.ts 默认看起来像这样。
export const environment = {
production: true
};
src/main.ts 在新搭建的项目中引用此配置。
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
请注意,环境对象始终从 ./environments/environment 导入,但每个环境都有不同的文件?

解决方案非常简单。
在 AOT 编译器运行并将 JavaScript 输出到 out-tsc 目录之后但在应用程序捆绑之前,我们必须交换文件。
cp out-tsc/src/environment/environment.prod.js out-tsc/src/environment/environment.js
上面的代码片段使用 cp Unix 命令将生产环境文件复制到默认的 environment.js。
将 environment.js 文件替换为当前环境后,应用程序将被捆绑,并且environment
应用程序中所有引用都与正确的环境相对应。
创建一个名为的新函数handleEnvironment
,并将选项作为参数传入。该函数与之前的函数类似,返回一个 Observable。
export function handleEnvironment(
options:ClosureBuilderSchema,
context: BuilderContext
): Observable<{}> {
}
如果我们env
在 schema.json 中将其定义为一个选项。
"env": {
"type": "string",
"description": "Environment to build for (defaults to prod)."
}
我们可以使用相同的参数通过 Architect CLI 运行此构建。
architect build_repo:closure_build --env=prod
在我们刚刚创建的方法中,我们可以引用对象env
上的参数options
。
const env = options.env ? options.env : 'prod';
为了复制正确的环境,我们可以使用节点中提供的名为的工具exec
。
import { exec } from 'child_process';
exec
允许您像在终端中一样运行 bash 命令。
exec
Node.js 中自带的类似函数都是基于 Promise 的。幸运的是,RxJS 的 Observable 可以与 Promise 互操作。我们可以使用of
Node.js 中自带的方法RxJS
将其转换exec
为 Observable。最终代码如下。
export function handleEnvironment(
options:ClosureBuilderSchema,
context: BuilderContext
): Observable<{}> {
const env = options.env ? options.env : 'prod';
return of(exec('cp '+
normalize('out-tsc/src/environments/environment.' + env + '.js') + ' ' +
normalize('out-tsc/src/environments/environment.js')
));
}
executeClosure
通过再次调用 ,将新方法添加到concatMap
。此时,感觉就像针和线一样。
return of(context).pipe(
concatMap( results => ngc(options, context) ),
concatMap( results => compileMain(options, context)),
concatMap( results => optimizeBuild(options, context)),
concatMap( results => handleEnvironment(options, context)),
concatMap( results => closure(options, context) ),
花点时间回顾一下你已成为的构建大师。所有步骤现已到位,可用于正式版构建!
