Vite 插件架构概览
Vite 的插件系统建立在 Rollup 插件接口之上,同时扩展了一些 Vite 特有的钩子。这意味着大部分 Rollup 插件可以直接在 Vite 中使用,而 Vite 插件则通过额外的钩子来处理开发服务器和 HMR 等场景。理解这套架构是开发高质量插件的基础。
一个 Vite 插件本质上是一个对象,它包含一个 name 属性和各种钩子函数。最简单的插件结构如下:
// vite.config.js
import { defineConfig } from 'vite'
function myPlugin() {
return {
name: 'my-plugin', // 必填,插件名称
// 各种钩子...
}
}
export default defineConfig({
plugins: [myPlugin()]
})
插件生命周期与钩子
Vite 插件的钩子可以分为两类:Rollup 兼容钩子和 Vite 独有钩子。下面按执行顺序梳理核心钩子。
Rollup 兼容钩子
- config — 在 Vite 配置被解析之前修改配置。可以接收原始配置并返回部分配置的修改。
- configResolved — 配置解析完成后触发,用于读取最终配置。
- buildStart — 构建开始时调用,适合做初始化工作。
- resolveId — 自定义模块解析逻辑。可以将虚拟模块 ID 映射到实际内容。
- load — 自定义模块加载逻辑。可以返回模块的源代码。
- transform — 对模块内容进行转换。这是最常用的钩子,比如编译 TypeScript、处理 CSS 等。
- buildEnd — 构建结束时调用,适合做清理或报告工作。
Vite 独有钩子
- configureServer — 配置开发服务器,可以添加中间件。
- transformIndexHtml — 转换 index.html。
- handleHotUpdate — 处理 HMR 更新。
实战:编写一个文件头注释插件
下面我们编写一个实用的插件,它会在每个 JS/TS 文件构建产物的顶部自动添加版权注释头。
// plugins/vite-plugin-banner.js
export default function vitePluginBanner(options = {}) {
const {
banner = '// Built with Vite',
include = /\.(js|ts|jsx|tsx)$/,
} = options
return {
name: 'vite-plugin-banner',
// 构建模式下生效
apply: 'build',
// 在 transform 阶段注入注释
transform(code, id) {
if (!include.test(id)) return null
// 排除 node_modules
if (id.includes('node_modules')) return null
return {
code: banner + '\n' + code,
map: null, // 不生成 sourcemap
}
},
buildEnd() {
console.log('[banner] 版权注释注入完成')
}
}
}
在配置中使用它:
// vite.config.js
import banner from './plugins/vite-plugin-banner'
export default defineConfig({
plugins: [
banner({
banner: '// Copyright 2026 MyCompany. All rights reserved.',
})
]
})
实战:Markdown 转 HTML 插件
一个更复杂的例子是将 Markdown 文件作为 Vue 组件导入。这个插件演示了 resolveId、load 和 transform 的配合使用。
// plugins/vite-plugin-md.js
import { marked } from 'marked'
import fs from 'node:fs'
export default function vitePluginMarkdown() {
const virtualPrefix = '\0virtual:md:'
return {
name: 'vite-plugin-markdown',
// 拦截 .md 文件的解析
resolveId(id) {
if (id.endsWith('.md')) {
return virtualPrefix + id
}
},
// 加载 Markdown 内容并转换为 HTML
load(id) {
if (!id.startsWith(virtualPrefix)) return null
const filePath = id.slice(virtualPrefix.length)
const raw = fs.readFileSync(filePath, 'utf-8')
const html = marked(raw)
// 导出为 Vue 函数组件
return `export default {
render() {
return {
template: ${JSON.stringify(`${html}`)}
}
}
}`
},
// 支持 HMR
handleHotUpdate({ file, server }) {
if (file.endsWith('.md')) {
server.ws.send({
type: 'full-reload',
path: '*',
})
}
}
}
}
HMR 热更新处理
handleHotUpdate 钩子是 Vite 插件的特色之一。它允许插件自定义热更新行为。其上下文对象包含以下关键信息:
handleHotUpdate({ file, server, modules, read, timestamp }) {
// file — 变更的文件路径
// server — Vite 开发服务器实例
// modules — 受影响的模块列表
// read — 读取文件内容的异步函数
// timestamp — 更新时间戳
// 返回空数组可以阻止默认的 HMR 行为
if (file.endsWith('.custom')) {
return []
}
// 返回模块数组可以自定义受影响的模块范围
return modules
}
插件执行顺序
插件顺序至关重要。Vite 提供了 enforce 属性来控制执行顺序:
enforce: 'pre'— 在核心插件之前执行(如别名解析、环境变量注入)。- 默认 — 在核心插件之后执行。
enforce: 'post'— 在构建插件之后执行(如压缩、分析)。
export default defineConfig({
plugins: [
{ name: 'a', enforce: 'pre' }, // 第 1 执行
{ name: 'b' }, // 第 3 执行(默认)
{ name: 'c', enforce: 'post' }, // 第 5 执行
{ name: 'd', enforce: 'pre' }, // 第 2 执行
{ name: 'e' }, // 第 4 执行
]
})
另外,apply 属性可以控制插件仅在特定模式下生效:apply: 'serve' 仅在开发服务器中生效,apply: 'build' 仅在构建时生效。
小结
Vite 的插件系统提供了强大的扩展能力。从简单的代码转换到完整的文件类型支持,都可以通过插件实现。开发插件时需要注意:始终提供唯一的 name;正确处理 sourcemap 以保证调试体验;合理使用 enforce 和 apply 控制执行时机;对于开发体验,务必实现 handleHotUpdate 钩子。掌握这些要点,你就能开发出高质量的 Vite 插件。