ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Vite实战笔记:从原理到构建优化的避坑指南

Vite实战笔记:从原理到构建优化的避坑指南 第一次把 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 环境用原生 ESMbuild 环境走 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 vitelatest 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把指向srcimport 路径清爽很多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 在构建时默认注入了 polyfillvite 却不做这件事于是报错直接暴露出来。这个问题在后来的前端面试题里也经常被拿来当考点考察的就是你对构建机制而不是框架 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对象挂到全局。实际操作中我会直接在入口文件显式 polyfillimport 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和processimport { 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 -rfWindows 和 Linux 环境下都能正常跑。这个细节看起来不起眼但它救过一次我们的线上发布那天因为之前的构建残留静态资源服务器上同时存在两套 dist 文件页面拿到了错版本排查了小半天。6. 让 vite 项目长期好维护的三个习惯写到这里我在实际操作中有三条很深的体会。第一遇到报错先看堆栈再动手不要盲目把 webpack 时代的配置搬到 vite 里。vite 的构建器和依赖预构建机制与 webpack 完全不同ProvidePlugin、DefinePlugin这套写法不能直接照搬先理解当前项目的依赖形态再决定上不上 polyfill、要不要排除某个包。第二把环境变量、代理、错误边界这些“基础配置”在项目第一天做掉而不是等联调炸了再补。你省的那十分钟后面一定用十倍时间去还。第三构建优化要有数据意识每次改动前后对比 dist 体积、构建时长、chunk 数量别只凭感觉说快或慢。vite 的--debug模式和rollup-plugin-visualizer都能把产物体积可视化用数据指导决策比翻文档有用得多。最后再分享一个小技巧我现在每次初始化完项目都会先把默认示例代码删干净然后把.env、路由、状态库、错误边界、构建清理脚本这五个骨架先搭好再开始写业务。这个准备动作看起来繁琐但正是这些不起眼的前置配置决定了你两周后是在愉快地迭代功能还是在为一个报错翻一晚上源码。
返回列表