Appearance
第17章 Web Worker 与特殊资源
开篇引言
现代 Web 应用不仅包含 JavaScript 和 CSS,还需要处理各种特殊类型的资源:Web Worker 提供了多线程计算能力,WebAssembly 带来了接近原生的执行性能,JSON 导入需要与 Tree Shaking 协作,动态导入变量需要在构建时被静态化,import.meta.glob 则提供了文件系统级的批量导入能力。
这些特殊资源的处理是 Vite 插件系统的高级应用。每一种资源类型都需要在开发和构建两种模式下提供一致的行为,同时还要与 HMR、Source Map、代码分割等核心机制协作。
本章将深入分析 Vite 对这些特殊资源的处理实现,重点关注 Worker 插件(plugins/worker.ts)、WASM 支持(plugins/wasm.ts)、动态导入变量(plugins/dynamicImportVars.ts)和 import.meta.glob(plugins/importMetaGlob.ts)。
本章要点
- 理解 Worker 插件的独立构建管线与产物缓存机制
- 掌握 Worker 的内联(inline)模式与 Blob URL 的设计
- 分析 WASM 的两种加载策略(fetch vs 文件系统)
- 理解动态导入变量到
import.meta.glob的转换 - 掌握
import.meta.glob的模式解析、代码生成与 HMR 联动
17.1 Web Worker 插件
17.1.1 Worker 的挑战
Web Worker 运行在独立的线程中,拥有独立的全局作用域。这给构建工具带来了独特的挑战:
- Worker 脚本需要被打包为独立的入口文件
- Worker 脚本可能依赖其他模块,需要递归处理
- 开发模式和构建模式下 Worker 的加载方式不同
- 内联 Worker 需要将代码转换为 Blob URL
- SharedWorker 不能使用 Blob URL(会导致多实例)
- IIFE 格式的 Worker 不支持
import.meta
17.1.2 Worker 插件架构
Worker 插件由两个主要部分组成:webWorkerPlugin(主插件)和 webWorkerPostPlugin(后处理插件)。
17.1.3 WorkerOutputCache
Worker 插件使用 WorkerOutputCache 管理构建产物的缓存和去重:
typescript
class WorkerOutputCache {
// Worker 打包信息:输入文件 -> 打包结果
private bundles = new Map<string, WorkerBundle>()
// 资源文件缓存
private assets = new Map<string, WorkerBundleAsset>()
// 文件名 hash -> 入口文件名的映射
private fileNameHash = new Map<string, string>()
// 因文件变更需要重新打包的 Worker
private invalidatedBundles = new Set<string>()
}缓存的设计确保了同一个 Worker 文件只被打包一次,即使它在多个地方被引用:
typescript
async function bundleWorkerEntry(config, id): Promise<WorkerBundle> {
const input = cleanUrl(id)
const workerOutput = workerOutputCaches.get(config.mainConfig || config)!
// 检查缓存(含失效检查)
workerOutput.removeBundleIfInvalidated(input)
const bundleInfo = workerOutput.getWorkerBundle(input)
if (bundleInfo) return bundleInfo // 命中缓存,直接返回
// 循环引用检测
const newBundleChain = [...config.bundleChain, input]
if (config.bundleChain.includes(input)) {
throw new Error(
'Circular worker imports detected. Vite does not support it. ' +
`Import chain: ${newBundleChain.map((id) =>
prettifyUrl(id, config.root)).join(' -> ')}`,
)
}
// 启动独立的 Rolldown 构建
const { rolldown } = await import('rolldown')
// ...
}17.1.4 独立构建管线
每个 Worker 文件都通过独立的 Rolldown 构建处理:
typescript
const workerEnvironment = new BuildEnvironment('client', workerConfig)
await workerEnvironment.init()
const bundle = await rolldown({
...rollupOptions,
input,
plugins: workerEnvironment.plugins.map((p) =>
injectEnvironmentToHooks(workerEnvironment, chunkMetadataMap, p),
),
preserveEntrySignatures: false,
experimental: { viteMode: true },
})
const result = await bundle.generate({
entryFileNames: path.posix.join(config.build.assetsDir, '[name]-[hash].js'),
chunkFileNames: path.posix.join(config.build.assetsDir, '[name]-[hash].js'),
format,
sourcemap: workerEnvironment.config.build.sourcemap,
minify: workerEnvironment.config.build.minify === 'oxc' ? true
: workerEnvironment.config.build.minify === false ? 'dce-only'
: undefined,
})17.1.5 内联 Worker 与 Blob URL
当 Worker 使用 ?inline 查询参数或通过 ?worker&inline 方式导入时,Worker 代码会被内联到主 bundle 中:
typescript
if (inlineRE.test(id)) {
const result = await bundleWorkerEntry(config, id)
const jsContent = `const jsContent = ${JSON.stringify(result.entryCode)};`
// Worker 使用 Blob URL
if (workerConstructor === 'Worker') {
const code = `${jsContent}
const blob = typeof self !== "undefined" && self.Blob &&
new Blob([${
workerType === 'classic'
? `'(self.URL || self.webkitURL).revokeObjectURL(self.location.href);',`
: `'URL.revokeObjectURL(import.meta.url);',`
}jsContent], { type: "text/javascript;charset=utf-8" });
export default function WorkerWrapper(options) {
let objURL;
try {
objURL = blob && (self.URL || self.webkitURL).createObjectURL(blob);
if (!objURL) throw ''
const worker = new Worker(objURL, ${workerTypeOption});
worker.addEventListener("error", () => {
(self.URL || self.webkitURL).revokeObjectURL(objURL);
});
return worker;
} catch(e) {
return new Worker(
'data:text/javascript;charset=utf-8,' + encodeURIComponent(jsContent),
${workerTypeOption}
);
}
}`
}
// SharedWorker 使用 data URL(避免多实例)
else {
const code = `${jsContent}
export default function WorkerWrapper(options) {
return new SharedWorker(
'data:text/javascript;charset=utf-8,' + encodeURIComponent(jsContent),
${workerTypeOption}
);
}`
}
}这段代码展示了三个关键设计:
- Blob URL 优先,data URL 回退:Blob URL 性能更好,但创建失败时回退到 data URL
- 自动 revoke:Worker 启动后通过注入的代码自动调用
revokeObjectURL,避免内存泄漏。对于classic类型使用self.location.href,对于module类型使用import.meta.url - SharedWorker 使用 data URL:Blob URL 每次创建都是新的 URL,SharedWorker 需要相同的 URL 才能共享实例
17.1.6 URL 占位符与 renderChunk 替换
非内联 Worker 在构建时使用占位符标记 URL:
typescript
private generateEntryUrlPlaceholder(entryFilename: string): string {
const hash = getHash(entryFilename)
if (!this.fileNameHash.has(hash)) {
this.fileNameHash.set(hash, entryFilename)
}
return `__VITE_WORKER_ASSET__${hash}__`
}在 renderChunk 阶段,这些占位符被替换为实际的相对路径:
typescript
renderChunk(code, chunk, outputOptions) {
workerAssetUrlRE.lastIndex = 0
if (workerAssetUrlRE.test(code)) {
const toRelativeRuntime = createToImportMetaURLBasedRelativeRuntime(
outputOptions.format, this.environment.config.isWorker,
)
let match
s = new MagicString(code)
while ((match = workerAssetUrlRE.exec(code))) {
const [full, hash] = match
const filename = workerOutputCache.getEntryFilenameFromHash(hash)
const replacement = toOutputFilePathInJS(
this.environment, filename, 'asset', chunk.fileName, 'js',
toRelativeRuntime,
)
s.update(match.index, match.index + full.length,
typeof replacement === 'string'
? JSON.stringify(encodeURIPath(replacement)).slice(1, -1)
: `"+${replacement.runtime}+"`,
)
}
}
}