好的 insomniac!下面用一次到位的方式,把 Vite 的插件机制讲清楚:从“能做什么”、到“生命周期/执行顺序”、到“常用钩子与场景”、再到“几个小而全的示例插件”,最后给你一份最佳实践与避坑清单。读完就能自己写出靠谱的 Vite 插件。

1)Vite 插件能做什么

  • 开发服务器(serve)打包(build) 两个阶段介入
  • 自定义 模块解析(别名/虚拟模块/跨端映射)
  • 自定义 加载 & 编译(把任意文件转成 JS/CSS,比如 Markdown/MDX、YAML、SVG、GLSL…)
  • 注入/改变 index.html(插入脚本、预加载、meta)
  • 参与 HMR(热更新)和 模块图(Module Graph) 的控制
  • 扩展 SSR & Preview 行为
  • 兼容 Rollup 插件(构建阶段 100% 兼容;开发阶段只支持 Vite 自己的扩展钩子)

2)插件结构与启用方式

// vite.config.ts
import type { Plugin } from 'vite'

function MyPlugin(options?: { /* ... */ }): Plugin {
  return {
    name: 'my-plugin',
    // enforce: 'pre' | 'post',       // 可调顺序
    // apply: 'serve' | 'build',      // 可限定环境
    // config / configResolved / configureServer / transformIndexHtml ...
  }
}

export default {
  plugins: [MyPlugin(/* opts */)]
}
  • enforce:
    • pre:在 Vite 内置核心插件之前运行(适合早期介入解析/替换)
    • 默认:普通阶段
    • post:在内置插件之后(适合做最终收尾或依赖前序产物)
  • apply: 只在 serve(开发)或 build(构建)阶段生效
  • 只想影响打包、且是纯 Rollup 能力 → 放到 build.rollupOptions.plugins 即可

3)执行顺序(直观版)

以常见钩子为主的顺序/行为:

开发阶段(dev server)

  1. config(合并用户配置,早期修改)
  2. configResolved(拿到最终 config)
  3. configureServer(拿到 Koa/Connect 中间件,能加自定义路由、代理)
  4. 模块请求时触发:a. resolveId(解析路径/别名/虚拟模块)b. load(读取文件内容或返回虚拟内容)c. transform(源码转换,如 TS/JSX/自定义语法)
  5. 首屏 HTML:transformIndexHtml
  6. 变更时:handleHotUpdate(定制 HMR 行为)
  7. 退出:buildEnd / closeBundle(dev 里很少用)

构建阶段(Rollup)

  1. config → configResolved
  2. buildStart
  3. 每个模块:resolveId → load → transform
  4. 打包产出:generateBundle
  5. 结束:buildEnd → closeBundle
注意:resolveId/load 是“谁先返回谁生效”,transform 是“流水线顺序串联”。

4)常用钩子速查

  • config(config, env):在用户配置合并前后做修改(如动态设定 alias、define)
  • configResolved(resolvedConfig):拿最终配置(比如拿 isProduction、root、base、logger)
  • configureServer(server):仅开发,可 server.middlewares.use() 增加路由、调试端点、返回虚拟文件等
  • transformIndexHtml(html|{ html, path }):注入 <script>、<link> 或改写 HTML
  • resolveId(source, importer, options):改写模块解析(别名、virtual: 前缀、跨包导入重定向)
  • load(id, options):返回源码字符串/源码与 map(虚拟模块或自定义读取)
  • transform(code, id, options):将任意文本转 JS/CSS,返回 { code, map }
  • handleHotUpdate(ctx):定制 HMR,可返回要重新加载的模块集
  • buildStart / buildEnd / generateBundle / closeBundle:构建期收尾、产物清单处理、生成额外文件等
  • configurePreviewServer(server):vite preview 阶段注入中间件(与 dev 不同的预览服务器)

5)重要参数与上下文

  • 钩子 options.ssr:区分 SSR 与非 SSR(不同处理逻辑)
  • 钩子 this 上下文(Rollup 语义):
    • this.parse(code):拿 AST
    • this.emitFile({ type:'asset'|'chunk', ... }):发射额外文件
    • this.addWatchFile(file):让 dev 监听额外文件
    • this.getModuleInfo(id):查询模块依赖信息
    • this.warn() / this.error():报告
  • 路径:用 import { normalizePath } from 'vite' 统一分隔符(Windows 友好)
  • 过滤文件:@rollup/pluginutils 的 createFilter(include, exclude) 很实用

6)几个“常见需求 → 最小可用”示例

A. 虚拟模块(virtual module)

// virtual:env 插件 —— 提供一个虚拟模块导出环境变量
import type { Plugin } from 'vite'
import { normalizePath } from 'vite'

