Vite内核解析-第17章 Web Worker 与特殊资源

13 阅读8分钟

《Vite 设计与实现》完整目录

第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)。

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

  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(后处理插件)。

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}
        );
      }`
  }
}

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

  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:

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.metawebWorkerPostPlugin 负责在后处理阶段进行替换:

// 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 的<br/>watchedFiles 中?"}
    D -->|"是"| E["标记 bundle 为失效"]
    D -->|"否"| F["跳过"]
    E --> G["下次 load Hook 调用时<br/>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 通过虚拟模块 \0vite/wasm-helper.js 提供辅助函数:

load: {
  filter: { id: [exactRegex(wasmHelperId), wasmInitRE] },
  async handler(id) {
    const ssr = this.environment.config.consumer === 'server'

    if (id === wasmHelperId) {
      return `
const instantiateFromUrl = ${ssr ? instantiateFromFileCode : instantiateFromUrlCode}
export default ${wasmHelperCode}
`
    }

    // 对 .wasm?init 文件,生成包装模块
    id = id.split('?')[0]
    let url = await fileToUrl(this, id, ssr)
    return `
import initWasm from "${wasmHelperId}"
export default opts => initWasm(opts, ${JSON.stringify(url)})
`
  },
},

辅助函数还处理了 data URL 格式的 WASM(Base64 编码),这在某些打包场景中会出现:

const wasmHelper = async (opts, url) => {
  let result
  if (url.startsWith('data:')) {
    const urlContent = url.replace(/^data:.*?base64,/, '')
    let bytes
    if (typeof Buffer === 'function') {
      bytes = Buffer.from(urlContent, 'base64')
    } else if (typeof atob === 'function') {
      const binaryString = atob(urlContent)
      bytes = new Uint8Array(binaryString.length)
      for (let i = 0; i < binaryString.length; i++) {
        bytes[i] = binaryString.charCodeAt(i)
      }
    }
    result = await WebAssembly.instantiate(bytes, opts)
  } else {
    result = await instantiateFromUrl(url, opts)
  }
  return result.instance
}

17.2.3 SSR 路径替换

在 SSR 构建的 renderChunk 阶段,WASM 资源的 URL 需要从服务器路径替换为基于 import.meta.url 的相对路径:

renderChunk: env.config.consumer === 'server'
  ? {
      filter: { code: wasmInitUrlRE },
      async handler(code, chunk, opts, meta) {
        const toRelativeRuntime =
          createToImportMetaURLBasedRelativeRuntime(opts.format, /*...*/)
        while ((match = wasmInitUrlRE.exec(code))) {
          const [full, referenceId] = match
          const file = this.getFileName(referenceId)
          chunk.viteMetadata!.importedAssets.add(cleanUrl(file))
          const { runtime } = toRelativeRuntime(file, chunk.fileName)
          s.update(match.index, match.index + full.length,
            `"+${runtime}+"`)
        }
      },
    }
  : undefined,

17.3 动态导入变量

17.3.1 问题描述

动态导入的参数如果包含变量,构建工具在编译时无法确定实际的模块路径:

const module = await import(`./pages/${name}.vue`)

Vite 的 dynamicImportVars.ts 插件将这类模式转换为 import.meta.glob,利用文件系统匹配来穷举所有可能的模块。

17.3.2 转换流程

flowchart TB
    A["import(`./pages/${name}.vue`)"] --> B["es-module-lexer 解析"]
    B --> C["提取动态导入的模板字符串"]
    C --> D["dynamicImportToGlob 转换为 glob 模式"]
    D --> E["'./pages/${name}.vue' -> './pages/*.vue'"]
    E --> F["生成 import.meta.glob 表达式"]
    F --> G["注入 __variableDynamicImportRuntimeHelper"]
    G --> H["运行时根据路径查找匹配的模块"]

核心转换逻辑:

export async function transformDynamicImport(importSource, importer, resolve, root) {
  // 非相对路径先尝试解析
  if (importSource[1] !== '.' && importSource[1] !== '/') {
    const resolvedFileName = await resolve(importSource.slice(1, -1), importer)
    if (!resolvedFileName) return null
    const relativeFileName = normalizePath(
      posix.relative(posix.dirname(normalizePath(importer)), resolvedFileName),
    )
    importSource = '`' + (relativeFileName[0] === '.' ? '' : './') + relativeFileName + '`'
  }

  const dynamicImportPattern = parseDynamicImportPattern(importSource)
  if (!dynamicImportPattern) return null

  const { globParams, rawPattern, userPattern } = dynamicImportPattern
  const params = globParams ? `, ${JSON.stringify(globParams)}` : ''
  const exp = `(import.meta.glob(${JSON.stringify(userPattern)}${params}))`

  return { rawPattern: newRawPattern, pattern: userPattern, glob: exp }
}

17.3.3 运行时辅助函数

转换后的代码使用 __variableDynamicImportRuntimeHelper 在运行时查找匹配的模块:

const dynamicImportHelper = (glob, path, segs) => {
  const v = glob[path]
  if (v) {
    return typeof v === 'function' ? v() : Promise.resolve(v)
  }
  return new Promise((_, reject) => {
    ;(typeof queueMicrotask === 'function' ? queueMicrotask : setTimeout)(
      reject.bind(null, new Error(
        'Unknown variable dynamic import: ' + path +
          (path.split('/').length !== segs
            ? '. Note that variables only represent file names one level deep.'
            : ''),
      )),
    )
  })
}

当路径匹配到 glob 对象中的键时,调用对应的 loader 函数(懒加载)或直接返回值(eager 加载)。匹配失败时抛出带有诊断信息的错误。

17.3.4 native 模式

在构建模式下,动态导入变量使用 Rolldown 的内置实现:

if (config.isBundled) {
  return perEnvironmentPlugin('native:dynamic-import-vars', (environment) => {
    const { include, exclude } =
      environment.config.build.dynamicImportVarsOptions
    return nativeDynamicImportVarsPlugin({
      include, exclude,
      resolver(id, importer) {
        return resolve(environment, id, importer)
      },
      sourcemap: !!environment.config.build.sourcemap,
    })
  })
}

17.4 import.meta.glob

17.4.1 功能概览

import.meta.glob 是 Vite 的标志性特性之一,它允许使用 glob 模式批量导入文件:

// 懒加载所有 .vue 文件
const modules = import.meta.glob('./components/*.vue')
// { './components/Foo.vue': () => import('./components/Foo.vue'), ... }

// eager 加载
const modules = import.meta.glob('./components/*.vue', { eager: true })
// { './components/Foo.vue': Module, ... }

// 指定导入
const modules = import.meta.glob('./components/*.vue', { import: 'default' })

// 自定义查询
const modules = import.meta.glob('./data/*.json', { query: '?raw' })

17.4.2 解析流程

importMetaGlob.tstransform Hook 中处理 import.meta.glob 调用:

flowchart TB
    A["检测代码中的 import.meta.glob"] --> B["stripLiteral 处理字符串"]
    B --> C["正则匹配 glob 调用位置"]
    C --> D["rolldown parseAstAsync 解析参数"]
    D --> E["提取 glob 模式和选项"]
    E --> F["tinyglobby 匹配文件系统"]
    F --> G["生成代码替换"]

    subgraph "选项处理"
        H["eager: boolean"]
        I["import: string"]
        J["query: string | object"]
        K["as: string"]
        L["exhaustive: boolean"]
    end

    E --> H
    E --> I
    E --> J
    E --> K
    E --> L

17.4.3 代码生成

对于非 eager 模式,import.meta.glob 被转换为一个对象字面量,每个匹配的文件对应一个懒加载函数:

// 输入
const modules = import.meta.glob('./pages/*.vue')

// 输出
const modules = {
  './pages/Home.vue': () => import('./pages/Home.vue'),
  './pages/About.vue': () => import('./pages/About.vue'),
  './pages/Contact.vue': () => import('./pages/Contact.vue'),
}

对于 eager 模式,生成静态导入:

// 输入
const modules = import.meta.glob('./pages/*.vue', { eager: true })

// 输出
import * as __glob_0 from './pages/Home.vue'
import * as __glob_1 from './pages/About.vue'
import * as __glob_2 from './pages/Contact.vue'
const modules = {
  './pages/Home.vue': __glob_0,
  './pages/About.vue': __glob_1,
  './pages/Contact.vue': __glob_2,
}

17.4.4 选项处理

parseGlobOptions 函数解析和验证 glob 选项:

const knownOptions = {
  as: ['string'],
  eager: ['boolean'],
  import: ['string'],
  exhaustive: ['boolean'],
  query: ['object', 'string'],
  base: ['string'],
}

function parseGlobOptions(rawOpts, optsStartIndex, logger) {
  let opts = evalValue(rawOpts)  // 静态求值
  if (opts == null) return {}

  // 类型验证
  for (const key in opts) {
    if (!(key in knownOptions)) {
      throw err(`Unknown glob option "${key}"`, optsStartIndex)
    }
    const allowedTypes = knownOptions[key]
    const valueType = typeof opts[key]
    if (!allowedTypes.includes(valueType)) {
      throw err(
        `Expected glob option "${key}" to be of type ${allowedTypes.join(' or ')}, but got ${valueType}`,
        optsStartIndex,
      )
    }
  }

  // base 路径验证
  if (opts.base) {
    if (opts.base[0] === '!') {
      throw err('Option "base" cannot start with "!"', optsStartIndex)
    }
    if (!opts.base.startsWith('/') &&
        !opts.base.startsWith('./') &&
        !opts.base.startsWith('../')) {
      throw err(`Option "base" must start with '/', './' or '../'`, optsStartIndex)
    }
  }
}

选项通过 evalValue 进行静态求值。这意味着选项必须是编译时可确定的字面量,不能使用变量。这个限制是必要的,因为 glob 匹配在编译时执行。

17.4.5 HMR 联动

import.meta.glob 与 HMR 系统深度集成。当新文件被添加或已有文件被删除时,使用了该 glob 模式的模块需要被重新转换:

hotUpdate({ type, file, modules: oldModules }) {
  if (type === 'update') return  // 内容变更不影响 glob 结果

  const importGlobMap = importGlobMaps.get(this.environment)
  if (!importGlobMap) return

  const modules: EnvironmentModuleNode[] = []
  for (const [id, globMatchers] of importGlobMap) {
    // 检查变更的文件是否匹配某个 glob 模式
    if (globMatchers.some((matcher) => matcher(file))) {
      const mod = this.environment.moduleGraph.getModuleById(id)
      if (mod) modules.push(mod)
    }
  }
  return modules.length > 0 ? [...oldModules, ...modules] : undefined
}
sequenceDiagram
    participant FS as 文件系统
    participant Watcher as 文件监听器
    participant Plugin as importGlobPlugin
    participant HMR as HMR 系统

    FS->>Watcher: 新建文件 pages/New.vue
    Watcher->>Plugin: hotUpdate({ type: 'create', file: 'pages/New.vue' })
    Plugin->>Plugin: 检查 importGlobMaps
    Plugin->>Plugin: 'pages/New.vue' 匹配 './pages/*.vue'
    Plugin-->>HMR: 返回使用该 glob 的模块列表
    HMR->>HMR: 触发模块重新转换
    HMR->>HMR: 发送 HMR update

Glob matchers 使用 picomatch 库生成高效的匹配函数,支持否定模式(!pattern):

const globMatchers = allGlobs.map((globs) => {
  const affirmed: string[] = []
  const negated: string[] = []
  for (const glob of globs) {
    if (glob[0] === '!') {
      negated.push(glob.slice(1))
    } else {
      affirmed.push(glob)
    }
  }
  const affirmedMatcher = picomatch(affirmed)
  const negatedMatcher = picomatch(negated)

  return (file: string) => {
    return (
      (affirmed.length === 0 || affirmedMatcher(file)) &&
      !(negated.length > 0 && negatedMatcher(file))
    )
  }
})

17.4.6 与 Object.keys/Object.values 的优化

插件检测 Object.keys(import.meta.glob(...))Object.values(import.meta.glob(...)) 模式,分别标记为 onlyKeysonlyValues,以便在代码生成阶段进行优化 -- 例如 onlyKeys 时不需要生成导入语句,只需要键列表。

const importGlobRE = /\bimport\.meta\.glob(?:<\w+>)?\s*\(/g
const objectKeysRE = /\bObject\.keys\(\s*$/
const objectValuesRE = /\bObject\.values\(\s*$/

17.5 Worker 环境注入

17.5.1 Worker 类型与环境变量

Worker 脚本需要访问 Vite 的环境变量(import.meta.env),但注入方式取决于 Worker 类型:

transform: {
  filter: { id: workerFileRE },
  async handler(raw, id) {
    const workerType = workerFileMatch[1] as WorkerType  // 'classic' | 'module' | 'ignore'

    if (workerType === 'classic') {
      // classic Worker: 通过 importScripts 加载环境变量
      const scriptPath = JSON.stringify(
        path.posix.join(config.base, ENV_PUBLIC_PATH),
      )
      injectEnv = `importScripts(${scriptPath})\n`
    } else if (workerType === 'module') {
      // module Worker: 通过 import 加载
      const scriptPath = JSON.stringify(ENV_PUBLIC_PATH)
      injectEnv = `import ${scriptPath}\n`
    } else if (workerType === 'ignore') {
      // 动态类型: 在开发模式下内联环境变量代码
      if (!config.isBundled) {
        const module = moduleGraph?.getModuleById(ENV_ENTRY)
        injectEnv = module?.transformResult?.code || ''
      }
    }
  },
}
graph TB
    A["Worker 环境注入"] --> B{"Worker 类型?"}
    B -->|"classic"| C["importScripts(/@vite/env)"]
    B -->|"module"| D["import '/@vite/env'"]
    B -->|"ignore (动态)"| E["内联 env 代码到文件头"]

    style C fill:#e3f2fd
    style D fill:#e8f5e9
    style E fill:#fff3e0

17.6 设计决策分析

17.6.1 Worker 独立构建的必要性

Worker 不能与主应用共享 bundle,因为它们运行在独立的线程中。每个 Worker 都需要一个完整的、自包含的入口文件。这就是为什么 Worker 插件为每个 Worker 创建独立的 Rolldown 构建 -- 即使这增加了构建时间和复杂度。

WorkerOutputCache 通过缓存和去重来缓解这个问题。同一个 Worker 文件被多次引用时只打包一次,输出的资源文件也会去重。

17.6.2 import.meta.glob 的编译时特性

import.meta.glob 必须在编译时处理,因为:

  1. Glob 匹配需要访问文件系统,运行时(浏览器)不具备这个能力
  2. 匹配结果决定了需要打包哪些模块,影响构建图
  3. Eager 模式下生成的静态导入必须在编译时确定

这意味着 glob 模式和选项必须是静态的字面量。任何尝试使用变量的做法都会导致编译错误。

17.6.3 SharedWorker 与 Blob URL 的不兼容

graph LR
    A["Worker + Blob URL"] --> B["每次 createObjectURL 生成唯一 URL"]
    B --> C["每次 new Worker(url) 创建新实例"]
    C --> D["正确: Worker 本来就是每次新建"]

    E["SharedWorker + Blob URL"] --> F["每次 createObjectURL 生成唯一 URL"]
    F --> G["每次 new SharedWorker(url) 创建新实例"]
    G --> H["错误: SharedWorker 应该共享实例"]

    style D fill:#e8f5e9
    style H fill:#ffcdd2

SharedWorker 的共享机制基于 URL 匹配。Blob URL 每次调用 createObjectURL 都会生成不同的 URL,导致无法复用同一个 SharedWorker 实例。因此 Vite 对 SharedWorker 使用 data URL,确保内容相同的 Worker 共享同一个 URL。

17.7 小结

本章深入分析了 Vite 对特殊资源的处理机制:

  • Web Worker 插件通过独立的 Rolldown 构建管线为每个 Worker 生成自包含的 bundle。WorkerOutputCache 实现了跨引用的产物缓存和去重,内联模式通过 Blob URL 和 data URL 的组合策略处理 Worker 和 SharedWorker 的差异。URL 占位符在 renderChunk 阶段被替换为相对路径,IIFE Worker 的 import.meta 被替换为 self.location.href

  • WASM 支持通过环境感知的加载策略,在客户端使用 fetch + WebAssembly.instantiateStreaming,在服务端使用 fs.readFile + WebAssembly.instantiate。辅助函数还处理了 data URL 格式和流式实例化的降级逻辑。

  • 动态导入变量被转换为 import.meta.glob 调用,利用文件系统匹配穷举所有可能的模块。运行时辅助函数在匹配失败时提供诊断友好的错误信息。

  • import.meta.glob 在编译时通过 tinyglobby 匹配文件系统,生成包含懒加载函数或静态导入的对象字面量。它与 HMR 系统深度集成:文件的添加和删除会触发使用相关 glob 模式的模块重新转换。picomatch 生成的高效匹配函数和否定模式支持使得这个联动既精确又高效。

这些特殊资源的处理展示了 Vite 插件系统的表达力。每种资源类型都利用了 Vite 插件的不同 Hook 组合(loadtransformrenderChunkgenerateBundlehotUpdate),在开发和构建两种模式下提供一致的行为,同时与 HMR、Source Map、代码分割等核心机制保持协作。