第一次把 vite 真正用进生产项目,是在一次旧后台改造里。当时需要在 Vue 2 老系统旁边新起一个 Vue 3 独立模块,webpack 的 dev server 启动一次要十几秒,热更新还要等,我顺手在旁边开了一个 vite 测试目录,命令敲下去两秒页面就出来了,那种体验真的会改变人。后来把 vue3 + vite 的组合带到正式业务线,我才逐渐意识到,用 vite 构建项目这件事远不止“启动快”三个字,后面藏着一系列和浏览器环境、Node 生态依赖、工程化链路相关的坑:process is not defined、vite 不识别 buffer、局域网 IP 访问白屏、build 产物越来越重、打包越来越慢,甚至 Jenkins 构建目录被旧文件堆满。这篇笔记就是把一次完整的“用 vite 构建一个项目”的实战过程整理出来。我不打算罗列命令让你复制粘贴,而是尽量讲清楚每一步为什么这样做、报错为什么发生、下次换一个项目该怎样迁移这套思路。它适合刚学完前端基础、准备动手做实战项目的同学,也适合正在评估老项目迁移到 vite、或者刚接手 vite 工程想尽快上手的人。
1. 为什么选 vite:三个反直觉的取舍
1.1 “快”的原理和代价
很多同学知道 vite 快,但说不清快在哪。vite dev server 启动时不会把整个项目打包成一个 bundle,它只做两件事:预构建 dependencies(用 esbuild 把 node_modules 里的依赖先转成 ESM,同时合并重复模块),然后启动一个极轻量的静态服务器,剩下的源码模块交给浏览器原生 ESM 按需加载。所以在 dev 模式下,你的页面每次请求什么模块,vite 才去转译那个模块,不需要提前分析整个依赖图。这个机制让启动速度和项目规模几乎无关,项目再大,dev 启动也能保持秒级。
不过代价也很现实。第一,浏览器直接加载成百上千个模块时,首屏请求数会非常高,如果组件拆分粒度极其细,又没有做好 dev 下的请求合并,低性能设备上反而比 webpack 还卡。第二,dev 环境用原生 ESM,build 环境走 Rollup,两条链路的转换逻辑不完全一致,一些在 dev 下正常的写法,构建时可能被 tree-shaking 或 CJS 转换折腾出问题。所以正确心态是把 vite 当成一个“快但要求规范”的工具,而不是无脑依赖它的快。
1.2 别拿构建工具和框架比:vite 与 nextjs 的边界
我看过太多人纠结“vite 和 nextjs 到底选谁”。严格说这不是同一层级的比较,nextjs 是一个全栈应用框架,自带了服务端渲染、文件路由和编译体系;vite 是一个构建层工具,理论上你也可以在 nextjs 内部把它当打包器用。业务上更合适的判断标准是产品形态:如果你的项目是管理后台、H5 活动页、组件库、纯前端工具站,vite + vue/react 就够用了,轻、快、可控;如果你的产品需要 SEO、需要 Node 侧做首屏渲染、需要 RSC 这类服务端能力,那应该直接选 nextjs,而不是硬用 vite 再自己补一套 SSR 方案,后面补出来的 SSR 往往要花好几倍精力去维护。
1.3 哪种项目形态最适合 vite
结合我自己的经验,最适合 vite 的项目有三类:一类是公司内部的中后台系统,页面多但不追求极致首屏加载,开发体验的权重最高;一类是组件库和工具型应用,这类项目本身源码结构清晰,构建要求高,vite 的按需编译特性很舒服;还有一类是快速原型验证,今天搭一个 demo 明天给产品看效果,create-vite 几分钟就能出一个可交互的页面。不适合的场景也清楚:强依赖 webpack 生态老插件的项目、深度定制 loader 的复杂工程、还有那些用了大量 Node 全局变量的 CJS 依赖库的项目——当然,第三类可以通过 polyfill 解决,这正好是后面要重点聊的部分。
2. 从脚手架到能开发:vite 项目初始化实操
2.1 create-vite 的正确打开方式
初始化项目我通常直接跑官方脚手架:
npm create vite@latest my-app -- --template vue-ts如果要在当前目录初始化,把my-app换成.就行。这里提醒两件事:第一,create-vite 在较新版本里会询问是否启用 rolldown-vite 实验性构建,慎选,除非你有明确理由,默认的标准 vite 更稳妥,生态插件兼容性更好;第二,如果公司内部有统一的前端规范,更建议把 vue3 + ts + vue-router + pinia + 代码规范基线做成私有模板,用degit或自建 CLI 拉取,省去每个项目重复安装依赖的时间。
2.2 目录结构按“职责”拆,不按“技术”拆
新建项目的目录规划我遵循一个原则:让新成员看到目录就知道去哪里改代码。一个比较稳的结构是这样:
src/ ├── api/ # 接口请求层,统一封装 axios ├── assets/ # 静态资源,图片、字体 ├── components/ # 跨页面复用的业务组件 ├── composables/ # 组合式函数,逻辑复用 ├── layout/ # 页面框架(侧边栏、顶部栏等) ├── router/ # 路由表和守卫 ├── stores/ # pinia 全局状态 ├── styles/ # 全局样式和 CSS 变量 ├── types/ # 全局类型定义 └── views/ # 页面级组件,一个路由对应一个目录注意一个容易犯的错:不要让 components 里放满某个页面独有的局部组件。那类组件应该跟着页面走,放在views/xxx/components/下。否则公共组件目录会越来越乱,维护的人无法判断这个组件到底是谁在用。
2.3 一上项目就配好的四件事
刚初始化完,我会立刻把下面四件事在vite.config.ts里配好,避免后面开发时反复改。
alias:把@指向src,import 路径清爽很多:
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }环境变量文件:至少建.env.development和.env.production,变量统一VITE_前缀。这里有个很隐蔽的坑:vite 只会在项目根目录读取.env文件,如果你的 package.json script 里用了--mode指定其他环境,文件名也要跟着匹配,比如.env.staging。
dev server:配置host: true、port和strictPort,后面联调的人才能通过局域网 IP 直接访问;同时把常用的/api代理配好:
server: { host: true, port: 5173, strictPort: true, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }build 输出:生产环境默认 sourcemap 是关闭的,但如果你需要保留调试能力又不想全部暴露源码,可以配build.sourcemap: 'hidden',让浏览器能定位行列号但不生成独立 map 文件供公网下载。这个细节在排查生产 bug 时救过我很多次。
3. 两个高频报错根治:process is not defined 与 buffer 不识别
3.1 process is not defined:先分清是“谁”在缺对象
vite 项目里最常看到的一个红屏报错就是process is not defined。根因一句话就能解释:浏览器环境里根本没有 Node.js 的process全局对象,而很多 npm 包,尤其是老旧的 CJS 包,在运行时会直接引用process.env或process.cwd()。webpack 5 在构建时默认注入了 polyfill,vite 却不做这件事,于是报错直接暴露出来。这个问题在后来的前端面试题里也经常被拿来当考点,考察的就是你对构建机制而不是框架 API 的理解。
处理这个问题的第一步不是装插件,而是定位谁抛的。我习惯先开控制台,看第一个报错的堆栈指向哪个模块,再判断是源码里误用了 Node API,还是某个第三方依赖的锅。如果是依赖的问题,优先查这个依赖有没有 ESM 版本或替代品;实在没有,再上 polyfill 方案。最常用的办法是装vite-plugin-node-polyfills,并按需启用:
import { nodePolyfills } from 'vite-plugin-node-polyfills' export default defineConfig({ plugins: [ nodePolyfills({ globals: { process: true } }) ] })同时建议在define里把常见的环境变量显式替换掉:
define: { 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') }但这里我要强调一个关键区别:define做的是编译期字符串替换,只能解决process.env.NODE_ENV这种字面量;如果某个依赖在运行时还访问了process的其他属性(比如process.nextTick),你必须额外把完整的process对象挂到全局。实际操作中我会直接在入口文件显式 polyfill:
import process from 'process' if (!window.process) { ;(window as any).process = process }两种手段配合起来,dev 和生产构建才会都稳定。
3.2 vite 不识别 buffer:一套兼容老库的通用配方
“vite 不识别 buffer”和 process 的问题同源但更棘手。Buffer 同样是 Node 的全局对象,浏览器里没有。报错往往来自类似 xlsx-style、sql.js 这类老库,它们内部直接require('buffer'),而你的项目可能压根没安装这个包,vite 又不会自动注入。
通用配方分三步。第一步,安装基础依赖:
npm install buffer process第二步,在入口文件挂全局对象:
import { Buffer } from 'buffer' import process from 'process' window.Buffer = window.Buffer || Buffer window.process = window.process || process第三步,给 vite 配置 node polyfills 插件,注意 include 里至少要包含buffer和process:
import { nodePolyfills } from 'vite-plugin-node-polyfills' plugins: [ nodePolyfills({ include: ['buffer', 'process', 'stream', 'util', 'path'], globals: { Buffer: true, process: true } }) ]有时候三步做完还是报错,这种情况多半是某个老库的 CJS 代码在 vite 的依赖预构建阶段被转换坏了。此时可以在optimizeDeps.exclude里排除它,然后绕过打包直接加载它的浏览器版本文件。这个方法虽然粗暴,但在没有替代库的场景下反而是最可靠的。
3.3 实战案例:vite 里让 xlsx-style 跑起来的全过程
以前做过一个报表导出项目,需要给 Excel 单元格设置样式,我第一个想到的就是 xlsx-style。装好依赖,npm run dev一启动,控制台直接显示Buffer is not defined,接着还有一串process is not defined的连锁报错。我没有急着在 vite.config 里堆插件,而是先做了一件事:关掉 dev server,看node_modules/xlsx-style的 package.json,确认它有没有 ESM 入口。答案是几乎没有,它就是典型的 CJS 老包。
于是按上面三步走完,dev server 起来了,但一调导出接口就又崩在对二进制格式的处理上。这时候optimizeDeps.exclude派上用场了:
optimizeDeps: { exclude: ['xlsx-style'] }然后在代码里改成直接引入它的打包产物:
import XLSX from 'xlsx-style/dist/xlsx.full.min.js'同时配合 nodePolyfills 的全局注入,报表导出功能终于跑通。这个案子给我最大的启发是:如果业务允许,优先用exceljs这种现代库替换 xlsx-style;如果只能用老库,记得把它单独隔离在optimizeDeps.exclude里,再靠 polyfill 支撑运行时。不要试图在 vite 里“完美兼容”一个已经没人维护的老依赖,该绕的绕,该换的换。
4. 联调白屏与自动开浏览器:局域网访问 vite 的完整排查链路
4.1 第一步:让 dev server 监听所有网卡
公司里前后端联调、或者同事想通过局域网 IP 访问你的开发页面,这是特别常见的需求。vite 默认只监听 localhost,外部访问必然不通,所以要把 server 配置改成这样:
server: { host: true, // 等效于 0.0.0.0 port: 5173, strictPort: true }strictPort很多人不配,我建议一定要加上。这样如果端口被别的进程占用了,vite 会直接报错,而不是悄悄换一个端口,联调的人拿着旧端口访问,就会觉得是你的服务没启动。
4.2 第二步:排查页面内部请求是不是指错了主机
配置完 host 还是空白,最常见的坑不是端口,而是页面内部的接口地址写死了 localhost。框架和 JS 文件都被浏览器加载了,但在请求后端接口时指向了访问者自己的机器的 localhost,当然什么数据都拿不到,白屏顺理成章。解决思路是把所有请求路径改成相对路径,再通过 dev server 的 proxy 转发:
server: { proxy: { '/api': { target: 'http://192.168.1.100:8080', changeOrigin: true } } }页面里统一用/api/xxx,这样无论你用哪个 IP 打开页面,请求都落在当前页面所在的服务端,再由服务端代转向后端,主机地址完全不用写死在业务代码里。
4.3 第三步:防火墙、缓存和错误边界的干扰
还有两种隐蔽情况。一种是防火墙阻止了局域网访问 5173 端口,现象是 IP 页面一直转圈或直接拒绝连接,用 telnet 一探就清楚。另一种是前端代码里有一个未捕获的异常,在 localhost 下因为错误信息完整还能看到,换到局域网 IP 时控制台信息不完整,页面渲染中断成空白。正确做法是先把错误边界或全局错误处理配好,让异常显示出来而不是整个页面销毁;排查时也先回 localhost 跑一遍,分清楚到底是代码问题还是网络问题。
4.4 顺手处理 vite 的 xdg-open 启动异常
在 Linux 上跑npm run dev,如果系统是无桌面环境或精简安装,经常会在启动阶段出现 xdg-open 相关的报错。原因是 vite 默认尝试自动打开浏览器,而它调用的系统命令 xdg-open 在无头环境里不存在或不工作。解决办法很简单:
server: { open: false }或者在命令行里加--no-open。顺带一提,server.open还可以配成一个路径,比如server.open: '/login',启动后自动跳到登录页,这在日常开发时挺顺手;但注意这个路径是应用内路由路径,不是文件路径,写错会变成打开一个不存在的地址。
5. 构建提速与微前端:从“打包太慢”聊到工程化分层
5.1 打包慢的三个真正原因
vite 打包慢,我遇到的百分之八九十都归结为三种原因。
第一种是依赖没有分层。大量 node_modules 包被 Rollup 一视同仁地分析、合并,chunk 数量爆炸,构建链路自然被拖慢。解法是先清点项目依赖,把体积大、稳定不变的库识别出来,再做手动 chunk 拆分。
第二种是类型检查塞进了构建命令。很多人习惯把tsc && vite build写进一个 script,但 vite 本身不做完整 TS 类型检查,它只通过 esbuild 做语法转译。真正拖慢构建的vue-tsc --noEmit如果跑在vite build前面,每次构建都在做全量类型校验,项目一大就是几十秒的差距。合理做法是把类型检查独立成 CI job,本地构建只跑vite build。
第三种是资源内联阈值被人为调大了。vite 默认把 4KB 以内的小资源转成 base64 内联进 JS,如果谁为了“减少请求数”把这阈值调成几百 KB,一大批图片、字体全变成超长字符串打进 bundle,产物体积和内存都会暴涨。保持默认阈值就好;真有内联需求,局部按文件类型处理。
5.2 manualChunks:让公共库稳定独立
手动拆 chunk 的收益我在实际项目中感受非常明显。改造前每次发版,vue 相关库的 hash 都会跟着业务代码一起变,浏览器缓存命中率很低;拆包之后,公共依赖基本不再变动,二次发版时用户只需下载少量业务代码增量。一个可参考的配置:
build: { rollupOptions: { output: { manualChunks(id) { if (id.includes('node_modules')) { if (id.includes('vue') || id.includes('pinia') || id.includes('vue-router')) { return 'vendor-vue' } if (id.includes('echarts') || id.includes('@antv')) { return 'vendor-charts' } if (id.includes('lodash') || id.includes('dayjs')) { return 'vendor-utils' } return 'vendor-other' } } } } }注意别用对象形式写死的映射,不同依赖之间可能有相互引用,函数形式可以根据实际路径更灵活地归堆。
5.3 vue3 + vite 微前端方案的取舍与构建分层
vue3 + vite 做微前端,是目前团队协作场景里的常见讨论点。方案主流有两种:qiankun/single-spa 这种应用隔离方案,以及基于 module federation 的模块共享方案。vite 初期的 module federation 生态不成熟,近几年已经好了很多,可以用@originjs/vite-plugin-federation或@module-federation/vite这类插件实现。
但我必须泼一盆冷水:如果团队规模不到 5 个前端、子应用不超过 3 个,不建议一开始就上微前端。微前端解决的是独立部署、技术栈隔离、多团队并行的问题,它同时引入了通信协议、构建链一致性、资源版本协同等一系列复杂度。真要上,先在基座里跑通一个最小子应用:壳、子应用、共享依赖三个包独立构建,把 vue、vue-router、pinia 这些框架依赖放到共享层,子应用构建时显式排除公共依赖,主应用加载远程模块时统一注入。这样整体构建时间能从“N 个全量构建”降到“N 个业务代码构建”,联调和内存占用都有明显改善。
5.4 别忘了 CI 里的清理:Jenkins 构建目录是无底洞
vite 构建本身不会自动清空 dist 之外的目录,如果你在 Jenkins 这类 CI 工具里配置了“构建后把 dist 复制到固定目录”,而没有清空上次的产物,磁盘很快就会堆满。更麻烦的是,旧 HTML 可能还引用旧 chunk,线上出现“找不到 JS 文件”的白屏。所以构建脚本里务必加清理动作:
"build": "rimraf dist && vite build"或者用跨平台的rimraf包替代rm -rf,Windows 和 Linux 环境下都能正常跑。这个细节看起来不起眼,但它救过一次我们的线上发布:那天因为之前的构建残留,静态资源服务器上同时存在两套 dist 文件,页面拿到了错版本,排查了小半天。
6. 让 vite 项目长期好维护的三个习惯
写到这里,我在实际操作中有三条很深的体会。
第一,遇到报错先看堆栈再动手,不要盲目把 webpack 时代的配置搬到 vite 里。vite 的构建器和依赖预构建机制与 webpack 完全不同,ProvidePlugin、DefinePlugin这套写法不能直接照搬,先理解当前项目的依赖形态,再决定上不上 polyfill、要不要排除某个包。
第二,把环境变量、代理、错误边界这些“基础配置”在项目第一天做掉,而不是等联调炸了再补。你省的那十分钟,后面一定用十倍时间去还。
第三,构建优化要有数据意识,每次改动前后对比 dist 体积、构建时长、chunk 数量,别只凭感觉说快或慢。vite 的--debug模式和rollup-plugin-visualizer都能把产物体积可视化,用数据指导决策,比翻文档有用得多。
最后再分享一个小技巧:我现在每次初始化完项目,都会先把默认示例代码删干净,然后把.env、路由、状态库、错误边界、构建清理脚本这五个骨架先搭好,再开始写业务。这个准备动作看起来繁琐,但正是这些不起眼的前置配置,决定了你两周后是在愉快地迭代功能,还是在为一个报错翻一晚上源码。