Vite内核解析-第17章 Web Worker 与特殊资源
作者:互联网
2026-04-15
第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)。
:::tip 本章要点
- 理解 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(后处理插件)。
graph TB
A["import MyWorker from './worker?worker'"] --> B{"开发模式?"}
B -->|"是"| C["fileToUrl: 生成开发服务器 URL"]
B -->|"否"| D{"内联模式?"}
D -->|"是 (?inline)"| E["bundleWorkerEntry: 打包并内联"]
D -->|"否"| F["workerFileToUrl: 打包为独立文件"]
C --> G["返回 Worker 构造函数代码"]
E --> H["返回 Blob URL Worker 代码"]
F --> I["返回带占位符的 Worker 代码"]
subgraph "webWorkerPostPlugin"
J["renderChunk: 替换占位符为实际路径"]
K["IIFE Worker: 替换 import.meta"]
end
I --> J
G --> K
style E fill:#fff3e0
style F fill:#e3f2fd
17.1.3 WorkerOutputCache
Worker 插件使用 WorkerOutputCache 管理构建产物的缓存和去重:
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 文件只被打包一次,即使它在多个地方被引用:
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 构建处理:
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,
})
sequenceDiagram
participant Main as 主构建
participant Cache as WorkerOutputCache
participant Worker as Worker 构建
Main->>Cache: getWorkerBundle(input)
alt 缓存命中
Cache-->>Main: 返回缓存的 WorkerBundle
else 缓存未命中
Main->>Worker: 创建独立 Rolldown 构建
Worker->>Worker: BuildEnvironment('client', workerConfig)
Worker->>Worker: rolldown({ input, plugins })
Worker->>Worker: bundle.generate({ format, sourcemap })
Worker-->>Cache: saveWorkerBundle(file, ...)
Cache-->>Main: 返回新的 WorkerBundle
end
Main->>Main: 生成 Worker 加载代码
17.1.5 内联 Worker 与 Blob URL
当 Worker 使用 ?inline 查询参数或通过 ?worker&inline 方式导入时,Worker 代码会被内联到主 bundle 中:
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:
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 阶段,这些占位符被替换为实际的相对路径:
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}+"`,
)
}
}
}
17.1.7 IIFE Worker 的 import.meta 处理
IIFE 格式的 Worker 不支持 import.meta,webWorkerPostPlugin 负责在后处理阶段进行替换:
// webWorkerPostPlugin
if (this.environment.config.worker.format === 'iife') {
await init
let imports = parse(code)[0]
for (const { s: start, e: end, d: dynamicIndex } of imports) {
if (dynamicIndex === -2) { // import.meta
const prop = code.slice(end, end + 4)
if (prop === '.url') {
s.overwrite(start, end + 4, 'self.location.href')
} else {
if (!injectedImportMeta) {
s.prepend('const _vite_importMeta = { url: self.location.href };n')
injectedImportMeta = true
}
s.overwrite(start, end, '_vite_importMeta')
}
}
}
}
import.meta.url 被替换为 self.location.href(Worker 的全局 self 引用),其他 import.meta 属性访问则使用一个注入的 polyfill 对象。
17.1.8 文件变更与缓存失效
watchChange(file) {
if (isWorker) return
workerOutputCaches
.get(config)!
.invalidateAffectedBundles(normalizePath(file))
}
当文件发生变更时,Worker 插件通过 watchChange Hook 检查该文件是否被某个 Worker bundle 引用。如果是,则将对应的 bundle 标记为失效,下次构建时重新打包。
flowchart TB
A["文件变更: utils.ts"] --> B["watchChange Hook"]
B --> C["遍历所有 Worker bundles"]
C --> D{"utils.ts 在此 bundle 的
watchedFiles 中?"}
D -->|"是"| E["标记 bundle 为失效"]
D -->|"否"| F["跳过"]
E --> G["下次 load Hook 调用时
removeBundleIfInvalidated"]
G --> H["重新执行 bundleWorkerEntry"]
17.2 WASM 支持
17.2.1 两种加载策略
plugins/wasm.ts 为 .wasm?init 导入提供支持。WASM 模块在客户端和服务端使用不同的加载策略:
graph TB
A["import init from './module.wasm?init'"] --> B{"consumer 类型?"}
B -->|"client"| C["fetch + WebAssembly.instantiateStreaming"]
B -->|"server"| D["fs.readFile + WebAssembly.instantiate"]
C --> E["const instance = await init(imports)"]
D --> E
style C fill:#e3f2fd
style D fill:#e8f5e9
// 客户端:通过 fetch 获取
const instantiateFromUrl = async (url, opts) => {
const response = await fetch(url)
const contentType = response.headers.get('Content-Type') || ''
if ('instantiateStreaming' in WebAssembly &&
contentType.startsWith('application/wasm')) {
return WebAssembly.instantiateStreaming(response, opts)
} else {
// 回退:先获取 ArrayBuffer 再实例化
const buffer = await response.arrayBuffer()
return WebAssembly.instantiate(buffer, opts)
}
}
// 服务端:通过文件系统读取
const instantiateFromFile = async (fileUrlString, opts) => {
const { readFile } = await import('node:fs/promises')
const fileUrl = new URL(fileUrlString, import.meta.url)
const buffer = await readFile(fileUrl)
return WebAssembly.instantiate(buffer, opts)
}
17.2.2 WASM Helper 注入
wasmHelperPlugin 通过虚拟模块
