0 Comments

从 Webpack 迁移到 Vite 完整指南:告别漫长的启动等待

如果你维护过一两年以上的前端项目,多半经历过这样的场景:改一行样式,保存之后端起杯子等十几秒,热更新才慢悠悠地刷出来;早上第一次启动开发服务器,先去倒杯水再回来,进度条还在转。这不是你电脑的问题,而是 Webpack 基于「打包一切」的架构在大型项目上难以回避的性能瓶颈。

Vite 从 2020 年诞生后迅速流行,如今已成为 Vue、React 新项目的默认选择。本文不讲空泛的宣传语,而是带你走一遍真实的迁移路径:先理解两者为什么快慢有别,再动手把配置从 webpack.config.js 搬到 vite.config.js,最后处理那些最容易翻车的兼容细节。

一、为什么 Vite 启动这么快

要理解迁移的价值,得先明白 Webpack 慢在哪。

Webpack(以及基于它的 CRA、Vue CLI)在启动时会递归遍历整个依赖图,把入口文件引用的所有模块逐个打包生成 bundle,再起服务。项目越大、模块越多,这个遍历和打包过程就越慢,热更新也常常需要重新打包影响的 chunk。

Vite 换了一条思路:开发环境下它不打包,直接利用浏览器原生 ES Module 的能力。

  • 启动时,Vite 只把入口 HTML 和它依赖的源码用 esbuild 做一次「预构建」(主要是把 CommonJS 依赖转成 ESM 并缓存到 node_modules/.vite),这一步是毫秒级的。
  • 请求某个模块时,Vite 按需用 esbuild「单文件转译」后直接返回,浏览器自己完成加载。
  • 热更新则只失效并重建「被编辑文件」那一条链路上的模块,粒度远小于 Webpack。

用一句话概括:Webpack 是先算好一切再上菜,Vite 是点哪道做哪道。 esbuild 本身是 Go 写的,转译速度又比 Babel(JS)快一个数量级,叠加下来差距非常明显。

生产环境 Vite 依然会用 Rollup 做真正的打包(tree-shaking、代码分割),所以开发快与产物质量并不矛盾。

二、迁移前的准备与目录调整

先把心态摆正:迁移不是无脑换命令,核心是配置翻译依赖替换。建议按下面的顺序推进。

第一步,确认 Node 版本。Vite 5 需要 Node 18+,Vite 6/7 需要 Node 18.18+ / 20+。老项目如果还卡在 Node 14、16,先升级 Node 再谈迁移。

第二步,清理旧依赖,安装新依赖。以 React 项目为例:

# 卸载 webpack 及其插件、loader
npm uninstall webpack webpack-cli webpack-dev-server \
  html-webpack-plugin css-loader style-loader babel-loader

# 安装 vite 和对应的框架插件
npm install -D vite @vitejs/plugin-react

第三步,移动 index.html。Vite 把 index.html 当作入口,要求它放在项目根目录(Vite 5 以后默认是根目录)。如果你之前是 CRA 结构,index.htmlpublic/ 下,需要把它移到根目录,并删掉里面的 %PUBLIC_URL% 占位符,改用相对路径或直接从 /src 引入。

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>我的应用</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

注意新增的 <script type="module" src="/src/main.jsx">,这是 Vite 找入口的关键。

三、翻译配置文件

这是迁移的核心工作。先看一份典型的 Webpack 配置:

// webpack.config.js(迁移前)
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/main.jsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
  },
  resolve: {
    extensions: ['.js', '.jsx', '.json'],
    alias: { '@': path.resolve(__dirname, 'src') },
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: { loader: 'babel-loader' },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/,
        type: 'asset/resource',
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './index.html' }),
  ],
  devServer: { port: 3000, historyApiFallback: true },
};

对应的 Vite 配置长这样:

// vite.config.js(迁移后)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { fileURLToPath, URL } from 'node:url';

