Vite 插件开发
js // my-plugin.js export default function myVitePlugin(options = {}) { return { name: 'vite-plugin-my', // 必须,用于标识和调试 // 接下来可以定义各种钩子... } }
### 钩子(Hooks)简介
钩子就是插件在特定时机执行的函数。Vite 的钩子可以分为两大类:
- **Rollup 共享钩子**:如 `buildStart`、`resolveId`、`load`、`transform`、`buildEnd` 等,在开发服务和生产构建中都会执行。
- **Vite 独有钩子**:如 `config`、`configResolved`、`configureServer`、`transformIndexHtml`、`handleHotUpdate` 等,只在与 Vite 开发交互时有效。
## 搭建开发环境
### 创建插件项目
推荐使用 npm 或 yarn 初始化一个独立的项目:
```bash
mkdir vite-plugin-demo
cd vite-plugin-demo
npm init -y
配置 package.json
插件的包名通常以 vite-plugin- 开头,便于被社区发现。同时需要配置入口文件:
{
"name": "vite-plugin-my-demo",
"version": "1.0.0",
"main": "src/index.js",
"files": ["src"],
"keywords": ["vite-plugin"],
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
建议在插件项目中创建一个 playground/ 目录,内嵌一个简易的 Vite 项目用于本地测试。
第一个插件:打印文件列表
让我们实现一个最简单的插件,它在构建开始时打印出所有入口文件的路径。
使用 buildStart 钩子
buildStart 是一个 Rollup 钩子,在每次构建开始前调用,可接收一个 options 参数,包含完整的构建配置。
// src/index.js
export default function fileListPlugin() {
return {
name: 'vite-plugin-file-list',
buildStart(options) {
console.log('本次构建的入口文件列表:');
if (options.input) {
const inputs = Array.isArray(options.input) ? options.input : [options.input];
inputs.forEach(input => console.log(` - ${input}`));
}
}
}
}
访问和过滤模块
你还可以在 moduleParsed 钩子中进一步分析已经被解析的模块信息,例如收集所有 CSS 模块的路径。
moduleParsed(moduleInfo) {
if (moduleInfo.id.endsWith('.css')) {
console.log('发现 CSS 模块:', moduleInfo.id);
}
}
深入钩子机制
通用钩子与 Vite 专用钩子
- 通用钩子:在开发模式和生产构建时均会调用,如
resolveId、load、transform。 - Vite 专用钩子:
- 开发专用:
configureServer、handleHotUpdate仅在vite dev时调用。 - 构建专用:
renderChunk、generateBundle等 Rollup 输出钩子仅在vite build时调用。
- 开发专用:
钩子执行顺序
了解钩子的顺序有助于处理数据依赖。大致流程如下:
- 用户配置阶段:
config→configResolved - 开发服务器启动:
configureServer - 模块解析加载(循环):
resolveId→load→transform - 构建阶段:
buildStart→ 上述解析 →renderChunk→generateBundle→writeBundle→buildEnd - 关闭阶段:
closeBundle、closeWatcher
虚拟模块与 ID 解析
虚拟模块是插件动态生成的“文件”,它在磁盘上并不存在,但对构建系统可见。常用于注入运行时配置、虚拟数据等。
resolveId 钩子
resolveId 用来拦截并自定义模块 ID 的解析。如果返回一个特殊字符串(如 \0virtual:my-module),就可以让后续的 load 钩子来处理该虚拟模块。
resolveId(id) {
if (id === 'virtual:config') {
return '\0virtual:config'; // \0 前缀约定,避免与其他插件冲突
}
return null; // 继续默认解析
}
load 钩子
load 钩子负责返回模块的源代码。当 id 匹配到我们解析出来的虚拟模块 ID 时,就可以返回自定义内容。
load(id) {
if (id === '\0virtual:config') {
return `export default ${JSON.stringify({ base: '/', env: 'dev' })};`;
}
return null;
}
示例:创建虚拟模块
用户代码中可以直接引入:
import config from 'virtual:config';
console.log(config.base); // '/'
转换与代码处理
transform 是使用最频繁的钩子之一,它在每个模块被加载后执行,可用于修改源码、进行预编译等操作。
transform 钩子
transform(code, id) {
if (id.endsWith('.myext')) {
// 将自定义文件转为 JS 模块
const transformed = `export default ${JSON.stringify(code.trim())}`;
return {
code: transformed,
map: null // 可选 source map
};
}
}
使用 @babel/core 或其他工具
你可以在 transform 中调用任何代码处理器,例如 Babel 转译、PostCSS 处理等。但注意:如果是对 .js/.ts 进行大量转换,建议优先使用 Vite 内置的 esbuild 或让用户配置相应的原生插件,避免过度影响性能。
import { transformAsync } from '@babel/core';
transform(code, id) {
if (id.endsWith('.js')) {
const result = await transformAsync(code, { /* babel config */ });
return { code: result.code };
}
}
示例:自动引入 CSS 模块
假设我们想让所有 .global.css 文件自动增加 index.css 的引用:
transform(code, id) {
if (id.endsWith('.global.css')) {
const injectedCode = `@import './index.css';\n${code}`;
return { code: injectedCode };
}
}
处理 HTML 与静态资源
transformIndexHtml 钩子
这个钩子可以修改 Vite 服务的 index.html 内容(字符串形式),常用于注入脚本、meta 标签或替换占位符。
transformIndexHtml(html, ctx) {
// 在 <head> 前注入自定义数据
return html.replace(
'</head>',
`<script>window.__MY_PLUGIN__ = { version: '1.0' }</script></head>`
);
}
也可以返回一个包含 { html, tags } 的对象,使用 Vite 的 tags 约定来注入资源。
configureServer 与中间件
通过 configureServer 可以访问 Vite 的开发服务器实例,从而添加自定义中间件,实现 API 代理、模拟数据等功能。
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url.startsWith('/api/custom')) {
res.statusCode = 200;
res.end(JSON.stringify({ message: 'Hello from plugin' }));
return;
}
next();
});
}
插件配置与选项
接受用户选项
在插件工厂函数中接收 options 参数,并在返回的配置中使用 config 钩子读取最终的用户配置。
export default function myPlugin(options = { enableLog: true }) {
return {
name: 'vite-plugin-demo',
config(config, env) {
// 访问最终合并的 vite.config.js
if (options.enableLog) {
console.log('当前命令:', env.command);
}
}
}
}
使用 config 钩子修改配置
config 钩子可以直接返回一个配置对象,来合并或覆盖用户原有的 Vite 配置。
config() {
return {
define: {
__PLUGIN_VERSION__: JSON.stringify('1.0.0')
},
resolve: {
alias: {
'@my-lib': '/path/to/lib'
}
}
}
}
注意:在 config 钩子中返回的值会被深合并,谨慎处理数组类配置。
发布与调试
本地测试
在插件项目根目录创建一个 example/ 或 playground/ 目录:
vite-plugin-demo/
├── src/
│ └── index.js
├── playground/
│ ├── index.html
│ ├── main.js
│ └── vite.config.js (引用本地插件)
└── package.json
在 playground/vite.config.js 中通过相对路径引用插件:
import myPlugin from '../src/index.js';
export default {
plugins: [myPlugin({ /* options */ })]
};