引言
从 Webpack 迁到 Vite,难点从来不是「配置怎么写」,而是心智模型的切换:Webpack 是「一切皆模块、一切靠 loader 与 plugin」,Vite 是「开发态原生 ESM 按需编译、生产态 Rollup 打包」。理解了这个差异,配置映射就只是查表工作。
本文给出一份实用的迁移对照手册:先把两套心智模型摆清楚,再逐块映射 entry/output/mode、loader、plugin、resolve、环境变量与 devServer,最后给出渐进迁移策略与高频坑的排查清单。
前置:Vite 配置体系、依赖预构建。开发代理见 Vite 开发代理与后端集成:server.proxy、路径重写与 Mock。
目录
- 1. 心智模型差异:打包器 vs 原生 ESM 开发服务器
- 2. 配置映射:entry、output 与 mode
- 3. loader 对应:Babel、CSS、静态资源的替代
- 4. 插件对应:DefinePlugin、CopyPlugin 与 HtmlWebpackPlugin
- 5. 别名与解析:resolve 配置的迁移
- 6. 环境变量:process.env 到 import.meta.env
- 7. 开发服务器:devServer 到 server 与 proxy
- 8. 常见坑:require、CommonJS 与动态导入
- 9. 渐进迁移策略:双构建与按页面切换
- 10. 迁移检查清单与性能对比
- 延伸阅读
1. 心智模型差异:打包器 vs 原生 ESM 开发服务器
1.1 两套模型
| 维度 | Webpack | Vite |
|---|---|---|
| 开发态 | 先打包再服务 | 原生 ESM 按需编译 |
| 启动速度 | 随项目线性增长 | 基本恒定 |
| 热更新 | 重打包受影响模块 | 按模块图精确更新 |
| 生产态 | 自身打包 | Rollup 打包 |
1.2 关键认知转变
Webpack:所有模块都是「模块」,靠 loader 转换、plugin 编排
Vite:开发态浏览器自己解析 ESM,Vite 只做「单文件转换 + 裸模块重写」
这意味着很多 Webpack 里必须写的配置(如 babel-loader 处理 TS),在 Vite 里内建就有,不需要额外配置。
记忆:Webpack 是先打包再服务、Vite 是原生 ESM 按需编译——大量 Webpack 里必写的 loader,在 Vite 里是内建能力,迁移第一步是删掉冗余配置。
2. 配置映射:entry、output 与 mode
2.1 基本映射表
| Webpack | Vite | 说明 |
|---|---|---|
| entry | build.rollupOptions.input | 多入口用对象 |
| output.path | build.outDir | 默认 dist |
| output.filename | build.rollupOptions.output | 命名模板 |
| output.publicPath | base | 部署基础路径 |
| mode | mode 参数 | dev/build 命令区分 |
2.2 一个对照示例
// webpack.config.js(迁移前)
module.exports = {
entry: { main: './src/main.ts' },
output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[hash].js', publicPath: '/' },
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
}
// vite.config.ts(迁移后)
export default defineConfig({
base: '/',
build: {
outDir: 'dist',
rollupOptions: {
input: { main: resolve(__dirname, 'src/main.ts') },
output: { entryFileNames: '[name].[hash].js' },
},
},
})
2.3 mode 的处理
Vite 用命令区分(vite 是 dev、vite build 是 build),不需要手动判 NODE_ENV。
记忆:entry/output/publicPath/mode 分别映射到 rollupOptions.input、outDir 与 output、base、命令——mode 交给命令,不再手判 NODE_ENV。
3. loader 对应:Babel、CSS、静态资源的替代
3.1 loader 到 Vite 的对照
| Webpack loader | Vite 对应 |
|---|---|
| babel-loader | 内建 esbuild(可选 @vitejs/plugin-react) |
| ts-loader | 内建 esbuild 转译 TS |
| css-loader + style-loader | 内建,直接 import 即可 |
| file-loader / url-loader | 内建资源管线 |
| svg-inline-loader | vite-plugin-svgr |
3.2 大部分 loader 可以直接删
迁移动作:
删除 babel-loader / ts-loader / css-loader / style-loader / file-loader 配置
删除对应的 npm 依赖
CSS 直接 import './x.css'
资源直接 import img from './x.png'
3.3 何时仍需要 Babel
只有需要特定 Babel 插件(如某些装饰器方案)或极老浏览器目标时才保留 Babel:
// 保留 babel 的少数场景
export default defineConfig({
plugins: [react({ babel: { plugins: ['babel-plugin-styled-components'] } })],
})
记忆:loader 是 Vite 迁移里删得最多的一块——babel/ts/css/file loader 全内建,只有需要特定 Babel 插件时才保留 Babel。
4. 插件对应:DefinePlugin、CopyPlugin 与 HtmlWebpackPlugin
4.1 常用插件对照
| Webpack 插件 | Vite 对应 |
|---|---|
| DefinePlugin | define / import.meta.env |
| HtmlWebpackPlugin | 内建 index.html 处理 |
| CopyWebpackPlugin | publicDir 或 vite-plugin-static-copy |
| MiniCssExtractPlugin | 内建 CSS 提取 |
| webpack-bundle-analyzer | rollup-plugin-visualizer |
4.2 DefinePlugin 的替代
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
},
})
注意:define 是文本替换,字符串值必须 JSON.stringify 包一层,否则会被当成变量名。
4.3 HtmlWebpackPlugin 的替代
Vite 以项目根目录的 index.html 为入口,自动注入产物引用,多页面时通过 rollupOptions.input 声明多个 HTML。
记忆:DefinePlugin 用
define(值要 JSON.stringify)、HtmlWebpackPlugin 由 Vite 内建接管、CopyWebpackPlugin 用 publicDir——大部分插件不用再装。
5. 别名与解析:resolve 配置的迁移
5.1 别名迁移
// webpack
resolve: { alias: { '@': path.resolve(__dirname, 'src') } }
// vite
export default defineConfig({
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },
})
5.2 扩展名与 mainFields
export default defineConfig({
resolve: {
extensions: ['.mjs', '.js', '.ts', '.tsx', '.json'],
mainFields: ['browser', 'module', 'main'],
},
})
5.3 别忘了 TS 的 paths
Vite 的 resolve.alias 不会自动同步到 TypeScript,tsconfig.json 里要单独配 paths,否则编辑器报红:
{
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["src/*"] }
}
}
记忆:别名迁移要改两处——
resolve.alias给打包器、tsconfig.paths给编辑器;只改一处会出现「构建能过、编辑器报红」。
6. 环境变量:process.env 到 import.meta.env
6.1 命名约定
Vite 只暴露以 VITE_ 开头的变量,且通过 import.meta.env 访问:
Webpack:process.env.REACT_APP_API_URL
Vite: import.meta.env.VITE_API_URL
6.2 迁移改写
// 迁移前
const api = process.env.REACT_APP_API_URL
// 迁移后
const api = import.meta.env.VITE_API_URL
批量改写可以用 codemod 或全局替换:process.env.REACT_APP_ → import.meta.env.VITE_。
6.3 内建变量
import.meta.env.MODE // 'development' | 'production'
import.meta.env.DEV // 布尔
import.meta.env.PROD // 布尔
import.meta.env.BASE_URL // base 路径
记忆:环境变量从
process.env.X改成import.meta.env.VITE_X——必须加VITE_前缀才会暴露,内建的 MODE/DEV/PROD/BASE_URL 可直接用。
7. 开发服务器:devServer 到 server 与 proxy
7.1 配置映射
| Webpack devServer | Vite server |
|---|---|
| port | server.port |
| host | server.host |
| proxy | server.proxy |
| historyApiFallback | 内建(SPA 默认) |
| https | server.https |
7.2 代理迁移
// webpack
devServer: {
proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } },
}
// vite
export default defineConfig({
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ''),
},
},
},
})
7.3 注意 historyApiFallback
Vite 的开发服务器默认就支持 SPA 回退,不需要额外配置;但如果同时配置了 proxy 与回退,要确认 API 路径不会误命中回退。
记忆:devServer 到 server 是字段级映射——proxy 结构几乎一致(多一个 rewrite),SPA 回退 Vite 默认就有,别再手配。
8. 常见坑:require、CommonJS 与动态导入
8.1 高频坑表
| 现象 | 原因 | 处理 |
|---|---|---|
| require is not defined | Vite 开发态是 ESM | 改成 import |
| module.exports 报错 | 混用 CommonJS | 依赖改 ESM 或预构建 |
| 动态 import 变量失效 | 无法静态分析 | 用 glob 或固定路径 |
| 环境变量读不到 | 缺 VITE_ 前缀 | 补前缀 |
| 依赖报 ESM 错误 | CJS 依赖未预构建 | 加入 optimizeDeps |
| 别名不生效 | 只配了一处 | 同时配 alias 与 paths |
8.2 动态导入的写法
// 错误:Vite 无法静态分析变量路径
const mod = await import(`./pages/${name}.tsx`)
// 正确:用 glob 预声明可能的路径
const pages = import.meta.glob('./pages/*.tsx')
const mod = await pages[`./pages/${name}.tsx`]()
8.3 CJS 依赖的处理
遇到报 require is not defined 的依赖,通常是预构建没覆盖到:
export default defineConfig({
optimizeDeps: { include: ['some-cjs-lib'] },
})
记忆:迁移最常翻车的三处是 require、CommonJS 依赖与变量动态导入——前两者靠改 import 与 optimizeDeps,后者必须换成
import.meta.glob。
9. 渐进迁移策略:双构建与按页面切换
9.1 三种策略
| 策略 | 做法 | 风险 |
|---|---|---|
| 大爆炸 | 一次性切完 | 高,回滚成本大 |
| 双构建 | 两套构建并行跑 | 中,维护成本翻倍 |
| 按页面 | 逐页迁移、共享入口 | 低,但需要过渡方案 |
9.2 推荐的渐进路径
第一步:先在 CI 上跑一遍 Vite 构建,对比产物体积与 chunk 数
第二步:本地开发切到 Vite,保留 Webpack 作为生产构建兜底
第三步:解决运行时差异(require / CJS / 动态导入)
第四步:生产构建切 Vite,保留一个版本的观察期
第五步:删除 Webpack 配置与冗余依赖
9.3 双构建的目录约定
webpack.config.js → 保留
vite.config.ts → 新增
两者共用同一份 src/,通过 npm scripts 区分:
npm run build:webpack
npm run build:vite
记忆:迁移不要大爆炸——先在 CI 并行跑两套构建对比产物,再本地切 Vite、最后切生产,把风险拆成可回滚的步骤。
10. 迁移检查清单与性能对比
10.1 检查清单
□ 已删除 babel/ts/css/file loader 相关依赖与配置
□ 环境变量已全量改为 import.meta.env.VITE_ 前缀
□ 别名在 resolve.alias 与 tsconfig.paths 两处都已配置
□ require / module.exports 已改为 ESM
□ 变量动态 import 已改为 import.meta.glob
□ CJS 依赖已加入 optimizeDeps.include
□ devServer 已映射到 server 与 proxy
□ 已在 CI 上对比两套构建的产物
10.2 性能对比参考
| 指标 | Webpack | Vite |
|---|---|---|
| 冷启动 | 30s 以上 | 1s 内 |
| HMR 更新 | 数百 ms 到秒级 | 数十 ms |
| 生产构建 | 视配置 | 通常更快 |
10.3 长期收益
长期收益有三:开发反馈循环从秒级降到毫秒级、配置量大幅减少(内建能力覆盖大部分 loader)、与 Rollup 插件生态对齐便于后续切 Rolldown。
记忆:迁移的价值主要在开发体验——冷启动与 HMR 提升一个数量级;验收标准是「配置更少、反馈更快、产物不退化」三件事同时满足。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。