Vite 进阶
大约 7 分钟约 2197 字
Vite 进阶
Vite 的双模式架构
Vite 的核心设计理念是开发模式和构建模式采用不同的策略,各自优化:
- 开发模式(Dev Server):基于浏览器原生 ESM,使用 esbuild 预构建依赖,实现毫秒级 HMR
- 构建模式(Build):使用 Rollup 打包,支持 Tree Shaking、代码分割、CSS 提取等优化
理解这种双模式架构有助于排查开发环境和生产环境行为不一致的问题。
构建优化配置
代码分割策略
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
export default defineConfig(({ mode }) => ({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@hooks': resolve(__dirname, 'src/hooks'),
'@stores': resolve(__dirname, 'src/stores'),
'@utils': resolve(__dirname, 'src/utils'),
},
},
build: {
// 目标浏览器
target: 'es2020',
// 输出目录
outDir: 'dist',
// 小于此阈值的资源内联为 base64
assetsInlineLimit: 4096,
// CSS 代码分割
cssCodeSplit: true,
// Source Map
sourcemap: mode === 'development',
// Chunk 大小警告阈值(KB)
chunkSizeWarningLimit: 500,
// Rollup 打包选项
rollupOptions: {
output: {
// 手动分包策略
manualChunks(id) {
// Vue 核心库
if (id.includes('node_modules/vue/') || id.includes('node_modules/@vue/')) {
return 'vendor-vue';
}
// Vue Router
if (id.includes('node_modules/vue-router')) {
return 'vendor-router';
}
// Pinia
if (id.includes('node_modules/pinia')) {
return 'vendor-pinia';
}
// UI 组件库
if (id.includes('node_modules/element-plus') || id.includes('node_modules/@element-plus')) {
return 'vendor-ui';
}
// 工具库
if (id.includes('node_modules/lodash') || id.includes('node_modules/axios')) {
return 'vendor-utils';
}
// 图表库
if (id.includes('node_modules/echarts')) {
return 'vendor-charts';
}
// 其他 node_modules
if (id.includes('node_modules')) {
return 'vendor-other';
}
},
// 文件命名
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: (assetInfo) => {
if (assetInfo.name?.endsWith('.css')) {
return 'assets/css/[name]-[hash][extname]';
}
if (/\.(png|jpe?g|gif|svg|webp|ico)$/.test(assetInfo.name || '')) {
return 'assets/images/[name]-[hash][extname]';
}
if (/\.(woff2?|eot|ttf|otf)$/.test(assetInfo.name || '')) {
return 'assets/fonts/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
},
},
},
// 压缩配置
minify: 'terser',
terserOptions: {
compress: {
drop_console: mode === 'production', // 生产环境移除 console
drop_debugger: true,
},
},
},
}));预构建优化
// vite.config.ts
export default defineConfig({
// 依赖预构建配置
optimizeDeps: {
// 强制预构建的依赖
include: [
'vue',
'vue-router',
'pinia',
'element-plus',
'axios',
'lodash-es',
],
// 不预构建的依赖
exclude: ['your-local-package'],
// esbuild 选项
esbuildOptions: {
target: 'es2020',
},
},
// 强制重新预构建
// server.force: true,
});插件开发
Vite 插件结构
Vite 插件遵循 Rollup 插件接口,并增加了 Vite 特有的钩子。
// vite-plugin-auto-import.ts
import type { Plugin } from 'vite';
import fs from 'fs';
import path from 'path';
interface AutoImportOptions {
imports: Record<string, string[]>;
dtsFile?: string;
}
export function autoImportPlugin(options: AutoImportOptions): Plugin {
const virtualModuleId = 'virtual:auto-imports';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
let isBuild = false;
return {
name: 'vite-plugin-auto-import',
// Vite 特有钩子 — 判断当前模式
configResolved(config) {
isBuild = config.command === 'build';
},
// 解析虚拟模块
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
return null;
},
// 加载虚拟模块内容
load(id) {
if (id !== resolvedVirtualModuleId) return null;
const lines: string[] = [];
for (const [from, imports] of Object.entries(options.imports)) {
for (const name of imports) {
lines.push(`export { ${name} } from '${from}';`);
}
}
const code = lines.join('\n');
// 生成类型声明文件
if (options.dtsFile && !isBuild) {
const dtsLines = lines.map(line =>
line.replace('export { ', '// ').replace(" } from '", " }: typeof import('").replace("');", "')")
);
const dtsContent = `declare module '${virtualModuleId}' {\n${dtsLines.join('\n')}\n}\n`;
fs.mkdirSync(path.dirname(options.dtsFile), { recursive: true });
fs.writeFileSync(options.dtsFile, dtsContent);
}
return code;
},
// 转换源码
transform(code, id) {
if (!id.endsWith('.ts') && !id.endsWith('.tsx') && !id.endsWith('.vue')) {
return null;
}
let transformed = code;
for (const [from, imports] of Object.entries(options.imports)) {
for (const name of imports) {
// 如果代码中使用了但未导入,自动添加导入
const regex = new RegExp(`(?<!import.*)(?<!\\w)${name}(?=\\s*[(\\[])`, 'g');
if (regex.test(transformed) && !transformed.includes(`import { ${name} }`)) {
transformed = `import { ${name} } from '${virtualModuleId}';\n` + transformed;
}
}
}
return { code: transformed, map: null };
},
};
}使用自定义插件
// vite.config.ts
import { autoImportPlugin } from './plugins/vite-plugin-auto-import';
export default defineConfig({
plugins: [
autoImportPlugin({
imports: {
'vue': ['ref', 'reactive', 'computed', 'watch', 'onMounted', 'nextTick'],
'vue-router': ['useRouter', 'useRoute'],
'pinia': ['defineStore', 'storeToRefs'],
},
dtsFile: 'src/auto-imports.d.ts',
}),
],
});开发服务器配置
代理配置
// vite.config.ts
export default defineConfig({
server: {
port: 3000,
host: true, // 允许局域网访问
open: true, // 自动打开浏览器
// 代理配置
proxy: {
// 代理 API 请求
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
// 重写路径
rewrite: (path) => path.replace(/^\/api/, ''),
},
// 代理 WebSocket
'/ws': {
target: 'ws://localhost:5000',
ws: true,
},
// 多个代理规则
'/auth-api': {
target: 'http://auth-server:5001',
changeOrigin: true,
},
'/data-api': {
target: 'http://data-server:5002',
changeOrigin: true,
},
},
// CORS 配置
cors: true,
// HTTPS(本地开发 HTTPS)
// https: true,
},
});HMR 配置
// vite.config.ts
export default defineConfig({
server: {
hmr: {
overlay: true, // 显示错误覆盖层
// 自定义 HMR 端口(多项目时避免冲突)
// port: 3001,
},
},
});环境变量管理
环境文件
# .env — 所有环境共享
VITE_APP_TITLE=我的应用
VITE_APP_VERSION=1.0.0
# .env.development — 开发环境
VITE_API_URL=http://localhost:5000
VITE_ENABLE_MOCK=true
VITE_SHOW_DEVTOOLS=true
# .env.staging — 预发环境
VITE_API_URL=https://staging-api.example.com
VITE_ENABLE_MOCK=false
# .env.production — 生产环境
VITE_API_URL=https://api.example.com
VITE_ENABLE_MOCK=false
VITE_SENTRY_DSN=https://xxx@sentry.io/xxx使用环境变量
// 只有 VITE_ 前缀的变量才会暴露给客户端代码
const apiUrl = import.meta.env.VITE_API_URL;
const isDev = import.meta.env.DEV;
const isProd = import.meta.env.PROD;
const mode = import.meta.env.MODE; // 'development' | 'production'
// 条件逻辑
if (import.meta.env.VITE_ENABLE_MOCK === 'true') {
setupMockServer();
}TypeScript 类型声明
// src/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string;
readonly VITE_API_URL: string;
readonly VITE_ENABLE_MOCK: string;
readonly VITE_SENTRY_DSN: string;
// ... 更多环境变量
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}SSR 支持
SSR 配置
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
// SSR 入口
ssr: resolve(__dirname, 'src/entry-server.ts'),
},
},
},
});// src/entry-server.ts
import { createApp } from './app';
export async function render(url: string) {
const { app, router } = createApp();
// 导航到目标 URL
await router.push(url);
await router.isReady();
const ctx = {};
const html = await renderToString(app, ctx);
return { html };
}常见误区
误区 1:环境变量不以 VITE_ 前缀
# Bad — 构建时不可用
API_URL=http://localhost:5000
# Good — 必须以 VITE_ 前缀
VITE_API_URL=http://localhost:5000误区 2:过度拆分 chunks
// Bad — 每个 node_modules 包单独一个 chunk,请求数过多
if (id.includes('node_modules')) {
return 'vendor-' + packageName;
}
// Good — 相关的包合并为一个 chunk
if (id.includes('node_modules/vue') || id.includes('node_modules/@vue')) {
return 'vendor-vue';
}误区 3:开发和生产环境不一致
// 常见原因:
// 1. 开发环境用 esbuild 预构建,生产环境用 Rollup
// 2. 开发环境支持 CommonJS,生产环境可能不支持
// 3. 环境变量不同
// 解决:
// 1. 在开发环境也检查构建是否正常
// 2. 使用 npm run build && npm run preview 预览构建产物
// 3. 确保 .env.development 和 .env.production 的变量一致性能优化清单
- 使用 manualChunks 分包 — vue/ui/charts/utils 分别打包
- 启用文件系统缓存 — 二次构建速度大幅提升
- 配置路径别名 — 避免深层相对路径
- CSS 代码分割 — 每个组件的 CSS 独立文件
- 生产环境移除 console — 减小包体积
- 合理设置 assetsInlineLimit — 小资源内联,大资源独立
构建产物分析
Bundle 分析工具
// 安装 rollup-plugin-visualizer
// npm install -D rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
vue(),
// 构建后生成 stats.html 可视化分析页面
visualizer({
open: true, // 构建后自动打开浏览器
gzipSize: true, // 显示 gzip 后的大小
brotliSize: true, // 显示 brotli 后的大小
filename: 'stats.html', // 输出文件名
}),
],
});构建产物优化检查
# 分析构建产物大小
npx vite-bundle-visualizer
# 检查每个 chunk 的大小
ls -lh dist/assets/js/
# 使用 source-map-explorer 分析 source map
npx source-map-explorer dist/assets/js/*.js
# 检查 gzip 后的大小
gzip -c dist/assets/js/index-abc123.js | wc -c依赖预打包优化
// vite.config.ts
export default defineConfig({
optimizeDeps: {
// 强制预构建 — 避免浏览器加载大量小文件
include: [
'vue',
'vue-router',
'pinia',
'axios',
'lodash-es',
'dayjs',
],
// 排除不需要预构建的包
exclude: [
'my-local-utils', // 本地工具不需要预构建
],
// 强制重新预构建(依赖变更时使用)
// 删除 node_modules/.vite 缓存也可达到同样效果
},
});多环境部署配置
不同环境的完整配置
// vite.config.ts
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
export default defineConfig(({ mode }) => {
// 加载环境变量
const env = loadEnv(mode, process.cwd(), '');
return {
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
// 开发服务器
server: {
port: Number(env.VITE_PORT) || 3000,
proxy: {
'/api': {
target: env.VITE_API_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
// 构建配置
build: {
target: 'es2020',
outDir: env.VITE_OUTPUT_DIR || 'dist',
sourcemap: mode !== 'production',
minify: mode === 'production' ? 'terser' : 'esbuild',
terserOptions: mode === 'production' ? {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.info'],
},
} : undefined,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('vue') || id.includes('@vue')) return 'vendor-vue';
if (id.includes('element-plus')) return 'vendor-ui';
if (id.includes('echarts')) return 'vendor-charts';
if (id.includes('lodash')) return 'vendor-utils';
return 'vendor-other';
}
},
},
},
},
// 部署基础路径 — 支持子目录部署
base: env.VITE_BASE_URL || '/',
};
});常见问题排查
开发环境问题
# 问题 1:预构建缓存导致的问题
# 清除缓存并重启
rm -rf node_modules/.vite
npm run dev
# 问题 2:HMR 不生效
# 检查文件是否在 node_modules 中(不会触发 HMR)
# 检查文件名是否有特殊字符
# 问题 3:依赖变更后页面报错
# 强制重新预构建
npx vite --force构建问题
# 问题 1:构建后页面空白
# 检查 base 配置是否正确(子目录部署需要设置)
# 检查路由是否使用 createWebHistory(需服务器配置)
# 问题 2:构建后资源 404
# 使用 preview 预览构建产物
npm run build && npm run preview
# 问题 3:chunk 过大警告
# 调整 manualChunks 策略或 chunkSizeWarningLimit最佳实践总结
- 统一使用 Vite 管理配置 — 开发和生产使用同一套配置
- 为大型依赖配置 manualChunks — 减小主包体积
- 环境变量使用 VITE_ 前缀 — 并添加 TypeScript 类型
- 使用 build preview 预览 — 确保构建产物正确
- 插件开发遵循 Rollup 接口 — 兼容 Rollup 生态
