从 Webpack 迁移到 Vite:配置映射、loader 与插件对应与常见坑

Webpack 项目迁移到 Vite 的完整对照手册:打包器与原生 ESM 开发服务器的心智模型差异、entry/output/mode 的配置映射、loader 到 Vite 内建能力的对应、DefinePlugin 与 HtmlWebpackPlugin 的替代方案、resolve 别名迁移、process.env 到 import.meta.env 的改写、devServer 到 server 与 proxy 的映射,以及渐进迁移与常见坑排查。

引言

从 Webpack 迁到 Vite,难点从来不是「配置怎么写」,而是心智模型的切换:Webpack 是「一切皆模块、一切靠 loader 与 plugin」,Vite 是「开发态原生 ESM 按需编译、生产态 Rollup 打包」。理解了这个差异,配置映射就只是查表工作。

本文给出一份实用的迁移对照手册:先把两套心智模型摆清楚,再逐块映射 entry/output/mode、loader、plugin、resolve、环境变量与 devServer,最后给出渐进迁移策略与高频坑的排查清单。

前置:Vite 配置体系、依赖预构建。开发代理见 Vite 开发代理与后端集成:server.proxy、路径重写与 Mock。


目录


1. 心智模型差异:打包器 vs 原生 ESM 开发服务器

1.1 两套模型

维度WebpackVite
开发态先打包再服务原生 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 基本映射表

WebpackVite说明
entrybuild.rollupOptions.input多入口用对象
output.pathbuild.outDir默认 dist
output.filenamebuild.rollupOptions.output命名模板
output.publicPathbase部署基础路径
modemode 参数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 loaderVite 对应
babel-loader内建 esbuild(可选 @vitejs/plugin-react)
ts-loader内建 esbuild 转译 TS
css-loader + style-loader内建,直接 import 即可
file-loader / url-loader内建资源管线
svg-inline-loadervite-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 对应
DefinePlugindefine / import.meta.env
HtmlWebpackPlugin内建 index.html 处理
CopyWebpackPluginpublicDir 或 vite-plugin-static-copy
MiniCssExtractPlugin内建 CSS 提取
webpack-bundle-analyzerrollup-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 devServerVite server
portserver.port
hostserver.host
proxyserver.proxy
historyApiFallback内建(SPA 默认)
httpsserver.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 definedVite 开发态是 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 性能对比参考

指标WebpackVite
冷启动30s 以上1s 内
HMR 更新数百 ms 到秒级数十 ms
生产构建视配置通常更快

10.3 长期收益

长期收益有三:开发反馈循环从秒级降到毫秒级、配置量大幅减少(内建能力覆盖大部分 loader)、与 Rollup 插件生态对齐便于后续切 Rolldown。

记忆:迁移的价值主要在开发体验——冷启动与 HMR 提升一个数量级;验收标准是「配置更少、反馈更快、产物不退化」三件事同时满足。


延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「vite」更多文章

  1. Vite 中的 3D 与 WebGL 工程化:Three.js、模型纹理压缩与渲染性能治理
  2. Vite 项目的 GraphQL 数据层:Apollo、urql、codegen 与缓存失效实战
  3. Vite 项目部署平台适配实战:Vercel、Netlify、Cloudflare Pages 与自建方案