Vue2/CLI 项目 Webpack → Rspack 迁移的完整指南

882 阅读6分钟

Vue2/CLI 项目从 webpack 迁移到 rspack 的完整指南

在前端开发领域,构建工具的选择对项目的开发效率和性能有着重要影响。近年来,rspack 作为一个由 Rust 语言编写的高性能 JavaScript 打包工具,因其卓越的构建速度和与 webpack 良好的兼容性,受到了越来越多开发者的关注。本文将详细记录如何将一个基于 Vue2/CLI(webpack)的项目迁移到 rspack,以获得更快的构建速度和更好的开发体验。

迁移背景

为什么选择 rspack?

  • 更快的构建速度:rspack 基于 Rust 开发,构建速度明显快于 webpack
  • 兼容性好:rspack 在设计上保持了与 webpack 的高度兼容
  • 更少的配置:rspack 简化了很多配置,使项目设置更加清晰
  • 现代化:支持最新的 JavaScript 特性和模块系统

迁移步骤

1. 移除 Vue CLI 相关依赖

首先,我们需要移除项目中与 Vue CLI 相关的依赖:

npm remove @vue/cli-service @vue/cli-plugin-babel @vue/cli-plugin-eslint core-js

2. 安装 rspack 相关依赖

接下来,安装 rspack 及其插件:

npm add @rsbuild/core @rsbuild/plugin-vue2 @rsbuild/plugin-babel @rsbuild/plugin-less @rsbuild/plugin-node-polyfill @rsbuild/plugin-sass cross-env dotenv-webpack postcss postcss-replace -D

这些依赖包各自的作用:

  • @rsbuild/core: rspack 的核心包
  • @rsbuild/plugin-vue2: 用于支持 Vue2 的插件
  • @rsbuild/plugin-babel: 用于 JS 转译的 Babel 插件
  • @rsbuild/plugin-less: 用于处理 Less 文件
  • @rsbuild/plugin-node-polyfill: 用于提供 Node.js 核心模块的 polyfill
  • @rsbuild/plugin-sass: 用于处理 Sass 文件
  • cross-env: 用于跨平台设置环境变量
  • dotenv-webpack: 用于加载 .env 文件
  • postcsspostcss-replace: 用于处理 CSS,特别是解决 Vue2 中的 /deep/ 选择器问题

3. 更新 package.json 中的 scripts

修改 package.json 文件中的 scripts 部分:

"scripts": {
  "serve": "cross-env NODE_ENV=development rsbuild dev --mode=development",
  "build": "cross-env NODE_ENV=production rsbuild build --mode=production",
  "build:stage": "cross-env NODE_ENV=staging rsbuild build --mode=production",
  "preview": "rsbuild preview",
  "lint": "rsbuild lint"
}

4. 创建 rspack 配置文件

在项目根目录创建 rsbuild.config.js 文件,这将是 rspack 的主要配置文件:

import { defineConfig, loadEnv, rspack } from '@rsbuild/core';
import { pluginVue2 } from '@rsbuild/plugin-vue2';
import { pluginBabel } from "@rsbuild/plugin-babel";
import { pluginLess } from "@rsbuild/plugin-less";
import { pluginSass } from "@rsbuild/plugin-sass";
import { pluginNodePolyfill } from "@rsbuild/plugin-node-polyfill"
import path from 'path';

// 加载环境变量
const { publicVars } = loadEnv({ prefixes: ['VUE_APP_', "BASE_URL", "NODE_ENV"] });

const isProduction = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'staging';

