Skip to content

第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.globplugins/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 运行在独立的线程中,拥有独立的全局作用域。这给构建工具带来了独特的挑战:

  1. Worker 脚本需要被打包为独立的入口文件
  2. Worker 脚本可能依赖其他模块,需要递归处理
  3. 开发模式和构建模式下 Worker 的加载方式不同
  4. 内联 Worker 需要将代码转换为 Blob URL
  5. SharedWorker 不能使用 Blob URL(会导致多实例)
  6. 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}
        );
      }`
  }
}

这段代码展示了三个关键设计:

  1. Blob URL 优先,data URL 回退:Blob URL 性能更好,但创建失败时回退到 data URL
  2. 自动 revoke:Worker 启动后通过注入的代码自动调用 revokeObjectURL,避免内存泄漏。对于 classic 类型使用 self.location.href,对于 module 类型使用 import.meta.url
  3. 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}+"`,
      )
    }
  }
}

基于 VitePress 构建