1. vite 插件介绍
-
服务器启动阶段 :
options
和buildStart
钩子会在服务启动时被调用。 -
请求响应阶段 : 当浏览器发起请求时,Vite 内部依次调用
resolveId
、load
和transform
钩子。 -
服务器关闭阶段 : Vite 会依次执行
buildEnd
和closeBundle
钩子。 -
moduleParsed: 模块解析后触发,允许你修改模块的元数据。 在开发阶段不被调用,因为 Vite 使用的是热重载和即时模块更新,不需要完整的模块解析阶段。
-
renderChunk:生成输出块时调用,允许你自定义生成的代码块。 开发阶段不调用,因为 Vite 主要依赖浏览器的即时更新,而非生成最终的打包输出。
1.1 强制插件排序
ts
import typescript2 from 'rollup-plugin-typescript2'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
{
...typescript2(),
apply: 'build',
},
{
...image(),
enforce: 'pre',
},
],
})
- pre: 在vite核心插件之前调用这个插件
- post: 在vite核心插件之后调用这个插件
- 默认:在 Vite 核心插件之后调用该插件
1.2 情景应用
- 开发 (serve) 和生产 (build) 默认都会调用
- apply 属性指明它们仅在 'build' 或 'serve' 模式时调用:
注意:名字 vite-plugin-xxx
2. 虚拟模块
- 虚拟模块就像是你凭空创造出来的 JS 文件,不在电脑上真实存在,但可以被其他模块像普通文件一样导入使用。
适用场景 - 动态生成配置 运行时变量注入 按需生成工具函数
2.1
js
import type { Plugin } from 'vite'
// 定义虚拟模块ID
const virtualModuleId = 'virtual:fibonacci'
const resolvedVirtualModuleId = '\0' + virtualModuleId
export default function virtualFibPlugin(): Plugin {
return {
name: 'vite-plugin-virtual-fib',
// 解析虚拟模块ID
resolveId(id) {
console.log(id, 'resolveId')
if (id === virtualModuleId) {
return resolvedVirtualModuleId
}
},
// 加载虚拟模块内容
load(id) {
console.log(id, 'load')
if (id === resolvedVirtualModuleId) {
return `
// 斐波那契数列实现
export function fib(n) {
return n <= 1 ? n : fib(n - 1) + fib(n - 2)
}
// 记忆化版本
export function memoFib(n, memo = {}) {
if (n in memo) return memo[n]
if (n <= 1) return n
memo[n] = memoFib(n - 1, memo) + memoFib(n - 2, memo)
return memo[n]
}
`
}
}
}
}
注意1:加 \0 公共插件使用 virtual:插件名 格式(如 virtual:posts)
注意2: 虚拟模块在生产的时候 最好显示配置一下
js
export default {
plugins: [virtualPlugin()],
build: {
rollupOptions: {
plugins: [virtualPlugin()] // 显式注册
}
}
}
🤔:不配置啥后果 虚拟模块在生产环境中的可用性取决于具体场景
-
基本虚拟模块(仅生成静态内容):
- 通常可以工作:如果插件只是生成简单的静态内容
- 原因:Vite 会继承主插件数组中的插件配置
-
复杂虚拟模块(依赖特定钩子或转换):
- ❌ 可能失败:如果插件依赖 build 阶段的特定钩子
- 报错表现:Cannot find module 'virtual:xxx' 或生成的内容不正确
3. vite特有钩子
- config 在解析 Vite 配置前调用。钩子接收原始用户配置
- configResolved 在解析 Vite 配置后调用 使用这个钩子读取和存储最终解析的配置
- configureServer 是用于配置开发服务器的钩子
- configurePreviewServer 用于定制预览服务器的钩子,类似configureServer 但专门用vite preview 命令启动的预览服务器。
- transformIndexHtml 专门用来修改 index.html 文件的钩子
- handleHotUpdate 自定义热模块替换(HMR)行为的钩子
3.1 配置处理钩子
config - 修改配置
js
config(config, { command, mode }) {
// command: 'serve'开发模式 | 'build'生产模式
// mode: 'development' | 'production' | 自定义模式
if (command === 'serve') {
return {
server: {
port: 3000, // 修改开发服务器端口
open: true // 自动打开浏览器
}
}
}
// 生产环境配置
return {
build: {
minify: 'terser'
}
}
}
configResolved - 配置确认
js
const myPlugin = () => {
let viteConfig // 用于存储配置
return {
name: 'my-plugin',
// configResolved 钩子
configResolved(resolvedConfig) {
// 存储最终解析的配置
viteConfig = resolvedConfig
console.log('当前运行模式:', viteConfig.command)
},
// 在其他钩子中使用配置
transform(code, id) {
if (viteConfig.command === 'serve') {
console.log('开发模式处理:', id)
} else {
console.log('生产构建处理:', id)
}
return code
}
}
}
js
configResolved(config) {
this.isDev = config.command === 'serve'
this.isProduction = !this.isDev
},
load(id) {
if (id === virtualModuleId) {
return this.isDev
? `export const mode = 'development'`
: `export const mode = 'production'`
}
}
3.2 开发阶段钩子
-
configureServer 开发服务器的钩子
- 添加新的功能比如中间件 或者文件监听
js
configureServer(server: ViteDevServer): void | (() => void)
参数 server 包含以下重要属性:
- middlewares: Connect 中间件实例
- httpServer: 底层 HTTP 服务器
- watcher: 文件监听器
- ws: WebSocket 服务器
- transformRequest(): 用于转换模块内容
eg
js
import type { Plugin } from 'vite'
import type { ViteDevServer } from 'vite'
export default function vitePluginTest(): Plugin {
let devServer: ViteDevServer
return {
name: 'vite-plugin-test',
configureServer(server) {
devServer = server
// 监听src目录下的所有文件变化(修正了watcher.add的用法)
server.watcher.add('src/**/*')
// 添加API接口
server.middlewares.use('/api/data', (_, res) => {
res.setHeader('Content-Type', 'application/json')
res.end(
JSON.stringify({
data: '测试数据',
timestamp: Date.now()
})
)
})
// 请求日志中间件
server.middlewares.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] 请求: ${req.method} ${req.url}`)
next()
})
// 监听文件变化(修正了监听逻辑)
server.watcher.on('change', (file) => {
console.log(`文件发生变化: ${file}`)
if (file.startsWith('src/')) {
console.log('src目录文件变化,刷新页面')
server.ws.send({
type: 'full-reload',
path: '*'
})
}
})
},
handleHotUpdate({ file }) {
if (file.endsWith('.tsx')) {
console.log('TSX文件修改,触发全量刷新')
devServer.ws.send({
type: 'full-reload',
path: '*'
})
}
}
}
}
注意
js
configureServer(server) {
// 这个会在Vite中间件之前执行
server.middlewares.use(...)
// 返回的函数会在Vite中间件之后执行
return () => {
server.middlewares.use(...)
}
}
- transformIndexHtml 可以定制html内容
可以 自动插入标签 修改内容
js
transformIndexHtml(html, ctx) {
const tags: Array<{
tag: string
injectTo: 'head' | 'body' | 'head-prepend' | 'body-prepend'
children?: string
attrs?: Record<string, string>
}> = []
const isDev = !!ctx.server
if (isDev) {
tags.push({
tag: 'script',
injectTo: 'body-prepend',
children: 'console.log("开发模式已启动")'
})
} else {
tags.push({
tag: 'meta',
injectTo: 'head',
attrs: { name: 'robots', content: 'index,follow' }
})
}
const newHtml = html.replace(
'<title>Vite + React + TS</title>',
'<title>我的定制应用</title>'
)
// 返回符合 Vite 要求的类型
return {
html: newHtml,
tags
}
},
- handleHotUpdate模块热更新
js
handleHotUpdate(ctx) {
if (ctx.file.endsWith('.tsx')) {
console.log('TSX文件修改,触发全量刷新')
devServer.ws.send({
type: 'full-reload',
path: '*'
})
}
// 2. 只处理项目文件,忽略node_modules
return ctx.modules.filter((module) => !module?.id?.includes('node_modules'))
}
graph TD
A[采购食材 config] --> B[确认菜单 configResolved]
B --> C[员工培训 configureServer]
C --> D[开门营业 buildStart]
D --> E{客人点餐}
E -->|HTML订单| F[摆盘 transformIndexHtml]
E -->|食材请求| G[接单→备货→烹饪 resolveId→load→transform]
E -->|变更需求| H[厨房监控 handleHotUpdate]
H --> I{是否打烊}
I -->|是| J[清理 buildEnd → 关店 closeBundle]
4. 例子
4.1 自动引入antd组件
一个自动引入插件 可以自动为你的项目按需导入 API,无需手动编写 import 语句,自动导入,按需加载。
- resolvers 是 unplugin-auto-import 中的一个高级配置选项,用于自定义解析自动导入的组件或工具函数的方式。
思考: 问题
- 我们如果开发的时候 使用antd组件 可以使用这个插件 导入很多antd组件,让vite.config文件内容很多 不太美观
- 并且antd组件中没有默认的字母开头 比如 Button 可能会造成与其他自定义组件冲突,想一下是不是可以给antd 组件加个前缀 就像AButton
所以的插件功能主要实现 自动导入全部antd组件 并且可以自定义前缀
js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import AutoImport from 'unplugin-auto-import/vite'
import antdResolver from './unplugin-auto-import-antd'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
AutoImport({
imports: [
'react',
{
// antd: [
// 'Button',
// 'Input'
// ]
}
],
resolvers: [
// antdResolver({
// prefix: 'A', // 可选:为所有组件添加 A 前缀
// packageName: 'antd' // 可选:默认为 'antd'
// }),
{
type: 'component',
resolve: (name: string) => {
console.log('resolve', name)
const supportedComponents = ['AButton', 'Button', 'AInput', 'Table'] // 扩展这个列表
if (supportedComponents.includes(name)) {
return {
from: 'antd',
name: 'Input',
as: `${name}` // 统一添加A前缀
}
}
return undefined
}
}
],
dts: true, // 生成类型声明文件
eslintrc: {
enabled: true // 生成 eslint 配置
}
})
]
})
4.2 svg直接作为组件导入
ts
import type { Plugin } from 'vite'
import fs from 'node:fs/promises'
import { transform } from '@svgr/core'
import { transform as esbuildTransform } from 'esbuild'
interface SvgrOptions {
defaultExport?: 'url' | 'component' // 导出类型:URL字符串 或 React组件
svgrOptions?: Record<string, any> // 自定义SVGR配置
}
export default function svgrPlugin(options: SvgrOptions = {}): Plugin {
// 设置默认值:默认导出为组件,空SVGR配置
const { defaultExport = 'component', svgrOptions = {} } = options
return {
name: 'vite-plugin-svgr',
// transform 钩子:转换文件内容
async transform(_, id) {
// 只处理 .svg 文件
if (!id.endsWith('.svg')) return
try {
// 1. 读取 SVG 文件内容
const svg = await fs.readFile(id, 'utf-8')
// 2. 使用 SVGR 将 SVG 转换为 React 组件代码
const componentCode = await transform(
svg, // SVG 原始内容
{
...svgrOptions, // 用户自定义配置
// 核心插件配置开始
plugins: [
'@svgr/plugin-jsx', // 转换 SVG 为 JSX
'@svgr/plugin-prettier' // 格式化生成的代码
],
// 其他重要配置
typescript: true, // 生成 TS 兼容代码
jsxRuntime: 'automatic', // 使用新版 JSX 运行时
exportType: 'named', // 使用命名导出
// 自定义模板:控制组件输出结构
template: ({ componentName, jsx }, { tpl }) => {
return tpl`
const ${componentName} = (props) => ${jsx};
export { ${componentName} };
`
}
},
{ componentName: 'ReactComponent' } // 设置组件名称
)
// 3. 清理生成的代码
let jsCode = componentCode
.replace(/^\/\*.*?\*\/\s*/gms, '') // 移除注释
.replace(/\n+/g, '\n') // 压缩空行
.trim()
// 4. 处理导出逻辑
if (defaultExport === 'url') {
// URL 模式:默认导出 SVG 路径
jsCode = `
${jsCode}
export default ${JSON.stringify(id)};
`.trim()
} else {
// 组件模式:默认导出 React 组件
jsCode = `
${jsCode}
export default ReactComponent;
`.trim()
}
// 5. 使用 esbuild 转换 JSX 为浏览器可执行代码
const result = await esbuildTransform(jsCode, {
loader: 'jsx', // 指定为 JSX 类型
jsx: 'automatic', // 使用新版 JSX 转换
sourcefile: id, // 源文件路径(用于 sourcemap)
format: 'esm', // 输出 ESM 格式
target: 'es2020', // 目标 ES 版本
logLevel: 'silent' // 不输出日志
})
// 6. 返回转换后的代码
return {
code: result.code,
map: result.map || null
}
} catch (error) {
// 错误处理:回退到原始 SVG 路径导出
console.error(`SVG转换失败 [${id}]:`, error)
return {
code: `export default ${JSON.stringify(id)};`,
map: null
}
}
}
}
}