export default defineConfig({
  mode: isProduction ? 'production' : 'development',
  plugins: [
    pluginVue2(),
    pluginBabel(),
    pluginLess(),
    pluginSass(),
    pluginNodePolyfill()
  ],
  source: {
    // 指定入口文件
    entry: {
      index: './src/main.js',
    },
    define: publicVars,
    include: ['src'],
    exclude: ['node_modules']
  },
  output: {
    path: 'dist',
    filename: 'static/js/[name].[contenthash:8].js',
    distPath: {
      root: 'dist' //dist目录
    },
    clean: true, //清理dist目录
    publicPath: '/',
    polyfill: "usage" // 浏览器兼容性
  },
  resolve: {
    extensions: ['.js', '.vue', '.ts', '.tsx', '.jsx'],
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  },
  modules: {
    rules: [
      {
        test: /\.vue$/,
        use: ['vue-loader', 'postcss-loader', {
          loader: 'postcss-loader',
          options: {
            postcssOptions: {
              config: true // 使用postcss.config.mjs
            }
          }
        }]
      },
      {
        test: /\.(css|less|sass|scss)$/,
        use: ['style-loader', 'css-loader', 'less-loader', 'sass-loader', {
          loader: 'postcss-loader', options: {
            postcssOptions: {
              config: true // 使用postcss.config.mjs
            }
          }
        }],
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/i,
        type: 'asset',
        generator: {
          filename: 'static/img/[name].[contenthash:8][ext]'
        }
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'static/font/[name].[contenthash:8][ext]'
        }
      },
       {
        test: /\.js$/,
        exclude: /node_modules[\\/]core-js/,
        use: {
          loader: 'builtin:swc-loader',
          options: {
            jsc: {
              target: 'es2020',
            },
            env: {
              mode: 'usage',
              coreJs: '3.26.1',
              targets: [
                'chrome >= 87',
                'edge >= 88',
                'firefox >= 75',
                'safari >= 14',
              ],
            },
            isModule: 'unknown',
            // ...other options
          },
        },
      },
    ]
  },
  html: {
    template: './public/index.html', //设置html的模板
  },
  server: {
    proxy: [
      {
        context: ['/api'],
        target: 'http://localhost:3000',
        changeOrigin: true,
        // pathRewrite:{
        // '^/api':''
        // }
        secure: false,
        ws: false,
        logLevel: 'debug',
      }
    ],
    open: true,
    hot: true,
    port: 8080,
  },
  optimization: {
    moduleIds: isProduction ? 'deterministic' : 'named',
    chunkIds: isProduction ? 'deterministic' : 'named',
    mergeDuplicateChunks: true,
    removeEmptyChunks: true,
    runtimeChunk: 'single',
    realContentHash: isProduction,
    innerGraph: isProduction,
    providedExports: isProduction,
    usedExports: isProduction,
    sideEffects: isProduction,
    concatenateModules: isProduction,
    minimize: true,
    minimizer: [
      
    ],
    splitChunks: {
      chunks: 'async',
      minChunks: 1,
      minSize: 20000,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        }
      }
    }
  },
  tools: {
    rspack: {
      plugins: [
        new rspack.optimize.LimitChunkCountPlugin({
          maxChunks: 5
        }),
        new rspack.SwcJsMinimizerRspackPlugin({
          extractComments: false, //移除注释
          minimizerOptions: {
            test: /\.[cm]?js$/,
            format: {
              comments: false  // 移除注释
            },
            compress: {
              passes: 8
            }

          }
        }),
        new rspack.LightningCssMinimizerRspackPlugin({
          minimizerOptions: {
            errorRecovery: false, // 错误恢复
            nonStandard: true, // 非标准语法
          }
        })
      ]
    }
  }
});

5. 处理 /deep/ 选择器问题

Vue2 项目中常用的 /deep/ 选择器在新的构建系统中可能会有兼容性问题,我们需要创建一个 PostCSS 配置文件来处理这个问题。

在项目根目录创建 postcss.config.mjs 文件:

// 为了解决/deep/问题,需要在postcss.config.mjs中添加如下代码
import postcss from 'postcss'
import postcssSelectorParser from 'postcss-selector-parser'

export default {
  plugins: [
    postcss.plugin('replace-deep', () => {
      return (root) => {
        root.walkRules((rule) => {
          rule.selector = postcssSelectorParser((selectors) => {
            selectors.walk((selector) => {
              if (selector.type === 'combinator' && selector.value === '/deep/') {
                selector.value = '::v-deep ';
              }
            })
          }).processSync(rule.selector);
        });
      }
    })
  ]
}

这个配置会将 CSS 中的 /deep/ 选择器自动转换为 ::v-deep,以保持兼容性。

配置详解

核心概念

让我们更详细地解释 rsbuild.config.js 文件中的关键配置项:

1. 模式和插件
mode: isProduction ? 'production' : 'development',
plugins: [
  pluginVue2(),
  pluginBabel(),
  pluginLess(),
  pluginSass(),
  pluginNodePolyfill()
],
  • mode 根据环境变量设置为开发模式或生产模式
  • plugins 加载各种必要的插件来支持 Vue2、Babel、Less、Sass 和 Node.js polyfill
2. 源代码配置
source: {
  entry: {
    index: './src/main.js',
  },
  define: publicVars,
  include: ['src'],
  exclude: ['node_modules']
},
  • entry 指定应用的入口文件,与 Vue CLI 项目保持一致
  • define 将环境变量注入到代码中,类似于 webpack 的 DefinePlugin
  • include/exclude 指定哪些文件应该被处理,哪些应该被忽略
3. 输出配置
output: {
  path: 'dist',
  filename: 'static/js/[name].[contenthash:8].js',
  distPath: {
    root: 'dist'
  },
  clean: true,
  publicPath: '/',
  polyfill: "usage"
},
  • pathdistPath 指定输出目录
  • filename 指定输出文件的名称和路径
  • clean 在每次构建前清理输出目录
  • polyfill 设置为 "usage" 表示自动添加所需的 polyfill
