如何让Angular自定义构建器按序执行?确保$templateCache完成后再主构建
Angular 13 自定义构建器执行顺序问题:确保$templateCache构建完成后再启动主构建
我有一个Angular 13项目,包含多个自定义构建器。其中一个构建器用于生成包含$templateCache模块的文件,这个过程需要几秒才能完成。
问题是主Angular构建器会在$templateCache构建器完成前启动,导致生成的模块文件未被纳入构建,进而引发应用加载崩溃,或构建失败并提示模块无法解析。
请问如何确保$templateCache构建器完成后再启动主构建?
我当前的调度构建器代码
import { CustomScheduleBuilderSchema } from './schema'; import { BuilderContext, BuilderOutput, BuilderRun, createBuilder, ScheduleOptions } from '@angular-devkit/architect'; import { Observable, of, from } from 'rxjs'; import { catchError, finalize, first, mergeMap } from 'rxjs/operators'; import { json, JsonObject } from '@angular-devkit/core'; function scheduleNgBuildTarget( options: JsonObject & CustomScheduleBuilderSchema, context: BuilderContext, ): Promise<BuilderRun> { return context.scheduleTarget({ target: options.target, project: context.target?.project ?? '' }); } function scheduleConcatenateLanguagesBuilder( context: BuilderContext ): Promise<BuilderRun> { return context.scheduleTarget({ target: 'concat-lang', project: context.target?.project ?? '' }, { watchModeEnabled: true }); } function scheduleTemplatesBuilder( context: BuilderContext ): Promise<BuilderRun> { return context.scheduleTarget({ target: 'templates', project: context.target?.project ?? '' }, { watchModeEnabled: true }); } function scheduleVersionBuilder( context: BuilderContext ): Promise<BuilderRun> { return context.scheduleTarget({ target: 'version', project: context.target?.project ?? '' }); } export function runCustomScheduleBuilder ( options: JsonObject & CustomScheduleBuilderSchema, context: BuilderContext ): Observable<BuilderOutput> { const scheduleLogger = context.logger.createChild('CustomSchedule'); scheduleLogger.info('Starting build'); return of({ success: true }).pipe( first(), mergeMap(_ => from(scheduleConcatenateLanguagesBuilder(context))), mergeMap(_ => from(scheduleTemplatesBuilder(context))), mergeMap(_ => from(scheduleVersionBuilder(context))), mergeMap(_ => from(scheduleNgBuildTarget(options, context))), // 期望在其他构建器完成后运行 mergeMap(target => target.output.pipe( finalize(() => { scheduleLogger.info('Shutting down build'); }), ), ), catchError(e => { scheduleLogger.error(JSON.stringify(e)); return of({ success: false }); }), ); } export default createBuilder<json.JsonObject & CustomScheduleBuilderSchema>(runCustomScheduleBuilder);
$templateCache构建器代码
import { TemplatesBuilderSchema } from './schema'; import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; import { json, normalize } from '@angular-devkit/core'; import { Observable, of, noop, from, fromEvent } from 'rxjs'; import { catchError, map, finalize, take, tap, mergeMap, first } from 'rxjs/operators'; import { join } from 'path'; import html2jsProcessor from 'angular-template-cache'; import defaults from 'angular-template-cache/lib/defaults'; import { watch } from 'chokidar'; async function populateTemplateCache( options: any, templatesLogger: any ) { await html2jsProcessor(options); templatesLogger.info(`Completed templates file: ${options.output}`) } export function runTemplatesBuilder( { outputPath, watchModeEnabled }: TemplatesBuilderSchema, { workspaceRoot, logger }: BuilderContext ): Observable<BuilderOutput> { const templatesLogger = logger.createChild('Templates'); templatesLogger.info('Creating templates file to populate $templateCache'); const options = { ...defaults, strict: false, filesGlob: 'src/app/**/*.html', moduleName: 'myApp.templates', output: join(workspaceRoot, normalize(outputPath)), }; // 设置文件监听器 const watcher = watch('src/app/**/*.html', { ignoreInitial: true }); watcher.on('add', () => { populateTemplateCache(options, templatesLogger); }) .on('change', () => { populateTemplateCache(options, templatesLogger); }) .on('unlink', () => { populateTemplateCache(options, templatesLogger); }); return fromEvent(watcher, 'ready').pipe( tap(() => { templatesLogger.info('templates watcher ready...'); }), first(), mergeMap(_ => from(populateTemplateCache(options, templatesLogger))), map(() => ({ success: true })), finalize(() => { templatesLogger.info('Shutting down templates file watcher'); watcher.close(); }), catchError(e => { templatesLogger.error(`Failed to create templates file: ${options.output}, Error: ${JSON.stringify(e)}`); return of({ success: false }); }), watchModeEnabled ? tap(noop) : take(1), ); } export default createBuilder<json.JsonObject & TemplatesBuilderSchema>(runTemplatesBuilder);
解决方案
问题根源
当前调度逻辑中,scheduleTarget返回的Promise会在构建器启动时就resolve,而非执行完成时。这导致主构建器在$templateCache构建器还没生成完文件就开始运行,最终找不到依赖模块。
修改方案
需要在调度每个依赖构建器后,等待其output流发出构建完成的信号(即BuilderOutput对象),再继续启动下一个构建器。具体来说,在mergeMap中,不仅要启动构建器,还要订阅其output流直到获取到第一个完成信号。
修改后的调度构建器代码
import { CustomScheduleBuilderSchema } from './schema'; import { BuilderContext, BuilderOutput, BuilderRun, createBuilder, ScheduleOptions } from '@angular-devkit/architect'; import { Observable, of, from } from 'rxjs'; import { catchError, finalize, first, mergeMap } from 'rxjs/operators'; import { json, JsonObject } from '@angular-devkit/core'; function scheduleNgBuildTarget( options: JsonObject & CustomScheduleBuilderSchema, context: BuilderContext, ): Promise<BuilderRun> { return context.scheduleTarget({ target: options.target, project: context.target?.project ?? '' }); } function scheduleConcatenateLanguagesBuilder( context: BuilderContext ): Promise<BuilderRun> { return context.scheduleTarget({ target: 'concat-lang', project: context.target?.project ?? '' }, { watchModeEnabled: true }); } function scheduleTemplatesBuilder( context: BuilderContext ): Promise<BuilderRun> { return context.scheduleTarget({ target: 'templates', project: context.target?.project ?? '' }, { watchModeEnabled: true }); } function scheduleVersionBuilder( context: BuilderContext ): Promise<BuilderRun> { return context.scheduleTarget({ target: 'version', project: context.target?.project ?? '' }); } export function runCustomScheduleBuilder ( options: JsonObject & CustomScheduleBuilderSchema, context: BuilderContext ): Observable<BuilderOutput> { const scheduleLogger = context.logger.createChild('CustomSchedule'); scheduleLogger.info('Starting build'); // 辅助函数:启动构建器并等待其完成 const runAndWaitForCompletion = (scheduleFn: () => Promise<BuilderRun>) => { return from(scheduleFn()).pipe( mergeMap(run => run.output.pipe(first())) // 等待构建器输出第一个完成信号 ); }; return of({ success: true }).pipe( first(), mergeMap(_ => runAndWaitForCompletion(() => scheduleConcatenateLanguagesBuilder(context))), mergeMap(_ => runAndWaitForCompletion(() => scheduleTemplatesBuilder(context))), mergeMap(_ => runAndWaitForCompletion(() => scheduleVersionBuilder(context))), mergeMap(_ => from(scheduleNgBuildTarget(options, context))), mergeMap(target => target.output.pipe( finalize(() => { scheduleLogger.info('Shutting down build'); }), ), ), catchError(e => { scheduleLogger.error(JSON.stringify(e)); return of({ success: false }); }), ); } export default createBuilder<json.JsonObject & CustomScheduleBuilderSchema>(runCustomScheduleBuilder);
关键修改点
- 新增
runAndWaitForCompletion辅助函数,封装“启动构建器 + 等待其完成”的逻辑 - 每个依赖构建器的调度都通过该辅助函数处理,确保前一个构建器完全完成后才进入下一个步骤
- 主构建器的启动逻辑保持不变,因为它需要持续监听文件变化(如果是watch模式)
针对watch模式的补充说明
如果是watch模式,模板构建器会一直运行并监听文件变化,此时主构建器启动后,模板文件的变更会触发模板构建器重新生成文件,进而触发主构建器重新构建,保证两者的同步性。
内容的提问来源于stack exchange,提问作者Bjørn Lyngwa
相关产品推荐
相关产品推荐

