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;
关键优化点
- 资源跟踪钩子:通过
onResolve+onLoad捕获所有经过esbuild处理的资源,包括CSS和静态文件 - 配置扫描:读取NX的
project.json,主动扫描assets和styles目录下的资源,覆盖Angular默认静态资源 - 路径统一:使用相对于输出目录的路径作为缓存URL,确保Service Worker能正确定位资源
- 去重逻辑:避免同一资源被多次添加到预缓存清单
- 正则修正:替换无效的
img匹配,改为具体图片格式,确保覆盖所有静态资源类型
内容的提问来源于stack exchange,提问作者Øystein Amundsen
相关产品推荐
相关产品推荐

