Vite 用到现在也有几年了,从 2.x 一路追到 6.x,我最大的感受是——配置项真不多,文档也写得清楚,但真到了项目里,大家翻车的点往往不是"不知道有这个配置",而是不知道这个配置在什么场景下该用、为什么这么写、改完为什么没生效。
举个最常见的例子:线上项目突然发现构建产物路径不对,资源全 404;或者本地热更新失灵,改 .vue 文件不刷新,改 .js 文件反而正常;再或者大项目跑 vite build 直接内存爆掉,终端报一段 node_options=--max-old-space-size=4096 的错误。这些我都踩过,而且每一条都不是靠背文档能解决的,得靠对配置机制的理解。
这篇文章我就按自己实际调项目的顺序,把 Vite 常用配置和优化配置完整过一遍。内容会覆盖基础路径配置、开发服务器、热更新、依赖预构建、构建分包、环境变量,以及大项目常见的内存和启动性能问题。适合已经用 Vite 建过项目、但还没系统梳理过配置的开发者,也适合正准备从 webpack 迁移、想搞清楚 Vite 配置思路的同学。
1. 确认版本和项目形态:配置前最容易忽视的两件事
1.1 Vite 4/5/6 的配置差异,别拿旧习惯写新项目
很多教程和网上的配置片段,其实是有版本前提的。Vite 的配置 API 总体稳定,但几个关键行为在不同版本里变化不小:
- Vite 3:Node 14.18+ 起步,
build.target默认是'modules',这时候define中的字符串需要显式用JSON.stringify()。 - Vite 4:Node 14.18+,默认构建目标变成了基于
baseline-widely-available的一套 browserslist 策略,对现代浏览器支持更好。 - Vite 5:Node 18+ 起步,
define配置里的未定义变量会直接报警告,optimizeDeps的行为也更激进,开发阶段依赖扫描不再那么"老实"。 - Vite 6:默认 Node 版本要求更高,同时
Environment API走向成熟,自定义环境配置的玩法更多。
最常见的坑是:网上抄了一段配置,里面写了 define: { 'process.env': {} },在 Vite 2/3 里没问题,到了 Vite 5 就开始告警,到了 Vite 6 直接提示不推荐。倒不是说这段配置一定错,而是你可能根本不需要它——Vite 默认只给 import.meta.env 做静态替换,你要是没装 process.env 相关的依赖,代码里就不该出现 process.env 这种写法。
所以拿到任何配置,第一件事是确认 package.json 里的 vite 版本。可以用下面这个命令看:
bash复制npx vite --version
然后再去看对应版本的官方迁移指南,别凭记忆写。
1.2 Vue 3 项目和 React 项目的配置差异
"Vite 创建 Vue3 项目"这个热门搜索词背后,很多人其实卡在插件选型上。Vite 本身是框架无关的,只有通过插件才能处理编译逻辑,所以项目形态决定了你要不要额外装插件。
以 Vue 3 项目为例,标准的配置文件长这样:
js复制import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
})
注意几个细节:
@vitejs/plugin-vue管的是.vue单文件组件编译,@vitejs/plugin-vue-jsx才是管 JSX 的。如果你在.vue文件里写 JSX,两个都要装。- 如果项目用了
<script setup>,plugin-vue会自动处理,不需要额外配置。 - React 项目则需要
@vitejs/plugin-react,别搞混。
CSS 预处理器这块也要提一下。Vite 内置了对 less、sass、stylus 的支持,前提是你先装好对应的 Node 依赖。没用过的人很容易漏这一步:
bash复制npm install -D sass
装完就能在 <style lang="scss"> 里直接写。如果你还想全局注入公共变量,再用 css.preprocessorOptions:
js复制export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/variables.scss" as *;`
}
}
}
})
这里有个隐藏坑:additionalData 里的 @ 别名能不能用,取决于你是否在 css.preprocessorOptions 里配置了 import 路径解析,Vite 不会默认帮你把 @ 转成 src。上面的写法我实际用过,@use 语法里 Vite 会走别名解析,但如果你用旧式 @import,有时候会遇到相对路径报错。建议统一用 @use。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础配置:入口、路径、别名与静态资源,改错一次全局 404
2.1 root 和 base 的边界:一个管开发,一个管构建
先说 root。默认情况下 Vite 会把 vite.config.ts 所在目录当作项目根目录,index.html 驱动整个应用入口。绝大多数项目不需要改 root,除非你的配置文件放在非根目录:
js复制export default defineConfig({
root: path.resolve(__dirname, 'client'),
server: {
fs: {
// 允许访问工作区根目录外的文件
allow: ['..']
}
}
})
一旦改了 root,index.html 的路径、public 目录的位置、resolve.alias 的基准都会跟着变。我在一个 monorepo 项目里被这个坑过一次,配置文件在仓库根目录,前端代码在 client/ 下,结果 public 目录没跟着移动,构建后一直找不到 favicon.ico。
再说 base。这是最容易引起线上故障的配置。base 决定了构建产物里静态资源引用的基础路径:
- 部署到域名根路径:
base: '/' - 部署到子路径:
base: '/admin/' - 部署到 CDN 或任意路径都不确定:
base: './'
js复制export default defineConfig({
base: process.env.NODE_ENV === 'production' ? '/admin/' : '/'
})
很多新手会问:为什么本地开发一切正常,npm run build 之后页面空白、资源全 404?十有八九是 base 没配。Vite 在构建时会把 HTML 里的资源地址拼上 base,如果你部署到服务器二级目录但 base 还是 /,资源就会指向根域名的错误路径。反之,如果你在 vite.config.ts 里写了 base: './',那么本地开发模式下 index.html 里的资源引用也会变成相对路径,偶尔会引发路由跳动问题。
我的实践经验是:开发环境保持默认 /,生产环境按部署路径显式指定,别依赖动态判断。如果代码仓库里不方便写死,就用环境变量覆盖:
js复制base: process.env.VITE_BASE_PATH || '/'
然后在构建命令里传:
bash复制VITE_BASE_PATH=/admin/ vite build
2.2 resolve.alias:别名配置的优雅解法与隐藏陷阱
别名是几乎每个项目都会用到的配置,用来解决 ../../../../src 这种地狱级相对路径:
js复制import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
这段配置在 Vite 官方创建的 Vue3 模板里就有。注意写法,用的是 fileURLToPath 和 import.meta.url,而不是 path.resolve(__dirname)。因为 vite.config.ts 如果是 ESM 模块,__dirname 是不存在的,用 URL 更通用。
别名配置有几个容易踩的坑:
- 别名别定义太宽。比如
alias: { '@': '.' },依赖预构建的时候容易把 node_modules 里的路径也匹配进来,导致开发时怪异报错。 - 配置完别名后 IDE 不识别。这跟 Vite 没关系,是你项目里缺
jsconfig.json或tsconfig.json的路径映射,需要在编译器配置里同步加:
json复制{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
- 别在运行时动态拼接路径。
import('@/views/' + routeName + '.vue')这种动态导入,Vite 无法静态分析别名,构建时会彻底迷失。这种情况要用import.meta.glob,后面环境变量和动态路由那节我会细说。
2.3 publicDir 和 assetsInclude:静态资源到底该放哪
很多项目里资源文件的放置是混乱的:有人把图片放 src/assets,有人直接丢 public,还有人从外部目录引用。这背后的规则其实很明确:
publicDir(默认public)里的文件原样拷贝到构建产物根目录,不会经过编译和处理。适合放 favicon、robots.txt、manifest.json 这类不被代码逻辑引用的文件。src/assets里的文件会经过 Vite 的静态资源处理,小于build.assetsInlineLimit的会被转成 base64 内联,大文件会被产出带 hash 的文件名,适合放被组件引用的图片、字体等。
js复制export default defineConfig({
publicDir: 'public',
assetsInclude: ['**/*.xlsx', '**/*.wasm']
})
assetsInclude 用来扩展 Vite 默认识别的静态资源类型。默认情况下 Vite 能处理常见的图片、视频、字体,但像 Excel、XML 这种文件,如果你直接 import 进来,Vite 会把它当源码处理。用 assetsInclude 声明之后,它就会按静态资源输出。
我遇到过最诡异的场景是:项目里有个 .glb 格式的 3D 模型文件,直接用 import modelUrl from '@/assets/model.glb',开发环境正常,构建后却报"Unexpected token"。原因就是 .glb 不在默认资源类型里,构建时被当成了 JS 解析。加上 assetsInclude: ['**/*.glb'] 后问题立刻消失。
3. 开发服务器:端口、代理与热更新排查实战
3.1 host、port、strictPort:端口被占用也别慌
开发服务器的配置大概是大多数人第一次接触 Vite 配置的地方:
js复制export default defineConfig({
server: {
host: '0.0.0.0',
port: 5173,
strictPort: false,
open: true,
cors: true
}
})
host: '0.0.0.0'表示监听所有网卡地址,便于局域网内手机或同事访问。port是期望端口,Vite 默认如果发现被占用,会自动往上递增(5174、5175……),此时终端会显示实际端口。strictPort: true设置后,端口被占用就直接报错退出,而不是默默换端口。CI 环境里建议开strictPort,否则自动化脚本拿到错误端口会莫名其妙跑飞。open: true会在启动后自动打开浏览器,但如果你同时开了host: '0.0.0.0',部分系统会打开的地址是0.0.0.0,访问不了,这时可以配合open: '/index.html'控制打开路径。
另一个开发阶段高频需求是 HTTPS。给 server.https 配上证书后,Vite 就能以 HTTPS 跑本地服务,这在调试微信、支付等需要安全域名的场景里很好用:
js复制server: {
https: {
key: fs.readFileSync('./certs/localhost-key.pem'),
cert: fs.readFileSync('./certs/localhost-cert.pem')
}
}
更省事的方式是装 @vitejs/plugin-basic-ssl,启动时自动生成临时证书,适合不需要固定证书的开发场景。
3.2 proxy 代理配置:跨域问题的标准答案
开发模式下跨域是绕不开的,Vite 的 server.proxy 本质是一个基于 http-proxy 的代理层。你前端请求 /api,代理帮你转发到后端真实地址,同时因为代理发生在服务端,浏览器不感知,自然就没有跨域问题:
js复制server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
几个容易被忽略的细节:
changeOrigin: true一定要开。不加的话,后端收到的请求头里Host还是前端地址,部分后端框架会做域名校验,直接返回 403。rewrite是修改转发路径的。上面例子中前端请求/api/user,代理转发到后端的实际路径是/user。如果你的后端接口本身就有/api前缀,就不需要rewrite。- 代理不生效的一个常见原因是
target写成了localhost,而后端实际监听的是 IPv6 的::1。遇到ECONNREFUSED时,尝试把target改成http://127.0.0.1:8080。
如果接口需要带 cookie 或鉴权头,还要加 cookieDomainRewrite 等配置。我在对接外部登录服务时发现,cookie 的 Domain 不重写,浏览器端永远存不住 session,排查了很久才定位到。这里直接补充一个比较完整的写法:
js复制proxy: {
'/api': {
target: 'http://127.0.0.1:8080',
changeOrigin: true,
cookieDomainRewrite: '',
headers: {
'X-Forwarded-Host': 'localhost:5173'
}
}
}
3.3 热更新失效排查:只改 Vue 不刷新,改 JS 反而正常
这是热门搜索词里出现频率很高的问题:"vite 改vue文件不热更新了,改js文件更新js"。我遇到过,而且不止一次。先说结论:热更新失效基本跟配置无关,跟文件系统事件有关,少数情况是依赖缓存导致。
排查链路建议按下面顺序走:
第一步:确认是不是 Vite 服务端和浏览器连接断了。打开浏览器开发者工具 Console,看看有没有 [vite] connecting... 或 WebSocket connection failed 的提示。有的话检查服务器是否开了代理,反向代理没有支持 WebSocket 会导致热更新消息传不到浏览器。
第二步:确认是不是文件监听没触发。Vite 默认使用 chokidar 监听文件变化,在 Windows 上、或者是用 Docker、VMware 挂载的目录,文件系统事件经常丢失。表现为:改 .js 文件有时候能触发,有时候不能,或者只触发一次。解决办法是改用轮询模式:
js复制server: {
watch: {
usePolling: true,
interval: 100
}
}
usePolling: true 会强制用轮询代替系统事件,代价是 CPU 占用升高,但能解决绝大多数"改了没反应"的问题。如果项目在 Docker 里,建议在 docker-compose.yml 里把项目目录挂载加上 :cached,减少 IO 压力。
第三步:如果你用的是 pnpm,Vite 对 pnpm 的符号链接结构有特殊处理。某些版本的 Vite 需要配合:
js复制server: {
watch: {
ignored: ['**/node_modules/**', '**/.git/**']
}
}
这里的 ignored 是减少监听范围的优化,不是越多越好。把 node_modules 忽略掉能显著降低 CPU 和内存占用,但如果你正巧在调试 node_modules 里某个包(比如改了源码想看效果),就会遇到底层依赖不热更新的问题。这种情况可以临时把 ignored 里的 node_modules 改成一个具体包的路径,比如 '**/node_modules/my-package/**'。
第四步:检查 optimizeDeps 缓存。如果某个依赖在预构建后变化了,热更新可能不会正确感知。删掉 node_modules/.vite 目录重新启动即可:
bash复制rm -rf node_modules/.vite
顺便提一句,vite.config.ts 本身改动会触发整站重启,这不是热更新,是 Vite 的自动重启机制。如果你改了配置文件发现页面刷新很久,别慌,这是预构建重新跑了一遍。
4. 依赖预构建:缓存机制与 include/exclude 的正确姿势
4.1 optimizeDeps 到底做了什么
Vite 开发服务器启动时,会先扫描项目里依赖到的第三方模块,然后用 esbuild 把它们预打包成 ESM 格式,放到 node_modules/.vite/deps 目录下。这一步被称为"依赖预构建"。
预构建解决了两个问题:
- 兼容性:很多 npm 包发布的是 CommonJS 格式(
require),浏览器原生不支持,Vite 用 esbuild 把它们转换成 ESM。 - 性能:原生 ESM 在浏览器里加载大量小模块时性能很差,预构建把几百个模块的依赖包合并成少量文件,减少请求次数。
这就引出了一个重要结论:开发模式下,node_modules/.vite 里缓存了依赖的预构建结果。你改了 vite.config.ts 里的 resolve.alias 或者 optimizeDeps.include,Vite 会自动判断是否需要重新构建,但判断逻辑偶尔会失灵,尤其是用了 pnpm 或者依赖做了本地 link 的时候。
4.2 include 和 exclude:什么时候该手动指定
默认情况下 Vite 会扫描源码里的 import 语句,找出需要预构建的依赖。但有些依赖是你不会直接 import、却又被间接引入的,比如某个插件内部依赖的某个包,或者通过动态方式加载的模块。这时候就需要 optimizeDeps.include 强制纳入预构建:
js复制export default defineConfig({
optimizeDeps: {
include: ['lodash-es', 'echarts', 'animejs']
}
})
exclude 则刚好相反,告诉 Vite 哪些依赖不需要预构建。使用场景比较少,通常用于调试依赖源码时,当你希望某个包以源码形式直接从 node_modules 加载,而不经过 esbuild 转换:
js复制optimizeDeps: {
exclude: ['some-experimental-package']
}
我遇到的实际案例是:项目里用了 monaco-editor,体积巨大,预构建一次要十几秒,而且每次配置变动都要重新构建。把 monaco-editor 放入 exclude 后,虽然请求数量变多了,但开发启动速度快了一截,因为在编辑器场景里,首屏加载时间本来就长,用户感知不明显。
有一点要特别注意:include 里加了包,不代表这个包不会走 CDN 优化。预构建只影响开发模式,生产构建看的是 build.rollupOptions。
4.3 缓存失效与强制重建:改配置却不生效的终极解药
依赖预构建最大的坑就是"我明明改了配置,为什么没反应"。原因是 Vite 只有当 package.json 里的 dependencies 变化、或 lockfile 变化时,才会重新预构建。你修改了 optimizeDeps.include,Vite 理论上会检测到配置变化并重建,但如果缓存文件损坏、或者配置里的写法导致 hash 判断错误,就会出现"旧缓存一直生效"的现象。
解决思路按顺序来:
- 删除依赖缓存目录:
bash复制rm -rf node_modules/.vite
-
删除 lockfile 和 node_modules 重新安装(这一步是终极方案,一般不用)。
-
在
vite.config.ts里加一个无关紧要的修改(比如加一行注释),触发服务重启。
我遇到过最离谱的一次是:include 里加了 dayjs,但启动后 Vite 依旧走旧缓存,浏览器里 dayjs 相关功能报错。删 .vite 目录重启后一切正常。后来我翻源码发现,Vite 的缓存 hash 是基于配置文件被序列化后的字符串计算的,如果你配置里写了非常见字符或者注释,可能导致 hash 计算有偏差。所以每次改了 optimizeDeps,稳妥起见直接删缓存,别过于信任自动判断。
5. 构建优化:分包、Gzip、CDN 与资源内联的取舍
5.1 手动分包:把 node_modules 拆开,让浏览器好好利用缓存
vite build 默认会把所有依赖打进一个文件吗?不会。Vite 内部基于 Rollup 已经做了基础分包,比如就地把 react 或 vue 单独拆分。但默认策略对中大型项目往往不够友好,原因在于:只要 node_modules 里任何一个包更新,依赖包整体 hash 就会变,浏览器缓存全部失效。
我的习惯是在 build.rollupOptions.output.manualChunks 里做手动分包:
js复制export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
'ui-vendor': ['antd', '@ant-design/icons'],
'chart-vendor': ['echarts']
}
}
}
}
})
这样 react-vendor、ui-vendor、chart-vendor 会被分别打包成独立文件。以后业务代码频繁改动,ui-vendor、chart-vendor 的 hash 稳定不变,浏览器可以直接命中缓存,大幅度减少重复下载。
但手动分包不是万能的,有两个问题要注意:
- 分包粒度太粗会导致每个 chunk 都很大,首屏加载反而变慢。比如
echarts全部打进一个 chunk,哪怕你只用一个折线图,也得加载整个包。这种情况更适合用echarts/core按需引入,而不是粗暴地整包分包。 - 分包之间的循环依赖。有些库内部互相引用,手动分组后 Rollup 可能报 "Cannot access before initialization",这种时候需要调整分组,把互相依赖的库放进同一组。
如果项目依赖很多,手动维护 manualChunks 列表很痛苦。推荐一个插件 vite-plugin-chunk-split,它可以用自定义函数按体积或路径自动分包。但对多数项目,手写几个 vendor 分组就够用了,别把这个搞得太复杂。
5.2 Gzip 压缩:服务器先开,插件后补
构建时开启 Gzip 压缩,能把 JS/CSS 体积减少 60%-70%,这是性价比最高的优化手段。但要注意:Vite 构建时做的 Gzip,是把文件预压缩成 .gz 文件放在产物里,真正交给浏览器还得看服务器支不支持。如果服务器没有配置 gzip static 模块,预压缩文件就白生成。
构建侧推荐 vite-plugin-compression:
bash复制npm install -D vite-plugin-compression
js复制import viteCompression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz'
})
]
})
threshold: 10240表示超过 10KB 的文件才压缩。algorithm: 'gzip'可以换成brotliCompress,Brotli 压缩率更高,但需要服务器支持。
服务器侧,Nginx 开启 gzip 有两种方式。如果你的静态资源服务器已经配置了 gzip on;,就不需要预生成 .gz 文件,直接让 Nginx 动态压缩。但如果流量大、且文件是构建产物,建议用预先压缩,让 Nginx 通过 gzip_static on; 直接读取 .gz 文件,省去每次请求的压缩开销。
这里我踩过坑:某个项目构建时用了 vite-plugin-compression,本地拿到产物目录一看,.gz 文件都在,但上线后网络面板里下载的还是原始大小。最后发现是 Nginx 没开 gzip_static。先把服务器配置检查好,再谈构建压缩,否则就是白费功夫。
5.3 静态资源内联阈值:小图变 base64 的平衡点
build.assetsInlineLimit 控制小于指定字节数的图片、字体等静态资源是否被内联为 base64。默认值是 4096(4KB)。这个值和 webpack 里的 url-loader 的 limit 思路一致。
js复制build: {
assetsInlineLimit: 8 * 1024 // 8KB
}
把阈值调大,更多小图会被内联到 JS/CSS 里,减少网络请求。但代价是产物体积增大。如果一个页面引用了 20 张 7KB 的小图标,全部内联后,加载时体积多了 140KB,但能省掉 20 次 HTTP 请求。
我的建议是:threshold 设在 4KB 到 8KB 之间最稳妥。HTTP/2 普及之后,多请求的开销没那么可怕,反而 base64 体积膨胀 33% 的问题被放大了。小项目用默认值就挺好,别刻意调大。
同样影响构建产物的还有 build.sourcemap。
js复制build: {
sourcemap: true
}
如果你在排查线上问题,sourcemap 是刚需;如果只是为了压缩体积,保持关闭。sourcemap: 'hidden' 是那种"生成 map 文件但不在源码注释里引用"的模式,可以配合错误监控平台使用,避免源码映射暴露在浏览器开发者工具里。
5.4 外部化第三方库:把体积还给 CDN
有的团队习惯把 react、vue、lodash 这类大库通过 CDN 引入,减少打包产物体积。Vite 里这需要两步:配置 external 告诉构建器不打进包,再用 vite-plugin-html 或者直接在 index.html 里加入 CDN script。
js复制build: {
rollupOptions: {
external: ['vue', 'axios']
}
}
但注意,配置了 external 之后,生产构建会报错,因为 vue 这个模块找不到声明,除非你在 index.html 里通过全局变量暴露它。实际操作中,如果你用的是 Vue 项目,官方推荐用 vite-plugin-cdn-import 这种插件,它会自动帮你把 CDN 链接注入 HTML,并且生成对应的 define 配置:
bash复制npm install -D vite-plugin-cdn-import
js复制import viteCDN from 'vite-plugin-cdn-import'
plugins: [
viteCDN({
modules: [
{ name: 'vue', var: 'Vue', path: 'dist/vue.global.prod.js' }
]
})
]
从我的经验看,除非公司有完善的 CDN 基础设施、或者项目部署在低带宽环境里,否则默认打包方案其实更省心。CDN 引入的版本锁定、SRI 完整性校验、离线可用性等风险,往往是上了线才发现的坑。更推荐的方式是先用 rollup-plugin-visualizer 分析包体积,找出真正的大头,再有针对性地优化。
js复制import { visualizer } from 'rollup-plugin-visualizer'
plugins: [
visualizer({ open: true, gzipSize: true, brotliSize: true })
]
这个插件会在构建后自动打开一个可视化 HTML 页面,列出每个 chunk 的体积。我每逢大项目必装它,一眼就能看出哪些依赖膨胀了,再决定是分包、按需加载,还是 CDN 外部化。
6. 环境变量、模式与大项目性能问题排查
6.1 env 文件与优先级:VITE_ 前缀规则
Vite 从 .env 系列文件加载环境变量,但只有以 VITE_ 开头的变量才会被暴露到客户端代码中。这个设计是为了避免把服务器敏感信息(比如数据库密码)打进前端产物。
常见文件命名:
.env:所有情况.env.development:开发模式(vite命令).env.production:生产模式(vite build命令).env.staging:自定义模式(需要--mode staging)
优先级方面,具体模式的文件 > 通用 .env。同名字段,.env.development 会覆盖 .env。加载顺序上,Vite 是先从 .env 读,再读 .env.local,再读对应模式文件,越靠后优先级越高。.local 后缀文件通常用于个人本地配置,一般会加进 .gitignore。
在代码里使用:
js复制const apiBase = import.meta.env.VITE_API_BASE_URL || '/api'
在配置文件 vite.config.ts 里也能读环境变量,但注意要用 Vite 提供的 loadEnv:
js复制import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
define: {
__APP_ENV__: JSON.stringify(env.APP_ENV)
}
}
})
loadEnv 第三个参数传 '' 表示加载所有前缀的环境变量,否则默认只加载 VITE_ 开头的。这个写法适合在配置里拿一些构建阶段才需要的变量。
6.2 import.meta.glob:动态路由和批量导入的现代解法
很多人在"Vue3 Vite 动态路由"这个场景里翻车,因为他们还在用 webpack 时代的 require.context() 思路。Vite 中对应的是 import.meta.glob。这个 API 能批量导入多个模块,而且支持懒加载:
js复制const modules = import.meta.glob('@/views/**/*.vue')
得到的 modules 是一个对象,key 是文件路径,value 是一个函数,调用后才返回模块:
js复制const loadView = (path) => {
return modules[`/src/views/${path}.vue`]()
}
配合 Vue Router 动态添加路由时就非常自然:
js复制const route = {
path,
component: () => import(`@/views${path}.vue`)
}
注意:import.meta.glob 的路径参数必须是静态字符串,不能拼接变量。这是 esbuild/Rollup 做静态分析的前提。如果需要过滤文件,可以加 eager: true 和 query 参数:
js复制const modules = import.meta.glob('./locales/*.json', { eager: true })
eager: true 表示同步导入,适合配置类文件。动态路由场景推荐默认的懒加载模式,这样每个页面会按需分包加载,不会首屏全量打包。
6.3 大项目内存溢出:node_options 报错的真相
最后说一个搜索热度非常高的错误:$ node_options=--max-old-space-size=4096 vite 'node_options' 不是内部或外部命令。
这个错误几乎都出现在 Windows 环境中。错误原因很简单:在 Windows 的终端里,节点前设置环境变量 这种 Unix 语法不生效。FOO=bar vite 在 Linux/macOS 里表示设置临时环境变量并执行命令,但在 Windows 的 CMD 或 PowerShell 里,FOO=bar 会被当做一个命令名,于是报"不是内部或外部命令"。
正确做法有几种:
方式一:在 package.json 的 scripts 里直接写 Node 参数:
json复制{
"scripts": {
"dev": "vite",
"build": "node --max-old-space-size=4096 node_modules/vite/bin/vite.js build"
}
}
Windows 和 Unix 都能识别 node --max-old-space-size=4096,因为它是 Node 进程自己的参数。
方式二:用 cross-env 统一环境变量语法:
bash复制npm install -D cross-env
json复制{
"scripts": {
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build"
}
}
cross-env 会处理跨平台环境变量设置差异。注意这里用了 NODE_OPTIONS 环境变量,而不是 node_options。Node 官方支持的环境变量是 NODE_OPTIONS,小写写法在 Linux 系统上可能能碰巧生效,但不推荐依赖。
回到内存问题本身。Vite 构建时内存溢出,根因通常是依赖太多、或者某几个 chunk 特别大,导致 Rollup 或者 terser 压缩时内存爆了。调整内存参数只是治标,真正要做的是:
- 用
rollup-plugin-visualizer确认哪些 chunk 体积超大。 - 对超大 chunk 做手动分包或按需加载。
- 压缩时选择体积更小的工具,比如用 esbuild 替代 terser 做压缩:
js复制build: {
minify: 'esbuild'
}
minify: 'esbuild' 比默认的 terser 快很多,内存占用也更低,代价是压缩率稍差一点。中大型项目我最常用这个方案,构建时间能缩短 40% 左右。
6.4 启动速度优化的三个实用手段
除了内存,开发阶段另一个常见痛点就是"启动慢"。Vite 虽然比 webpack 快,但依赖多了之后,冷启动依然会卡在预构建阶段。我的提速经验:
- 用
optimizeDeps.include提前声明核心大依赖。预构建分散到多个小任务,而不是启动时一次性扫描全量。
js复制optimizeDeps: {
include: ['vue', 'pinia', 'vue-router', 'lodash-es']
}
-
减少
server.watch的监听范围。在 monorepo 项目里,配置ignored把无关工作区排除掉,监听事件变少,处理器负担降低。 -
升级到 Vite 5+。Vite 5 之后官方对依赖扫描和预构建流程做了大量性能优化,同一个项目我从 4 升到 5,冷启动时间从 8 秒降到 3 秒左右,配置代码基本没动。如果项目还在旧版本,优先考虑升级而不是盲目调配置。
-
开启
server.fs.strict: false要谨慎。很多教程会让你关闭文件系统权限校验,这在 monorepo 里确实能解决跨包访问问题,但它会削弱安全性,不该作为默认选项。更好的方式是精确配置server.fs.allow列表,把需要访问的工作区目录加进去:
js复制server: {
fs: {
allow: [searchForWorkspaceRoot(process.cwd()), path.resolve(__dirname, '../packages')]
}
}
还有一个我自己用得比较多的操作:如果项目里同时存在多个 Vite 应用,可以在 optimizeDeps 里用 force: true 强制更新预构建缓存。但要注意 force: true 在 CI 环境会拖慢构建,正常开发不建议长期开启。
从 Vite 2 到 Vite 6,这个构建工具的配置体系总体是收敛的,核心思路也很稳定:开发阶段解决依赖预构建和模块转换,生产阶段解决分包、压缩、缓存。很多问题的排查其实不需要背配置文件,而是理解 Vite 在什么阶段做了什么、缓存从哪里来、产物怎么被服务器消费。希望这篇博客能帮你少走几趟弯路——至少下次再看到"改了 Vue 文件不热更新"或者"内存溢出报错"的时候,你能直接定位到问题出在哪一层。
