背景
前端不断在探索打包,构建的天花板,产出了 vite , Rspack, Turbopack 等一系列优秀的打包构建工具
在好奇心的驱使下, 尝试 以 vue-element-admin 这个仓库作为改造目标, 首先向我们的前辈 【花裤衩】致敬! 产出如此优秀的后台管理系统模版
- 这个仓库代表了大部分的国内中后台系统, 它改造通过,且数据惊人,会在一定程度上驱使大家尝试Rspack
- 仓库基于 @vue/cli @4.4.4 版本, 背后是 webpack @4.x , 我们亲自尝试一下,从 webpack 迁移到Rspack需要改造哪些内容
- 该仓库包含了N种基础的业务(echart, 拖动, element-ui),看看我们接入Rspack对业务的侵入性有多大
- 从实际数据看,Rspack VS webpack 冷启动,HMR, 打包耗时,是否有巨大提升
步骤
所有操作均基于 npm 安装包依赖, 未使用 pnpm
基于webpack启动项目
先说明一下, 老的原始代码,是基于 node: 14.21.3 运行的, 高版本的Node 可能有些 npm包不兼容,无法启动项目
- 下载仓库代码
git clone https://github.com/PanJiaChen/vue-element-admin.git
- 移除掉富文本相关的内容, 这个在 node 14.21.3 下面,否则无法启动项目
package.json
"tui-editor": "1.3.3",
移除这个包
有几个字段也移除一下, 虽然没啥影响
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions"
],
移除业务代码入口,或者直接注释掉
src/components/MarkdownEditor/default-options.js
src/components/MarkdownEditor/index.vue
// 移除路由入口
src/router/modules/components.js
启动项目, 查看耗时
71.495 s
打包时间也挺久,没有记录,大家可以clone 我 gitee 仓库的 master分支代码,自行尝试
下面上我们的 主菜
基于rspack启动项目
原始的package.json里面的 devDependencies 有很多依赖,全部干掉,重新从0开始配置
新建一个分支, 我是把 github 原仓库的代码, clone 到自己的 gitee 仓库,解决了 报错
现在我们基于 gitee 仓库的 master分支开始改造步骤
git checkout -b rspack
安装rspack 依赖
node 可以使用 20.10.0
npm i @rspack/cli @rspack/core -D
安装 vue-laoder
vue2 只能安装到如下版本
npm i vue-loader@15.11.1 vue-style-loader@4.1.3 -D
原始仓库项目使用的是 vue2.6版本, 这里我们顺便升级为 2.7.16
npm i vue@2.7.16 -S
对了, vue 在 2.7 以后,就不需要 vue-template-compiler 这个包了,大家都知道吧。 😊😊😊 快去检查一下你们公司的项目。
配置sass/less
是的,这些包,在使用当前文档,均为最新!最新!最新! 版本
npm i less less-loader sass sass-loader -D
到此,我们已经安装了基础的所需要的依赖
配置静态资源(图片,svg, 字体文件)
webpack 使用 module.rules , rspack也是一样的
module.exports = {
module: {
rules: [
{
test: /\.(png|svg|jpg|jpeg|gif|webp)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset',
}
]
}
}
如果是原始的基于 webpack 而非 @vue/cli 脚手架的配置, 这块可以copy过来, 静态资源也不需要指定 loader
rspack 内置帮我们处理好了, 指定 type 字段即可, 这里跟最新的 webpack@5 的用法一致
路由中省略.vue的问题,无法直接识别vue文件
见下面的 extensions
处理 vue文件
不管是基于 webpack 还是 rspack, 还是 vite, 都不能直接识别 .vue文件, 都是通过 插件等将.vue转成普通的js语法
const path = require('path')
const { VueLoaderPlugin } = require("vue-loader");
const { defineConfig } = require("@rspack/cli");
module.exports = defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, 'src')
},
extensions: [".js", ".json", ".wasm", '.vue', '.jsx', '.tsx']
},
module: {
rules: [
{
test: /\.vue$/,
use: [
{
loader: "vue-loader",
options: {
experimentalInlineMatchResource: true
}
}
]
},
]
}
})
vue2 支持jsx
jsx语法在 vue的生态真是个麻烦的东西,确实,如果手摸手 配置过 rollup 打包 vue2 且支持jsx, webpack 处理vue2的jsx ,就能体会这句话
loader 可能不更新了,然后生态断层了等等问题 😭😭😭
npm i @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props -D
调整 rspack.config.js
const path = require('path')
const { defineConfig } = require("@rspack/cli");
module.exports = defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, 'src')
},
extensions: [".js", ".json", ".wasm", '.vue', '.jsx', '.tsx']
},
module: {
rules: [
{
test: /\.[jt]sx$/,
use: {
loader: "babel-loader",
options: {
presets: [
["@vue/babel-preset-jsx", { compositionAPI: true }]
]
}
}
},
]
}
})
这里我们添加了 babel-loader, @vue/babel-preset-jsx , 还有上面的 文件后缀扩展的支持
以前在vue里面直接写 render jsx 语法,是不需要 写lang=”jsx”, 这里 稍微改一下
process.env 改造
vite 还有 @vue/cli 都可以读取 项目根目录里面的 .env 文件, 这里依旧实现
npm i dotenv dotenv-expand -D
dotenv-expand 是让 env 文件中支持变量的包
const { defineConfig } = require("@rspack/cli");
const env = process.env.NODE_ENV || "development";
const dotEnvFiles =
env === "development" ? [".env.development"] : [".env.production"];
dotEnvFiles.forEach((doteEnvFile) => {
let content = dotenv.config({ path: doteEnvFile })
console.log('读取到的内容');
console.log(content);
expand(content)
});
const VUE_APP = /^VUE_APP_/i;
const filterEnv = {};
const define = Object.keys(process.env)
.filter((key) => VUE_APP.test(key))
.reduce((env, key) => {
filterEnv[key] = process.env[key];
env[`process.env.${key}`] = JSON.stringify(process.env[key]);
return env;
}, {});
module.exports = defineConfig({
plugins: [
new rspack.DefinePlugin({
"process.env": JSON.stringify(filterEnv)
})
]
})
在页面等使用到 process.env.xxx的地方测试一下,变量是否注入
我们在 .env.development 文件中,看到有这个 VUE_APP_BASE_API
测试接口, 已经注入到 axios的 baseURL 中
mockjs改造
我们前面删除了所有的 dev 依赖, 这里重新安装
npm i mockjs -D
在 vue.config.js 中我们这样去配置 mock 服务, 我们在 rspack 需要类似配置
const { defineConfig } = require("@rspack/cli");
module.exports = defineConfig({
devServer: {
historyApiFallback: true,
setupMiddlewares (middlewares, devServer) {
let server = require('./mock/mock-server.js')
server(devServer.app)
return middlewares
}
},
})
http://localhost:8080/dev-api/vue-element-admin/user/info?token=admin-token
vscode终端,有日志输出
解决index.html中变量问题
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= webpackConfig.name %></title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
里面有变量: BASE_URL, 目前rspack 还不认识, 但是我们一般期望在运行后, 替换为 /favicon.cion 这种, 或者类似于 publicPath 等
module.exports = defineConfig({
plugins: [
new rspack.HtmlRspackPlugin({
template: "./public/index.html",
templateParameters: {
BASE_URL: '/'
}
}),
]
})
重启项目,终端就不会报错, 目前图标正常展示
解决element-ui样式编译问题
原项目中,直接引入的是 element-ui的 scss 文件,我们现在不直接使用这个文件, 直接使用编译好的css文件
因为我们现在下载的 sass 属于比较新的版本, element-ui用的以前的版本node-sass 去编译的 element-ui 里面的sass文件
// element-ui@2.15.14
"node-sass": "^4.11.0",
语法存在较大差异, sass语法的变化在 sass官网有说明
main.js
import Element from 'element-ui'
// 之前在src/styles/element-variables.scss 里面引入,去掉,改为这里引入
import 'element-ui/lib/theme-chalk/index.css';
sass变量在vue中使用问题
sass变量一般是不能直接在 vue中直接导入使用的,现在改一下实现
侧边栏 组件 有使用 sass 里面的一些变量
src/layout/components/Sidebar/index.vue
删除掉导入, 都是非标准的一些用法, 我们现在改为 css 原生支持的 变量实现,效果是一样的
上面 template 中的部分不用改任何代码, 修改 computed 计算属性
<script>
export default {
computed: {
variables() {
// 去掉,只要保证我们导出一个上面模版可以使用的对象即可
// return variables
return {
menuBg: 'var(--menuBg)',
menuText: 'var(--menuText)',
menuActiveText: 'var(--menuActiveText)'
}
},
}
}
</script>
现在我们稍微修改一下 scss文件
styles/variables.scss
:root {
--menuText: #{$menuText};
--menuActiveText: #{$menuActiveText};
--subMenuActiveText: #{$subMenuActiveText};
--menuBg: #{$menuBg};
--menuHover: #{$menuHover};
--subMenuBg: #{$subMenuBg};
--subMenuHover: #{$subMenuHover};
--sideBarWidth: #{$sideBarWidth};
}
这里变量名都没有改,是避免原始代码中 template 里面也要改字段,比较麻烦,保持代码前后统一问题
vue SFC 单文件中的sass语法改造问题
src/components/MDinput/index.vue
<style lang="scss" scoped>
// 需要引入这个
@use "sass:math";
$bottomPadding: $spacer - $apixel * 10;
$leftPadding: math.div($spacer,2);
.material-input {
// 原来: padding: $spacer $spacer $spacer - $apixel * 10 $spacer / 2;
padding: $spacer $spacer $bottomPadding $leftPadding;
}
</style>
主要就是 sass除法的改造, 这在 sass 废弃语法中有提到
解决svg图标不展示问题
这个主要是svg-sprite-loader 可能跟 webpack 5.x 的一个适配问题导致的, 在 rspack的 issue中我也有提到 Issue #5450 · web-infra-dev/rspack , 调试了一下,没有解决。
我们从代码里面看,本质是svg 加载的问题
目前官方并没有说明svg 属于一等公民,暂时没有很好的处理方式,简单粗暴的,把代码copy到 index.html中
或者直接把svg 作为普通的HTML 放在 vue组件中
vue2 官方文档有说明 svg在 vue中如何使用,我们项目如果迁移到 rspack , 这个也不是什么大问题。我在rspack改造完本项目以后,也试一下, 就把svg当做普通的HTML 拷贝到某个vue文件中,可以加载svg
echart 报错
ReferenceError: exports is not defined
升级echart 到 V5版本,修改vue中引用的方式
"echarts": "4.2.1", 升级到 "echarts": "^5.4.3",
import * as echarts from 'echarts';
别的不需要改
element-ui 表格 空白问题
npm i element-ui@2.9.0 -S
降低element-ui 版本
有说跟 core-js 版本太高有关系。
打包指定输出目录
js, css ,图片做分类
module.exports = defineConfig({
output: {
clean: true,
filename: 'js/[name].js',
cssFilename: 'css/[name].css',
assetModuleFilename: 'assets/[hash][ext]'
},
})
接入Rsdoctor
截止到 2024-01-26 这个插件在 rspack中会报错,相信官方团队很快会解决 Bug #158
介绍: Rspack 和 Webpack 构建分析器
npm add @rsdoctor/rspack-plugin -D
安装 cross-env
npm i cross-env -D
在 package.json中增加一个 脚本
"scripts": {
"doctor": "cross-env RSDOCTOR=true npm run build"
},
npm run doctor
// 分析插件
const { RsdoctorRspackPlugin } = require('@rsdoctor/rspack-plugin');
let plugins = [
new VueLoaderPlugin(),
new rspack.HtmlRspackPlugin({
template: "./public/index.html",
templateParameters: {
BASE_URL: '/'
}
}),
new rspack.DefinePlugin({
"process.env": JSON.stringify(filterEnv)
}),
]
// 仅在 RSDOCTOR 为 true 时注册插件,因为插件会增加构建耗时
if (process.env.RSDOCTOR) {
plugins.push(
new RsdoctorRspackPlugin({
// 插件选项
})
)
}
部分小毛病
- 目前项目中有的 .vue 文件的 热更新存在问题,改动了内容以后,热更新失效了,需要重启项目才可以,官方已经在跟进 vue2项目,热更新失效 · Issue #5445 · web-infra-dev/rspack (github.com)
- rsdoctor目前有一点点兼容问题, 前面也说到了,相信不久就可以解决,问题不大。
- svg 这个loader 生态接入问题,如果项目没有非要跟webpack 项目一样的效果,其实无影响
这些其实相比 rspack带来的变化,可以忽略
还有 rspack 跟 webpack其实是一脉的, 项目越大, 路由越多, vite生态 + vue 可能在路由跳转上并没有那么丝滑,这个问题目前vite也解决不了, rspack 因为打包了资源,资源也都下载到客户端了,所以,不会存在切换路由的卡顿感, 处理机制不一样
完整配置 rspack.config.js
或者去仓库 vue-element-admin rspack分支 copy 查看, 因为rspack 升级后,我可能不定期跟着更新,测试rspack的变化
rspack.config.js
const path = require('path')
const rspack = require("@rspack/core");
const { VueLoaderPlugin } = require("vue-loader");
const { defineConfig } = require("@rspack/cli");
const dotenv = require('dotenv')
const { expand } = require('dotenv-expand')
// 分析插件
const { RsdoctorRspackPlugin } = require('@rsdoctor/rspack-plugin');
const env = process.env.NODE_ENV || "development";
const dotEnvFiles =
env === "development" ? [".env.development"] : [".env.production"];
dotEnvFiles.forEach((doteEnvFile) => {
let content = dotenv.config({ path: doteEnvFile })
// console.log('读取到的内容');
// console.log(content);
expand(content)
});
const VUE_APP = /^VUE_APP_/i;
const filterEnv = {};
const define = Object.keys(process.env)
.filter((key) => VUE_APP.test(key))
.reduce((env, key) => {
filterEnv[key] = process.env[key];
env[`process.env.${key}`] = JSON.stringify(process.env[key]);
return env;
}, {});
// console.log('拿到的内容');
// console.log(define);
let plugins = [
new VueLoaderPlugin(),
new rspack.HtmlRspackPlugin({
template: "./public/index.html",
templateParameters: {
BASE_URL: '/'
}
}),
new rspack.DefinePlugin({
"process.env": JSON.stringify(filterEnv)
}),
]
// 仅在 RSDOCTOR 为 true 时注册插件,因为插件会增加构建耗时
if (process.env.RSDOCTOR) {
plugins.push(
new RsdoctorRspackPlugin({
// 插件选项
})
)
}
/**
* @type {import('@rspack/cli').Configuration}
*/
module.exports = defineConfig({
context: __dirname,
entry: {
main: "./src/main.js"
},
output: {
clean: true,
filename: 'js/[name].js',
cssFilename: 'css/[name].css',
assetModuleFilename: 'assets/[hash][ext]'
},
devServer: {
historyApiFallback: true,
setupMiddlewares (middlewares, devServer) {
let server = require('./mock/mock-server.js')
server(devServer.app)
return middlewares
}
},
plugins,
resolve: {
alias: {
"@": path.resolve(__dirname, 'src')
},
extensions: [".js", ".json", ".wasm", '.vue', '.jsx', '.tsx']
},
module: {
rules: [
{
test: /\.vue$/,
use: [
{
loader: "vue-loader",
options: {
experimentalInlineMatchResource: true
}
}
]
},
{
test: /\.[jt]sx$/,
use: {
loader: "babel-loader",
options: {
presets: [
["@vue/babel-preset-jsx", { compositionAPI: true }]
]
}
},
},
{
test: /\.less$/,
loader: "less-loader",
type: "css",
},
{
test: /\.scss$/,
loader: "sass-loader",
type: "css",
exclude: /node_modules/,
},
{
test: /\.(png|svg|jpg|jpeg|gif|webp)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset',
}
]
}
})
改动的步骤细节
改动的每一个细节,我都单独的提交一个 commit, 希望如果你有改造业务系统的计划,可以有帮助和参考
仓库地址: vue-element-admin
最终
好了, 到此,我们改造了这个仓库,里面所有涉及的坑全部排查完毕, 也给出了对应的解决方案, 我们来看一看最终的成绩吧 😊😊😊
运行效果截图
构建耗时
对比webpack, 成绩相当不错(撒花 😊), 构建时间从 71.495s 到 6s 。
Rspack 打包时间在 3.87s
结语
总之:牛逼~
希望rspack 继续发力,兼容前端大部分的应用场景
文章编写不易,希望给一个小小的 点赞,转发, 谢谢
再贴一次仓库地址: gitee.com/luoriwushen…
原文链接:https://juejin.cn/post/7328213780310196264 作者:知了清语