跳过正文
  1. 文章/

Vite 插件钩子详解:Build 与 Dev 模式

·1984 字·4 分钟·
hujiacheng
作者
hujiacheng
Front-end Developer / Strive To Become Better
目录

Vite 的插件系统基于 Rollup 插件接口扩展而来,分为通用钩子(build + dev 均可用)和 Vite 专属钩子(dev 专用或特定场景)。

一、钩子执行顺序总览
#

Dev 模式(vite dev)
#

config → configResolved → configureServer → buildStart
         ↓ (每次请求)
resolveId → load → transform → configureServer middleware

Build 模式(vite build)
#

config → configResolved → buildStart → resolveId → load
       → transform → moduleParsed → buildEnd → generateBundle
       → writeBundle → closeBundle

二、通用钩子(Universal Hooks)
#

这些钩子在 dev 和 build 两种模式下都会执行。

1. config
#

在解析 Vite 配置之前调用,可用于修改或扩展配置。

{
  name: 'my-plugin',
  config(config, { command, mode }) {
    // command: 'build' | 'serve'
    // mode: 'development' | 'production'
    if (command === 'build') {
      return {
        build: {
          sourcemap: true
        }
      }
    }
  }
}
  • 返回值:返回的对象会被深度合并进配置
  • 执行顺序:按插件顺序执行,支持 async

2. configResolved
#

在 Vite 配置解析完成后调用,此时配置已经完全确定,只读

{
  name: 'my-plugin',
  configResolved(resolvedConfig) {
    // 保存配置以供后续使用
    config = resolvedConfig
    isBuild = resolvedConfig.command === 'build'
  }
}
  • 典型用途:保存最终配置供其他钩子使用
  • 注意:不能在此修改配置

3. buildStart
#

构建开始时调用(dev 模式下每次冷启动或模块失效重载时触发)。

{
  name: 'my-plugin',
  buildStart(options) {
    // options 是 Rollup InputOptions
    console.log('构建开始,入口:', options.input)
  }
}

4. resolveId
#

解析模块路径,可用于自定义模块解析逻辑(虚拟模块的核心钩子)。

const virtualModuleId = 'virtual:my-module'
const resolvedVirtualModuleId = '\0' + virtualModuleId  // 约定加 \0 前缀

{
  name: 'virtual-module-plugin',
  resolveId(id) {
    if (id === virtualModuleId) {
      return resolvedVirtualModuleId  // 返回解析后的 id
    }
    // 返回 null/undefined 表示交给下一个插件处理
  }
}
  • 参数(source, importer, options)
  • 返回 false:表示将该 id 标记为外部模块(external)

5. load
#

根据模块 id 加载模块内容,可返回自定义代码。

{
  name: 'virtual-module-plugin',
  load(id) {
    if (id === resolvedVirtualModuleId) {
      return `export const msg = "Hello from virtual module!"`
    }
  }
}
  • 返回值:字符串(code)或 { code, map } 对象
  • 与 resolveId 配合实现虚拟模块

6. transform
#

对每个模块的源码进行转换,是最常用的钩子之一。

{
  name: 'transform-plugin',
  transform(code, id) {
    if (!id.endsWith('.vue')) return

    // 对代码进行转换
    const newCode = code.replace(/console\.log\(.*?\)/g, '')

    return {
      code: newCode,
      map: null  // 如有 sourcemap 则返回
    }
  }
}
  • 参数(code: string, id: string)
  • 返回 null/undefined:表示不做任何转换

7. buildEnd
#

构建结束时调用(不论成功还是失败)。

{
  name: 'my-plugin',
  buildEnd(error) {
    if (error) {
      console.error('构建失败:', error)
    }
  }
}

8. closeBundle
#

bundle 关闭时调用,是整个构建流程最后一个钩子

{
  name: 'my-plugin',
  async closeBundle() {
    // 清理临时文件、发送构建完成通知等
    await notifyDeploySystem()
  }
}

三、Build 专属钩子
#

1. renderStart
#

generateBundle 之前、开始渲染 chunk 时调用。

{
  renderStart(outputOptions, inputOptions) {
    console.log('输出目录:', outputOptions.dir)
  }
}

2. renderChunk
#

对每个输出 chunk 进行转换,类似 transform 但针对的是打包后的 chunk

{
  name: 'banner-plugin',
  renderChunk(code, chunk, options) {
    return {
      code: `/* My Library v1.0 */\n${code}`,
      map: null
    }
  }
}
  • chunk 信息:包含 chunk.fileName, chunk.imports, chunk.exports

3. generateBundle
#

生成 bundle 文件之前调用,可以增删改 bundle 中的文件。

{
  name: 'my-plugin',
  generateBundle(options, bundle) {
    // bundle 是一个对象,key 是文件名
    for (const [fileName, chunk] of Object.entries(bundle)) {
      if (fileName.endsWith('.js')) {
        // 修改输出内容
        chunk.code = chunk.code + '\n// generated by my-plugin'
      }
    }

    // 也可以新增文件
    this.emitFile({
      type: 'asset',
      fileName: 'version.json',
      source: JSON.stringify({ version: '1.0.0' })
    })
  }
}

