ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vite搭建前端框架实践:从Webpack迁移到性能优化全记录

Vite搭建前端框架实践:从Webpack迁移到性能优化全记录 好开工。今天这篇是开发日记系列的第10篇主题是用Vite搭前端框架。不写那些虚头巴脑的东西直接进入正题。我把最近在项目里从Webpack迁移到Vite的完整过程、踩过的坑、还有最终的性能优化方案都整理了出来。内容不涉及具体的业务代码聚焦在工程化的通用层面。如果你是前端负责人、架构师或者正在承受Webpack慢速构建之苦的开发者这篇应该能帮上忙。好了开始。1. 技术选型复盘为什么在这个节点切到Vite1.1 从Webpack迁移的动机我在之前的日记里提过老项目用的是Webpack 4打包配置已经堆到700行。组件几百个路由全量加载开发环境冷启动一次要40秒热更新平均在3秒左右。这在多人协作时非常要命改一行代码等热更新刷新前后要5到6秒一天下来光等待的时间就让人抓狂。更麻烦的是Webpack需要处理模块打包的依赖图构建。项目大了之后dev server的依赖收集、编译、打包整个过程都变得异常缓慢。这不是某一段配置写错了而是Webpack的工作机制决定的修改代码时要重新构建模块图时间会随着项目体积不断增长。所以在这个节点我决定完整评估Vite。我要的不仅是一个能跑的构建工具而是一个能帮我从根上解决等待问题的方案同时还要兼容老项目留下的各种历史包袱。1.2 Vite快在哪里一次预构建带来的体验提升Vite能在现代前端框架搭建中脱颖而出核心在于两个机制依赖预构建和原生ESM。依赖预构建简单说把项目里的依赖比如vue、vue-router、pinia这些node_modules包先用esbuild逐一扫描并打包成ESM格式生成一个预构建产物放在node_modules/.vite/deps目录。这一步在项目启动时自动完成esbuild用Go语言写的并行处理能力极强几百个依赖也就耗时几秒。省掉了Webpack入口递归解析依赖、逐字节生成bundle的漫长过程。原生ESM则是另一个关键。现代浏览器已经原生支持ES模块script标签加个typemodule就能直接加载。Vite在开发环境把源码原样发给浏览器让浏览器自己管理模块加载。当浏览器请求某个模块Vite把当前模块和它的依赖路径返回即可编译延迟只与当前实际被加载的文件相关与项目总文件数无关。这就是为什么Vite项目无论在多大冷启动都能保持在眨眼级别。对比数据我实测过同一个中等规模项目Webpack冷启动平均36秒Vite只需要800毫秒左右。热更新Webpack在2到4秒之间波动Vite基本在100到200毫秒。开发体验完全不是一个量级。1.3 项目需求适配评估在动手前我做了几个方向的评估第一老项目用到的Webpack Loader是否都能在Vite插件市场找到替代。Vite采用Rollup的插件体系官方提供了vitejs/plugin-vue和vitejs/plugin-react社区也有各种常用插件的兼容方案。我之前项目里用了dotenv、url-loader、file-loader等能力Vite都有对应的原生处理方式不需要额外引入Loader。第二Node版本是否满足要求。Vite 4要求Node 14.18以上Vite 5要求Node 18以上。我们生产环境Node版本已经是18.12满足要求。如果你的服务器Node还停留在12或14建议先升级Node再考虑切Vite。第三团队学习成本。Vite配置比Webpack精简太多了同样的功能Webpack需要好几个Loader和Plugin串起来Vite很多时候只需要一个配置项。团队转型成本很低不需要额外培训就能上手。评估下来除了老项目里一个老旧的Web Worker加载方式需要额外处理之外其他场景Vite都能友好承接。所以决定切下面开始搭。2. 脚手架搭建与基础配置2.1 创建项目与目录结构设计创建项目我直接用了官方脚手架命令npm create vitelatest frontend-web -- --template vue这里需要注意Vite官方模板有几个变体vue、vue-ts、react、react-ts、vanilla等。我这边项目用的是Vue 3加TypeScript所以选择vue-ts模板。如果你的团队业务复杂建议从一开始就用TypeScript半途加类型系统会带来很大的历史债务。生成出来的目录结构大体是这样的src/ ├── api/ # 接口请求 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── composables/ # 组合式函数 ├── layouts/ # 布局组件 ├── router/ # 路由配置 ├── stores/ # 状态管理 ├── styles/ # 全局样式 ├── utils/ # 工具函数 ├── views/ # 页面视图 ├── App.vue └── main.ts这个结构是Vue社区比较常用的一种约定没有额外解释成本。老员工接手新人入职看到这个目录就大概知道一个组件该放哪里一个页面该放哪里。后续团队扩展时这个约定能降低合代码时的冲突率。2.2 vite.config.ts关键配置逐项说明Vite配置文件叫vite.config.ts放在项目根目录。我把我最终的配置贴出来再做逐项拆解。// vite.config.ts import { defineConfig, loadEnv } from vite import vue from vitejs/plugin-vue import path from path import { createSvgIconsPlugin } from vite-plugin-svg-icons import viteCompression from vite-plugin-compression export default defineConfig(({ mode }) { // 按环境加载 .env 文件 const env loadEnv(mode, process.cwd(), ) return { // 部署基础路径根据环境区分 base: env.VITE_BASE_PATH || /, plugins: [ vue(), // svg雪碧图 createSvgIconsPlugin({ iconDirs: [path.resolve(process.cwd(), src/assets/icons)], symbolId: icon-[name] }), // gzip压缩 viteCompression({ verbose: true, disable: false, threshold: 10240, algorithm: gzip, ext: .gz }) ], // 开发服务器配置 server: { host: 0.0.0.0, port: 3000, open: true, proxy: { // 接口代理解决开发环境跨域 /api: { target: env.VITE_API_TARGET || http://10.0.0.10:8080, changeOrigin: true, // 如果后端接口不带/api前缀可以在这里重写 rewrite: (path) path.replace(/^\/api/, ) } }, // 文件监听配置 watch: { usePolling: false, interval: 100 } }, // 路径别名 resolve: { alias: { : path.resolve(__dirname, src), #: path.resolve(__dirname, src/types) } }, // 构建配置 build: { outDir: dist, assetsDir: static, sourcemap: mode ! production, chunkSizeWarningLimit: 1500, rollupOptions: { output: { // 手动分包 manualChunks: { vue: [vue, vue-router, pinia], vendor: [axios, dayjs, echarts] } } } } } })下面对几个关键配置项做说明。先看base。这个配置决定了构建产物中静态资源引用的基础路径。如果你部署到域名根路径填/即可。如果需要部署到子路径比如nginx的location指向/web/目录就得设置成/web/否则构建出来的index.html会引用根路径资源刷新就404。再看server.proxy。开发环境请求后端接口会遇到跨域公司内部测试环境又经常没有配置CORS头。用proxy代理是最省事的方案。上面配置把/api开头请求转发到VITE_API_TARGET所在服务器并自动处理跨域。注意changeOrigin一定要设为true否则后端拿到的Host头是前端的域名可能触发一些服务端的防跨域校验。resolve.alias是很多人忽略的配置。用指向src目录能大幅提升代码可维护性不需要写一堆相对路径。路径过深时类似../../../../utils/request这种跨层引用一旦调整目录结构经常出现找不到模块的报错。有了别名后统一从开始找清爽利落。如果项目里TypeScript使用了路径别名记得在tsconfig.json里同步配置paths映射不然编辑器会标红构建也会跑不起来。2.3 环境变量与多环境适配Vite的环境变量机制和Webpack的DefinePlugin不同它默认只暴露以VITE_开头的变量到import.meta.env。有个加载细节必须严查loadEnv(mode, process.cwd(), )第三个参数是loadEnv内部调用dotenv时使用的前缀过滤。如果不传默认值是VITE_意味着只有VITE_开头的变量会被加载到env对象里。但如果我在上述配置文件里还有其他自定义变量比如VITE_API_TARGET就必须把第三个参数设为否则env对象里拿不到。我遇到过一位同事他在.env文件里写了一个非VITE_开头的变量然后vite.config.ts里读不到查了很久最后发现是这个参数没传。这种问题日志定位很难所以写在这里提醒一下。我一般会维护三个环境文件.env # 通用配置 .env.development # 开发环境 .env.production # 生产环境文件内容类似这样# .env.development VITE_BASE_PATH/ VITE_API_TARGEThttp://10.0.0.10:8080 VITE_MOCK_ENABLEDtrue# .env.production VITE_BASE_PATH/web/ VITE_API_TARGEThttps://api.example.com VITE_MOCK_ENABLEDfalse这样切换环境只需要修改.env文件内容不需要动代码。打包时运行vite build --mode productionVite自动加载对应的.env.production配置。3. 命令行报错踩坑实录内存溢出与热更新失效搭建过程不是一帆风顺的有几个问题排查了挺久这里完整记录一下。3.1 node_options不是内部或外部命令的排查过程在Windows开发机上按照网上一些文档执行node_options--max-old-space-size4096 vite结果控制台直接报错node_options 不是内部或外部命令也不是可运行的程序或批处理文件。这个原因很直接Windows的cmd和PowerShell不支持Unix/Linux系统下的环境变量前置赋值语法。这种赋值方式是Bash的特性Windows下执行时系统把node_options当成一个可执行程序去找自然找不到。解决方案有两个方案一使用cross-env工具统一设置环境变量。npm install -D cross-env然后在package.json中scripts: { dev: cross-env NODE_OPTIONS--max-old-space-size4096 vite, build: cross-env NODE_OPTIONS--max-old-space-size4096 vite build }cross-env的原理是在启动vite之前通过Node脚本临时设置process.env.NODE_OPTIONS然后执行后续命令。这样跨平台有效Linux、macOS、Windows都能跑。方案二使用Node的--max-old-space-size参数scripts: { dev: node --max-old-space-size4096 node_modules/vite/bin/vite.js, build: node --max-old-space-size4096 node_modules/vite/bin/vite.js build }这种更直接直接把Node堆内存上限调到4GB绕过环境变量语法问题。实际项目中我推荐用cross-env。一方面是统一规范后续如果需要配其他环境变量直接写在命令里不会因为平台不同而炸掉。另一方面是跨平台协作时不需要每个成员都在本地特殊配置。3.2 vite改vue文件不热更新了但改js文件正常这是我在开发过程中遇到的非常诡异的问题陆续困扰了我一整天。现象是修改.vue文件时页面不更新控制台也没有任何响应但修改.js文件时热更新又完全正常。排查路径如下第一检查文件是否被IDE自动格式化破坏。有些编辑器在保存时会对文件做格式化把script标签内的代码结构改掉导致Vue SFC编译失败。Vite在HMR更新失败时会在终端打印错误日志但有时因为控制台输出太多会滚动查看时错过关键信息。我仔细看了终端日志没有发现任何编译报错排除这个可能。第二检查文件是否加了防抖或手动触发的机制。如果项目里有自定义的HMR处理逻辑或plugins可能会影响热更行为。但我的项目刚搭好除了官方插件没有其他自定义HMR逻辑。第三检查devtools是否处于打开状态。这是我在之前项目里遇到的坑——浏览器开发者工具开着时如果页面有某种特定的事件监听会导致热更新消息无法正确触发DOM更新。我当时没有开devtools排除。最终排查到问题了是依赖预构建缓存和文件监听冲突。具体来说我在项目里使用了svg-icons插件它在启动时会生成一个vite.svg.js模块这个模块被缓存后如果svg文件有变化Vite监听到了svg文件变化但依赖树中没有把这个依赖标记为需要热更新导致.vue文件的变化没有触发页面刷新。最后清除了node_modules/.vite下的缓存目录直接rm -rf node_modules/.vite或npx vite --force重新启动后问题解决。所以如果你也遇到类似的“改特定类型文件不热更新”的问题第一时间先尝试强制重新预构建npx vite --force这个命令会跳过预构建缓存重新执行依赖预构建大部分因缓存导致的热更失效问题都能解决。3.3 依赖预构建与缓存问题的处理关于依赖预构建还有几个隐藏的坑。第一个是依赖新增时的自动重启。如果项目运行时你通过npm install新装了一个依赖Vite会在终端监测到package.json变化然后自动重启dev server。这个功能默认是开启的但是有时它不会生效表现在依赖装好了但项目里引用它却报模块找不到。这种情况也是清除缓存重来或者干脆手动重启dev server。第二个是依赖更新后预构建缓存未失效。比如某个npm包发了个修复版本你升级了版本号但Vite的预构建缓存仍然缓存了旧版本的数据。这种情况Vite是能感知到package.json中锁定版本的变化并重新预构建的但如果你用了npm/yarn的缓存机制偶尔会出现预构建产物和实际依赖不一致。稳妥做法同样是清理缓存。第三个是私有npm包的编译问题。Vite预构建时esbuild会扫描node_modules下所有以ESM格式提供的依赖并做转译但如果某个依赖的导出方式比较老比如CommonJS的module.exports fnesbuild也能处理。真正的问题是如果某个依赖代码不符合严格模式或包含浏览器不兼容的语法预构建就会报错。这种时候可以通过optimizeDeps.include配置强制把该依赖纳入预构建范围并在启动前先执行一遍vite optimize命令。// vite.config.ts optimizeDeps: { include: [element-plus, echarts, mylib] }4. 构建阶段的核心优化与部署踩坑4.1 手动分包策略让缓存真正生效构建阶段的第一个优化是手动分包。Vite默认打包策略会把所有业务代码和一个巨大的vendor chunk放在一起这样带来的问题是第一缓存失效严重。用户访问过一次页面后理论上浏览器会缓存这些静态资源。但如果每次发版vendor chunk的hash值都变化因为业务代码的改动会触发整个chunk重新编译那么用户每次发版后都要重新下载完整的vendor代码体验很差。第二首屏加载速度。如果vendor chunk体积过大比如引入了echarts、antv这类大型可视化库下载JS的时间会明显拖慢首屏。我的做法是在rollupOptions里配置manualChunks把体积较大、变动频率较低的第三方库拆成独立chunkbuild: { rollupOptions: { output: { manualChunks: { vue: [vue, vue-router, pinia], vendor: [axios, dayjs], charts: [echarts] } } } }这里用到一个经验不要把所有第三方依赖一股脑丢进vendor而是按实际业务使用频率和体积拆分。vue相关的框架代码单独拆一份因为升级频率极低缓存命中率极高。echarts这种动不动就几百KB的库单独拆一份如果某个页面不用echarts就不要在入口文件里加载它结合路由懒加载来使用。搭配路由懒加载效果更佳。Vue Router官方推荐使用动态import的方式组织路由组件const routes [ { path: /dashboard, component: () import(/views/dashboard/index.vue) }, { path: /chart, component: () import(/views/chart/index.vue) } ]这样只有访问对应路由时才会加载该页面及其依赖的第三方库首屏体积可以进一步压缩。4.2 构建体积与压缩优化构建时打开的sourcemap会显著增大产物体积影响部署速度。我建议生产环境关闭sourcemap或者只在需要排查问题时临时打开。开发环境中建议开启sourcemap遇到报错能直接定位到源码位置。我上面的配置里写了sourcemap: mode ! production实现开发环境有地图、生产环境无地图的效果。gzip压缩插件也是个好帮手。vite-plugin-compression会在构建完成后自动对js/css等静态资源生成.gz后缀的压缩文件。配合nginx的gzip_static模块可以让nginx直接返回预压缩的.gz文件从而省掉服务器实时压缩的CPU开销。上面配置里threshold: 10240表示只有大于10KB的文件才会生成gz文件避免小文件压缩后反而体积更大。构建产物出来我一般会再跑一下体积分析npx vite-bundle-visualizer这个工具会打开一个可视化页面按体积大小展示所有chunk。看到体积异常的chunk就去排查到底引入了什么依赖。我之前在某个组件里误引用了一个完整版的lodash体积直接多了200KB用这个工具一眼就发现了。4.3 部署到nginx后刷新404history路由模式的坑最后一个经典问题也是网上高频搜索的关键词——前端部署到nginx后F5刷新页面就404但首页能正常访问。原因在于项目的路由使用了HTML5 History模式。前端路由切换是浏览器端行为不需要请求服务器。但用户手动刷新或直接访问子路径比如https://xxx.com/user/list时浏览器会向nginx发起一个真实的HTTP请求路径是/user/list。nginx默认会根据路径去找对应的静态文件找不到就返回404而实际上这个路径应该由前端路由接管。解决方案是在nginx配置里添加try_files指令把不存在的文件路径重写到index.htmlserver { listen 80; server_name your-domain.com; root /usr/share/nginx/html/dist; location / { try_files $uri $uri/ /index.html; } }这里关键配置是try_files /index.html。它的含义是如果请求的文件不存在就返回index.html内容同时把请求路径交给前端路由处理。注意如果你部署在子路径比如/web/nginx的location需要相应调整location /web/ { alias /usr/share/nginx/html/dist/; try_files $uri $uri/ /web/index.html; }这样部署后前端路由的刷新404问题就解决了。5. 切换后的性能数据与团队协作变化5.1 构建与开发体验的量化对比迁移完成后我做了完整的对比记录以下是在同一个项目上的实际数据开发环境冷启动耗时Webpack 436秒Vite 51.2秒热更新耗时Webpack 42到5秒Vite 530到80毫秒生产构建耗时Webpack 43分20秒Vite 51分10秒初次加载体积未压缩前Webpack 4产物约为4.8MBVite产物在做完手动拆包后约为3.9MBgzip后差异不大都在1MB以下最大的提升在开发环境的即时反馈上。这种提升对团队的影响是隐性的但真实可感。编辑器保存手还没离开键盘页面就已经刷新了。调试时不再需要频繁等待专注度有了显著提升。5.2 Vite对团队工作流的影响这里分享一个细节切换Vite后团队成员的本地开发只需要一条命令npm run dev然后访问本地端口。不需要再像Webpack一样需要手动清理临时文件、重启dev server。Vite的依赖预构建机制让环境初始化变得非常干净。还有一点因为Vite内置了对tsconfig、JSX、Vue SFC的转译能力团队配置工具的负担也大幅降低。新成员加入时不需要了解Webpack中那一堆loader到底解决什么问题只需要知道vite.config.ts里的配置和对应的文档说明即可。当然Vite也不是万能的。如果你需要在构建阶段做复杂的自定义转换比如某些老式插件、自定义代码生成器可能需要评估是否能找到替代方案或自己封装Rollup插件。但从我的实际经验看绝大多数标准的前端工程需求Vite都能覆盖得很好。6. 常见问题速查表前面正文里已经讲了不少详细案例我最终整理了一张速查表方便你对照排查。问题现象根因解决方案node_options 不是内部或外部命令Windows不支持Unix环境变量前置赋值使用cross-env或在package.json中显式调用node改.vue文件不热更新改.js正常依赖预构建缓存异常或文件监听冲突npx vite --force 强制重新预构建新增依赖后模块找不到预构建缓存未更新重启dev server或手动执行vite optimize内存溢出JavaScript heap out of memoryNode默认堆上限不足通过NODE_OPTIONS或node命令调大--max-old-space-size部署到nginx后F5刷新404history路由模式缺少服务端回退nginx配置try_files $uri $uri/ /index.html构建产物太大未做分包、懒加载或压缩manualChunks拆包、动态import组件、gzip压缩环境变量读取不到loadEnv第三个参数未传空字符串loadEnv(mode, process.cwd(), )这里再补充两条我在实际运行中沉淀下来的经验。一条是如果你发现Vite在构建时CPU占用率特别高先看看build配置里有没有设置minify: terser。Terser对代码的压缩彻底但速度非常慢。Vite默认使用esbuild作为压缩器速度比terser快一个数量级。如果你的代码需要兼容超级旧的浏览器如IE11那确实需要用terser但如果不需要优先用esbuild。IE11本身也被Vite官方放弃了所以直接用esbuild即可。另一条是如果你的项目引入了大量图标建议试试vite-plugin-svg-icons把svg图标打包成雪碧图而不是每个图标都发一个单独的请求。这个方案对减少HTTP请求数量非常有效和Webpack时代的svg-sprite-loader是同类方案但配置更简单。最后再分享一个我个人的小习惯我在本地开发时会开两个终端窗口一个跑vite dev server另一个跑类型检查vue-tsc --watch。Vite本身构建很快但类型检查仍然需要持续运行让类型问题在编辑器、命令行两个维度都能及时暴露。这一点在做大型项目时尤其重要能避免很多运行时才暴露的类型错误。另外一个建议是在项目的package.json里显式锁定vite的版本。虽然Vite发版很快但保持版本的稳定性对团队协作很重要。如果某天某个依赖升级后突然出现兼容问题你可以先通过package-lock或pnpm-lock把版本回退到之前稳定状态快速恢复开发环境而不是花时间在最新的版本上排查不兼容问题。好了这篇开发日记就到这里。Vite搭建前端框架技术上不复杂但里面涉及的环境差异、缓存机制、部署配置都是实际开发中大概率会踩到的地方。希望这些记录能帮你少走一些弯路。
返回列表