4. 解析配置
resolve: {
  extensions: ['.js', '.vue', '.ts', '.tsx', '.jsx'],
  alias: {
    '@': path.resolve(__dirname, './src')
  }
},
  • extensions 指定可以省略的扩展名
  • alias 设置模块别名,保持与 Vue CLI 项目一致,使 @ 指向 src 目录
5. 模块规则
modules: {
  rules: [
    // Vue 文件处理
    {
      test: /\.vue$/,
      use: ['vue-loader', 'postcss-loader', {
        loader: 'postcss-loader',
        options: {
          postcssOptions: {
            config: true
          }
        }
      }]
    },
    // CSS 处理
    {
      test: /\.(css|less|sass|scss)$/,
      use: ['style-loader', 'css-loader', 'less-loader', 'sass-loader', {
        loader: 'postcss-loader', options: {
          postcssOptions: {
            config: true
          }
        }
      }],
    },
    // 图片处理
    {
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/i,
      type: 'asset',
      generator: {
        filename: 'static/img/[name].[contenthash:8][ext]'
      }
    },
    // 字体处理
    {
      test: /\.(woff|woff2|eot|ttf|otf)$/i,
      type: 'asset/resource',
      generator: {
        filename: 'static/font/[name].[contenthash:8][ext]'
      }
    }
  ]
},

这部分配置定义了如何处理不同类型的文件:

  • Vue 单文件组件
  • CSS、Less 和 Sass 样式文件
  • 图片和 SVG 文件
  • 字体文件
6. HTML 模板配置
html: {
  template: './public/index.html',
},

6.1 HTML 的模板
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> 改为下边的
    + <link rel="icon" href="<%= assetPrefix %>/favicon.ico">

指定 HTML 模板文件位置,与 Vue CLI 项目保持一致。

7. 开发服务器配置
server: {
  proxy: [
    {
      context: ['/api'],
      target: 'http://localhost:3000',
      changeOrigin: true,
      secure: false,
      ws: false,
      logLevel: 'debug',
    }
  ],
  open: true,
  hot: true,
  port: 8080,
},

配置开发服务器,包括:

  • API 代理设置
  • 自动打开浏览器
  • 热更新
  • 端口设置
8. 优化配置
optimization: {
  moduleIds: isProduction ? 'deterministic' : 'named',
  chunkIds: isProduction ? 'deterministic' : 'named',
  mergeDuplicateChunks: true,
  removeEmptyChunks: true,
  runtimeChunk: 'single',
  realContentHash: isProduction,
  innerGraph: isProduction,
  providedExports: isProduction,
  usedExports: isProduction,
  sideEffects: isProduction,
  concatenateModules: isProduction,
  minimize: true,
  // ... 压缩配置和代码分割配置
}

这部分配置关注性能优化,根据环境(开发或生产)设置不同的优化选项。

迁移后的性能改进

从 webpack 迁移到 rspack 后,你可能会观察到以下性能改进:

  1. 构建速度显著提升:rspack 基于 Rust 开发,构建速度可能比 webpack 快 5-10 倍
  2. 内存占用减少:rspack 通常比 webpack 消耗更少的内存
  3. 热更新速度更快:开发模式下的修改反映更迅速
  4. 构建产物优化:通过优化配置,可以得到更小、更高效的输出文件

迁移中可能遇到的问题及解决方案

1. 插件兼容性问题

问题:webpack 有丰富的插件生态,但并非所有插件都与 rspack 兼容。

解决方案

  • 优先使用 rspack 官方提供的插件
  • 寻找替代插件或解决方案
  • 当实在没有替代方案时,可能需要保留部分 webpack 配置或修改代码

2. 样式处理问题

问题:Vue2 中特有的 /deep/ 选择器可能会导致样式失效。

解决方案: 如我们在上面配置的 postcss.config.mjs,通过 PostCSS 插件将 /deep/ 转换为 ::v-deep

3. 环境变量处理

问题:Vue CLI 和 rspack 处理环境变量的方式有所不同。

解决方案: 使用 loadEnv 函数并指定正确的前缀(如 'VUE_APP_')来加载环境变量。

总结

将 Vue2/CLI 项目从 webpack 迁移到 rspack 可能需要一定的工作,但回报是显著的性能提升和更好的开发体验。本文详细介绍了迁移步骤、配置详情和潜在问题的解决方案,希望能帮助你顺利完成迁移过程。

rspack 作为一个相对较新的打包工具,其生态系统仍在发展中,但其与 webpack 的高兼容性使得迁移过程相对平滑。随着项目的发展和 rspack 的不断完善,你可能需要进一步调整配置以满足特定需求。

参考资料