前言
在项目中添加
monaco-editor以及amis后,打包时间急剧增加,最长时间甚至到了半个小时,严重影响效率。提高打包速度成了迫在眉睫的事。Vite是一个很好的选择,但由于本项目与其他项目进行对接时,用到了__webpack_public_path__,Vite对此迁移可能改动就很大了。然而Rspack宣称:使用兼容 API 无缝替换webpack,那么它是一个很好的选择。
- 未优化打包时,最长的一次打包时间
报错的一些截图&解决方案
- 问题一
- Parsing error: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>? (7:0)
将
.eslintrc中的"parser": "@babel/eslint-parser"换成"parser": "vue-eslint-parser"
- 问题二
- ESLintError: [eslint] Parsing error: This experimental syntax requires enabling one of he following parser plugin(s): "jsx", "flow", "typescript".
在
.eslintrc中添加"plugins": ["react"];在babel.config.js的presets加上'@vue/babel-preset-jsx','@babel/preset-react'
- 问题三
- Module build faild: `Expression expected
Expression expected这种有很多类型,在这只截图了一种(在swc替换babel时)
pluginBabel({
babelLoaderOptions: (config, { addPresets }) => {
addPresets(['@babel/preset-env']);
}
}),
- 问题四
- Parsing error: Cannot find module '@vue/cli-plugin-babel/preset' Require stack:
这个没有记录到,但根据下文的配置,可以解决
babel与swc时间消耗对比
| babel | swc | |
|---|---|---|
| dev | 45.2s | 20.3s |
| build | 46.3s | 12.6s |
迁移
文档
- 先查看
Rsbuild的迁移文档,进行基础的迁移步骤。将 Vue CLI 项目迁移到 Rsbuild
本项目的vue.config.js
const CircularDependencyPlugin = require('circular-dependency-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const DeadCodePlugin = require('webpack-deadcode-plugin');
const path = require('path');
function resolve(dir) {
return path.resolve(__dirname, dir);
}
const hotServer = () => {
const path = './server-config.js';
// require请求文件信息时,node会解析出我们传入的字符串的文件路径的绝对路径,并且以绝对路径为键值,对该文件进行缓存
// require.resolve可以通过相对路径获取绝对路径
// 以绝对路径为键值删除require中的对应文件的缓存
delete require.cache[require.resolve(path)];
// 重新获取文件内容
const { serverOrigin } = require(path);
return serverOrigin || '';
};
const fePort = 8081;
module.exports = {
publicPath: '/',
lintOnSave: true,
assetsDir: './',
devServer: {
port: fePort, // 端口号
compress: true,
host: 'localhost',
server: 'https',
open: true, //配置自动启动浏览器
proxy: {
'/audit-apiv2': {
secure: false,
// target: 'that must have a empty placeholder',
target: 'http://127.0.0.1:10086',
router: () => hotServer(),
onProxyReq(proxyReq) {
// 绕过后端的csrf验证
proxyReq.setHeader('referer', hotServer());
}
},
},
client: {
overlay: false
}
},
css: {
loaderOptions: {
less: {
prependData: '@import "@/assets/css/mixins.less";'
}
}
},
productionSourceMap: false,
runtimeCompiler: true,
pluginOptions: {
'style-resources-loader': {
preProcessor: 'less',
patterns: [resolve('src/assets/css/global.less')]
}
},
chainWebpack: (config) => {
/** 添加对react的支持 */
config.module
.rule('jsx')
.test(/components-react\/.*\.jsx$/)
.use('babel-loader')
.loader('babel-loader')
.tap((options) => {
return {
...options,
presets: ['@babel/preset-env', '@babel/preset-react']
};
});
config.resolve.extensions.add('.tsx').add('.ts').add('.jsx').add('.js');
config.plugin('monaco-editor').use(new MonacoWebpackPlugin());
/** monaco-editor的代码用了很多新特性,需要babel转译 */
config.module
.rule('monaco-editor')
.test(/monaco-editor[\\/].*\.js$/)
.include.add(resolve('node_modules/monaco-editor/esm/vs'))
.end()
.exclude.add(resolve('node_modules/monaco-editor/esm/vs/language'))
.end()
.use('babel-loader')
.loader('babel-loader')
.options({
presets: [
'@babel/preset-env' // Added preset to convert code to ES5
]
})
.end();
// 这里跳过对loginSdk进行转意处理
config.module
.rule('js')
.exclude.add(resolve('node_modules/loginSdk'))
.end();
config.module.rule('svg').exclude.add(resolve('src/assets/svg')).end();
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/assets/svg'))
.end()
.use('svg-sprite')
.loader('svg-sprite-loader')
.end();
config.plugin('html').tap((args) => {
args[0].minify = {
...args[0].minify,
removeAttributeQuotes: false
};
return args;
});
},
configureWebpack: {
cache: {
type: 'filesystem',
allowCollectingMemory: true
},
optimization: {
usedExports: true
},
plugins: [
new CompressionPlugin({
test: /\.(js|css)?$/i, // 压缩文件类型
filename: '[path][base].gz', // 压缩后的文件名
algorithm: 'gzip', // 使用 gzip 压缩
minRatio: 0.8, // 压缩率小于 1 才会压缩
threshold: 10240, // 对超过 10k 的数据压缩
deleteOriginalAssets: false // 是否删除未压缩的文件(源文件),不建议开启
}),
new DeadCodePlugin({
patterns: ['src/**/*'],
exclude: ['**/*.md']
}),
// 检测src目录下存在的循环依赖现象
new CircularDependencyPlugin({
include: /src/,
failOnError: true,
allowAsyncCycles: false,
cwd: process.cwd()
})
]
}
};
本项目迁移的变化(babel)
ci环境打包时间
开发启动时间&打包时间
时间将近1min
rsbuid dev
rsbuid build
index.html文件的变化不计其中
对应配置项迁移
css => @rsbuild/plugin-less
css: {
loaderOptions: {
less: {
prependData: '@import "@/assets/css/mixins.less";'
}
}
},
pluginOptions: {
'style-resources-loader': {
preProcessor: 'less',
patterns: [resolve('src/assets/css/global.less')]
}
},
- 对于以上可以使用
@rsbuild/plugin-less插件中的additionalData - 在编译 Less 文件时,通过
additionalData参数自动引入两个 Less 文件:mixins.less和global.less。
pluginLess({
lessLoaderOptions: {
additionalData: `@import "@/assets/css/mixins.less";@import "@/assets/css/global.less";`
}
}),
chainWebpack => tools.bundlerChain
具体参照上文的
vue.config.js和下文rsbuild.config.mjs
configureWebpack => tools.rspack
具体参照上文的
vue.config.js和下文rsbuild.config.mjs
package.json
"scripts": {
- "serve": "vue-cli-service serve",
- "build": "vue-cli-service build",
- "lint": "vue-cli-service lint",
+ "serve": "rsbuild dev",
+ "build": "rsbuild build",
+ "lint": "eslint --ext .ts,.vue --ignore-path .gitignore --fix src",
}
- 移除与添加的依赖:
"devDependencies": {
- "@vue/cli-plugin-babel": "~5.0.4",
- "@vue/cli-plugin-eslint": "~5.0.4",
- "@vue/cli-plugin-vuex": "~5.0.4",
- "@vue/cli-service": "~5.0.4",
+ "@babel/preset-env": "^7.26.0",
+ "@rsbuild/core": "^1.1.10",
+ "@rsbuild/plugin-babel": "^1.0.3",
+ "@rsbuild/plugin-less": "^1.1.0",
+ "@rsbuild/plugin-vue2": "^1.0.2",
+ "@vue/babel-preset-jsx": "^1.4.0",
+ "babel-loader": "^9.2.1",
}
- 移除
Vue Cli的依赖
npm remove @vue/cli-service @vue/cli-plugin-babel @vue/cli-plugin-eslint @vue/cli-plugin-vuex core-js
- 安装
rsbuild的依赖
npm install @rsbuild/core @rsbuild/plugin-vue2 @rsbuild/plugin-babel @rsbuild/plugin-eslint @rsbuild/plugin-less @rsbuild/plugin-vue2-jsx -D
- 安装
babel相关依赖
npm install @vue/babel-preset-jsx babel-loader -D
babel.config.js
- 修改
babel.config.js文件中的内容
module.exports = {
presets: ['@babel/preset-env', '@vue/babel-preset-jsx', '@babel/preset-react']
};
rsbuild.config.mjs
vue.config.js文件中的内容迁移到rsbuild.config.mjs中
rsbuild.config.mjs
import { defineConfig } from '@rsbuild/core';
import { pluginVue2 } from '@rsbuild/plugin-vue2';
import { pluginLess } from '@rsbuild/plugin-less';
import { pluginBabel } from '@rsbuild/plugin-babel';
import { pluginVue2Jsx } from '@rsbuild/plugin-vue2-jsx';
import { pluginEslint } from '@rsbuild/plugin-eslint';
import { pluginBasicSsl } from '@rsbuild/plugin-basic-ssl';
import CircularDependencyPlugin from 'circular-dependency-plugin';
import CompressionPlugin from 'compression-webpack-plugin'; // 兼容
import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'; // 兼容
import DeadCodePlugin from 'webpack-deadcode-plugin';
import path from 'path';
function resolve(dir) {
return path.resolve(__dirname, dir);
}
const fePort = 8081;
const serverOrigin = 'https://192.168.70.203';
// const serverOrigin = 'https://192.168.70.17';
export default defineConfig({
// 用于注册 Rsbuild 插件。
plugins: [
pluginBabel({
babelLoaderOptions: (config, { addPresets }) => {
addPresets(['@babel/preset-env', '@babel/preset-react']);
}
}),
pluginVue2(),
pluginVue2Jsx(),
pluginLess({
lessLoaderOptions: {
additionalData: `@import "@/assets/css/mixins.less";@import "@/assets/css/global.less";`
}
}),
pluginEslint(),
/**
* 支持本地使用https开发
* 默认设置了 server.https 选项
*/
pluginBasicSsl()
],
dev: {
progressBar: true // 在编译过程中展示进度条
// lazyCompilation: true // 按需编译,从而提升启动时间,不建议开启,切换页面时很慢
},
resolve: {
alias: {
'@': resolve('src')
}
},
output: {
// publicPath: '/', // rspack中的publicPath配置项
assetPrefix: '/', // publicPath与之效果一致
/**
* 浏览器兼容
* usage:注入的 polyfill 体积更小,适合对包体积有较高要求的项目使用
* entry:注入的 polyfill 较为全面,适合对兼容性要求较高的项目使用
*/
polyfill: 'usage'
},
source: {
// 指定入口文件
entry: {
index: './src/main.js'
}
},
html: {
template: './public/index.html'
},
server: {
port: fePort, // 端口号
compress: true,
host: 'localhost',
open: true, //配置自动启动浏览器
proxy: {
'/audit-apiv2': {
secure: false,
target: serverOrigin,
onProxyReq(proxyReq) {
// 绕过后端的csrf验证
proxyReq.setHeader('referer', serverOrigin);
}
},
},
client: {
overlay: false
}
},
tools: {
/**
* tools.rspack修改Rspack的配置项
*/
rspack: (config, { rspack }) => {
config.cache = true;
config.plugins.push(
new CompressionPlugin({
test: /\.(js|css)?$/i, // 压缩文件类型
filename: '[path][base].gz', // 压缩后的文件名
algorithm: 'gzip', // 使用 gzip 压缩
minRatio: 0.8, // 压缩率小于 1 才会压缩
threshold: 10240, // 对超过 10k 的数据压缩
deleteOriginalAssets: false // 是否删除未压缩的文件(源文件),不建议开启
}),
/**
* 关闭警告:
* Critical dependency: require function is used in a way in which dependencies cannot be statically extracted
*/
new rspack.ContextReplacementPlugin(
/require\(\[".*"\]\)/,
resolve('public')
)
);
},
bundlerChain: (chain) => {
/** 添加对react的支持 */
chain.module
.rule('jsx')
.test(/components-react\/.*\.jsx$/)
.use('babel-loader')
.loader('babel-loader')
.tap((options) => {
return {
...options,
presets: ['@babel/preset-env', '@babel/preset-react']
};
});
chain.resolve.extensions.add('.tsx').add('.ts').add('.jsx').add('.js');
chain.plugin('monaco-editor').use(new MonacoWebpackPlugin());
/** monaco-editor的代码用了很多新特性,需要babel转译 */
chain.module
.rule('monaco-editor')
.test(/monaco-editor[\\/].*\.js$/)
.include.add(resolve('node_modules/monaco-editor/esm/vs'))
.end()
.exclude.add(resolve('node_modules/monaco-editor/esm/vs/language'))
.end()
.use('babel-loader')
.loader('babel-loader')
.options({
presets: [
'@babel/preset-env' // Added preset to convert code to ES5
]
})
.end();
// 这里跳过对loginSdk进行转意处理
chain.module
.rule('js')
.exclude.add(resolve('node_modules/loginSdk'))
.end();
chain.module.rule('svg').exclude.add(resolve('src/assets/svg')).end();
chain.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/assets/svg'))
.end()
.use('svg-sprite')
.loader('svg-sprite-loader')
.end();
}
}
});
.eslintrc
"parser": "vue-eslint-parser", // 解析vue文件
// 配置解析器的选项
"parserOptions": {
"parser": "@babel/eslint-parser",
"sourceType": "module", // 使用 ES6 模块
"ecmaVersion": 2018, // 设置 ECMAScript 版本为 2018
"ecmaFeatures": {
"jsx": true
},
"requireConfigFile": false
},
/**
* 启用 React 插件
* Parsing error: This experimental syntax requires enabling one of the following parser plugin(s): "jsx", "flow", "typescript"
*/
"plugins": ["react"],
SWC 替换 bable
SWC(Speedy Web Compiler)是基于 Rust 语言编写的高性能 JavaScript 和 TypeScript 转译和压缩工具。SWC 提供与 Babel 和 Terser 相似的能力,在单线程上它比 Babel 快 20 倍,在四核上它比 Babel 快 70 倍。
- builtin:swc-loader转换 JavaScript 和 TypeScript 代码,它是 swc-loader 的 Rust 版本。
ci环境打包时间
开发启动时间&打包时间
时间比
babel缩短了一半!
rsbuid dev
rsbuid build
package.json
- 移除的依赖
- "@babel/core": "^7.26.0",
- "@babel/eslint-parser": "^7.12.16",
- "@babel/polyfill": "^7.12.1",
- "@babel/preset-react": "^7.25.9",
- "babel-loader": "^9.2.1",
rsbuild.config.mjs
- 使用
builtin:swc-loader替换babel-loader
import { defineConfig } from '@rsbuild/core';
import { pluginVue2 } from '@rsbuild/plugin-vue2';
import { pluginLess } from '@rsbuild/plugin-less';
import { pluginBabel } from '@rsbuild/plugin-babel';
import { pluginVue2Jsx } from '@rsbuild/plugin-vue2-jsx';
import { pluginEslint } from '@rsbuild/plugin-eslint';
import { pluginBasicSsl } from '@rsbuild/plugin-basic-ssl';
import CompressionPlugin from 'compression-webpack-plugin'; // 兼容
import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'; // 兼容
import path from 'path';
function resolve(dir) {
return path.resolve(__dirname, dir);
}
const fePort = 8081;
// const serverOrigin = 'https://192.168.70.203';
const serverOrigin = 'https://192.168.70.17';
export default defineConfig({
// 用于注册 Rsbuild 插件。
plugins: [
pluginBabel({
babelLoaderOptions: (config, { addPresets }) => {
addPresets(['@babel/preset-env']);
}
}),
pluginVue2(),
pluginVue2Jsx(),
pluginLess({
lessLoaderOptions: {
additionalData: `@import "@/assets/css/mixins.less";@import "@/assets/css/global.less";`
}
}),
pluginEslint(),
/**
* 支持本地使用https开发
* 默认设置了 server.https 选项
*/
pluginBasicSsl()
],
// 与本地开发有关的选项
dev: {
progressBar: true // 在编译过程中展示进度条
// lazyCompilation: true // 按需编译,从而提升启动时间,不建议开启,切换页面时很慢
},
// 与模块解析相关的选项
resolve: {
alias: {
'@': resolve('src')
}
},
// 与构建产物有关的选项
output: {
// publicPath: '/', // rspack中的publicPath配置项
assetPrefix: '/', // publicPath与之效果一致
/**
* 浏览器兼容
* usage: 注入的 polyfill 体积更小,适合对包体积有较高要求的项目使用
* entry: 注入的 polyfill 较为全面,适合对兼容性要求较高的项目使用
*/
polyfill: 'entry'
},
// 与输入的源代码相关的选项
source: {
// 指定入口文件
entry: {
index: './src/main.js'
}
},
// 与 HTML 生成有关的选项
html: {
template: './public/index.html'
},
// 与 Rsbuild 服务器有关的选项
server: {
port: fePort, // 端口号
compress: true,
host: 'localhost',
open: true, //配置自动启动浏览器
proxy: {
'/audit-apiv2': {
secure: false,
target: serverOrigin,
onProxyReq(proxyReq) {
// 绕过后端的csrf验证
proxyReq.setHeader('referer', serverOrigin);
}
},
}
},
// 与构建性能、运行时性能有关的选项
performance: {
// bundleAnalyze: {} // 开启webpack-bundle-analyzer分析产物体积
},
// 与底层工具有关的选项
tools: {
/**
* tools.rspack修改Rspack的配置项
*/
rspack: (config, { rspack }) => {
config.cache = true;
config.plugins.push(
new CompressionPlugin({
test: /\.(js|css)?$/i, // 压缩文件类型
filename: '[path][base].gz', // 压缩后的文件名
algorithm: 'gzip', // 使用 gzip 压缩
minRatio: 0.8, // 压缩率小于 1 才会压缩
threshold: 10240, // 对超过 10k 的数据压缩
deleteOriginalAssets: false // 是否删除未压缩的文件(源文件),不建议开启
}),
/**
* 关闭警告:
* public/tools/regulex中存在动态require
* Critical dependency: require function is used in a way in which dependencies cannot be statically extracted
*/
new rspack.ContextReplacementPlugin(
/require\(\[".*"\]\)/,
resolve('public')
)
);
},
bundlerChain: (chain) => {
/** 添加对react的支持 */
chain.module
.rule('jsx')
.test(/components-react\/.*\.jsx$/)
.use('builtin:swc-loader')
.loader('builtin:swc-loader')
.options({
jsc: {
parser: {
syntax: 'ecmascript',
jsx: true
},
transform: {
react: {
pragma: 'React.createElement' // 如果你用的是 React 17 的新 JSX 转换,可以去掉这行
}
}
}
})
.end();
chain.resolve.extensions.add('.tsx').add('.ts').add('.jsx').add('.js');
chain.plugin('monaco-editor').use(new MonacoWebpackPlugin());
/** monaco-editor的代码用了很多新特性,需要babel转译 */
chain.module
.rule('monaco-editor')
.test(/monaco-editor[\\/].*\.js$/)
.include.add(resolve('node_modules/monaco-editor/esm/vs'))
.end()
.exclude.add(resolve('node_modules/monaco-editor/esm/vs/language'))
.end()
.use('builtin:swc-loader')
.loader('builtin:swc-loader')
.options({
jsc: {
parser: {
syntax: 'ecmascript'
},
target: 'es5' // 将代码转译为 ES5
}
})
.end();
// 这里跳过对loginSdk进行转意处理
chain.module
.rule('js')
.exclude.add(resolve('node_modules/loginSdk'))
.end();
chain.module.rule('svg').exclude.add(resolve('src/assets/svg')).end();
chain.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/assets/svg'))
.end()
.use('svg-sprite')
.loader('svg-sprite-loader')
.end();
}
}
});
.eslintrc
- 解析器用回了默认的
espree ECMAScript版本提高
其他
开启开发阶段的进度条
- 默认情况下,在执行
npm run serve时,在终端是没有显示进度条的,如果你需要显示这个进度条可以这样设置:
dev: {
progressBar: true // 在编译过程中展示进度条
},
插件替代品
- 像
circular-dependency-plugin和webpack-deadcode-plugin这两个插件,在rsbuild是没有相应的替代品的,但是可以使用madge和unimported分别进行替代
madge
以下是如何在 rsbuild 项目中集成 madge 来检测循环依赖:
-
安装
madge:npm install madge --save-dev
-
创建一个脚本来运行
madge:在项目根目录下创建一个脚本文件,例如
check-circular-dependencies.js:
const madge = require('madge');
madge('./src')
.then((res) => {
const circular = res.circular();
if (circular.length) {
console.error('Circular dependencies detected:');
console.error(circular);
process.exit(1);
} else {
console.log('No circular dependencies found.');
}
})
.catch((err) => {
console.error('Error detecting circular dependencies:', err);
process.exit(1);
});
- 在 package.json 中添加一个脚本来运行
madge:
{
"scripts": {
"check-circular-dependencies": "node check-circular-dependencies.js"
}
}
-
在构建过程中运行
madge:你可以在构建之前或之后运行这个脚本,以确保没有循环依赖。例如,在构建之前运行:
{
"scripts": {
"build": "npm run check-circular-dependencies && rsbuild"
}
}
通过这种方式,你可以在 rsbuild 项目中检测循环依赖,而不需要依赖 Webpack 插件。
unimported
以下是如何在 rsbuild 项目中集成 unimported 来检测未使用的代码:
- 安装
unimported:
npm install unimported --save-dev
-
创建一个配置文件
unimported.config.js:在项目根目录下创建一个配置文件,例如
unimported.config.js:
module.exports = {
entry: ['./src'],
extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue'],
ignorePatterns: ['node_modules', 'dist', 'build'],
ignoreUnresolved: ['@babel/preset-env', '@babel/preset- react', '@vue/babel-preset-jsx'],
};
- 在 package.json 中添加一个脚本来运行
unimported:
{
"scripts": {
"check-deadcode": "unimported"
}
}
-
在构建过程中运行
unimported:你可以在构建之前或之后运行这个脚本,以确保没有未使用的代码。例如,在构建之前运行:
{
"scripts": {
"build": "npm run check-deadcode && rsbuild"
}
}
通过这种方式,你可以在 rsbuild 项目中检测未使用的代码,而不需要依赖 Webpack 插件。
浏览器兼容性
测试过
chrome90版本的兼容性,使用swc是完全没有问题的
rsbuild.config.mjs
output: {
/**
* 浏览器兼容
* usage: 注入的 polyfill 体积更小,适合对包体积有较高要求的项目使用
* entry: 注入的 polyfill 较为全面,适合对兼容性要求较高的项目使用
*/
polyfill: 'entry'
},
.browserslistrc
> 0.5%
last 2 versions
chrome >= 87
edge >= 88
firefox >= 78
safari >= 14
具体对应的浏览器列表可以查看 browserslist.dev。
结语
这种迁移直接问AI是比较费时间的,AI给出了也比较多错误的回答。兄弟们,还是要多看文档,一次没看懂,那就多看几遍。也可以多看看别人的迁移记录,总会有所收获。