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

Angular17+NX中自定义esbuild集成Workbox的缓存文件获取问题

解决Angular 17+NX集成Workbox时esbuild无法获取CSS/静态资源的问题

核心原因

NX/Angular基于esbuild的构建流程中,CSS、静态资源、HTML等文件并非全部经过esbuild的主打包链路:

  • CSS由Angular单独提取为独立文件
  • 静态资源通过专属拷贝逻辑处理,未进入esbuild的outputFiles集合
  • nx serve采用内存构建,无法依赖磁盘文件读取

解决方案:结合钩子跟踪+配置扫描

修改你的wbInject.plugin.ts,通过esbuild资源跟踪钩子+读取NX构建配置扫描静态资源的方式,完整收集所有需要预缓存的文件:

import * as esbuild from 'esbuild';
import { readFileSync, readdirSync } from 'fs';
import * as path from 'path';

// 修正匹配正则,覆盖所有需要预缓存的资源类型
const MATCH_FILES = /^.+\.(js|css|svg|png|jpg|jpeg|gif|html|txt|webmanifest)$/i;
const manifest: { url: string; revision: string }[] = [];
const projectRoot = 'apps/myApp/src';
const projectConfigPath = path.resolve(process.cwd(), 'apps/myApp/project.json');
const projectConfig = JSON.parse(readFileSync(projectConfigPath, 'utf-8'));
const outDir = path.resolve(process.cwd(), projectConfig.targets.build.options.outputPath || 'dist/apps/myApp');

const wbInject: esbuild.Plugin = {
  name: 'wbInject',
  setup(build: esbuild.PluginBuild) {
    // 添加Service Worker入口
    const options = build.initialOptions;
    Object.assign(options.entryPoints || {}, {
      'ngsw-worker': path.resolve(projectRoot, 'ngsw-worker.ts'),
    });
    let workerCode = '';

    // 跟踪所有被esbuild处理的资源文件
    build.onResolve({ filter: MATCH_FILES }, (args) => {
      return { path: args.path, namespace: 'track-resources' };
    });

    build.onLoad({ filter: MATCH_FILES, namespace: 'track-resources' }, async (args) => {
      const content = readFileSync(args.path);
      await addToManifest(args.path, content);
      return { contents: content, loader: 'file' };
    });

    // 预构建Service Worker并扫描静态资源
    build.onStart(async () => {
      console.log('Building service-worker');
      const result = await esbuild.build({
        entryPoints: [path.resolve(projectRoot, 'ngsw-worker.ts')],
        bundle: true,
        write: false,
        platform: 'browser',
        target: 'es2017',
      });
      workerCode = result.outputFiles[0].text;

      // 扫描Angular配置中定义的静态资源
      await scanStaticAssets();
    });

    // 注入清单到Service Worker
    build.onEnd(async result => {
      if (result.errors.length !== 0) return;

      // 添加esbuild输出的JS文件(避免重复)
      const jsFiles = result.outputFiles?.filter(f => f.path.match(/\.js$/) && !f.path.match(/ngsw-worker/)) || [];
      for (const file of jsFiles) {
        const relPath = path.relative(outDir, file.path);
        if (!manifest.find(i => i.url === relPath)) {
          await addToManifest(file.path, file.contents);
        }
      }

      const workerFile = result.outputFiles?.find(file => file.path.match(/ngsw-worker.*\.js$/));
      const workerSourceMap = result.outputFiles?.find(file => file.path.match(/ngsw-worker.*\.js.map$/));
      if (!workerFile) return;

      // 移除Service Worker文件的缓存哈希
      [workerFile, ...(workerSourceMap != null ? [workerSourceMap] : [])].forEach(
        file => (file.path = file.path.replace(/\-[A-Z0-9]{8}\./, '.')),
      );

      // 注入清单到Service Worker代码
      const updatedWorkerCode = workerCode.replace('self.__WB_MANIFEST', JSON.stringify(manifest));
      workerFile.contents = new TextEncoder().encode(updatedWorkerCode);
    });
  },
};

async function addToManifest(filePath: string, contents?: Uint8Array) {
  // 计算相对于输出目录的路径,作为缓存URL
  const relPath = path.relative(outDir, filePath);
  if (manifest.findIndex(i => i.url === relPath) !== -1) return;

  const content = contents || readFileSync(filePath);
  const hash = await createHash(content);
  manifest.push({ url: relPath, revision: hash });
}

async function createHash(content: BufferSource) {
  const hash: ArrayBuffer = await crypto.subtle.digest('SHA-256', content);
  return Array.from(new Uint8Array(hash))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

async function scanStaticAssets() {
  const assetsConfig = projectConfig.targets.build.options.assets || [];
  const stylesConfig = projectConfig.targets.build.options.styles || [];

  // 处理assets目录
  for (const asset of assetsConfig) {
    const assetInput = typeof asset === 'string' ? asset : asset.input;
    const fullPath = path.resolve(process.cwd(), assetInput);
    await scanDirectory(fullPath);
  }

  // 处理样式文件
  for (const style of stylesConfig) {
    const styleInput = typeof style === 'string' ? style : style.input;
    const fullPath = path.resolve(process.cwd(), styleInput);
    if (fullPath.match(MATCH_FILES)) {
      await addToManifest(fullPath);
    }
  }
}

async function scanDirectory(dirPath: string) {
  const entries = readdirSync(dirPath, { withFileTypes: true });
  for (const entry of entries) {
    const fullPath = path.join(dirPath, entry.name);
    if (entry.isDirectory()) {
      await scanDirectory(fullPath);
    } else if (entry.isFile() && entry.name.match(MATCH_FILES)) {
      await addToManifest(fullPath);
    }
  }
}

export default wbInject;

关键优化点

  1. 资源跟踪钩子:通过onResolve+onLoad捕获所有经过esbuild处理的资源,包括CSS和静态文件
  2. 配置扫描:读取NX的project.json,主动扫描assets和styles目录下的资源,覆盖Angular默认静态资源
  3. 路径统一:使用相对于输出目录的路径作为缓存URL,确保Service Worker能正确定位资源
  4. 去重逻辑:避免同一资源被多次添加到预缓存清单
  5. 正则修正:替换无效的img匹配,改为具体图片格式,确保覆盖所有静态资源类型

内容的提问来源于stack exchange,提问作者Øystein Amundsen

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 18:55:04