本文中打包的commit基于tag v19.0.0. 也就是commit hash 7aa5dda3b3 本文打包的出产物只能用于调试,不能用于生产环境
react18及其之前打包出sourcemap的umd产物见这篇文章
背景
react19官方的打包产物并没有umd格式的,官方只提供了cjs格式的打包产物jsdelivr。在react18的时候可以看到明显是由umd产物的jsdelivr。在18到19中间有一个pr删除了umd格式的打包pr,导致官方现在打包的react和react-dom只有cjs格式的。对于umd格式,用于自己调试react源码十分方便。对于18及其之前的版本,自带umd格式,怎么打包出带有sourcemap的umd产物见这篇文章。对于react19我们的目标是打包出umd格式的产物并且带有sourcemap。
另外,官方并没有提供esm格式的react,对于esm.sh等react的esm版本,都是通过esbuild去打包cjs格式的react产物二次生成的esm版本。本次也会通过源码把esm版本的react一起打包出。
自定义编译
环境准备
clone react的完整源码,并切换到tag v19.0.0
.切换出新的打包分支
git clone https://github.com/facebook/react.git
git checkout -b build-my-v19.0.0
git reset --hard v19.0.0
打包分析
打包工具分析
react源码使用rollup
打包。对于react19之前的版本,使用的rollup
版本为^1.19.4
。react19源码中的rollup
版本为^3.29.5
。对于低版本的rollup
,插件格式为rollup-plugin-*
这种格式,对于高版本的rollup
,插件格式为@rollup/plugin-*
,高低版本互相不兼容。
对于react18及其以前的版本,因为本身带有umd格式的产物打包出,只需要去修改react的打包逻辑即可得到对应的sourcemap。在之前的文章中也是通过修改react打包中的一些逻辑得到了sourcemap。
对于react19和react18有所不同,由于官方的打包逻辑中删除了umd格式的相关代码,我们去官方的逻辑上加反而会更麻烦,因为官方的打包逻辑涉及的代码和逻辑很多。这时更好的办法是自己去实现一个全新打包的逻辑,rollup
的配置除了format
和官方不一样其他全部保持一致。
另,为什么之前react18及其之前不采用全部重写的打包,而且去官方的逻辑里面修改。react17出时
rollup
的版本就已经很新了,这时的rollup
插件已经是新格式,自己实现一个会和react17使用的老的rollup
版本产生冲突,而且很多rollup-plugin-*
格式的插件都已经废弃,文档难找,所以使用官方的逻辑修改。
官方产物分析
业务使用的官方的cjs产物主要是两个npm包,一个是react
,一个是react-dom
。
对于react
这个npm包来讲,react "version": "19.0.0"
是一个独立包,这个包并不依赖其他的包。在react18及其之前,这个包的依赖有一个loose-envify
,在react19中这个依赖被删除。这个包还有其他的子包,如react/jsx-runtime
,这个包在开启new jsx runtime
的时候会使用,但是在打包umd格式的react版本时不用考虑,react
umd 版本里是React.createElement
。
对于react-dom
这个npm来讲,这个包依赖一个react源码中打出的包scheduler
。同样和react
的npm包一样,这个包也有子包react-dom/client
,和react
不一样的是react-dom/client
在react19版本中是在业务中一定会用到的代码。对于react19而言,react-dom
生成的的cjs代码体积很小,而react-dom/client
很大。在react18及其以前,react-dom/client
是从react-dom
里面重新导出了两个函数createRoot
和hydrateRoot
下面是react18及其之前的打包关系图,注意图中的包和文件夹都是主要的,像是shared
之类的文件夹以及react/jsx-runtime
之类的并没有算进去
对于react19的打包关系图如下
对于react19的umd版本的产物依赖关系,只需按照和react18及其以前的umd版本依赖关系即可。
打包过程分析
打包的代码位于scripts/rollup目录下,其中最主要的是scripts/rollup/build.js。通读整个文件发现,整个react打包主要包括不同bundle的不同channel的打包,以及打包完后的封装。不同channel指的是在stable也就是稳定版本发的包,以及experimental包,也就是npm版本带有experimental的包,这些包通常是测试人员才会使用。
整个打包的核心还是一个rollup的使用,其中最主要的是rollup插件的使用,react封装了很多自己的插件完成react的打包目标。插件的是由函数getPlugins动态生成的。
接下来调试每个插件以及分析插件的作用
将scripts/rollup/bundles中的bundles数组删除至只剩react
react-dom
以及react-dom/client
,其中的bundleTypes只剩NODE_PROD
,运行yarn build
开始调试
以下是插件列表
-
dynamicImports
这是一个自定义插件,这个插件是为生成代码的动态加载提供修改,并无断点命中,所以在打包umd时可以直接删除。
-
rollup-plugin-flow-remove-types
这也是一个自定义插件,目的是在编译时删除
flow
的types
,这个插件并没有生成sourcemap
,所以这个插件在打包生成sourcemap
时应该去掉。对于flow
的处理在自己的打包逻辑中全部换成使用babel
处理。在flow
的官网中使用babel处理flow应该使用下面的代码{ "presets": ["@babel/preset-flow"], "plugins": ["babel-plugin-syntax-hermes-parser"], }
-
useForks
这个插件也是一个自定义插件,这个插件的作用是在react打包时通过不同入口去替换不同的文件。react源码里面有大量带有
fork
的文件和文件夹,在不同的打包添加下同一个入口文件会被替换成不同的文件。这个插件在新的打包逻辑里面一定需要,而且逻辑需要和cjs下的对应react
和react-dom
react-dom/client
的打包保持一致.useForks的参数是动态更改的,这里直接调试不同的入口得到对应的forks值
在entry为react时forks有以下的值,
其他两个entry也可以按照此调试方法得到对应的forks值,这时把对应的forks值保存下来,打包umd时把对应的forks传入即可。
-
forbidFBJSImports,自定义插件
这是打包到FB时所用的插件,调试发现并没有命中断点,新的逻辑里面删除即可
-
resolve
这是rollup官方的插件,去node_modules找包的,新的逻辑同样需要,复制options就可以
-
stripBanner
这是rollup的三方插件,去除源码里面的开头和结尾的注释的,对于新的逻辑来说无用而且不影响js的逻辑,删除就行
-
babel rollup的babel插件,这里babel的options无需按照原本的babel options。直接使用上面提到的
flow
的处理即可。注意这里删除了原本的babel options,会导致production模式下react内部报错的error message无法被转化为error code. 需要此功能的按照官方的options配置。由于我们的打包是调试的所以无需这个
-
remove 'use strict'
强行删除代码里面的remove 'use strict',这个插件在新逻辑也不需要
-
replace
官方的replace插件,新的逻辑里面的值为下面的值
{ __DEV__: 'false' __PROFILE__: 'false', 'process.env.NODE_ENV': "'production'", __EXPERIMENTAL__: 'false', }
-
top-level-definitions
在生成的代码里面加东西,这里调试发现这个插件对于
react
react-dom
react-dom/client
并没有改变生成后的代码。所以新逻辑里面删除这个插件就行 -
剩下的三个
剩下三个插件都是代码优化以及体积检测的分别是
closure
prettier
sizes
,新逻辑里面直接删除就行。
重写打包
根据上面的分析重写一个打包的逻辑,其中核心的插件代码
const plugins = [
useForks(forks), // forks的值是上面复制的值
resolve(),
babel({
babelrc: false,
configFile: false,
presets: [
[
"@babel/flow",
{
allowDeclareFields: true,
},
],
],
plugins: ["babel-plugin-syntax-hermes-parser"],
babelHelpers: "bundled",
}),
replace({
preventAssignment: true,
values: {
__DEV__: false ? "false" : "true",
__PROFILE__: false ? "true" : "false",
"process.env.NODE_ENV": false ? "'production'" : "'development'",
__EXPERIMENTAL__: "false",
},
}),
];
整个新的打包流程就跑通了。完整的打包代码位于mybuild。运行node mybuild/build.js
可以把umd格式以及esm格式的代码完整的打包出来。打包出的js的cdn链接为
测试
这里只写umd版本的测试,通过webpack externals react以及react-dom,使用cdn引入
发现并没有报错并且成功渲染
断点createRoot
并跳进函数,成功跳进源码的函数声明
去sources里面可以看到完整的源码目录
总结
对于react源码打包其中最重要的plugin是useFork
,这是动态解析react源码文件的一个插件。打包umd时需要去断点复制出对应的forks变量来初始化插件。对于打包的重写首先搞清楚打包后的产物关系,其次比较和18及其之前版本的差别,之后进行自己的逻辑的构建。
其他
- 本文的代码位于2239559319/react
- 打包出的代码只能用于调试不能用于生成
author: xiaochuan
于2024-12-24