You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让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);

关键修改点

  1. 新增runAndWaitForCompletion辅助函数,封装“启动构建器 + 等待其完成”的逻辑
  2. 每个依赖构建器的调度都通过该辅助函数处理,确保前一个构建器完全完成后才进入下一个步骤
  3. 主构建器的启动逻辑保持不变,因为它需要持续监听文件变化(如果是watch模式)

针对watch模式的补充说明

如果是watch模式,模板构建器会一直运行并监听文件变化,此时主构建器启动后,模板文件的变更会触发模板构建器重新生成文件,进而触发主构建器重新构建,保证两者的同步性。

内容的提问来源于stack exchange,提问作者Bjørn Lyngwa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 11:15:43