Vite利用浏览器原生ES模块实现按需加载,开发环境启动时间从Webpack的分钟级降到毫秒级,已成为Vue3生态和React框架项目的主流构建工具。理解Vite的热更新HMR机制和插件开发体系,是前端工程化进阶的关键。Vite的插件API兼容Rollup插件接口,同时在开发环境扩展了HMR相关能力。
Vite热更新HMR机制与ESM原生加载原理
Vite开发环境通过拦截浏览器ES模块请求实现热更新,核心流程分为三步:
- 请求拦截:Vite dev server拦截所有模块请求,将CommonJS和CSS等非ESM资源转换为ESM格式返回
- 依赖预构建:首次启动时使用esbuild扫描入口文件,将node_modules中的CJS依赖预构建为ESM格式并缓存
- HMR推送:文件变更后通过WebSocket通知浏览器,浏览器请求更新后的模块并执行热替换回调
// Vite注入的HMR客户端代码(简化版)
const hotModulesMap = new Map()
export function createHotContext(ownerPath) {
return {
accept(callback) {
hotModulesMap.set(ownerPath, { accept: callback })
},
dispose(callback) { /* 模块销毁时清理 */ },
prune(callback) { /* 模块移除时回调 */ }
}
}
socket.addEventListener('message', ({ data }) => {
const payload = JSON.parse(data)
if (payload.type === 'update') {
payload.updates.forEach(update => {
if (update.type === 'js-update') {
fetchUpdate(update)
} else if (update.type === 'css-update') {
const link = document.querySelector(`link[href="${update.path}"]`)
link.href = update.path + `?t=${Date.now()}`
}
})
}
})
async function fetchUpdate(update) {
const mod = await import(update.path + `?t=${Date.now()}`)
const hotModule = hotModulesMap.get(update.path)
if (hotModule && hotModule.accept) {
hotModule.accept(mod)
} else {
location.reload()
}
}
自定义Vite插件开发与插件钩子执行顺序
Vite插件是一个返回对象的函数,核心是各阶段钩子函数。插件钩子分为三类:
- Vite独有钩子:config、configResolved、configureServer、transformIndexHtml
- Rollup兼容钩子:resolveId、load、transform、renderChunk
- 构建关闭钩子:buildEnd、closeBundle
// my-vite-plugin.js — 自动注入版本号和环境变量
export default function versionPlugin(options = {}) {
let config
return {
name: 'vite-plugin-version',
// 修改Vite配置
config(userConfig, { command }) {
return {
define: {
__APP_VERSION__: JSON.stringify(options.version || '1.0.0'),
__BUILD_TIME__: JSON.stringify(new Date().toISOString())
}
}
},
// 配置解析完成
configResolved(resolvedConfig) {
config = resolvedConfig
},
// 配置dev server(仅开发环境)
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === '/__version') {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ version: options.version, mode: config.mode }))
} else {
next()
}
})
server.watcher.on('change', (file) => {
console.log(`文件变更: ${file}`)
})
},
// 转换HTML入口文件
transformIndexHtml(html) {
return html.replace(
'',
` \n`
)
},
// 模块解析
resolveId(source, importer) {
if (source === '@version') return '\0virtual:version'
return null
},
// 加载模块内容
load(id) {
if (id === '\0virtual:version') {
return `export const version = "${options.version}"\nexport const buildTime = "${new Date().toISOString()}"`
}
return null
},
// 转换模块代码
transform(code, id) {
if (id.endsWith('.vue')) {
return code.replace(
/export default/,
`const __version = "${options.version}"\nexport default`
)
}
return null
},
// 构建完成
buildEnd(error) {
if (error) console.error('构建失败:', error)
else console.log('构建完成')
}
}
}
在项目中使用自定义插件
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import versionPlugin from './plugins/version-plugin'
export default defineConfig({
plugins: [
vue(),
versionPlugin({ version: '2.3.0' })
],
server: {
port: 3000,
hmr: {
protocol: 'ws',
host: 'localhost',
port: 3000,
overlay: true
}
},
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia'],
exclude: ['@vitejs/plugin-vue']
}
})
CSS HMR与PostCSS集成处理
Vite对CSS文件的热更新做了特殊处理。CSS文件变更不会触发JS模块重新加载,而是直接替换link标签的href,实现样式无缝切换:
// CSS模块热更新回调注册
import style from './button.module.css'
// Vite自动为CSS Modules注册HMR
import.meta.hot.accept('./button.module.css', (newStyle) => {
console.log('CSS模块已更新', newStyle)
})
// 自定义CSS预处理插件
export function cssPreprocessPlugin() {
return {
name: 'css-preprocess',
transform(code, id) {
if (!id.endsWith('.css')) return null
let result = code
result = result.replace(
/@theme\((.+?)\)/g,
(match, key) => {
const themeMap = {
primary: '#3b82f6',
secondary: '#64748b',
success: '#22c55e'
}
return themeMap[key.trim()] || match
}
)
return { code: result, map: null }
}
}
}
插件执行顺序与enforce属性控制
Vite插件的执行顺序影响最终构建结果,通过enforce属性控制:
// enforce: 'pre' — 在Vite核心插件之前执行
// enforce: 'post' — 在Vite核心插件之后执行
// 不设置 — 正常执行顺序
export function prePlugin() {
return {
name: 'pre-plugin',
enforce: 'pre',
transform(code, id) { /* 最先执行 */ }
}
}
export function postPlugin() {
return {
name: 'post-plugin',
enforce: 'post',
transform(code, id) { /* 最后执行 */ }
}
}
// 执行顺序:
// pre plugins -> Vite core -> normal plugins -> Vite build -> post plugins
虚拟模块与批量文件导入实现
// 自动导入Markdown文件为路由配置
export function markdownRoutesPlugin() {
const virtualModuleId = 'virtual:md-routes'
const resolvedVirtualId = '\0' + virtualModuleId
return {
name: 'md-routes',
resolveId(id) {
if (id === virtualModuleId) return resolvedVirtualId
},
async load(id) {
if (id !== resolvedVirtualId) return null
const glob = await import('fast-glob')
const files = await glob.default(['src/docs/**/*.md'])
const imports = files.map((file, i) =>
`const m${i} = () => import('${file}')`
).join('\n')
const routes = files.map((file, i) => {
const path = file.replace('src/docs', '').replace(/\.md$/, '').replace(/\/index$/, '')
return `{ path: '${path}', component: m${i} }`
}).join(',')
return `${imports}\nexport const routes = [${routes}]`
}
}
}
插件调试与常见问题排查
// 启用插件调试日志
export default defineConfig({
plugins: [{
name: 'debug-hooks',
enforce: 'pre',
transform(code, id) {
if (id.includes('.vue')) {
console.log(`[transform] ${id}, 长度: ${code.length}`)
}
},
resolveId(source, importer) {
console.log(`[resolveId] ${source} from ${importer}`)
}
}]
})
// 查看Vite内部日志:DEBUG=vite:* vite
// 强制重新预构建:vite --force
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vite-gou-jian-gong-ju-cha-jian-kai-fa-yu-re-geng-xin-hmr-ji/