背景
我们团队的项目一直是使用 vite 作为打包工具的,由于 vite 开发环境和生产环境的打包策略不同(开发环境按需打包,生产环境全量打包),所以存在开发环境和线上环境打包产物表现不一致的风险。其中 css 样式不一致的问题尤为容易发生,目前在需求开发过程中,已多次遇到本地开发环境 css 样式符合 UI 稿,发布到测试环境或者线上之后就出现了差异的场景。
基于上述背景,我们期望迁移至一款开发和生产环境打包产物一致的工具。
方案调研
除上述背景外,我们期望的打包工具还应该满足以下特性:
- 开发和生产环境打包产物一致
- 打包性能优异
- 生态良好、项目足够稳定
- 迁移成本较低
要满足打包性能优异的特性,除 vite 外,我们的打包工具范围最终锁定在由 rust 开发项目内:
Turbopack | Farm | Rsbuild(基于rspack) | |
---|---|---|---|
产物一致性 | ❌ 不一致 | ✅ 一致 | ✅ 一致 |
打包性能 | ✅ 快 | ✅ 快 | ✅ 快 |
生态及稳定性 | ❌ 全新生态,生态链较差 | ✅ 兼容 vite 插件(先试验了下 farm,遇到的坑太多了且解决不了,根据 github 的用户数以及 npm 下载量,感觉目前尚未达到稳定可用状态) | ✅ 兼容 webpack 插件且已有众多线上项目验证 |
迁移成本 | ❌ 高(没有对应的插件) | ❌ 高(踩坑太多且无解决思路,应该是farm本身的坑) | ✅ 中 |
经过调研和实践,最终选定 rsbuild 作为最终的工具。
最终收益
-
打包时长:300s 左右 > 60s 左右,性能提升大概 400% 左右
- 迁移前 vite 打包时长:292s
- 迁移后 webpack 打包时长:66s
-
流水线构建时长:360s 左右 > 130s 左右,减少 150%(整条流水线上除了打包之外,还有拉代码仓库、代码质量检测、安装依赖、上传 CDN 等其他工作,而构建工具只是减少了打包这一阶段的工作时长,但打包依然是占最大头的,所以收益还是很明显)
-
冷启动时长:
- 首页面渲染:10s 左右 > 25s 左右(这里指的是从执行
npm run dev
到页面渲染出内容的时长) - 其他页面首次切换:5s 左右 > 1s 内(vite 由于资源是按需加载,在加载其他页面时需要对新页面资源进行 esbuild 构建,所以会慢)
可以看到首屏冷启动时长性能有比较大的劣化,这是因为 vite 是根据页面按需资源打包,而 rsbuild 是全量打包。(但全量打包目前应该是解决开发和生产产物不一致的必要条件,为解决我们的背景问题必不可少)
冷启动时长综合对比,如果在开发过程中切换页面比较多的话,最终的等待时长 rspack 会胜于 vite;如果只关注于一个页面的开发,vite 这方面占优
- 首页面渲染:10s 左右 > 25s 左右(这里指的是从执行
-
热更新:0.5s 内 > 1.5s 内(都是很低的数值,体感没有差别)
-
资源分包数:以我们项目的首屏为例,可以看到 rsbuild 的分包策略要由于 vite(打包总体积几乎无变化的情况下,分包数少了很多):
- css 文件数: 38项 > 9 项
- js 文件数:81项 > 55 项
迁移过程及踩坑
安装相关依赖
安装 rsbuild 核心包:
sql
yarn add @rsbuild/core @rsbuild/plugin-babel @rsbuild/plugin-less @rsbuild/plugin-sass @rsbuild/plugin-vue @rsbuild/plugin-vue-jsx -D
其他插件根据自己项目的需要进行安装,可以在 rsbuild 官网找到。常用的有:
less
@rsbuild/plugin-babel
@rsbuild/plugin-less
@rsbuild/plugin-sass
@rsbuild/plugin-vue
@rsbuild/plugin-vue-jsx
更改打包配置文件
下面是以我们的项目为例(一些敏感代码没有在上面展示),将 vite.config.ts
迁移到 rsbuild.config.ts
,可以进行参考:
-
vite.config.ts:
tsimport { defineConfig, loadEnv, UserConfigExport } from 'vite'; import vue from '@vitejs/plugin-vue'; import vueJsx from '@vitejs/plugin-vue-jsx'; import Inspect from 'vite-plugin-inspect'; import AutoImport from 'unplugin-auto-import/vite'; import { codeInspectorPlugin } from 'code-inspector-plugin'; import path from 'path'; const isProduction = env => env === 'production'; const cdn = 'https://xxx.com/xyz/'; export default ({ mode }: { mode: string }): UserConfigExport => defineConfig({ base: isProduction(mode) ? cdn : '/', plugins: [ vue(), vueJsx(), codeInspectorPlugin({ bundler: 'vite', }), // 开发环境 Meta + shift 切换开启/关闭,点击 dom 跳转源代码 AutoImport({ imports: ['vue', 'vue-router'], dts: 'src/auto-import.d.ts', }), ], resolve: { alias: [ { find: '@', replacement: path.resolve(__dirname, './src'), }, ], }, server: { host: '0.0.0.0', port: 4000, open: true, cors: true, proxy: { '/api/': { target: loadEnv(mode, process.cwd()).VITE_APP_HOST, // 转发域名 changeOrigin: true, headers: { referer: loadEnv(mode, process.cwd()).VITE_APP_REFERER, host: loadEnv(mode, process.cwd()).VITE_APP_HOST, Cookie: loadEnv(mode, process.cwd()).VITE_APP_COOKIE, // 转发 }, }, }, } });
-
rsbuild.config.ts:
tsimport { defineConfig } from '@rsbuild/core'; import { pluginVue } from '@rsbuild/plugin-vue'; import { pluginVueJsx } from '@rsbuild/plugin-vue-jsx'; import { pluginBabel } from '@rsbuild/plugin-babel'; import { pluginLess } from '@rsbuild/plugin-less'; import AutoImport from 'unplugin-auto-import/rspack'; import { codeInspectorPlugin } from 'code-inspector-plugin'; import path from 'path'; const isProduction = env => env === 'production'; const cdn = 'https://xxxx.com/xyz/'; const { VITE_APP_HOST, VITE_APP_COOKIE, VITE_APP_REFERER } = import.meta.env; export default defineConfig(({ env }) => ({ output: { assetPrefix: isProduction(env) ? cdn : '/', distPath: { root: 'dist', html: './', js: './', jsAsync: './', css: 'assets', cssAsync: 'assets', svg: 'assets', font: 'assets', image: 'assets', media: 'assets', }, sourceMap: { js: isProduction(env) ? 'source-map' : 'cheap-module-source-map', css: true, }, }, plugins: [ pluginLess({ lessLoaderOptions: { lessOptions: { javascriptEnabled: false, }, implementation: require.resolve('less'), }, }), pluginBabel({ include: /.(?:jsx|tsx)$/, }), pluginVue(), pluginVueJsx(), ], tools: { htmlPlugin: { template: './index.html', }, rspack: { plugins: [ AutoImport({ imports: ['vue', 'vue-router'], dts: 'src/auto-import.d.ts', }), codeInspectorPlugin({ bundler: 'rspack', }), ], module: { rules: [ { resourceQuery: /raw/, type: 'asset/source', }, ], }, }, }, source: { alias: { '@': path.resolve(__dirname, './src'), }, define: { 'import.meta.env.VITE_APP_HOST': `'${VITE_APP_HOST}'`, }, }, server: { host: '0.0.0.0', port: 4000, open: true, cors: true, proxy: { '/api/': { target: loadEnv(mode, process.cwd()).VITE_APP_HOST, // 转发域名 changeOrigin: true, headers: { referer: loadEnv(mode, process.cwd()).VITE_APP_REFERER, host: loadEnv(mode, process.cwd()).VITE_APP_HOST, Cookie: loadEnv(mode, process.cwd()).VITE_APP_COOKIE, // 转发 }, }, }, }, }));
修改对应指令
json
// package.json
{
"scripts": {
"dev": "rsbuild dev",
"build": "rsbuild build",
"preview": "rsbuild preview",
// others...
},
// ...others
}
修改 index.html
移除掉 index.html
之前的 <script src="main.ts"></script>
引入代码。
踩坑
import.meta 不兼容
import.meta
是浏览器 module 模块中独有的语法,rsbuild 在 rsbuild.config.js
中可以使用 import.meta
,但是在项目源代码中并不支持,所以我们需要对原本代码中的 import.meta
进行适配。
以我们项目为例,大部分代码中的 import.meta.XXX
都是在本地的 .env.local
文件中定义的,我们可以借助 source.define
去进行全局的声明:
ts
import { defineConfig } from '@rsbuild/core';
const { VITE_APP_HOST } = import.meta.env;
export default defineConfig(({ env }) => ({
source: {
define: {
'import.meta.env.VITE_APP_HOST': `'${VITE_APP_HOST}'`,
},
},
}));
另外我们项目还用到了 import.meta.url
去引入资源,这种需要改为 import 方式去引入:
javascript
// 改之前:
const href = new URL('../assets/icon.png', import.meta.url).href;
// 改之后:
import IconPNG from '../assets/icon.png';
const href = IconPNG;
:deep 和 & 兼容问题
目前已知 @rsbuild/plugin-less 插件对于在 :deep 内部使用 less 的 & 符号会有问题,例如:
- case1: 生效
less
.container {
.content {
&_title {
font-weight: bold;
}
}
}
- case2: 生效
less
.container {
:deep(.content) {
.content_title {
font-weight: bold;
}
}
}
- case3: 不生效
less
.container {
:deep(.content) {
&_title {
font-weight: bold;
}
}
}
后面会去 github 提个 issue,我们项目内部这种使用情况很少,所以暂时手动修改为不使用 &
。
css 中使用 v-bind 不支持三元运算符
不支持在 css 中 v-bind 使用三元运算符,可以改成在 js 中使用三元运算符替代
less
// 改之前:
.container {
height: v-bind(title ? '24px' : '48px');
}
// 改之后:
const containerHeight = title ? '24px' : '48px'; // js中写
.container {
height: v-bind(containerHeight);
}
结论
我们的项目目前有 2500+ 文件、30w+ 行代码,算是一个很大的项目了。对于这样一个大型项目而言,从 vite 迁移 rsbuild 的过程中遇到的坑可以算很少了,而且迁移后的收益很明显,足以证明 rsbuild 及 rspack 是一个十分优秀的项目,且完全可以在生产环境中去应用了。
对于 webpack 构建的项目,我相信收益会更加明显,有折腾想法的朋友可以年底迁移一波作为 KPI 试试
最后的最后,快手基础平台团队招前端,欢迎在平台私信我投简历~