Vite是当前前端构建领域广泛采用的开发服务器与打包工具,其基于浏览器原生ES模块的Dev Server设计和Rollup的生产构建方案,显著提升了开发体验。Vite的插件机制兼容Rollup插件接口并在此基础上扩展,HMR(Hot Module Replacement)通过精确的模块依赖图实现细粒度的热更新。本文分析Vite的插件架构和HMR实现原理。
Vite开发服务器与ES模块按需编译机制
Vite的Dev Server不进行预打包,而是利用浏览器对ES模块的原生支持,在请求到达时实时编译单文件。当浏览器请求/src/main.ts时,Vite实时执行TypeScript转译并返回JS模块。对于import语句中引用的依赖,浏览器会发起额外的HTTP请求获取。
这种设计的优势在于冷启动速度极快:不需要处理整个依赖图,只编译当前请求的文件。代价是开发环境下的HTTP请求数量较多。对于node_modules中的依赖,Vite通过esbuild预构建(Pre-bundle)将其合并为单文件并转换为ESM格式,减少请求数量。
// vite.config.ts 基础配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': '/src'
}
},
optimizeDeps: {
include: ['lodash-es', 'axios'],
exclude: ['@my/local-package']
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})
optimizeDeps.include指定需要预构建的依赖。对于体积较大或导致大量请求的依赖应显式声明。exclude用于跳过预构建,适用于仅在特定条件下加载的包。
Vite插件接口与Rollup插件兼容性
Vite插件是一个包含name属性和多个钩子函数的对象。Vite扩展了Rollup的插件接口,新增了Vite特有的钩子:
// 自定义Vite插件示例
import type { Plugin } from 'vite'
function myPlugin(): Plugin {
return {
name: 'vite-plugin-my',
// Vite特有钩子:配置Vite服务器
configureServer(server) {
server.middlewares.use('/my-api', (req, res) => {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ status: 'ok' }))
})
},
// Vite特有钩子:转换HTML入口
transformIndexHtml(html) {
return html.replace(
'</head>',
'<script>console.log("injected")</script></head>'
)
},
// Rollup兼容钩子:转换模块内容
transform(code, id) {
if (id.endsWith('.svg')) {
return `export default ${JSON.stringify(code)}`
}
return null
},
// Rollup兼容钩子:解析import路径
resolveId(source, importer) {
if (source === 'virtual:my-module') {
return source
}
return null
},
// Rollup兼容钩子:加载虚拟模块
load(id) {
if (id === 'virtual:my-module') {
return 'export const msg = "from virtual module"'
}
return null
}
}
}
export default myPlugin
关键钩子说明:
- configResolved:在Vite配置解析完成后调用,可获取最终的resolved config。适用于需要读取最终配置做条件处理的场景。
- transform:文件内容转换钩子,在模块解析后执行。按注册顺序串行调用所有插件的transform。Vue的SFC编译、CSS预处理都在此阶段完成。
- configureServer:用于给Dev Server添加自定义中间件。中间件注册顺序在Vite内置中间件之前,可通过返回函数实现后置注册。
- handleHotUpdate:HMR事件处理钩子,可自定义模块更新行为。
HMR热更新实现原理与模块依赖图
Vite的HMR实现基于WebSocket通信和模块依赖图。Dev Server启动时建立WebSocket连接,当文件变化时,Vite通过chokidar监听文件变更事件,计算受影响的模块列表,通过WebSocket推送更新指令到浏览器端。
浏览器端的HMR runtime收到更新消息后,执行以下流程:
// Vite客户端HMR处理简化逻辑
async function hotUpdate(type, path, acceptedModules) {
// 1. 请求更新后的模块代码
const mod = await import(`${path}?t=${Date.now()}`)
// 2. 检查模块是否声明了HMR接受
if (mod.hot) {
// 模块通过import.meta.hot.accept声明自身可热更新
const callbacks = hmrCallbackMap.get(path)
callbacks.forEach(cb => cb(mod))
} else {
// 3. 沿依赖图向上查找,直到找到声明accept的模块
const boundary = findHMRBoundary(path)
if (boundary) {
// 执行边界模块的accept回调
boundary.acceptCb(mod)
} else {
// 4. 无HMR边界,整页刷新
location.reload()
}
}
}
在业务代码中声明HMR接受:
// React组件配合@vitejs/plugin-react的HMR
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
export default Counter
// Vue组件由@vitejs/plugin-vue自动注入HMR接受逻辑
// 手动声明HMR(适用于非框架场景)
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// 新模块的导出替换旧模块
console.log('Module updated:', newModule)
})
}
生产构建优化与Rollup输出配置
Vite的生产构建基于Rollup,通过build.rollupOptions暴露完整的Rollup配置。关键优化项:
// vite.config.ts 生产构建优化
export default defineConfig({
build: {
target: 'es2020',
minify: 'esbuild',
cssCodeSplit: true,
sourcemap: false,
chunkSizeWarningLimit: 1000,
rollupOptions: {
output: {
// 手动分包策略
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react')) return 'react-vendor'
if (id.includes('lodash')) return 'lodash-vendor'
return 'vendor'
}
},
// 入口文件名规则
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]'
}
}
}
})
cssCodeSplit启用CSS代码分割,每个异步chunk的CSS单独提取为文件,避免未使用的样式被加载。manualChunks用于控制第三方依赖的分包策略,将不频繁变化的依赖分离为独立chunk以提高缓存命中率。esbuild作为压缩器比terser快20-40倍,但terser的压缩率略高(约小2-5%),对包体积有极致要求时可切换为terser。
插件执行顺序与enforce属性
Vite插件的执行顺序通过enforce属性控制。设置enforce: ‘pre’的插件先执行,普通插件居中,enforce: ‘post’的插件后执行。在同类插件内部,按注册顺序执行。Vue和React的官方插件默认设置enforce: ‘pre’,确保模板/JSX编译在业务代码transform之前完成。
// 插件顺序控制
plugins: [
{ ..., enforce: 'pre' }, // 第一轮
{ ..., enforce: 'pre' }, // 第一轮(按注册顺序)
vue(), // 第二轮(默认pre,归入第一轮)
myPlugin(), // 第三轮(normal)
{ ..., enforce: 'post' }, // 第四轮
viteManifest(), // 第五轮(Vite内置post)
]
开发自定义插件时,如果需要在框架插件之前处理文件(如自定义文件格式转换),设置enforce: ‘pre’。如果需要处理框架插件输出的结果(如对编译后的JS做二次转换),设置enforce: ‘post’。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vite-gou-jian-gong-ju-cha-jian-ji-zhi-yu-hmr-re-geng-xin/