Vite利用浏览器原生ESM(ECMAScript Module)实现按需加载的开发服务器,但node_modules中的CommonJS模块、大体积UMD包无法直接被浏览器ESM加载。Vite通过依赖预构建(Dependency Pre-Bundling)使用esbuild将依赖转换为浏览器兼容的ESM格式,同时将多个细粒度模块合并为单文件减少HTTP请求数。前端工程化实践中,理解Vite预构建机制和插件系统对于解决依赖加载异常、优化冷启动速度至关重要。
Vite依赖预构建触发机制与esbuild转换原理
Vite开发服务器启动时扫描入口文件的import语句,识别bare import(以包名导入的依赖),使用esbuild将这些依赖编译为ESM格式并缓存到node_modules/.vite/deps目录。
// Vite预构建前后的模块格式转换
// 原始 lodash-es (ESM, 但有600+子模块)
import { debounce } from 'lodash-es'
// 预构建后 (合并为单文件, 浏览器直接加载)
// node_modules/.vite/deps/lodash-es.js
// 所有导出合并到一个文件中, 减少HTTP请求
// 原始 CommonJS 模块 (如 react)
// react/index.js: module.exports = React
// 预构建后转换为 ESM
// node_modules/.vite/deps/react.js: export default React; export { useState, useEffect, ... }
# Vite项目初始化
npm create vite@latest my-app -- --template vue-ts
cd my-app
npm install
# 手动触发预构建(清除缓存重新构建)
npx vite optimize --force
# 查看预构建缓存
ls node_modules/.vite/deps/
# 输出: react.js react.js.map lodash-es.js vue.js ...
# 预构建元数据文件
cat node_modules/.vite/_metadata.json
# {
# "hash": "a1b2c3d4...",
# "browserHash": "e5f6g7h8...",
# "optimized": {
# "react": { "file": "react.js", "src": "react", "needsInterop": true },
# "lodash-es": { "file": "lodash-es.js", "src": "lodash-es", "needsInterop": false }
# }
# }
vite.config.ts预构建配置与依赖优化
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
optimizeDeps: {
// 强制预构建的依赖(即使没有被静态import)
include: [
'vue',
'vue-router',
'pinia',
'axios',
'lodash-es',
'dayjs',
],
// 排除预构建的依赖(保持原样加载)
exclude: [
'@mycompany/internal-ui', // 本地Linked包
],
// esbuild配置
esbuildOptions: {
target: 'es2020',
define: {
global: 'globalThis',
},
banner: {
js: `if (typeof globalThis === 'undefined') { globalThis = window; }`,
},
},
},
server: {
port: 3000,
cacheDir: 'node_modules/.vite',
},
build: {
target: 'es2020',
rollupOptions: {
output: {
manualChunks: {
'vendor-vue': ['vue', 'vue-router', 'pinia'],
'vendor-utils': ['lodash-es', 'dayjs', 'axios'],
},
},
},
},
})
Vite插件钩子系统与自定义插件开发
Vite插件基于Rollup插件接口扩展,增加Vite特有的钩子用于开发服务器和HMR(热模块替换)。插件是对象或对象数组,通过name字段标识,各钩子在构建的不同阶段执行。
import type { Plugin, PluginOption } from 'vite'
import { createFilter } from '@rollup/pluginutils'
import fs from 'fs'
import path from 'path'
// 自定义插件:将Markdown文件编译为Vue组件
function markdownToVue(): Plugin {
const filter = createFilter(['**/*.md'], ['node_modules/**'])
return {
name: 'vite-plugin-md-to-vue',
enforce: 'pre',
// resolveId钩子:解析虚拟模块路径
resolveId(source) {
if (source.startsWith('virtual:md:')) {
return '\0' + source // \0前缀标记虚拟模块
}
return null
},
// load钩子:加载虚拟模块内容
load(id) {
if (id.startsWith('\0virtual:md:')) {
const filePath = id.replace('\0virtual:md:', '')
const content = fs.readFileSync(filePath, 'utf-8')
return compileMarkdownToVue(content)
}
},
// transform钩子:转换模块源码
transform(code, id) {
if (!filter(id)) return null
if (id.endsWith('.md')) {
const compiled = compileMarkdownToVue(code)
return {
code: compiled,
map: null,
}
}
return null
},
// Vite特有:configureServer钩子,配置开发服务器
configureServer(server) {
server.middlewares.use('/api/health', (req, res) => {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ status: 'ok', timestamp: Date.now() }))
})
// 监听文件变化,触发HMR
server.watcher.on('change', (file) => {
if (file.endsWith('.md')) {
const modulePath = '/' + path.relative(process.cwd(), file)
const mod = server.moduleGraph.getModuleById(modulePath)
if (mod) {
server.moduleGraph.invalidateModule(mod)
server.ws.send({ type: 'full-reload' })
}
}
})
},
// Vite特有:transformIndexHtml钩子,修改HTML入口
transformIndexHtml(html) {
return html.replace(
'',
``
)
},
// HMR处理
handleHotUpdate(ctx) {
if (ctx.file.endsWith('.md')) {
ctx.server.ws.send({
type: 'custom',
event: 'md-update',
data: { file: ctx.file }
})
return []
}
},
}
}
function compileMarkdownToVue(source: string): string {
const lines = source.split('\n')
const html = lines
.map(line => {
if (line.startsWith('# ')) return `${line.slice(2)}
`
if (line.startsWith('## ')) return `${line.slice(3)}
`
if (line.startsWith('### ')) return `${line.slice(4)}
`
if (line.startsWith('- ')) return `${line.slice(2)} `
if (line.trim().startsWith('```')) return ''
if (line.trim() === '') return '
'
return `${line}
`
})
.join('\n')
return `${html}
`
}
export default defineConfig({
plugins: [
vue(),
markdownToVue(),
],
})
Vite插件API完整钩子执行顺序与实战
Vite插件钩子按构建阶段分为三类:构建前(options)、构建中(resolveId/load/transform)、构建后(generateBundle)。开发服务器还有独立的生命周期钩子。
// 演示所有核心钩子的执行顺序
function lifecycleLogger(): Plugin {
return {
name: 'lifecycle-logger',
// === Rollup通用钩子 ===
options(options) {
console.log('[1] options - Rollup配置解析前')
return options
},
buildStart() {
console.log('[2] buildStart - 构建开始')
},
resolveId(source, importer) {
console.log(`[3] resolveId - 解析: ${source}`)
return null
},
load(id) {
console.log(`[4] load - 加载: ${id}`)
return null
},
transform(code, id) {
console.log(`[5] transform - 转换: ${id} (${code.length} chars)`)
return null
},
buildEnd() {
console.log('[6] buildEnd - 构建结束')
},
generateBundle(opts, bundle) {
console.log('[7] generateBundle - 生成产物')
console.log(' 产物文件:', Object.keys(bundle))
},
writeBundle(opts, bundle) {
console.log('[8] writeBundle - 写入磁盘完成')
},
// === Vite特有钩子 ===
config(config, { command }) {
console.log(`[Vite] config - 修改Vite配置 (command: ${command})`)
return {
define: {
__APP_VERSION__: JSON.stringify('1.0.0'),
},
}
},
configResolved(resolvedConfig) {
console.log('[Vite] configResolved - 最终配置已解析')
console.log(' root:', resolvedConfig.root)
console.log(' mode:', resolvedConfig.mode)
},
configureServer(server) {
console.log('[Vite] configureServer - 配置开发服务器')
server.middlewares.use((req, res, next) => {
if (req.url?.startsWith('/api/mock')) {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ mock: true, data: [] }))
return
}
next()
})
},
transformIndexHtml(html, ctx) {
console.log('[Vite] transformIndexHtml - 修改HTML')
return {
html,
tags: [
{
tag: 'meta',
attrs: { name: 'generator', content: 'Vite' },
injectTo: 'head',
},
],
}
},
handleHotUpdate({ file, server }) {
console.log(`[Vite] handleHotUpdate - HMR: ${file}`)
},
}
}
预构建失效问题排查与性能优化
开发过程中经常遇到依赖更新后预构建缓存未刷新、动态import的依赖未被预构建等问题。Vite提供了调试工具和强制刷新机制。
# 启动时显示详细预构建日志
DEBUG=vite:deps npx vite
# 强制重新预构建(删除缓存)
rm -rf node_modules/.vite
npx vite --force
# 常见问题排查
# 问题1: 动态import的依赖未预构建
# 解决: 在optimizeDeps.include中显式声明
# optimizeDeps: {
# include: ['my-dynamic-dep']
# }
# 问题2: 预构建后模块导出丢失
# 原因: CommonJS模块需要interop
# 解决:
# optimizeDeps: {
# esbuildOptions: {
# define: { global: 'globalThis' }
# }
# }
# 问题3: Monorepo中Linked包无法预构建
# 解决:
# optimizeDeps: {
# exclude: ['@mycompany/shared'],
# }
# 问题4: 预构建超时(大型依赖)
# 解决: 增加esbuild超时
# optimizeDeps: {
# esbuildOptions: {
# timeout: 120000,
# },
# }
// 测量Vite冷启动时间
import { build } from 'vite'
const start = performance.now()
await build({
logLevel: 'silent',
build: { write: false },
})
const duration = performance.now() - start
console.log(`Build time: ${duration.toFixed(0)}ms`)
// 优化前后对比
// 优化前(未配置预构建include): 3200ms
// 优化后(显式include + manualChunks): 1800ms
// 生产构建(rollup): 12000ms -> 8500ms
Vite预构建依赖的hash计算依据包括package.json的dependencies字段、lockfile版本以及vite.config.ts中的optimizeDeps.include配置。任何一项变化都会触发重新预构建。对于频繁切换Git分支的场景,可以在CI缓存中保存node_modules/.vite目录,将二次冷启动时间从3-5秒降至500毫秒以内。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vite-yi-lai-yu-gou-jian-ji-zhi-yuan-li-yu-zi-ding-yi-cha/