开发服务器启动 25 秒、改一行代码 HMR 转 800 毫秒——等编译耗掉的时间比写代码还多,前端工程化被逼着换路。当时团队讨论过三条路:继续用 Webpack 5 调优、上 Turbopack、迁移 Vite 5。权衡开发体验、生态成熟度和迁移成本后,我们选了 Vite,迁移完成那天启动从 25s 降到 1.5s、HMR 从 800ms 降到 80ms,全组都松了一口气。下面把我们在生产环境里验证过的 Vite 全套配置和踩过的坑完整写出来,覆盖从基础配置到多环境构建的每一个环节。
一、为什么选 Vite
先看一组我们内部实测的对比数据:
| 维度 | Vite 5 | Webpack 5 |
|---|---|---|
| 启动速度(dev) | 1.5s(按需编译) | 25s(全量打包) |
| HMR | < 100ms | 500-2000ms |
| 构建速度 | 快(Rollup + esbuild) | 慢 |
| 配置复杂度 | 简单 | 复杂 |
| 生态 | 较新但活跃 | 成熟 |
选 Vite 不是因为它快这么简单。核心差异在架构:Webpack 冷启动要把整个依赖图预先打包,Vite 在 dev 期直接按需编译单个模块,借助浏览器原生 ESM 加载,文件越大优势越明显。我们这种 200+ 页面的项目,Webpack 启动 25s,Vite 只要 1.5s,几乎是零等待。迁移过程中我们也担心过生态问题,实际跑了两个月,常用插件都覆盖得不错。
二、基础配置
Vite 的配置入口是 vite.config.ts,一个文件管 dev 和 build 两种场景:
// 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: {
host: '0.0.0.0',
port: 5173,
open: true
},
build: {
target: 'es2020',
outDir: 'dist',
assetsDir: 'assets',
sourcemap: mode !== 'production',
minify: 'terser',
terserOptions: {
compress: { drop_console: true }
}
}
}
})
这里几个点值得说明。loadEnv 第三个参数传空字符串,才能把环境变量全部加载出来;sourcemap 只在非生产开启,生产关了能省体积也避免源码泄露;drop_console 在生产去掉 console 输出。我们最初没配 drop_console,线上排查问题时看到一堆业务日志在打,反而干扰真正的告警日志。
三、环境变量
多环境部署离不开环境变量,Vite 约定按文件区分:
// .env
VITE_API_BASE=https://api.example.com
VITE_APP_NAME=AI 评测平台
// .env.development
VITE_API_BASE=http://localhost:8080
// .env.production
VITE_API_BASE=https://api.prod.com
// 代码中
const base = import.meta.env.VITE_API_BASE
环境变量必须带 VITE_ 前缀才能在客户端代码里访问,这是 Vite 的安全设计——不带前缀的变量是给 Node 端用的,直接塞进浏览器会泄露敏感信息。我们迁移时第一版踩过这个坑,写了个不带前缀的密钥变量,好在上线前 review 拦住了。注意 .env 里只能放非敏感配置,真正的密钥要走服务端注入。
四、Vue 插件
Vue 项目必备插件组合如下,一个是 SFC 编译,一个是 JSX 支持:
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
plugins: [
vue(),
vueJsx() // 支持 JSX/TSX
]
我们的评测配置页用了不少 JSX 写动态表单,没有 vueJsx 插件 SFC 里的 tsx 文件直接编译报错。插件顺序一般固定,vue 放前面,出错时优先检查版本是否和 Vite 大版本匹配,我们遇到过插件和 Vite 5 不兼容导致 HMR 失效的版本。
五、自动导入
手工 import 组件和 API 是样板代码的大头,我们引入 unplugin 系列解决:
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()],
imports: ['vue', 'vue-router', 'pinia'],
dts: 'src/auto-imports.d.ts'
}),
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/components.d.ts'
})
]
配置后模板里直接 <el-button @click="onClick"> 不用 import,代码里直接用 ref/computed 也不用手动引,Vite 编译期自动补全。两个 dts 文件是自动生成的类型声明,要提交进仓库,IDE 才能正确提示。注意自动导入省了写法,但依赖隐式,团队约定还是要在文件顶部保留关键 import 以便 code review 时快速定位依赖来源。
六、TypeScript 集成
Vite 本身不做类型检查,它只管转译,类型检查要交给 tsc 单独跑:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue()
],
// tsconfig.json 的 paths 必须配
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
}
})
tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
这是最容易踩的坑:vite.config.ts 里配了 @ 别名,但 tsconfig.json 没同步配 paths,IDE 就一路飘红。我们 CI 里加了 tsc --noEmit 做类型门禁,顺便把 @ 路径映射统一成一份配置维护,避免两边漂移。
七、CSS 处理
SCSS 变量全局注入可以省掉每个文件顶部的 import 样板:
// vite 自动处理 SCSS/Less/Stylus
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
},
modules: {
localsConvention: 'camelCase'
}
}
})
additionalData 会把 variables.scss 的内容自动拼到每个 SCSS 文件开头,所有组件都能直接用变量。副作用是构建时间会略增,毕竟每个文件都多一次解析,我们觉得收益大于成本。CSS Modules 在 Vue 里就是 <style module>,localsConvention 设为 camelCase 后模板里访问 $style.title 这种写法更顺手。
八、静态资源处理
静态资源的输出路径可以按类型分类,保持目录整洁:
export default defineConfig({
assetsInclude: ['**/*.md', '**/*.svg'],
build: {
rollupOptions: {
output: {
assetFileNames: (assetInfo) => {
if (assetInfo.name?.endsWith('.png')) {
return 'img/[name]-[hash][extname]'
}
if (assetInfo.name?.endsWith('.woff2')) {
return 'fonts/[name]-[hash][extname]'
}
return 'assets/[name]-[hash][extname]'
}
}
}
}
})
默认情况下所有资源都堆在 assets 目录,图片、字体混在一起,部署后定位问题麻烦。按类型分目录后,CDN 规则也好写,比如字体文件单独设缓存策略。assetsInclude 是给 import 特定后缀文件的场景用的,我们用它支持直接 import Markdown 说明文档。
九、构建优化
构建优化要按顺序做,跳步容易白费功夫:
- 先用 visualizer 分析产物体积分布;
- 再按依赖类型拆 chunk;
- 然后开启 terser 压缩并去掉调试代码;
- 最后用 vite-plugin-compression 生成 Gzip 和 Brotli 预压缩文件。
四步都做齐,产物才能同时拿到”体积小、缓存细、传输快”三个收益。下面分别看每步的配置。
1. 拆 chunk
按依赖来源拆 chunk,让浏览器并行加载互不阻塞:
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('echarts')) return 'echarts-vendor'
if (id.includes('element-plus')) return 'element-plus'
if (id.includes('vue')) return 'vue-vendor'
return 'vendor'
}
}
}
}
}
2. 压缩
生产环境用 terser 压缩并去掉调试代码:
import terser from '@rollup/plugin-terser'
build: {
minify: 'terser',
terserOptions: {
compress: { drop_console: true, drop_debugger: true }
}
}
3. Gzip / Brotli
构建时预压缩,省去网关实时压缩的 CPU 开销:
import viteCompression from 'vite-plugin-compression'
plugins: [
viteCompression({ algorithm: 'gzip' }),
viteCompression({ algorithm: 'brotliCompress', ext: '.br' })
]
拆 chunk 和压缩这套组合拳打下来,我们首屏产物从 900KB 降到 320KB gzip。三个配置要一起上才有效果:只拆 chunk 不压缩,传输体积下不去;只压缩不拆 chunk,缓存粒度太粗,改一行代码整包缓存失效。
十、路径别名
多级目录下用别名代替相对路径,重构时不用改一堆 import:
// vite.config.ts
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils')
}
}
别名的意义不只是少打字,更重要的是模块边界清晰。我们要求跨模块引用一律走 @ 别名,禁止 ../../ 相对路径,这样目录调整时能快速确认引用范围。老项目里用 vim 全局替换相对路径的那段经历,再也不想体验第二遍。
十一、代理配置
dev 期跨域问题用 Vite 代理解决,还能把 WebSocket 一并转发:
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api')
},
'/ws': {
target: 'ws://localhost:8080',
ws: true
}
}
}
我们的评测任务进度走 WebSocket,ws: true 打开后长连接也能穿透代理。有个细节:代理只对 dev server 生效,生产环境要把同源策略交给网关处理,我们刚开始不知道这点,上线后接口全部 404,排查半天才发现是生产没配 Nginx 转发。
十二、HMR 配置
HMR 参数调整能明显改善开发手感:
server: {
hmr: {
overlay: false, // 关错误遮罩
port: 5174 // 避免与 server 端口冲突
},
watch: {
usePolling: true, // Docker/WSL 必加
ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**']
}
}
overlay 关掉后语法错误不再用全屏红罩盖住编辑器;usePolling 是 Docker/WSL 环境的救命配置,这些环境下 inotify 监听经常失效,开了轮询文件变更才能及时触发 HMR。ignored 把 node_modules 等目录排除在监听外,能减少大量无效文件事件。我们在 CI 容器里构建过,不加 usePolling 时 HMR 完全不工作,加上之后一切正常。
十三、SVG 图标
SVG 图标统一管理,避免 iconfont 的字体加载抖动:
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
plugins: [
createSvgIconsPlugin({
iconDirs: [resolve(__dirname, 'src/assets/icons')],
symbolId: 'icon-[dir]-[name]'
})
]
<template>
<svg><use href="#icon-edit" /></svg>
</template>
我们把所有图标文件放进 src/assets/icons,插件构建时自动生成雪碧图,模板里用 symbol 引用。相比 iconfont,SVG 不依赖字体加载,不会出现首屏图标变成方块的问题。命名规则 icon-[dir]-[name] 建议固定下来,新图标按目录归属,引用时一眼能找到来源。
十四、生产构建分析
体积分析是构建优化的第一步,看不清体积分布就没法做决策:
import { visualizer } from 'rollup-plugin-visualizer'
plugins: [
visualizer({ open: true, gzipSize: true, brotliSize: true })
]
构建完会自动打开 stats.html,按包大小直观看到哪个依赖是体积大户。我们就是靠它发现 Element Plus 全量引入占了 700KB,才下定决心做按需引入。建议只在本地构建或带参数的环境打开,别把可视化报告塞进每次 CI 输出。
十五、多环境构建
一套代码打多种环境包,用 mode 区分:
// package.json
{
"scripts": {
"dev": "vite",
"build:dev": "vite build --mode development",
"build:staging": "vite build --mode staging",
"build:prod": "vite build --mode production"
}
}
每个环境对应一个 .env.{mode} 文件,vite build --mode staging 会加载 .env.staging。注意 --mode 只影响环境变量加载,构建产物本身没有差异,所以环境差异要全部收敛在环境变量里,不要让代码里出现写死的环境判断。
十六、SSR/SSG
评测平台的登录页和营销落地页需要秒开,我们用了 Vite SSG 预渲染:
// vite.config.ts
import { defineConfig } from 'vite'
import ViteSSG from 'vite-ssg'
export default ViteSSG(
{ /* 同上 */ },
{
// routes
}
)
构建时把指定路由预渲染成静态 HTML,用户拿到的第一帧就是完整内容,首屏秒开。SSG 的前提是页面不依赖登录态和动态数据,我们只对落地页和登录页启用,评测工作台这类强交互页面继续走 SPA 模式。
十七、踩过的坑
工程化迁移的坑大多藏在细节里,列出来给后来人省点时间:
- 环境变量不生效:必须
VITE_前缀,且要在.env而非process.env。 - 别名 IDE 报错:要在
tsconfig.json也配paths,不能只在 vite.config.ts 配。 - HMR 不更新:Docker/WSL 下文件监听失效,加
usePolling: true。 - CSS 闪烁:CSS 被打进 JS 里异步加载。检查 build 是否抽离 CSS。
- 大文件 dev 期慢:import 一个 5MB 的 JSON,dev 启动慢。改用 fetch 按需加载。
- 生产 sourcemap 泄露:生产关闭 sourcemap 或用
.map后缀上传 Sentry。 - 路径别名大小写:Linux 严格区分大小写,Windows 不区分。CI 跑在 Linux 上要严格统一。
十八、平台构建配置示例
把前面所有配置串起来,就是平台当前在用的完整配置:
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { visualizer } from 'rollup-plugin-visualizer'
import viteCompression from 'vite-plugin-compression'
import { resolve } from 'path'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
dts: 'src/auto-imports.d.ts'
}),
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/components.d.ts'
}),
mode === 'production' && visualizer({ open: true }),
viteCompression({ algorithm: 'gzip' })
],
resolve: {
alias: { '@': resolve(__dirname, 'src') }
},
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: env.VITE_API_PROXY,
changeOrigin: true
}
}
},
build: {
target: 'es2020',
sourcemap: mode !== 'production',
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'element-plus': ['element-plus'],
'echarts-vendor': ['echarts']
}
}
}
}
}
})
这份配置可以当模板直接用,改几处域名和目录就行。Vite 的价值在于把”工程配置”从一门手艺变成一件顺手的事,剩下的精力应该留给业务本身。
常见问题(FAQ)
Q1:Vite 适合大型项目吗?
适合。Vite 5 已经在大量大型项目验证(Vue/Vite 官方文档都迁到 Vite)。
Q2:迁移 Webpack 成本大吗?
不大。Vite 兼容 Webpack 多数概念,主要改 config 文件和 import 方式。
Q3:Vite 生产构建用什么?
Rollup。Vite 在 dev 用原生 ESM,在 build 用 Rollup。