Vite的插件系统基于Rollup插件接口扩展,在开发环境通过esbuild和原生ESM实现毫秒级热更新,生产构建则走Rollup打包流程。理解Vite插件Hook生命周期和虚拟模块机制,是开发自定义转译管线、集成非标资源处理的基础。本文从插件架构到实战开发,覆盖完整流程。
Vite插件架构与Hook生命周期
Vite插件是一个返回插件的函数,Hook分为Rollup通用Hook和Vite专用Hook:
// Vite插件基本结构
export default function myPlugin(options) {
return {
name: 'vite-plugin-my',
// ===== Vite专用Hook =====
// 配置解析前调用,可修改Vite配置
config(config, { command }) {
return {
resolve: {
alias: {
'@': '/src'
}
}
}
},
// 配置解析后调用,可读取最终配置
configResolved(config) {
console.log('Build target:', config.build.target)
},
// 自定义Dev Server中间件
configureServer(server) {
server.middlewares.use('/api/mock', (req, res) => {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ code: 200, data: [] }))
})
},
// 转换index.html
transformIndexHtml(html) {
return html.replace(
'',
''
)
},
// 自定义热更新处理
handleHotUpdate({ file, server }) {
if (file.endsWith('.md')) {
server.ws.send({ type: 'full-reload' })
}
},
// ===== Rollup通用Hook =====
// 解析模块ID,返回虚拟模块标记
resolveId(source, importer) {
if (source === 'virtual:my-module') {
return source // 返回source表示已解析
}
return null
},
// 加载模块内容
load(id) {
if (id === 'virtual:my-module') {
return `export default ${JSON.stringify(options.data)}`
}
return null
},
// 转译代码
transform(code, id) {
if (id.endsWith('.md')) {
const html = markdownToHtml(code)
return `export default ${JSON.stringify(html)}`
}
return null
},
// 生成产物后调用
generateBundle(opts, bundle) {
// 可修改或添加产物文件
}
}
}
Hook执行顺序:
开发环境:
config -> configResolved -> configureServer ->
[请求模块] resolveId -> load -> transform ->
[文件变更] handleHotUpdate
生产构建:
config -> configResolved ->
[构建阶段] resolveId -> load -> transform ->
[输出阶段] renderChunk -> generateBundle -> writeBundle -> closeBundle
自定义插件开发与虚拟模块导入
虚拟模块允许在代码中导入不存在的文件路径,通过插件动态生成内容:
// plugins/vite-plugin-virtual-routes.js
import fg from 'fast-glob'
import path from 'path'
export default function virtualRoutesPlugin(options = {}) {
const { pagesDir = 'src/pages', extensions = ['vue', 'tsx'] } = options
let moduleId = 'virtual:routes'
const resolvedId = '\0' + moduleId
return {
name: 'vite-plugin-virtual-routes',
enforce: 'pre', // 在其他插件之前执行
resolveId(id) {
if (id === moduleId) {
return resolvedId // \0前缀标记为虚拟模块
}
},
load(id) {
if (id !== resolvedId) return null
// 扫描页面目录生成路由配置
const files = fg.sync(`${pagesDir}/**/*.{${extensions.join(',')}}`, {
cwd: process.cwd()
})
const routes = files.map(file => {
const ext = path.extname(file)
const routePath = file
.replace(pagesDir + '/', '')
.replace(ext, '')
.replace(/\/g, '/')
.replace(/index$/, '')
.replace(/\[(\w+)\]/, ':$1') // [id] -> :id
const componentPath = file.replace(ext, '')
return ` {
path: '/${routePath}',
name: '${routePath.replace(/\//g, '_') || 'index'}',
component: () => import('/${componentPath}${ext}')
}`
})
return `export default [
${routes.join(',\n')}
]`
}
}
}
在项目中使用:
// vite.config.js
import { defineConfig } from 'vite'
import virtualRoutes from './plugins/vite-plugin-virtual-routes'
export default defineConfig({
plugins: [
virtualRoutes({ pagesDir: 'src/pages' })
]
})
// src/router.js
import routes from 'virtual:routes'
// TypeScript类型声明(src/vite-env.d.ts)
// declare module 'virtual:routes' {
// import type { RouteRecordRaw } from 'vue-router'
// const routes: RouteRecordRaw[]
// export default routes
// }
console.log(routes)
// [{ path: '/', name: 'index', component: [AsyncFunction] }, ...]
转译管线配置与自定义文件处理器
通过transform Hook可实现自定义文件格式转译。以下插件将YAML配置文件转译为ES模块:
// plugins/vite-plugin-yaml.js
import yaml from 'js-yaml'
export default function yamlPlugin(options = {}) {
const { include = ['**/*.yaml', '**/*.yml'] } = options
return {
name: 'vite-plugin-yaml',
enforce: 'pre',
transform(code, id) {
// 过滤非YAML文件
const isYaml = include.some(pattern =>
new RegExp(pattern.replace('**/', '.*?').replace('.', '\.')).test(id)
)
if (!isYaml) return null
try {
const data = yaml.load(code)
return {
code: `export default ${JSON.stringify(data)}`,
map: null // 可选:返回source map
}
} catch (err) {
this.error(`YAML parse error in ${id}: ${err.message}`)
}
}
}
}
// 进阶版:支持YAML中嵌入JS表达式
export default function yamlPluginAdvanced() {
return {
name: 'vite-plugin-yaml-advanced',
enforce: 'pre',
transform(code, id) {
if (!id.match(/\.(ya?ml)$/)) return null
const data = yaml.load(code)
// 将嵌套函数字符串转为实际导出
const serialized = JSON.stringify(data, (key, value) => {
if (typeof value === 'string' && value.startsWith('!function')) {
return value.slice(1) // 去掉!前缀,标记为函数
}
return value
})
return {
code: `export default ${serialized}`,
map: null
}
}
}
}
插件开发实战:Markdown转HTML组件
完整示例:将Markdown文件转译为Vue SFC组件,支持代码高亮和Front Matter:
// plugins/vite-plugin-md-to-component.js
import markdownIt from 'markdown-it'
import matter from 'gray-matter'
import { highlightPlugin } from './highlight'
export default function mdToComponentPlugin(options = {}) {
const {
markdownOptions = {},
includeFrontMatter = true
} = options
const md = markdownIt({
html: true,
linkify: true,
typographer: true,
highlight: highlightPlugin
})
return {
name: 'vite-plugin-md-to-component',
enforce: 'pre',
transform(code, id) {
if (!id.endsWith('.md')) return null
const { content, data: frontMatter } = includeFrontMatter
? matter(code)
: { content: code, data: {} }
const html = md.render(content)
// 生成Vue SFC
const sfc = `
${html}
`
return {
code: sfc,
map: null
}
}
}
}
React版本的Markdown组件插件:
// plugins/vite-plugin-md-to-react.js
export default function mdToReactPlugin() {
const md = markdownIt({ html: true, linkify: true })
return {
name: 'vite-plugin-md-to-react',
enforce: 'pre',
transform(code, id) {
if (!id.endsWith('.md')) return null
const { content, data } = matter(code)
const html = md.render(content)
const component = `import React from 'react'
export const frontMatter = ${JSON.stringify(data)}
export default function MarkdownComponent() {
return React.createElement('div', {
className: 'markdown-body',
dangerouslySetInnerHTML: { __html: ${JSON.stringify(html)} }
})
}`
return { code: component, map: null }
}
}
}
插件调试与单元测试方法
// 使用Vite插件容器测试插件
import { createServer } from 'vite'
async function testPlugin() {
const server = await createServer({
configFile: false,
root: './test-fixture',
plugins: [myPlugin()],
logLevel: 'error'
})
// 加载虚拟模块验证输出
const result = await server.ssrLoadModule('virtual:my-module')
console.log('Module output:', result)
await server.close()
}
// 单元测试(vitest)
import { describe, it, expect } from 'vitest'
import { transform } from '../plugins/my-plugin'
describe('vite-plugin-md-to-component', () => {
it('should convert markdown to SFC', () => {
const input = '# Hello\n\nThis is a test.'
const result = transform(input, 'test.md')
expect(result.code).toContain('Hello
')
expect(result.code).toContain('')
})
it('should extract front matter', () => {
const input = '---\ntitle: Test\n---\n# Content'
const result = transform(input, 'test.md')
expect(result.code).toContain('"title":"Test"')
})
})
插件发布到npm与版本管理
// package.json
{
"name": "vite-plugin-md-to-component",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"peerDependencies": {
"vite": "^5.0.0"
},
"scripts": {
"build": "tsup src/index.ts --dts --format esm,cjs",
"test": "vitest run",
"publish": "npm run build && npm publish"
}
}
// tsup.config.ts
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
external: ['vite']
})
Vite插件系统的enforce属性控制执行顺序:pre在Vite内置插件之前、post在之后,默认在中间。开发转译类插件通常用pre确保在esbuild处理之前拦截目标文件。虚拟模块ID使用\0前缀是Vite约定,避免被其他插件误处理。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vite-gou-jian-gong-ju-cha-jian-kai-fa-ji-zhi-yu-zi-ding-yi/