4. writeBundle
#

bundle 写入磁盘之后调用(generateBundle 是写入前)。

{
  name: 'post-build-plugin',
  writeBundle(options, bundle) {
    console.log('文件已写入:', options.dir)
    // 适合做:压缩、上传 CDN、生成 manifest 等后处理
  }
}

四、Dev 专属钩子(Vite 独有)
#

1. configureServer
#

配置开发服务器,可以添加自定义中间件(基于 Connect)。

{
  name: 'my-plugin',
  configureServer(server) {
    // server: ViteDevServer 实例

    // 添加中间件(在 Vite 内部中间件之前)
    server.middlewares.use('/api/hello', (req, res) => {
      res.end(JSON.stringify({ message: 'Hello!' }))
    })

    // 返回函数 = 在 Vite 中间件之后执行
    return () => {
      server.middlewares.use((req, res, next) => {
        // 后置中间件
        next()
      })
    }
  }
}
  • server.moduleGraph:模块依赖图
  • server.watcher:文件监听器(chokidar)
  • server.ws:WebSocket 服务(HMR)

2. configurePreviewServer
#

配置 vite preview 预览服务器,用法与 configureServer 相同。

{
  name: 'my-plugin',
  configurePreviewServer(server) {
    server.middlewares.use('/health', (req, res) => {
      res.end('OK')
    })
  }
}

3. transformIndexHtml
#

转换 index.html 的内容,可注入标签、修改 HTML 结构。

{
  name: 'html-plugin',
  transformIndexHtml(html) {
    return html.replace(
      '<title>App</title>',
      '<title>My Custom App</title>'
    )
  }
}

// 高级用法:返回注入描述符
{
  name: 'inject-plugin',
  transformIndexHtml(html) {
    return {
      html,
      tags: [
        {
          tag: 'script',
          attrs: { src: '/analytics.js', defer: true },
          injectTo: 'body'  // 'head' | 'body' | 'head-prepend' | 'body-prepend'
        },
        {
          tag: 'meta',
          attrs: { name: 'description', content: 'My App' },
          injectTo: 'head'
        }
      ]
    }
  }
}

4. handleHotUpdate
#

自定义 HMR(热模块替换) 更新处理逻辑。

{
  name: 'hmr-plugin',
  handleHotUpdate({ file, server, modules, timestamp }) {
    if (file.endsWith('.json')) {
      // 手动使某些模块失效
      const mod = server.moduleGraph.getModuleById('/src/config.ts')
      if (mod) {
        server.moduleGraph.invalidateModule(mod)
        // 发送自定义 HMR 事件
        server.ws.send({
          type: 'custom',
          event: 'config-changed',
          data: {}
        })
        return []  // 返回空数组阻止默认 HMR 行为
      }
    }
    // 返回需要更新的模块列表,不返回则走默认逻辑
  }
}

五、钩子执行顺序控制
#

通过 enforce 属性控制插件执行顺序:

{
  name: 'my-plugin',
  enforce: 'pre',   // 在普通插件之前执行
  // enforce: 'post', // 在普通插件之后执行
  transform(code, id) { /* ... */ }
}

顺序为:pre 插件 → 普通插件 → Vite 内建插件 → post 插件

通过 apply 属性限制执行环境:

{
  name: 'build-only-plugin',
  apply: 'build',   // 只在 build 模式执行
  // apply: 'serve', // 只在 dev 模式执行

  // 也支持函数形式
  apply(config, { command }) {
    return command === 'build' && !config.build.ssr
  }
}

六、完整插件模板
#

export default function myPlugin(options = {}) {
  let viteConfig

  return {
    name: 'vite-plugin-my',
    enforce: 'pre',
    apply: 'build',

    // ---- 通用钩子 ----
    config(config, env) {},
    configResolved(resolved) { viteConfig = resolved },
    buildStart(options) {},
    resolveId(source, importer) {},
    load(id) {},
    transform(code, id) {},
    buildEnd(error) {},
    closeBundle() {},

    // ---- Build 专属 ----
    renderChunk(code, chunk) {},
    generateBundle(options, bundle) {},
    writeBundle(options, bundle) {},

    // ---- Dev 专属 ----
    configureServer(server) {},
    transformIndexHtml(html) {},
    handleHotUpdate(ctx) {},
  }
}

七、钩子速查表
#

钩子DevBuild说明
config修改配置
configResolved读取最终配置
buildStart构建开始
resolveId解析模块路径
load加载模块内容
transform转换模块代码
buildEnd构建结束
closeBundle关闭 bundle
renderChunk转换输出 chunk
generateBundle生成 bundle 前
writeBundle写入磁盘后
configureServer配置开发服务器
configurePreviewServer✅*配置预览服务器
transformIndexHtml转换 HTML
handleHotUpdate自定义 HMR

configurePreviewServer 仅在 vite preview 时触发

相关文章