我整理了一份 Vite 工程化配置清单环境变量、代码规范、性能优化一次说透如果只用npm run dev把 Vite 跑起来那你其实只利用了它一半的功力。真正决定一个前端项目能走多远的是工程化配置这层看不见的骨架环境变量切得顺不顺、代码规范能不能让每位成员的提交都保持一致、构建产物能不能又快又小。最近我在某个跨平台系统里用 Vite 重构前端底座把环境变量、代码规范、性能优化这三块从头到尾重新捋了一遍踩了不少坑也沉淀出一套可以直接复用的配置思路。这篇就把实际项目里的配置过程和踩坑经验完整分享出来适合已经用 Vite 写过页面、但还没系统整理过工程化配置的团队参考。1. 环境变量体系VITE_前缀的底层逻辑与多环境切换姿势1.1 为什么 Vite 只暴露 VITE_ 前缀的变量很多团队把环境变量配置单纯理解成往 .env 文件里写东西但没弄明白 Vite 到底会把这些变量送去哪。Vite 里import.meta.env的值是构建期静态替换的不是运行时去服务器读的。也就是说打包工具在产物里会把import.meta.env.VITE_API_BASE这个位置直接替换成你配置的字面量字符串所以这个机制天然就不是为敏感信息设计的。Vite 默认只暴露以VITE_开头的变量给客户端代码这个设计并不是形式主义。如果所有环境变量都被注入到前端 bundle那么服务器上配置的数据库密码、鉴权密钥、内部服务地址就会跟着静态资源一起泄漏到浏览器端。我见过一个真实案例某团队运维在构建机的环境变量里加了数据库连接串就因为没注意前缀问题最终这些信息被打包进了 JS 文件在浏览器里能直接读到。反过来的情况也很常见——有人习惯写APP_SECRETxxx然后在代码里用import.meta.env.APP_SECRET拿值结果永远是undefined。这不是 bug是过滤机制在起作用没加VITE_前缀的变量根本进不了客户端代码。如果你确实有需求想自定义前缀可以在vite.config.ts里改envPrefixexport default defineConfig({ envPrefix: [VITE_, APP_], })不过我在实际项目里不太建议这样做因为团队里混用多种前缀容易让哪些变量会暴露到浏览器变得模糊。保持单一VITE_前缀是让配置边界清晰最省力的办法。1.2 多套环境文件的命名规则与覆盖顺序Vite 读取环境变量文件是有约定和加载顺序的。基础层面你需要这几套文件文件作用典型使用场景.env所有模式共同加载的默认配置通用常量、共享标识.env.development开发模式独有本地调试接口、mock 开关.env.staging预发布/验收模式独有联调环境、测试环境域名.env.production生产构建独有线上接口域名、CDN 开关关键点是mode的对应关系vite dev默认 mode 是developmentvite build默认 mode 是production。如果团队要发布预发布包执行vite build --mode staging此时会加载.env.staging。这个参数经常被忽略结果就是打出来的包永远走 production 配置。优先级上特定模式的.env.xxx会覆盖.env里的同名变量因为.env是所有模式都会读取的公共文件。我记得有一次全局排查问题发现.env和.env.production里都定义了接口域名团队成员以为生产会以.env为准实际上是.env.production赢了。这不算 Bug但暴露了团队约定问题共享值放.env模式私有值放模式文件同一个变量尽量只在一个地方定义。这里还埋伏了一个很容易被忽略的细节Vite 加载环境变量是在 dev server 启动时完成的运行期间修改.env文件不会热重载。我一开始也天真地以为改完文件页面一刷新就能拿到新值实际必须重启 dev server。这个点几乎每个接手的新人都会踩一次建议写进团队 Wiki。另外要注意.env文件里的值默认全是字符串VITE_ENABLE_LOGtrue读出来是字符串true。如果你要做布尔判断必须自己处理const enableLog import.meta.env.VITE_ENABLE_LOG true1.3 在 vite.config.ts 里用 loadEnv 读取自定义变量还有一类需求构建配置本身也要读取环境变量比如根据某个变量决定是否启用 CDN 插件、是否开启 sourcemap。很多人会直接写process.env.VITE_XXX但 vite.config.ts 跑在 Node 环境里它并不会自动加载项目里的.env文件。这里应该用 Vite 提供的loadEnvimport { defineConfig, loadEnv } from vite export default defineConfig(({ mode }) { const env loadEnv(mode, process.cwd(), ) const useCDN env.VITE_USE_CDN true // true 时走 CDN 路线 return { // 其他配置... } })这里有个容易翻车的点loadEnv的第三个参数是指定加载哪些前缀的变量。如果不传默认只加载VITE_开头的变量如果你传才会把.env里的所有变量都加载进来。我在一个项目里就是因为在配置文件里要读一个非VITE_前缀的私有变量又没传第三个参数结果一直拿到undefined排查了半个多小时才注意到这个 API 的细节。另外一个在配置阶段常用的手段是define它本质也是构建期文本替换。比如你想在页面里展示构建时间和 Git 分支信息可以在 define 里注入export default defineConfig({ define: { __BUILD_TIME__: JSON.stringify(new Date().toISOString()), }, })代码里直接用__BUILD_TIME__访问即可。注意值一定要包JSON.stringify否则字符串内容会被当成变量名或直接报语法错误这是个低级但出现频率很高的错误。2. 代码规范链路从编辑器提示到提交闸门再到自动导入插件的隐藏冲突2.1 ESLint 和 Prettier 的分工先把职责边界划清楚代码规范这件事我觉得最大的误区是把 ESLint 和 Prettier 混为一谈。我见过不少项目让 ESLint 兼职做格式化最后两边规则冲突保存时代的代码跟 lint 通过的代码互相打架。我习惯的分工是这样的ESLint 负责找代码中的逻辑问题和潜在错误——未使用变量、未定义变量、可疑的类型断言、import 顺序等Prettier 只负责格式统一——单引号还是双引号、行宽多少、尾逗号要不要。一个管对错一个管长相互不干涉。Vite 官方脚手架生成的工程默认带了 ESLint 配置文件但如果你想落地一套能长期维护的规范建议直接采用 Flat Config 写法eslint.config.js这是 ESLint 新一代的配置格式。对比旧的.eslintrc版本它最大的变化是删掉了.eslintignore用ignores字段统一处理忽略规则整体配置扁平化看起来更直观export default [ { ignores: [dist/**, node_modules/**, public/**], }, { files: [src/**/*.{js,jsx,ts,tsx,vue}], rules: { no-unused-vars: warn, no-console: process.env.NODE_ENV production ? warn : off, }, }, ]在实际团队落地时我跟团队约定了少而精的规则策略先接一组推荐配置和 TypeScript/React 相关推荐配置当基础规范再根据项目踩过的坑追加少数几条自定义规则不追求规则数量多。规则拉得太满团队每天花在改 lint 上的时间比写代码还多最终没人配合规范也就名存实亡了。2.2 husky lint-staged 才是提交前最后那道闸规范如果只停留在编辑器里提示意义有限因为总有人不看提示就提交。我的经验是把检查卡在 Git 提交前用自动化强制索引这样任何违反规范的代码根本进不了仓库。这里推荐组合是huskylint-staged。husky负责往.git/hooks里注入钩子lint-staged负责只对暂存区里改动的文件执行检查。为什么强烈建议用lint-staged因为大型项目跑一次全量 lint 可能要好几分钟而只检查暂存区文件通常只需要几秒反馈速度快开发者才愿意等。核心配置我放在package.json{ scripts: { lint: eslint . --max-warnings0, format: prettier --write . }, lint-staged: { *.{js,ts,jsx,tsx,vue,json,css}: [eslint --fix, prettier --write] } }再配合 commitlint 约束提交信息格式type(scope): subject这种结构。可能有人觉得提交信息规范没意义但一旦出线上问题要回滚代码、要定位某次改动是哪个需求引入的规范清晰的 git log 能帮你快速锁定边界。我在实际项目里曾经因为提交信息混乱找一次回归的引入点搜索了快两小时后来上了 commitlint历史提交终于变得可读。这套事前 lint 事后规范提交信息的组合可以说是工程化里性价比最高的投资。2.3 自动导入 API 与 ESLint 的冲突是 Vue/React 项目里最隐蔽的坑现代前端项目里unplugin-auto-import和unplugin-vue-components这两个插件用得很广它们会自动按需导入 API 和组件免去了大量import代码。但这里藏着一个特别容易被忽略的冲突代码能跑编辑器却一片红。原因很简单ESLint 不认识这些自动导入的全局 API。比如你从vue里自动导入了ref代码里直接写ref(0)ESLint 会报ref is not defined。解决方式有两种核心思路都是让 ESLint 知道这些是合法全局变量第一种是在 ESLint 配置里手动声明 globalsrules: { // ... }, languageOptions: { globals: { ref: readonly, computed: readonly, }, }第二种是我更推荐的让自动导入插件生成类型声明文件通常是auto-imports.d.ts然后把这个文件提交到仓库并 ensure 被 TypeScript 和 ESLint 同时感知。ESLint 侧可以在配置里增加环境认识Vue 官方生态也有对应的辅助处理方式。这里有一个非常典型的翻车场景团队某个人在本地运行插件后auto-imports.d.ts被.gitignore忽略了结果其他同事克隆下来项目编辑器里全是红色报错代码却编译运行正常。排查一整天才发现是 d.ts 没进仓库。所以我的建议是这类自动生成但影响类型推断的文件必须强制提交到仓库除非你能保证团队每个成员都手动跑一次生成命令。3. 性能优化先量化构建瓶颈再拆产物、压资源、上缓存3.1 构建时间到底花在哪从预构建到压缩器的取舍性能优化这件事我强烈建议先量再改。不要凭感觉优化先用vite build跑一次记录构建总耗时和产物信息再判断瓶颈在哪。Vite 开发模式快是因为它用了 esbuild 做依赖预构建、用原生 ESM 按需加载但生产构建会切换到 Rollup所以生产构建速度和你的 Rollup 配置直接相关。构建慢的原因通常集中在几个方向。第一个是大依赖重新预构建。Vite 会把依赖缓存在node_modules/.vite目录下如果某个大依赖更新后下次构建要重新预构建一次。如果你的项目特别大可以主动把构建频率高、体积大的依赖放进optimizeDeps.include列表让它提前完成预构建export default defineConfig({ optimizeDeps: { include: [axios, echarts, lodash-es], }, })第二个是TypeScript 类型检查拖后腿。很多人为了省事把vue-tsc直接放在build脚本里构建时先做全量类型检查。类型检查本身很耗时如果项目有几万行代码这块时间非常可观。我的做法是本地提交时用 lint 做轻量检查CI 流水线里单独跑vue-tsc --noEmit和生产构建解耦这样开发同学不用每次 build 都等类型检查跑完。第三个是压缩器选型。生产构建默认用 esbuild 压缩速度已经很快如果追求更高的压缩率可以换成rollup/plugin-terser代价是构建时间显著变长。我的建议是除非你的产物对体积极其敏感否则保持 esbuild 压缩即可它的压缩效果在大多数场景下已经够用。vite build构建时间长了优先看日志里哪一段耗时最多再用 Google 搜索对应场景的优化手段比盲目改配置有效得多。3.2 manualChunks 拆包策略不是拆得越碎越好拆包这件事很多人的第一反应是把所有依赖都单独拆出来。我最早也这么干过结果产物倒是拆得很开心请求数暴增首屏加载反而更慢。原因在于拆包的本质是改善缓存命中而不是减少总体积。如果拆出来的 chunk 每次改动都会变那缓存策略就失效了。我实际项目里采用的策略是三段式框架和核心运行时拆成一个包vue、vue-router、pinia这类几乎不变的代码大型可视化/图表/三方 UI 库单独拆包比如echarts、ant-design-vue这部分体积大单独拆出来后不会被业务代码的频繁改动影响缓存业务代码按路由懒加载或常规逻辑拆分。典型写法build: { rollupOptions: { output: { manualChunks(id) { if (id.includes(node_modules)) { if (id.includes(echarts)) return charts if (id.includes(vue) || id.includes(pinia) || id.includes(vue-router)) { return vue-vendor } return vendor } }, }, }, }拆包的粒度控制有个很直白的判断标准打开浏览器 Network 面板看加载瀑布图。如果首屏同时发起超过 15-20 个资源请求说明拆得太细了HTTP/1.1 时代浏览器对同一域名并发连接数有限制HTTP/2 虽然缓解了这个问题但请求数太多仍然会增加解析开销。具体的平衡点因项目而异我的经验是把大而稳的依赖单独拆其他交给 Rollup 默认策略然后看可视化报告再微调不要净想着拆尽。3.3 产物与运行时提速gzip、图片、CDN 三条线并行构建产物体积减小和传输体积减小是两回事。产物层面收益最高的优化我第一个推荐开启 gzip/brotli 压缩。用vite-plugin-compression在构建时产出.gz文件配合服务端开启gzip_static通常能帮静态资源体积下降 60%-70%。有些 CDN 会自动协商压缩但如果你的静态资源托管在 Nginx 上别忘了加一行gzip_static on; gzip_types text/plain text/css application/json application/javascript image/svgxml;第二个是图片资源。很多项目没意识到图片带来的体积压力尤其是 PPT 截图、背景图这类大图丢进src/assets后原封不动被打包进去。工程化阶段应该给 Vite 配一个图片压缩插件批量做有损/无损压缩。但要注意超大图片几百 KB 以上只靠压缩插件还是不够更好的方式是直接走 CDN 加图片处理服务用缩略图和 WebP 格式。配置层面对大图可以做到的是让它们不要打进 bundle。第三条线是base路径和 CDN。如果你的部署目录不是域名根路径Vite 会默认把静态资源引用写成绝对路径导致 404。这个问题的本质是base没配对线上部署在子路径时应该把base配成对应的路径前缀。如果要走 CDN把构建产物上传到 CDN bucket并把base设为 CDN 域名前缀让所有静态资源从 CDN 域名加载。最后别漏了运行时层面的优化路由级懒加载。Vue 路由里能写() import(/views/Home.vue)就尽量别用静态 import尤其是在页面很多、包体积大的中后台项目里。懒加载配合手动拆包能让首屏只加载真正需要的代码。4. 实际落地中的配置失误与排查思路一个环境变量没生效的完整定位过程4.1 复现现场改完 .env 不生效团队一起排查了半小时工程化配置写的时候是一套实际项目跑起来又是另一套。这里记录一个我印象深刻的排查过程。现象是这样的某位同事在本地开发时改了.env.development里的接口地址页面刷新后发出去的请求还是旧地址。他第一反应是清浏览器缓存无效第二反应是重启 dev server还是无效。最后找到我我陪他走了一遍完整的排查链路。第一步确认调用方式。代码里如果写的是process.env.VITE_API_BASE而在浏览器端跑这个变量在本地的运行其实拿不到应该改成import.meta.env.VITE_API_BASE。这是最多人犯的第一个错但这次代码里写法没错。第二步确认 mode 和文件加载顺序。见他还开着 dev server我让他把 dev server 停掉再启一次并查看控制台输出。意外的是启动命令带了--mode staging原来 package.json 的 dev script 被改成vite --mode staging导致他改了.env.development但实际加载的是.env.staging。这个根因藏得很深因为肉眼看不出来启动参数有什么问题。第三步确认文件名和变量名前缀。检查发现文件名写错了他改的是.env.development文件但实际读取的文件是.env.development没错再检查变量名前缀VITE_也在。最终定位问题就出在模式错位。修复方式非常简单——dev script 去掉--mode staging恢复成vite让开发模式默认走development。这个案例最有价值的不是修复本身而是把配置问题可能出错的环节拆成了可复现的检查项调用方式、mode 开关、文件名、前缀、是否需要重启。以后团队再遇到同类问题照着这一套链路去验证效率比边猜边试高得多。4.2 配置类问题的通用定位思路让问题可复现而不是靠猜工艺工程配置出问题的场景千变万化但定位方法有共套路。我总结成四个步骤最小复现不要在大项目里瞎试先写一个最小环境比如新建一个临时 Vite 项目把有问题的变量和调用复现出来。如果最小项目能复现说明配置逻辑有问题如果最小项目正常说明是项目级配置干扰比如多份配置文件合并、插件覆写等。从启动日志找线索Vite 在启动的时候会解析配置加载环境变量任何解析错误都会打印在控制台。很多人不看日志就直接上网搜其实日志里的第一行报错往往就是答案。对照文档确认 API 参数像loadEnv这样的 API参数差异会导致行为完全不同。遇到诡异问题先把文档里所有参数和返回值注释对照一遍往往能发现为什么我没传第三个参数就拿不到非前缀变量这类坑。二分排查如果是插件配置引发的冲突把所有插件注释掉再逐个开启哪个插件开启后出问题那么基本就是它的锅。我见过不少构建异常最后都是这样一步步锁定的。这套思路不仅适用于 Vite对 Webpack、Rollup 的工程配置问题同样通用。配置问题大多是预期和实际不一致只要能把复现范围缩小、把变量路径走通找到根因只是时间问题。4.3 一份可以照着抄的中大型项目配置清单结合前面所有内容这里给出一个精简但完整的配置骨架覆盖三大主题。依赖安装命令npm i -D eslint eslint/js prettier husky lint-staged commitlint/cli commitlint/config-conventional npm i -D vite-plugin-compression rollup-plugin-visualizerpackage.json脚本参考{ scripts: { dev: vite, build: vite build, type-check: vue-tsc --noEmit, lint: eslint . --max-warnings0, format: prettier --write ., prepare: husky, build:report: vite build --mode production vite-bundle-visualizer } }vite.config.ts核心配置片段import { defineConfig, loadEnv } from vite export default defineConfig(({ mode }) { const env loadEnv(mode, process.cwd(), ) return { envPrefix: [VITE_], build: { reportCompressedSize: true, chunkSizeWarningLimit: 1000, rollupOptions: { output: { manualChunks(id) { if (id.includes(node_modules)) { if (id.includes(echarts)) return charts if (id.includes(vue) || id.includes(vue-router) || id.includes(pinia)) { return vue-vendor } return vendor } }, }, }, }, } })这段配置不是万能模板需要根据项目依赖调整但结构足够清晰。真正的关键在于环境变量有明确敌人前缀规则代码规范有自动化闸门性能优化有可量化的观察手段。把这三点都落到配置里工程化才不会只是挂在 README 上的一句口号。最后再分享一个小技巧团队在新项目落地这套配置时可以先选一个内部项目试点跑一个月看大家的使用反馈再决定要不要推广到所有仓库。工程化规范是给人用的推行节奏、README 文档、示例模板都和配置本身一样重要。我经历过太多次规范很完美、执行很稀碎的失败所以现在更愿意强调让团队真正用起来而不是把配置文件写得像教科书。