2024年了你还不会“水”Rsbuild的PR?
要用魔法打败魔法
前言
首先非常感谢🙏大佬 chenjiahan!我前后一共提了 3 次 PR,感谢大佬耐心的指导,给了我“水” PR 的方向,最终成功被大佬Merged,成为了Contributor。最终被合并的 PR 链接附上:feat(core): add type
option to dev.watchFiles
to support reload server
这篇文章可能更像一次记录,记录PR的前后,如果你想尝试开源贡献,不妨看看我的经历,如果你是大佬,欢迎评论指正🙇
契机
因为最近公司的项目都迁移到了 rsbuild
,确实速度非常快,构建时间从十分钟降到一分钟内,本地启动服务器也从一二十秒降到三秒,热更新也是快得离谱,简直像在开发者工具里简单改了下 <style>
一样,非常极致的开发体验
但我在开发过程中,经常需要切代理环境,比如某个bug在A环境下有现成的数据可稳定复现,而我目前代理的又是B环境,就需要切换代理环境,也就是修改 server.proxy
配置。因为 server.proxy
配置有一些写入本地登录态的逻辑、一些异步的操作和代理服务器的事件监听处理逻辑,还算比较复杂。所以将这段逻辑抽了出去做为单独的模块 ./configs/proxy.ts
来导出 server.proxy
部分配置
但 rsbuild dev
并不能监听配置文件的依赖变更帮你重新 rsbuild dev
,所以我的操作通常是:
- 保存
proxy.ts
- 找到我的命令行终端
ctrl + c
杀掉进程然后rsbuild dev
况且我在工作中通常至少会同时启动着三个项目和两个显示器和多个屏幕以及各种页面(PRD文档、原型图、视觉稿、etc.)所以找终端这件事是真的烦人,公司发的Mac是 8G 的,切一下屏就可能卡一下🤪
所以,我的诉求很简单:配置变更就自动帮我重启开发服务器
分析
期间我尝试了几个其他的脚手架,比如 vite
的 esbuild
在编译时对生成了配置文件依赖图,vite
只需要监听这些依赖改变重启开发服务即可。为什么 rsbuild
不做呢?后来大佬解释了原因,大概就是不必要的依赖追踪反而影响了速度,毕竟 rspack
就是在速度上对标 webpack
。所以权衡之下,只对入口配置文件用 jiti
做了运行时转译
过程
我首先快速过了一遍 rsbuild
的 cli
部分源码,找到了这一段:源码链接
const command = process.argv[2];
if (command === 'dev') {
const files = [...envs.filePaths];
if (configFilePath) {
files.push(configFilePath);
}
watchFiles(files);
}
于是我直接在 node_modules/@rsbuild/core/dist/index.js
找到对应代码,在 watchFiles();
之前加上一行
files.push('/path/to/project-root-dir/configs/proxy.ts')
重新 rsbuild dev
,尝试修改 proxy.ts
,自动重启开发服务器了,ok搞定!
这确实是当时我的临时解决方案
当然不是长久之计,这样的话依赖每更新一次就需要再改一次。更何况本着极客精神,肯定要完美解决。于是下班后就去官方 github 的 Discussions 下提问(问题链接),我没什么耐心,等了一会没人回复,直接fork源码开始写了起来。顺便一提,这个问题直到目前写文章,还没有人回复...
开头也提到了,一共发起了 3 次 PR,下面详细说一下 3 次 PR 的内容和大佬是怎么指导我的
第一次 PR
feat(core): add the --watch-files
arg to the devCommand
目前分支已经被我删了,看不到代码改动了,大概就是直接给 dev
命令加了一个参数 -wf --watch-files <extra-configs>
// packages/core/src/cli/commands.ts
// ...
devCommand
.option('-wf --watch-files <extra-configs>', 'specify the extra config files, you can split them with `^`')
// ...
然后把分割出来的所有路径都 push
到 watchFiles(files)
里
if (commonOpts.extraConfigs) {
files.push(...commonOpts.extraConfigs.split('^'));
}
watchFiles(files);
并将他们作为 chokidar.watch
的参数,监听 change
事件重新启动开发服务
const watcher = chokidar.watch(files, options);
watcher.on('change', () => {
watcher.close();
restartDevServer();
})
很快就被拒绝了:原因是这个参数跟配置对象里的 dev.watchFiles
重名了,且他更倾向自动分析依赖再监听
确实,这个命令参数我自己都觉得难用,脸红之下直接删了分支😄
我不太懂这个依赖分析应该做到哪,鉴于 vite
的 esbuild
提供了依赖分析,我便追问是否做到 rspack
里
大佬告诉我也许可以做到 @rsbuild/core
里,但要先弄清楚别的框架都是怎么实现它的
第二次 PR
feat(core): analyzing the modules that rsbuild.config.ts depends on and watching them.
如果直接看GO语言实现的 esbuild
源码肯定不现实,于是我借鉴了 webpack
,使用 acorn
生成 AST
来做依赖分析,我直接在 @rsbuild/core
里引入了两个新的依赖包:
acorn
acorn-typescript
然后写了个一百多行的简单版的递归依赖追踪,支持三种形式的依赖引入
import foo from './foo'
const foo = require('./foo')
const foo = await import('./foo')
主要代码如下
// ...
acornWalk.simple(ast, {
// parse it like `import foo from './foo'`
ImportDeclaration(node) {
if (typeof node.source.value === 'string') {
appendDep(node.source.value);
}
},
ImportExpression(node) {
// parse it like `const foo = await import('./foo')`
if (
node.source.type === 'Literal' &&
typeof node.source.value === 'string'
) {
appendDep(node.source.value);
}
},
CallExpression(node) {
// parse it like `const foo = require('./foo')`
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
node.arguments?.[0].type === 'Literal' &&
typeof node.arguments[0].value === 'string'
) {
appendDep(node.arguments[0].value);
}
},
});
// ...
同样,很快就被拒绝了:原因是 acorn
解析太慢了,也是,不然还干嘛用Rust写一个 rspack
同时大佬也是在这里告诉我,为了速度用了 jiti
,舍弃了对配置文件做依赖分析
于是我便告诉了他我频繁切代理环境想自动重启开发服务器的诉求,但是否可以做一个可选配置项呢?就像第一次 PR 里给 dev
命令加 --watch-files
参数一样让用户自行选择需要监听的文件即可
大佬同意了,也给我了一个明确的需求:
第三次 PR
第三次 PR 也就是前言里的最终被合并的 PR 了,详细改动可以自行查看
在配置对象的 dev.watchFiles
里新增了一个类型
type WatchFiles = {
paths: string | string[];
options?: WatchOptions;
++type?: 'reload-page' | 'reload-server';
};
type传 reload-page
或者不传时,与之前的逻辑保持一致
export default {
dev: {
watchFiles: {
type: 'reload-page',
paths: ['public/foo.txt'],
},
},
};
type传 reload-server
时监听对应的 paths
改变,重启开发服务
import bar from './path/to/bar.ts';
export default {
dev: {
watchFiles: {
type: 'reload-server',
paths: ['./path/to/bar.ts'],
},
},
server: {
proxy,
},
};
代码实现不是很难,我还补了两条 e2e
测试用例,很快代码被合并了,而且我很开心的是刚好赶上了官方 release: 1.0.1-beta.7 发布,我的PR也被带上了!!!
总结
通过这次 PR,我不仅更加深入地理解了 rsbuild
的核心源码,也熟悉了playwright
e2e 测试库。希望这个经历能对你们有所帮助,也鼓励更多的人参与开源贡献,让前端生态越来越好!
最后截个图纪念一下!Peace an love~

转载自:https://juejin.cn/post/7397616334653538342