export default defineConfig({
  plugins: [react()],
  resolve: {
    extensions: ['.js', '.jsx', '.json'],
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  server: {
    port: 3000,
    historyApiFallback: true,
  },
});

对照着看,你会发现大部分概念是能一一映射的:

Webpack Vite
resolve.extensions resolve.extensions(同名,直接搬)
resolve.alias resolve.alias(路径写法略有差异)
babel-loader + .babelrc @vitejs/plugin-react(内置 JSX / Fast Refresh)
style-loader + css-loader 内置支持 CSS,什么都不用写
asset/resource(图片等) 内置支持,直接 import 即可
HtmlWebpackPlugin 自动处理根目录的 index.html
devServer.port server.port
devServer.historyApiFallback server.historyApiFallback

几个易踩的坑要单独说:

  • JSX 不需要 babel 配置了@vitejs/plugin-react 默认支持 JSX 转换和 Fast Refresh,原来 .babelrc / babel.config.js 里的 @babel/preset-reactreact-refresh 等都可以删掉。但如果你用了装饰器等 Babel 特有插件,需要保留对应配置。
  • process.env 不再可用。Vite 用 import.meta.env 暴露环境变量,只有 VITE_ 前缀的变量会注入到客户端代码。全局搜索替换 process.env.REACT_APP_Ximport.meta.env.VITE_X
  • 别名路径写法不同。Vite 是 ESM 项目,__dirname 没了,要像上面那样用 fileURLToPath(new URL(...)),或者直接用相对路径。
  • 环境变量文件.env.development 里的变量同样需要加 VITE_ 前缀。

四、处理那些「非标配」依赖

真正的难点通常不在标准配置,而在项目里那些乱七八糟的 loader 和插件。逐个排查:

1. 动态 require

老代码里 CommonJS 风格的动态加载在 Vite 里会直接报错:

// 迁移前(Webpack 支持)
const icon = require(`./icons/${name}.svg`);

Vite 是基于 ESM 的,需要改成 import.meta.glob,这是 Vite 提供的批量懒加载方案:

// 迁移后
const icons = import.meta.glob('./icons/*.svg', { eager: true });

function getIcon(name) {
  return icons[`./icons/${name}.svg`]?.default;
}

eager: true 表示启动时全部加载;去掉 eager 则返回懒加载函数,按需 import()

2. Sass / Less 等预处理器

Vite 内置了对 .scss.less.styl 的识别,但对应的预处理器(sasslessstylus)需要自己装:

npm install -D sass

装完即可,.scss 文件无需任何配置就能 import。额外的全局变量注入可以通过 css.preprocessorOptions 配置。

3. SVG as React Component

原来用 @svgr/webpack 把 SVG 当组件用,迁移到 Vite 可以改用 vite-plugin-svgr

npm install -D vite-plugin-svgr
import svgr from 'vite-plugin-svgr';

export default defineConfig({
  plugins: [svgr(), react()],
});

4. Webpack 特有的 ProvidePluginDefinePlugin

  • DefinePlugin(注入全局常量)→ 用 define 配置项。
  • ProvidePlugin(自动引入全局变量,比如让所有文件都能直接用 $)→ 没有直接等价物,一般建议改成显式 import,更清晰也更利于 tree-shaking。

5. 代理配置

devServer.proxy 翻译成 server.proxy,写法几乎一样:

server: {
  proxy: {
    '/api': {
      target: 'http://localhost:8000',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/api/, ''),
    },
  },
}

五、验证与收尾

迁移完先别急着合并,按清单自测一遍:

  1. npm run dev 能秒开,热更新正常(试试改 CSS,看是否即时生效)。
  2. 所有路由能直接刷新(确认 historyApiFallback 生效)。
  3. npm run build 成功,且产物体积没有异常膨胀。
  4. 图片、字体、CSS、静态资源都能正常加载(检查浏览器 Network 面板有没有 404)。
  5. 环境变量在开发、生产环境下取值正确。

顺带更新 package.json 里的脚本,把原来的 react-scripts startwebpack-dev-server 换成 Vite:

{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}

vite preview 用来本地预览生产构建产物,相当于本地起一个静态服务器跑 dist,上线前可以用它快速自测。

六、总结

从 Webpack 迁到 Vite,本质是把「面向打包器」的思维换成「面向浏览器 ESM」的思维。一次迁移能带来实打实的体验提升:开发服务器启动从几十秒降到一两秒,热更新从「等一等」变成「几乎秒刷」,而生产构建依旧由 Rollup 保证质量。

迁移的节奏建议如下:先处理入口和 HTML 结构,再翻译核心配置,最后逐个击破动态 require、SVG、代理等「长尾问题」。如果项目里有大量依赖 Webpack 特性的自定义 loader 或微前端架构,不要一刀切,可以先在子应用或独立仓库上试点,跑顺了再推广。

一个忠告:迁移的收益主要集中在开发体验,而不是生产性能的飞跃。如果你的团队开发机普遍配置不错、项目也不算太大,那就别为了迁移而迁移;但如果每次启动、每次热更新都让人烦躁,那这场迁移绝对值得。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注