项目从Vite迁移到Rsbuild的踩坑记录

1,880 阅读5分钟

Rsbuild 相对于Vite的优点

团队内使用Vite的脚手架已经有一段时间了,目前倒也没遇到什么完全解决不了的问题,但有几朵潜在的乌云一直在头上:

  • 开发过程和生产过程环节代码不一致,如CSS等可能会出现只在生产环境复现的问题
  • 开发过程中,当组件拆分的越细,文件越多,首次打开页面就越慢(浏览器请求数量的上限导致的)
  • 由于vite在开发环境支持的是ES,会在.bin中缓存部分commonjs转换为esm可解析的模块,在重新install相关包之后,必须删除node_modules重新构建,否则开发环境命中的还是缓存的文件。 之前的webpack的框架是不存在此类问题,但是为了享受vite的快速启动和对modern浏览器更好的加载性能,切换到了vite。不过得益于前端脚手架的不断发展,我们有了Rsbuild这个选择,能较好得避免了Vite的这两个问题,同时兼顾开发效率和打包效率。

Rsbuild 简要介绍

在介绍Rsbuild之前,先看下更早推出的RspackRspack是一个基于Rust开发的前端底层打包工具,与rollup定位类似,与webpack相比缺少了 devServer 等配套开发功能。Rsbuild是在Rspack的基础上拓展的上层构建工具,在打包功能的基础上添加了开发服务器和默认配置,内建了一些插件的支持,集成了 SWC/ LightningCSS 等高性能工具。 在插件层面,完全兼容了Webpack的生态,大部分已有的Webpack生态的插件可以直接接入使用,与Vite相比再开发和生产环境提供了相同的构建工具,确保开发和生产环境的一致性。目前最新的文档已经更新了各种已有框架往Rspack上转的示例,大大降低了配置的学习成本。

配置替换过程

  • 先替换Vite安装Rsbuild包 在项目根目录建立rsbuild.config.ts文件
  • 构建入口替换,这块大部分逻辑可以复用,之前的项目使用了一个vite插件让html的配置在webpack和vite之间通用,现在就可以直接弃用这个插件,直接让rsbuild接管html模板即可。
  • 在第二点的基础上,一些打包过程中在html中插入脚本等逻辑,都可以在rsbuild的配置文件中增加配置解决,这块通过配置又少了三个vite插件。
  • 兼容性配置,这次又变成统一使用 .browserlistrc 文件配置,关于兼容性,需要认真阅读文档,目前在output中配置了usage,更稳妥的是配置为entry。

踩坑1: 本地开发服务器配置

  1. 配置html属性,需要注意配置mountId,在rsbuild中默认是root,之前一直是app
  2. 配置dev对象,需要注意配置 assetPrefix 属性,这个跟output.assetPrefix 有点类似,建议这里配置为 auto,能兼容绝大部分情况。
  3. 本地开发开启https支持要通过插件实现,可以使用 @rsbuild/plugin-basic-ssl
  4. 项目中使用了TS来开发,预设类型要替换成@rsbuild/core这个库提供的,在d.ts文件中增加/// <reference types="@rsbuild/core/types" />

踩坑2: 打包部署配置

  1. 如果需要使用webpack插件,需要在tools.rspack.plugins这个数组中进行配置,只有rsbuild的插件才放到plugins的数组中配置(跟tools一个层级)。
  2. 如果要配置sentry上传sourceMap,需要在output中手动开启sourceMap: { js: 'source-map'}
  3. 针对原来vite项目使用的public目录下的静态资源,之前想的是放在新的public文件夹下就行,但实际体验下来发现不能在源代码中直接引用public下的资源,官方建议是放一份到assets下,也是怪怪的。
  4. 针对一些二进制的静态文件,需要在配置文件里面声明将某些文件作为资源文件处理,并且指定其打包后的输出目录和名称,要注意的是如果是spine导出资源,不允许重命名,不要加hash。
bundlerChain: (chain, { CHAIN_ID }) => {
 // 添加 .skel 文件的处理规则, 实际走静态资源,不能有hash
 chain.module
   .rule('skel')
   .test(/\.(skel|atlas)$/)
   .type('asset/resource') // 将文件作为资源文件处理
   .set('generator', {
     filename: 'assets/[name][ext]',
   });
},

踩坑3: 代码写法变更

由于在原项目中用到了类似import.meta.glob这类与Vite本身相关的API,在迁移rsbuild后是不识别的,需要改一下,以下是改之前和改之后的代码对比:

// 改之前
const CompsPathMap: Record<string, { [key: string]: React.FC | undefined }> =
  import.meta.glob('../templates/*/index.tsx', {
    eager: true,
  });
const CompsNameMap: Record<string, any> = {};
Object.keys(CompsPathMap).forEach((item: string) => {
  const pathArr = item.split('/');
  const compName = pathArr[pathArr.length - 2];
  if (compName === 'Example') return;
  if (CompsPathMap[item][compName]) {
    CompsNameMap[compName] = CompsPathMap[item][compName];
  } else {
    CompsNameMap[compName] = CompsPathMap[item].default;
  }
});
// 改之后
const CompsContext = import.meta.webpackContext('../templates', {
  recursive: true,
  regExp: /\/index\.tsx$/,
  mode: 'sync',
});
const CompsNameMap: Record<string, any> = {};
for (const path of CompsContext.keys()) {
  const mod = CompsContext(path) as { [key: string]: React.FC | undefined };

  const pathArr = path.split('/');
  const compName = pathArr[pathArr.length - 2];

  if (compName !== 'Example') {
    CompsNameMap[compName] = mod[compName] ?? mod.default;
  }
}

可以看到除了API名称不同,两种打包框架在获取文件的API和具体模块处理上也是不同的。这部分改动目前没有工具,还是得实际用一下才能知道写法的具体改动。

写在最后

目前到这一步,已经可以在开发环境跑起来了,并且也能打包出来,但组内讨论的结果是暂时不上线,基于以下几个原因:

  • 目前大家对vite这套比较熟悉,如果贸然切换到rsbuild,担心SWC的代码转换和一些内部的分包策略的变更,会对代码线上表现有影响,引入不必要的风险。
  • 目前开发环境并没有明显感知到Vite工具的瓶颈,另外就是期待后续rolldown能解决Vite两套环境流程的痛点,从更长远角度,可以等等。
  • 目前Rsbuild在页面兼容性上的变更需要更大范围的回测,如果改成entry又会导致包体积变大,影响线上性能,从当前的业务容忍度来看,存在上线引入风险的情况,经向上反馈后,决定暂缓。 不过经过这一系列的探索,确实也让团队熟悉了rsbuild的配置思路和优缺点,开阔了眼界,也切实感觉到了rust开发的相关工具链对前端的改变,还是非常值得的,也希望这篇文章能给其他人带来帮助。