Skip to content

第4章 插件系统与 Hook 机制

开篇引言

如果说配置系统定义了 Vite "做什么",那么插件系统就定义了 Vite "怎么做"。Vite 的核心能力——模块解析、代码转换、CSS 处理、HTML 注入、HMR 更新——全部通过插件实现。这不是修辞上的夸张:打开 src/node/plugins/ 目录,你会发现 30 多个内置插件文件,它们组成了 Vite 的全部处理管线。

Vite 的插件系统脱胎于 Rollup,但做了关键性的扩展。Rollup 插件只需要处理构建阶段的事务,而 Vite 插件需要同时覆盖开发服务器和生产构建两个截然不同的场景。开发阶段,模块按需处理,没有打包过程;构建阶段,所有模块被打包成最终产物。这种双模态运行的需求催生了 Vite 独有的 Hook 分类体系:配置钩子、服务器钩子、通用钩子、构建钩子。

更深层的挑战来自 Vite 6 引入的 Environment API。一个插件可能需要在 clientssredge-worker 等多个运行环境中分别执行,每个环境有独立的模块图和插件容器。这意味着插件容器不再是单例,而是每个环境独立一份。

本章将从类型定义出发,沿着"定义 -> 注册 -> 排序 -> 执行"的完整链路,逐层剖析 Vite 的插件系统。我们会深入 pluginContainer.ts——这个灵感来源于 WMR 项目的核心文件,理解它如何在开发服务器中模拟 Rollup 的插件执行环境。

本章要点

  1. Vite 插件类型 Plugin 在 Rolldown 插件基础上扩展了 configconfigureServerhotUpdate 等 Vite 专有钩子
  2. 30+ 内置插件按 pre -> core -> normal -> post 的严格顺序注册
  3. 每个 Hook 独立排序,支持 order: 'pre' | 'post' 控制同名 Hook 的执行优先级
  4. 插件容器 EnvironmentPluginContainer 为每个环境提供独立的 Rollup 兼容执行上下文
  5. enforce 字段将插件分为 'pre'、正常、'post' 三个层级,决定插件在全局管线中的位置
  6. applyToEnvironment 允许插件按环境动态启用或替换

4.1 插件类型定义

4.1.1 从 Rolldown 到 Vite

Vite 插件的类型定义位于 src/node/plugin.ts。核心接口 Plugin 继承自 Rolldown 的 RolldownPlugin

typescript
// src/node/plugin.ts
export interface Plugin<A = any> extends RolldownPlugin<A> {
  enforce?: 'pre' | 'post'
  apply?: 'serve' | 'build' | ((this: void, config: UserConfig, env: ConfigEnv) => boolean)
  applyToEnvironment?: (environment: PartialEnvironment) => boolean | Promise<boolean> | PluginOption

  // Vite 独有的 Hooks
  config?: ObjectHook<(this: ConfigPluginContext, config: UserConfig, env: ConfigEnv) => ...>
  configEnvironment?: ObjectHook<(this: ConfigPluginContext, name: string, config: EnvironmentOptions, env: ConfigEnv) => ...>
  configResolved?: ObjectHook<(this: MinimalPluginContextWithoutEnvironment, config: ResolvedConfig) => void | Promise<void>>
  configureServer?: ObjectHook<ServerHook>
  configurePreviewServer?: ObjectHook<PreviewServerHook>
  transformIndexHtml?: IndexHtmlTransform
  handleHotUpdate?: ObjectHook<(this: MinimalPluginContextWithoutEnvironment, ctx: HmrContext) => ...>
  hotUpdate?: ObjectHook<(this: MinimalPluginContext & { environment: DevEnvironment }, options: HotUpdateOptions) => ...>
  buildApp?: ObjectHook<BuildAppHook>
}

这个继承关系揭示了一个重要的设计决策:Vite 没有发明新的插件格式,而是在 Rolldown 的基础上做增量扩展。这意味着所有 Rolldown/Rollup 插件天然兼容 Vite(前提是它们不强耦合于打包阶段的输出钩子),而 Vite 插件也可以在 Rolldown 构建中直接使用。

4.1.2 三个关键控制字段

enforce 决定插件在全局管线中的位置层级。源码中对此有明确注释:

typescript
/**
 * Plugin invocation order:
 * - alias resolution
 * - `enforce: 'pre'` plugins
 * - vite core plugins
 * - normal plugins
 * - vite build plugins
 * - `enforce: 'post'` plugins
 * - vite build post plugins
 */
enforce?: 'pre' | 'post'

apply 控制插件在哪个命令阶段激活。它支持两种形式:字符串字面量 'serve''build',以及一个接收完整配置的判断函数。这个函数形式让插件可以根据任意条件决定是否启用——比如仅在特定环境变量存在时激活。

applyToEnvironment 是 Environment API 引入的新字段。它在每个环境初始化时被调用,返回 false 则该插件在此环境中不注册,返回 true 则正常注册,返回一个 PluginOption 则用返回的插件替代原插件:

typescript
// src/node/plugin.ts
export async function resolveEnvironmentPlugins(
  environment: PartialEnvironment,
): Promise<Plugin[]> {
  const environmentPlugins: Plugin[] = []
  for (const plugin of environment.getTopLevelConfig().plugins) {
    if (plugin.applyToEnvironment) {
      const applied = await plugin.applyToEnvironment(environment)
      if (!applied) {
        continue  // 跳过此插件
      }
      if (applied !== true) {
        // 用返回的插件替代原插件
        environmentPlugins.push(
          ...((await asyncFlatten(arraify(applied))).filter(Boolean) as Plugin[]),
        )
        continue
      }
    }
    environmentPlugins.push(plugin)
  }
  return environmentPlugins
}

