__WB_MANIFEST注入前压缩导致service-worker.js兼容性异常
最近在配置 Workbox 和 CompressionWebpackPlugin 时遇到了一个棘手的问题:压缩后的 service-worker.js.gz 和最终部署的 service-worker.js 内容不一致,导致 PWA 预缓存功能失效。
问题现象
我在 compression-webpack-plugin 的 filename 回调里加了 console.log 来调试,发现了关键的执行顺序问题:
- 构建初期,CompressionPlugin 就会触发日志,此时
sw.js刚被复制到service-worker.js,但文件里还没有self.__WB_MANIFEST这个预缓存标记——这个标记要等 Workbox 的precacheAndRoute注入真实的预缓存条目数组。 - 构建收尾阶段,Workbox 才会把包含资源路径和版本号的预缓存数组注入到
service-worker.js,但这一步不会触发 CompressionPlugin 的日志,说明压缩操作早就完成了。 - 最终结果就是:压缩版的
service-worker.js.gz对应的是未注入预缓存信息的“半成品”,而实际运行的是已经注入后的完整文件,两者不匹配,直接导致预缓存逻辑失效。
相关配置
Webpack 插件配置
{ plugins: [ // ... 其他插件 new WorkboxPlugin.InjectManifest({ swSrc: './src/setup/sw.js', swDest: 'service-worker.js', exclude: [/\.(gz|br)$/], maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, }), // ... 其他插件 new CompressionPlugin({ filename(pathData) { console.log(pathData) return '[path][base].gz' }, algorithm: 'gzip', minRatio: 0.8, test: /\.(js|css|html|svg|wasm)$/, }) ] }
package.json 依赖版本
{ "webpack": "^5.4.0", "compression-webpack-plugin": "^6.0.5", "workbox-webpack-plugin": "^6.0.0-alpha.3" }
解决方案
这个问题的核心是插件执行时机不匹配:Workbox 的 InjectManifest 在异步钩子中生成最终的 service-worker.js,而 CompressionPlugin 默认在同步钩子中执行,导致压缩提前完成。这里提供几个可行的解决思路:
1. 升级依赖到稳定版本
你当前使用的 workbox-webpack-plugin 是 alpha 测试版,钩子时机可能存在问题。建议升级到稳定的 v6 版本(比如 ^6.6.0),同时将 compression-webpack-plugin 升级到与 Webpack 5 兼容的版本(比如 ^10.0.0),新版本通常会修复这类钩子顺序的兼容性问题。
2. 调整插件执行顺序与钩子类型
如果不想升级依赖,可以尝试:
- 确保
CompressionPlugin位于WorkboxPlugin.InjectManifest之后(虽然你已经这么做了,但可以确认是否钩子类型冲突)。 - 手动自定义插件,在
afterEmit钩子中专门压缩最终生成的service-worker.js:
const fs = require('fs'); const zlib = require('zlib'); // 添加到 plugins 数组末尾 { apply(compiler) { compiler.hooks.afterEmit.tapAsync('CompressServiceWorker', (compilation, callback) => { const swFileName = 'service-worker.js'; const swPath = `${compilation.outputOptions.path}/${swFileName}`; const swGzPath = `${swPath}.gz`; // 读取原始文件并压缩 fs.createReadStream(swPath) .pipe(zlib.createGzip()) .pipe(fs.createWriteStream(swGzPath)) .on('finish', () => { // 将压缩后的文件加入编译资产,确保能被正确输出 compilation.assets[`${swFileName}.gz`] = { source: () => fs.readFileSync(swGzPath), size: () => fs.statSync(swGzPath).size }; callback(); }) .on('error', (err) => { console.error('压缩 service-worker.js 失败:', err); callback(err); }); }); } }
同时,在原有的 CompressionPlugin 配置中添加 exclude: /service-worker\.js$/,避免重复压缩。
3. 验证钩子执行顺序
如果还是不确定问题所在,可以添加一个调试插件,打印钩子执行顺序:
// 添加到 plugins 数组最前面 { apply(compiler) { compiler.hooks.emit.tap('DebugEmitOrder', () => { console.log('=== Emit 阶段开始 ==='); }); compiler.hooks.afterEmit.tap('DebugAfterEmitOrder', () => { console.log('=== AfterEmit 阶段开始 ==='); }); } }
通过日志可以清晰看到 Workbox 和 CompressionPlugin 各自在哪个阶段执行,进而针对性调整。
内容的提问来源于stack exchange,提问作者amirhe

