react-native bundle 解释与拆解
0. 成果
声明: 避免修改 RN 依赖下面代码,fork facebook/metro-bundler 进行修改
github: https://github.com/4ndroidev/metro-bundler
原理:
细心分析, bundle 文件中每一行都是一个Module.js
对应的数据结构;打包过程中,在分析依赖时,引入base.js
先进行基础依赖遍历,并对Module
元素标记base: true
,然后再对入口文件进行依赖分析,这种先后顺序能保证基础模块 id 在前,业务模块 id 在后;在打包输出时,将标记base: true
的Module
打包到base.bundle
,否则打包到business bundle
使用方式:
|
|
结果:
android 加载示例:
|
|
1. 需求
在实际 RN 开发中,往往涉及多个业务,业务间可能不存在耦合,而且业务需要独立的bundle
,由此就会出现多个bundle
的情况,而每个bundle
基本上都包含react
和react-native
依赖,导致总体积较大;一般存在线上下发需求,还会耗费用户流量。
由经验可知,JS 中的react
和react-native
依赖版本基本上与原生 RN 版本对应, 可以抽离这两个依赖打包成base.bundle
内置于app
中
2. 目标
- 抽离
react
和react-native
打包成base.bundle
- 减小线上下发业务
bundle
体积,节省用户流量 - 可预加载
base.bundle
,提升打开页面速度
3. 分析
通过分析bundle结构和依赖查找,最终可以通过标记法进行分包,下文逐一讲解
3.1 bundle 结构分析
- polyfills : 最早执行的一些function,声明es语法新增的接口,定义模块声明方法
__d
等 - module difinitations : 模块声明,以
__d
开头,每一行代表一个JS的定义 - require calls : 执行
InitializeCore
和Entry File
,最后一行require(0);
3.2 bundle 代码分析
由 3.1 可知,JSLoader加载bundle
时,优先执行 polyfill function
,然后定义JS模块
,接着使用 require
调用 init
和 入口文件
Resolver/polyfills/require.js
分析 (代码缩略展示)
|
|
由上述代码可知,
__d
实际是require.js
的define
方法,__d
参数列表为(factory: FactoryFn
,moduleId: number
,dependencyMap?: DependencyMap
)
再贴上混淆打包后的入口模块代码,分析其意义
|
|
__d
: require.js 的 define 方法
__d参数 | 意义 |
---|---|
function(e, t, r, i) | factory方法 |
0 | 模块id,目前入口文件的id必为0,id按照深度遍历方式递增 |
factory参数 | 意义 |
---|---|
e | global对象 |
t | require方法 |
r | 模块对象 |
i | 模块暴露 |
3.3 bundle 依赖分析
打包bundle时,根据entryFile进行深度遍历依赖分析,模块id不断递增,即越早引用的模块,id越小
下文针对0.46.0+代码为抽离react
和react-native
作base.bundle
分析
从 node node_modules/react-native/local-cli/cli.js bundle ....
出发,调用链如下:
|
|
4. 拆包实现
上文说到使用标记法进行分包,是指在分析依赖期间,标记哪些模块属于base.bundle,哪些模块属于业务bundle;
特别地,polyfills属于base.bundle,require-calls和entry-file属于业务bundle
实现: 在DependencyGraph.js
的getDependencies
方法中,引入base.js
,先收集base.bundle的模块,标记成base,接着再根据entryFile
进行依赖收集,保证了base.bundle的模块id都在前面。最少修改代码实现
|
|
输出bundle文件代码:
|
|
5. 总结
优点:
- 一次性打出base.bundle和业务bundle,效率高
- 可自定义哪些模块属于base.bundle
- 原生代码,可预加载base.bundle
缺点:
- 维护成本较高
- 事实上,直接引用
react-native
作为基础,可能会引入一些你用不到的模块。其实我不推介直接引用react-native
,用到其中模块直接引用,这样也能减小部分体积
总体来说,利大于弊,目前真没看到几个开发同事不直接引react-native
,哈哈
6. 展望
从上述分包方案,理论上可以制定规则,解耦业务,根据不同业务模块,划分更多bundle,每个bundle的模块id按照某个值开始,避免重复,类似android插件化处理资源id策略,按需加载业务bundle,另外可能带来管理困难的问题。
7. 附加
事实上,还有更易于维护的方法,前后对base.js
和index.js
进行bundle
,然后以这两个bundle作为输入,进行字符串操作,输出最后我们想要的分包结果。但前提是:index.js
最开始的依赖引用必须与base.js
一致,保证两者打包的基础模块 id 一致。分包代码如下:
|
|