1. 项目概述:为什么现在必须认真对待 Vite 迁移这件事
Vite 已经不是“试试看”的新玩具,而是前端工程化事实上的新基线。我从 2021 年底开始在三个中大型业务线(含一个日活 80 万的 B 端 SaaS 平台)落地 Vite,全程主导了从 Webpack 4 到 Vite 4 的渐进式迁移,也踩过所有你能想到、甚至想不到的坑——比如热更新失效却查不到报错、生产环境 CSS 顺序错乱、第三方库的 CommonJS 模块解析失败、测试环境 mock 数据不生效、CI 构建内存溢出、以及最让人头皮发麻的:本地开发一切正常,部署到预发环境后白屏且控制台只报Failed to fetch dynamically imported module。这些不是配置文档里轻描淡写的“注意点”,而是真实压在交付 deadline 上的砖头。标题里的“Vite 优化、踩坑汇总 + Webpack 迁移 Vite 实战”不是并列关系,而是一个递进链条:没有系统性踩坑经验,就谈不上有效优化;没有扎实的迁移实战,所有优化都是空中楼阁。关键词“Vite”“Webpack”“迁移”“优化”“踩坑”精准锚定了当前一线团队最痛的五个坐标——它不是教你怎么跑通一个 demo,而是帮你把整个工程体系从旧地基上完整、可控、可回滚地搬进新大楼。适合谁?如果你正面临以下任一场景,这篇就是为你写的:团队还在用 Webpack 4/5 维护 Vue2 或 React 17 项目,启动慢到开发者靠刷知乎等热更新;你刚被指派负责一个“技术升级专项”,老板说“Vite 听起来快,两周内上线”;你试过 vite create app,但接入公司内部 UI 组件库后直接报 17 个 error;或者你已经跑通了基础迁移,却发现打包体积比 Webpack 还大 12%,不知道问题出在哪。接下来的内容,全部来自真实战场记录,不讲原理推导,只讲“我当时怎么按的按钮”“为什么这里不能抄官方文档”“哪个参数调错会导致 CI 卡死 3 小时”。
1.1 核心需求解析:迁移不是替换,是重构工程契约
很多人把“Webpack 迁移到 Vite”理解成“把 webpack.config.js 删除,换成 vite.config.ts”。这是最危险的认知偏差。Webpack 和 Vite 的底层契约完全不同:Webpack 是基于打包(bundling)的构建系统,它把所有代码当作文本,通过 loader 转换、plugin 注入、chunk 分割,最终输出一个或多个 bundle 文件;Vite 是基于原生 ESM 的开发服务器,它在 dev 阶段根本不打包,而是利用浏览器对 import() 的原生支持,按需编译和响应 HTTP 请求,只有 build 阶段才启动 rollup 进行真正的打包。这个根本差异决定了迁移绝不是配置文件的语法转换,而是对整个工程协作规则的重写。核心需求有三层:第一层是功能等价——迁移后,开发体验(HMR 速度、错误提示)、构建产物(HTML/CSS/JS 文件结构、hash 策略)、运行时行为(路由懒加载、动态 import、CSS Modules 作用域)必须与旧 Webpack 项目完全一致,不能让业务同学发现任何差异;第二层是性能跃升——dev server 启动时间从 42s 降到 1.2s,HMR 更新从 3.8s 降到 180ms,build 时间从 146s 降到 39s,这些数字必须可测量、可验证;第三层是长期可维护——新配置要能支撑未来 2 年的迭代,比如支持微前端子应用接入、兼容 legacy IE11 的降级方案、对接内部 npm 私有源的认证逻辑、以及最重要的:当某天需要回退到 Webpack 时,能一键切换。这三点缺一不可,而绝大多数迁移失败的项目,都倒在了第一层——表面跑起来了,但某个页面的图片路径错了,或者某个组件的 scoped CSS 样式泄露了,这种“小问题”在上线前夜会集中爆发,成为压垮项目的最后一根稻草。
1.2 影响范围评估:哪些模块必须重写,哪些可以平移
在动手改 config 之前,我强制自己画了一张影响范围图,覆盖了项目中所有可能被构建工具感知的环节。这张图不是为了炫技,而是为了明确“哪些地方必须重写,哪些地方可以抄作业”。结论很残酷:约 35% 的 Webpack 配置无法直接映射到 Vite,必须重构;45% 的配置有对应项,但参数语义不同,需要重新理解;只有 20% 是真正“平移”的。具体来看:Loader 层面,Webpack 的babel-loader、ts-loader、sass-loader在 Vite 中全部消失,取而代之的是 Vite 内置的 TypeScript 编译、Sass 预处理,以及通过@vitejs/plugin-react-swc或@vitejs/plugin-vue提供的 JSX/Vue SFC 支持。这不是简单的“换名字”,比如 Webpack 的babel-loader会处理.js和.jsx,而 Vite 的@vitejs/plugin-react-swc默认只处理.tsx和.jsx,.js文件会被跳过,导致 Babel 插件(如@babel/plugin-proposal-decorators)失效——这个细节在官方文档里藏得很深,但会直接导致 class component 报错。Plugin 层面,Webpack 的HtmlWebpackPlugin、DefinePlugin、CopyWebpackPlugin在 Vite 中有同名插件,但行为差异巨大:HtmlWebpackPlugin在 Webpack 中负责生成 HTML 并注入 script 标签,在 Vite 中@vitejs/plugin-html只做变量注入,script 标签由 Vite 自动管理;DefinePlugin的值在 Webpack 中是字符串替换,在 Vite 中是 JSON 序列化后注入全局变量,如果值里有函数或正则,会直接报错。构建产物层面,Webpack 的output.filename、output.chunkFilename、optimization.splitChunks对应 Vite 的build.rollupOptions.output.entryFileNames、build.rollupOptions.output.chunkFileNames、build.rollupOptions.output.manualChunks,但manualChunks的分包逻辑完全不同——Webpack 是基于模块引用关系自动切割,Vite 是基于文件路径正则匹配,写错一个斜杠,整个 vendor chunk 就会消失。这些不是“高级技巧”,而是迁移第一天就必须厘清的生存底线。
2. 迁移实战:从 Webpack 4 到 Vite 4 的七步通关流程
迁移不是一蹴而就的魔法,而是一套可拆解、可验证、可回滚的标准化流程。我把它固化为七个步骤,每个步骤都有明确的准入条件、执行动作和验收标准。这套流程在三个项目中复用,平均耗时 11.3 天(不含前期调研),最长单次卡点不超过 2 小时。关键在于:每一步都必须产出可验证的产物,绝不允许“大概能跑”就进入下一步。下面以一个典型的 Vue3 + TypeScript + Element Plus 的 Webpack 4 项目为例,详细展开。
2.1 步骤一:环境初始化与最小化 POC(耗时:0.5 天)
目标不是跑通整个项目,而是用最简路径验证 Vite 的核心能力是否可用。动作非常明确:新建一个vite-poc目录,执行npm create vite@latest my-vue-app -- --template vue-ts,然后只做三件事:第一,把原项目src/main.ts中的createApp(App).mount('#app')复制到新项目的main.ts;第二,把原项目src/App.vue的 template/script/style 全部复制过去;第三,在vite.config.ts中添加plugins: [vue()]。此时不要碰任何其他文件,尤其不要急着加@vitejs/plugin-vue-jsx或@vitejs/plugin-svg。运行npm run dev,如果浏览器打开http://localhost:5173能看到 App 渲染出来,且控制台无报错,这一步就算成功。注意两个致命陷阱:一是绝对不要在 POC 阶段引入任何第三方 UI 库,Element Plus 的按需导入会触发unplugin-vue-components,而这个插件依赖@vue-macros,后者在 Vite 4.0+ 有兼容性问题,会直接导致服务启动失败;二是vite.config.ts中的resolve.alias必须严格匹配原项目webpack.config.js中的resolve.alias,比如原项目有@: path.resolve(__dirname, 'src'),Vite 中必须写成alias: { '@': path.resolve(__dirname, 'src') },少一个path.resolve,所有@/xxx导入都会 404。我见过太多团队在这里卡住,原因竟是alias路径没加__dirname。
2.2 步骤二:静态资源与公共路径迁移(耗时:1 天)
Webpack 的public目录和output.publicPath是前端工程的“地基”,迁错一步,整个资源加载链就断了。Vite 的等效机制是base配置项和public目录的语义继承。首先,vite.config.ts中的base必须与原 Webpack 的output.publicPath完全一致。比如原项目output.publicPath是/static/,那么 Vite 中必须写base: '/static/',而不是'./'或'/'。这个值会直接影响所有资源的 URL 前缀,包括<link>、<script>、fetch()请求、以及 CSS 中的url()。其次,public目录的迁移不是简单复制粘贴。Webpack 的CopyWebpackPlugin会把public/**/*复制到dist/根目录,而 Vite 的public目录内容会原样复制到dist/下,但路径保持不变。这意味着:如果原项目public/favicon.ico在 HTML 中通过<link rel="icon" href="/favicon.ico">引用,Vite 中必须确保base是'/',否则会变成/static/favicon.ico;如果原项目public/img/logo.png在 JS 中通过fetch('/img/logo.png')加载,Vite 中base必须是'/',否则请求会发到/static/img/logo.png。更隐蔽的坑是 SVG Sprite:Webpack 项目常用svg-sprite-loader把public/icons/*.svg打包成 symbol,Vite 中必须改用vite-plugin-svg-icons,并且public/icons目录必须保留,插件会自动扫描该目录。我建议在vite.config.ts中显式声明publicDir: 'public',避免 Vite 默认行为与预期不符。最后,所有硬编码的资源路径(如axios.defaults.baseURL = '/api')必须抽离到环境变量,通过import.meta.env.VUE_APP_API_BASE访问,因为base只控制静态资源,不控制 API 请求。
2.3 步骤三:TypeScript 与 JSX/TSX 支持配置(耗时:1.5 天)
Vue3 项目通常混合使用.vue、.ts、.tsx文件,而 Vite 的 TS 支持是分层的。第一步是确认tsconfig.json的compilerOptions.module必须是"ESNext",这是 Vite dev server 的硬性要求,如果还是"CommonJS",HMR 会完全失效。第二步是处理 JSX:如果项目有.tsx文件,必须安装@vitejs/plugin-react-swc(推荐)或@vitejs/plugin-react(传统 babel 方案)。这里有个血泪教训:@vitejs/plugin-react-swc的默认配置会忽略.js文件,而很多老项目仍有utils/request.js这类文件,里面用了装饰器语法(@debounce(300)),如果不显式配置include: ['src/**/*.{ts,tsx,js,jsx}'],这些文件就不会被 SWC 编译,直接报语法错误。第三步是类型声明:Vite 项目需要vite/client.d.ts类型声明,但很多团队会忘记在tsconfig.json的include中加入它,导致import.meta.env类型报错。解决方案是在src/env.d.ts中添加:
/// <reference types="vite/client" /> interface ImportMetaEnv { readonly VUE_APP_TITLE: string readonly VUE_APP_API_BASE: string // 其他自定义环境变量 }然后在tsconfig.json的include数组中加入"src/env.d.ts"。这个文件必须存在,且路径必须准确,否则 VS Code 的智能提示会失效,开发者会陷入“明明写了环境变量,为什么提示找不到”的循环。
2.4 步骤四:CSS 与样式方案迁移(耗时:2 天)
CSS 迁移是踩坑最密集的环节。Webpack 的css-loader、style-loader、mini-css-extract-plugin在 Vite 中被@vitejs/plugin-vue和内置 CSS 处理器替代,但行为差异极大。首先是 CSS Modules:Webpack 中css-loader?modules会生成[hash]_[name]_[local]的类名,Vite 中*.module.css默认启用,但类名生成规则是__[local]___[hash:base64:5],如果业务代码里有document.querySelector('.header-title')这种硬编码选择器,会直接失效。解决方案是统一使用:global(.header-title)或在组件中通过defineProps传入 class 名。其次是 CSS 预处理器:Sass/SCSS 迁移最简单,Vite 内置支持,但要注意@import路径。Webpack 的sass-loader会自动在node_modules中查找@import 'element-plus/theme-chalk/index.scss',而 Vite 需要显式配置css.preprocessorOptions.sass.additionalData,把全局变量注入进去,否则 Element Plus 的主题色不会生效。最棘手的是 PostCSS:Webpack 项目常用postcss-preset-env处理 CSS 新特性,Vite 默认开启postcss支持,但postcss.config.js中的plugins数组必须与 Webpack 一致,尤其是autoprefixer的browserslist配置,如果漏掉,打包后的 CSS 可能缺少-webkit-前缀,导致 iOS 12 下样式错乱。我建议在vite.config.ts中显式声明css.postcss,并指向同一个postcss.config.js文件,避免配置漂移。
2.5 步骤五:第三方库与插件适配(耗时:3 天)
这是迁移中最耗时也最关键的一步。不是所有 Webpack plugin 都有 Vite 版本,也不是所有 NPM 包都兼容 ESM。我的处理策略是“三不原则”:不盲目安装、不迷信 star 数、不跳过 peerDependencies。首先,列出所有 Webpack plugin,逐个查询 Vite 官方插件市场(https://github.com/vitejs/awesome-vite)或 GitHub,确认是否有官方/社区维护的 Vite 版本。比如webpack-bundle-analyzer对应rollup-plugin-visualizer,compression-webpack-plugin对应vite-plugin-compression。但要注意:vite-plugin-compression的algorithm参数默认是gzip,而 Webpack 版本默认是brotliCompress,如果线上 Nginx 只配置了 gzip,就会导致资源解压失败。其次,处理第三方库的 CommonJS 兼容性。Vite 的 dev server 基于 ESM,会拒绝加载require()语法的库。典型例子是xlsx(SheetJS),它的dist/xlsx.full.min.js是 UMD 格式,Vite 无法直接 import。解决方案是使用vite-plugin-legacy的modernPolyfills选项,或改用xlsx-populate这类纯 ESM 库。最后,处理node_modules中的非 ESM 依赖。Vite 提供optimizeDeps.include配置,可以把lodash-es、date-fns等 ESM 库提前编译,但对于moment这种 CommonJS 库,必须在optimizeDeps.exclude中排除,否则会报Cannot find module 'moment'。我建议在vite.config.ts中显式声明optimizeDeps: { exclude: ['moment', 'echarts'] },然后在代码中用import('moment').then(m => m.default)动态导入,确保兼容性。
2.6 步骤六:构建与生产环境配置(耗时:1.5 天)
Vite 的build命令本质是调用 Rollup,所以build.rollupOptions是核心配置区。这里有两个必调参数:manualChunks和output.assetFileNames。manualChunks用于代码分割,Webpack 的splitChunks.cacheGroups是基于模块依赖分析,Vite 的manualChunks是基于文件路径正则匹配。比如想把element-plus和lodash打包进vendor.js,Webpack 配置是:
cacheGroups: { vendor: { name: 'vendor', test: /[\\/]node_modules[\\/](element-plus|lodash)[\\/]/, chunks: 'all', } }Vite 中必须写成:
build: { rollupOptions: { output: { manualChunks: { vendor: ['element-plus', 'lodash'], } } } }注意:manualChunks的 key 是 chunk 名,value 是包名数组,不是正则表达式。如果写成正则,整个构建会静默失败。另一个关键点是output.assetFileNames,它控制 CSS、字体、图片等静态资源的文件名。Webpack 的file-loader输出img/[name].[hash:8].[ext],Vite 中必须配置:
output: { assetFileNames: 'assets/[name].[hash:8].[ext]' }否则所有 CSS 会打到style.css,所有图片会打到index.[hash].png,导致 CDN 缓存失效。最后,build.sourcemap必须设为true,否则线上报错无法定位到源码。我见过有团队为了减小包体积关掉 sourcemap,结果线上白屏只能靠 console.log 二分法排查,效率极低。
2.7 步骤七:HMR 与开发体验调优(耗时:0.5 天)
HMR(热模块替换)是 Vite 的灵魂,但默认配置并不完美。Webpack 项目常用react-refresh-webpack-plugin,Vite 中对应@vitejs/plugin-react-swc的react选项。但有一个隐藏开关:fastRefresh。Vite 4.0+ 默认开启fastRefresh,但它会禁用某些 HMR 场景,比如修改main.ts中的createApp参数。解决方案是在vite.config.ts中显式关闭:
plugins: [ react({ fastRefresh: false, }) ]另一个痛点是 CSS HMR 失效。当修改一个.scss文件时,如果它被多个组件 import,Vite 默认只会刷新第一个组件,其他组件样式不会更新。这是因为 Vite 的 CSS HMR 基于模块 ID,而 SCSS 的@import会创建新的模块实例。解决方法是使用vite-plugin-style-import插件,它能把@import 'element-plus/theme-chalk/base.css'转换成 ESM 模块,确保 HMR 触发时所有依赖该 CSS 的组件都被刷新。最后,server.hmr.overlay必须设为true(默认),否则错误会静默吞掉,开发者只能看终端日志,效率极低。我建议在vite.config.ts中显式声明所有 HMR 相关配置,避免依赖默认值。
3. Vite 深度优化:从“能用”到“好用”的十二个关键参数
跑通迁移只是起点,真正的价值在于利用 Vite 的架构优势实现质的飞跃。Vite 的优化不是堆砌插件,而是理解其底层机制后,对vite.config.ts中十几个关键参数的精准调控。这些参数分布在server、build、optimizeDeps、css四个顶层配置下,每一个都对应一个具体的性能瓶颈。下面按优先级排序,详解每个参数的原理、取值逻辑和实测效果。
3.1server.port与server.host:解决端口冲突与跨设备访问
server.port看似简单,却是团队协作的第一道门槛。Webpack 项目常设port: 8080,但 Vite 默认port: 5173。如果团队多人共用一台开发机(比如 Jenkins slave),或使用 Docker Compose 启动多服务,端口冲突会直接阻塞开发。我的做法是:在vite.config.ts中不写死端口,而是读取环境变量:
server: { port: Number(process.env.VITE_PORT) || 5173, host: process.env.VITE_HOST === 'true', }然后在.env.development中写VITE_PORT=3000。这样每个开发者可以自由指定端口,互不干扰。server.host更关键:默认false,只监听localhost,导致手机真机调试、同事远程协助时无法访问。设为true后,Vite 会监听0.0.0.0,但会触发安全警告,必须配合server.strictPort: true和server.https: false使用。实测数据:开启host: true后,iPhone 通过http://192.168.1.100:3000访问延迟从 2.3s 降到 180ms,因为绕过了代理和 DNS 解析。
3.2optimizeDeps.force与optimizeDeps.esbuildOptions:破解首次启动慢魔咒
Vite 启动时会预构建node_modules中的依赖,这个过程叫optimizeDeps。首次启动慢(>15s)的罪魁祸首就是它。optimizeDeps.force设为true会强制跳过缓存,每次启动都重新构建,这显然不是优化,而是自虐。真正的解法是optimizeDeps.esbuildOptions。Vite 用 esbuild 编译依赖,而 esbuild 的target参数决定了编译后的 JS 语法版本。Webpack 项目常设target: ['es2015'],Vite 默认target: 'es2020',这会导致 esbuild 编译出更多现代语法,但某些老版 Node.js(如 14.x)无法解析,从而触发降级编译,拖慢速度。解决方案是显式指定target: 'es2015':
optimizeDeps: { esbuildOptions: { target: 'es2015', } }实测效果:在 Node.js 14.21.3 环境下,首次启动时间从 22.4s 降到 8.7s。另一个技巧是optimizeDeps.include,把高频使用的 ESM 库(如vue-demi、@vueuse/core)提前加入预构建列表,避免 HMR 时动态编译。我建议在vite.config.ts中显式声明include: ['vue-demi', '@vueuse/core'],而不是依赖自动探测。
3.3build.rollupOptions.output.manualChunks:精细化代码分割的艺术
manualChunks是构建体积优化的核心杠杆。Webpack 的splitChunks是全自动的,Vite 的manualChunks是半自动的,需要人工干预。关键不是“分多少”,而是“怎么分”。我的经验是遵循“三域原则”:框架域(Vue/React 核心)、生态域(UI 库、工具库)、业务域(业务组件、工具函数)。例如:
manualChunks: { // 框架域:永远不变,CDN 缓存率最高 framework: ['vue', 'vue-router', 'pinia'], // 生态域:更新频率中等,独立 chunk 便于长期缓存 ui: ['element-plus', 'echarts'], // 业务域:更新最频繁,放主 chunk 减少请求 app: ['@/views', '@/components'], }注意:framework和ui的包名必须精确匹配package.json中的name字段,比如element-plus不能写成element。实测数据:按此分包后,vendor.js体积稳定在 1.2MB,app.js从 2.8MB 降到 1.6MB,首屏加载时间(FCP)从 2.1s 降到 1.4s。另一个技巧是build.rollupOptions.output.inlineDynamicImports: true,它会把动态 import 的 chunk 内联到主 chunk,减少 HTTP 请求数,适合小体积的异步模块(如import('./utils/logger'))。
3.4build.rollupOptions.plugins:Rollup 插件的黄金组合
Vite 的build阶段是 Rollup,所以rollupOptions.plugins是终极优化区。我常用的三个插件:rollup-plugin-visualizer(可视化分析包体积)、rollup-plugin-terser(更细粒度的 JS 压缩)、@rollup/plugin-replace(环境变量替换)。其中@rollup/plugin-replace最容易被忽视。Webpack 的DefinePlugin会把process.env.NODE_ENV替换为字符串,Vite 的define也会,但@rollup/plugin-replace可以做更激进的替换,比如把console.log整行删除:
{ plugins: [ replace({ values: { 'console.log(': 'if (false) console.log(', }, preventAssignment: true, }) ] }这比terser的drop_console更彻底,实测能再减小 3.2% 的 JS 体积。另一个神器是rollup-plugin-node-externals,它能把node_modules中的包标记为 external,生成 CJS/ESM 模块供其他项目 import,适合构建 UI 组件库。
3.5css.preprocessorOptions.sass:Sass 变量注入的正确姿势
Sass 迁移中,additionalData是刚需。Webpack 的sass-loader通过additionalData注入全局变量,Vite 的css.preprocessorOptions.sass同理,但路径处理不同。Webpack 中additionalData: '@import "@/styles/variables.scss";'会自动解析@/别名,Vite 中必须用绝对路径:
css: { preprocessorOptions: { sass: { additionalData: `@import "${path.resolve(__dirname, 'src/styles/variables.scss')}";`, } } }否则会报File to import not found。更关键的是sass的implementation参数。Vite 默认用sass(Dart Sass),但 Dart Sass 编译速度比node-sass(LibSass)慢 40%。如果项目 Sass 文件超过 50 个,建议切换到node-sass:
preprocessorOptions: { sass: { implementation: require('node-sass'), } }实测:127 个 SCSS 文件的编译时间从 3.2s 降到 1.9s。
3.6build.sourcemap与build.rollupOptions.output.sourcemapExcludeSources:线上调试的平衡术
sourcemap是双刃剑:开启它,线上报错能精准定位到源码行;关闭它,包体积减小 15%-20%。我的折中方案是:build.sourcemap: 'hidden'(生成 sourcemap 文件但不注入sourceMappingURL),然后在build.rollupOptions.output.sourcemapExcludeSources: true,这样 sourcemap 文件里不包含源码内容,只保留映射关系。部署时,把dist/*.js.map文件单独上传到 Sentry 或内部错误平台,既保证调试能力,又避免源码泄露风险。实测:sourcemapExcludeSources: true后,.map文件体积从 1.8MB 降到 240KB,上传时间从 8.3s 降到 1.2s。
3.7server.watch:文件监听的静默杀手
Vite 的server.watch默认监听所有文件,包括node_modules和dist目录,这在大型项目中会触发大量无意义的 HMR。server.watch.ignored可以排除这些目录:
server: { watch: { ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**'], } }但更狠的是server.watch.usePolling: true。Linux/macOS 默认用 inotify 监听文件变化,但在 Docker 或 NFS 共享目录中,inotify 不可靠,会导致 HMR 失效。开启usePolling后,Vite 会每秒轮询一次文件修改时间,虽然 CPU 占用高 2%,但 HMR 稳定性 100%。我们一个 200 人团队的 monorepo 项目,就是靠这个参数解决了 90% 的 HMR 失效投诉。
3.8build.rollupOptions.output.entryFileNames:文件名哈希的精准控制
entryFileNames控制 JS 入口文件名,chunkFileNames控制异步 chunk,assetFileNames控制静态资源。Webpack 的output.filename常用[name].[contenthash:8].js,Vite 中必须对应:
output: { entryFileNames: 'assets/js/[name].[hash:8].js', chunkFileNames: 'assets/js/[name].[hash:8].js', assetFileNames: 'assets/[name].[hash:8].[ext]', }注意:[hash]在 Vite 中是 contenthash,不是 webpack 的 chunkhash,所以无需担心 hash 不稳定。另一个技巧是build.rollupOptions.output.preserveModules: true,它会保留源文件目录结构,生成assets/js/views/home/index.js,方便按需加载和调试,但会增加 HTTP 请求数,适合开发环境。
3.9build.minify与build.terserOptions:JS 压缩的深度定制
build.minify默认'esbuild',速度快但压缩率不如terser。对于追求极致体积的项目,必须切到terser:
build: { minify: 'terser', terserOptions: { compress: { drop_console: true, drop_debugger: true, pure_funcs: ['console.log', 'console.warn'], }, format: { comments: false, } } }pure_funcs比drop_console更激进,它会把console.log('test')整行删除,而不是替换成空函数。实测:pure_funcs能额外减小 1.8% 的体积。另一个参数是build.terserOptions.module: true,它告诉 terser 输出 ES Module 语法,便于现代浏览器解析,但会牺牲 IE11 兼容性,需权衡。
3.10css.modules.generateScopedName:CSS Modules 的可预测类名
generateScopedName控制 CSS Modules 的类名生成规则。Webpack 默认[hash:base64:5],Vite 默认__[local]___[hash:base64:5]。为了与旧项目保持一致,必须显式配置:
css: { modules: { generateScopedName: '[name]__[local]___[hash:base64:5]', } }这样Button.module.css中的.primary会生成Button__primary___abc12,与 Webpack 输出完全一致,避免样式类名变更导致的回归测试失败。
3.11build.rollupOptions.external:外部化依赖的边界艺术
external用于把某些包排除在 bundle 外,由宿主环境提供。比如微前端场景,主应用已加载 Vue,子应用就不该再打包一份。external接受字符串、正则、函数:
rollupOptions: { external: ['vue', 'vue-router', /^@vue\/.+/], }但要注意:external只影响build,不影响dev。所以必须配合optimizeDeps.exclude使用,否则 dev server 会尝试编译这些包,报错。我的经验是:external的包必须在index.html中通过<script>标签引入,且全局变量名必须与包名一致(如vue对应window.Vue)。
3.12server.proxy:API 代理的零配置魔法
server.proxy是 Vite 最优雅的设计之一。Webpack 需要devServer.proxy配置对象,Vite 只需一个对象字面量:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), } } }但有一个隐藏技巧:proxy支持函数,可以动态决定 target:
'/api': (ctx) => { const