这种设计允许框架(如 Nuxt、Astro)为不同的运行环境提供完全不同的插件实现,而用户无需感知这种差异。

4.1.3 Hook 的两种形态

每个 Hook 都支持两种形态:函数形式和对象形式。对象形式通过 ObjectHook 类型表达:

typescript
// 函数形式
{
  name: 'my-plugin',
  resolveId(source, importer) { ... }
}

// 对象形式——可以指定 order 和 filter
{
  name: 'my-plugin',
  resolveId: {
    order: 'pre',
    filter: { id: /\.custom$/ },
    handler(source, importer) { ... }
  }
}

getHookHandler 工具函数负责统一这两种形态:

typescript
// src/node/plugins/index.ts
export function getHookHandler<T extends ObjectHook<Function>>(
  hook: T,
): HookHandler<T> {
  return (typeof hook === 'object' ? hook.handler : hook) as HookHandler<T>
}

4.2 Hook 分类体系

Vite 的 Hook 可以按生命周期阶段分为四大类。理解这个分类是编写高质量插件的基础。

4.2.1 配置钩子(Config Hooks)

钩子调用时机作用
config配置解析前修改或扩展用户配置
configEnvironment每个环境配置解析时修改特定环境的配置
configResolved配置解析完成后读取最终配置,通常用于存储引用

配置钩子在所有其他操作之前执行,且只执行一次(不按环境重复)。config 钩子的返回值会与现有配置深度合并,这使得插件可以安全地注入默认值而不覆盖用户的显式设置。

4.2.2 服务器钩子(Server Hooks)

configureServer 在开发服务器创建后、内部中间件安装前调用。它的独特之处在于返回值:

typescript
export type ServerHook = (
  this: MinimalPluginContextWithoutEnvironment,
  server: ViteDevServer,
) => (() => void) | void | Promise<(() => void) | void>

如果返回一个函数,该函数会被收集为"后置钩子"(post hook),在所有内部中间件安装完成后执行。这个机制让插件可以选择在内部中间件之前或之后注入自定义中间件。

4.2.3 通用钩子(Universal Hooks)

通用钩子在开发和构建中都会执行,它们直接来自 Rollup/Rolldown 的插件接口:

  • resolveId:将模块标识符解析为文件路径。采用 hookFirst 策略——第一个返回非空结果的插件获胜
  • load:加载模块内容。同样是 hookFirst
  • transform:转换模块代码。采用链式调用——每个插件的输出作为下一个插件的输入

4.2.4 开发专有钩子

hotUpdate 是 Environment API 中新增的钩子,替代旧的 handleHotUpdate。关键区别在于 hotUpdatethis 上下文包含 environment 属性,可以访问当前环境的信息。这使得同一个插件可以为不同环境实现不同的 HMR 策略。

4.3 内置插件的注册顺序

src/node/plugins/index.ts 中的 resolvePlugins 函数定义了所有内置插件和用户插件的精确注册顺序。这是 Vite 最重要的架构决策之一——插件的顺序直接决定了模块的处理流水线。

typescript
// src/node/plugins/index.ts
export async function resolvePlugins(
  config: ResolvedConfig,
  prePlugins: Plugin[],
  normalPlugins: Plugin[],
  postPlugins: Plugin[],
): Promise<Plugin[]> {
  const isBuild = config.command === 'build'
  const isBundled = config.isBundled
  const isWorker = config.isWorker

  return [
    // ======= 预处理阶段 =======
    !isBundled ? optimizedDepsPlugin() : null,
    !isWorker ? watchPackageDataPlugin(config.packageCache) : null,
    !isBundled ? preAliasPlugin(config) : null,
    aliasPlugin(...),                    // 路径别名解析

    // ======= 用户 pre 插件 =======
    ...prePlugins,

    // ======= Vite 核心插件 =======
    modulePreloadPolyfillPlugin(config),
    ...oxcResolvePlugin(...),            // 模块解析(核心)
    htmlInlineProxyPlugin(config),
    cssPlugin(config),
    esbuildBannerFooterCompatPlugin(config),
    oxcRuntimePlugin(),
    oxcPlugin(config),                   // OXC 转换
    nativeJsonPlugin(...),               // JSON 处理
    wasmHelperPlugin(),
    webWorkerPlugin(config),
    assetPlugin(config),
    forwardConsolePlugin(...),

    // ======= 用户 normal 插件 =======
    ...normalPlugins,

    // ======= 后处理阶段 =======
    nativeWasmFallbackPlugin(),
    definePlugin(config),
    cssPostPlugin(config),
    buildHtmlPlugin(config),
    workerImportMetaUrlPlugin(config),
    assetImportMetaUrlPlugin(config),
    ...buildPlugins.pre,
    dynamicImportVarsPlugin(config),
    importGlobPlugin(config),

    // ======= 用户 post 插件 =======
    ...postPlugins,

    // ======= 构建后处理 =======
    ...buildPlugins.post,

    // ======= 开发服务器专有(永远在最后)=======
    ...(isBundled ? [] : [
      clientInjectionsPlugin(config),
      cssAnalysisPlugin(config),
      importAnalysisPlugin(config),
    ]),
  ].filter(Boolean) as Plugin[]
}

基于 VitePress 构建