export default function VirtualEnv(): Plugin {
  const VIRTUAL_ID = 'virtual:env'
  const RESOLVED = '\0' + VIRTUAL_ID  // Rollup 约定:内部/虚拟用 \0 前缀

  return {
    name: 'virtual-env',
    resolveId(id) {
      if (id === VIRTUAL_ID) return RESOLVED
    },
    load(id) {
      if (id === RESOLVED) {
        return `export const MODE = ${JSON.stringify(process.env.NODE_ENV)}`
      }
    }
  }
}

B. Markdown 作为模块(最常见 transform)

// 把 .md 转成导出 HTML 字符串的模块
import type { Plugin } from 'vite'
import { createFilter } from '@rollup/pluginutils'
import { marked } from 'marked'

export default function MarkdownAsHtml(opts?: { include?: any; exclude?: any }): Plugin {
  const filter = createFilter(opts?.include ?? ['**/*.md'], opts?.exclude)
  return {
    name: 'md-as-html',
    async transform(code, id) {
      if (!filter(id)) return
      const html = marked.parse(code)
      return {
        code: `export default ${JSON.stringify(html)};`,
        map: null
      }
    }
  }
}

C. 注入首屏脚本(transformIndexHtml)

// 给 index.html 注入性能监控脚本
export default function InjectPerf(): import('vite').Plugin {
  return {
    name: 'inject-perf',
    transformIndexHtml(html) {
      return {
        html,
        tags: [
          {
            tag: 'script',
            children: `console.log('[perf]', performance.now());`,
            injectTo: 'head' // 'head' | 'body' | 'head-prepend' | 'body-prepend'
          }
        ]
      }
    }
  }
}

D. HMR 精准更新(handleHotUpdate)

// 当某个外部配置文件变更时,主动触发依赖它的虚拟模块热更新
import fs from 'node:fs'

export default function ExternalConfigHMR(configPath: string) {
  return {
    name: 'external-config-hmr',
    configureServer(server) {
      server.watcher.add(configPath) // 让 chokidar 监听
    },
    handleHotUpdate(ctx) {
      if (ctx.file === configPath) {
        const mod = ctx.server.moduleGraph.getModuleById('\0virtual:env')
        if (mod) return [mod] // 仅更新这个模块,避免整页刷新
      }
    }
  }
}

E. 只在构建期生成额外文件(generateBundle)

// 打包后生成 manifest.json 摘要
export default function BuildManifest(): import('vite').Plugin {
  return {
    name: 'build-manifest',
    apply: 'build',
    generateBundle(_, bundle) {
      const files = Object.keys(bundle)
      this.emitFile({
        type: 'asset',
        fileName: 'assets-summary.json',
        source: JSON.stringify({ files }, null, 2)
      })
    }
  }
}

7)插件在 SSR/Preview/优化阶段的差异

  • SSR:很多钩子会带 options.ssr===true,例如 transform(code, id, { ssr: true }),你可以选择输出与浏览器不同的产物(如去掉 DOM 相关逻辑)
  • Preview(vite preview):与 dev 不同,configurePreviewServer 用来给静态预览加中间件
  • 依赖预构建(esbuild):如果你要影响 optimizeDeps 阶段(把第三方包预构建),需要在 optimizeDeps.esbuildOptions.plugins 里写 esbuild 插件(与 Vite 插件不同)

8)调试与性能

  • 打开日志:vite --debug plugin:my-plugin(或在 config.logger 使用)
  • 输出 Source Map:transform 时返回 map,利于调试
  • 充分利用 include/exclude早返回 避免无谓开销
  • 跨平台路径用 normalizePath;尽量用 path.posix 规范化
  • HMR 相关逻辑尽量 最小化更新范围(返回精确模块列表)

9)最佳实践清单

  • ✅ 给插件起 唯一且语义明确 的 name
  • ✅ 用 apply: 'serve'|'build' 控制生效环境
  • ✅ 用 enforce: 'pre'|'post' 明确与其他插件顺序关系(尤其和框架插件如 React/Vue)
  • ✅ 使用 createFilter() 做 include/exclude
  • ✅ 生成虚拟模块 id 时使用 \0 前缀避免与真实路径冲突
  • ✅ SSR 分支:if (options?.ssr) { ... }
  • ✅ 在 configureServer 里添加自定义 dev 中间件,而不是劫持外部服务器
  • ✅ 在构建阶段产出额外文件使用 emitFile / generateBundle
  • ❌ 不要在 transform 里做阻塞 I/O;异步为佳
  • ❌ 不要在 dev 阶段依赖只存在于 build 的产物(反之亦然)

Vite 插件机制