RN拆包解析
一. 拆包动机
RN作为非常优秀的移动端跨平台开发框架,在近几年得到众多开发者的认可。国内各大厂采用在当前原生应用内集成RN的方式,使得App应用的灵活性得到了很大的提升。在原生应用内嵌入RN,就是需要在原生应用内加载RN模块(1个或多个JSBundle),并得以显示。JSBundle中包含了当前RN模块的js代码。如果存在多个RN模块需要被加载时,就需要分别打出多个JSBundle,并且多个JSBundle包含了很多重复的代码(例如:第三方依赖)。拆包的方式,就是将其中重复不变的代码打成基础包,动态变化的打成业务包。那么就做到了JSBundle的拆分。JSBundle的拆分,对降低内存的占用,减少加载时间,减少热更新时流量带宽等,在优化方面起到了非常大的作用。
二.bundle简要分析
1.bundle命令
- entry-file:即入口文件,打包时以该文件作为入口,一步步进行模块分析处理。
- platform:用于区分打包什么平台的 bundle
- dev:用于区分 bundle 使用环境,非 dev 时,会对代码进行 minified
- bundle-output:打包产物输出地址,即打包好的 bundle 存放地址
- sourcemap-output:打包时生成对应的 sourcemap 文件存放地址,在跟踪查找错误或崩溃时,能帮助开发快速定位到代码
- assets-dest:bundle 中使用的静态资源文件存放地址
1.结构分析
var
__DEV__ = false,
__BUNDLE_START_TIME__ = this.nativePerformanceNow ? nativePerformanceNow() : Date.now(),
process = this.process || {};
process.env=process.env || {};
process.env.NODE_ENV = "production";
!(function(r) {
"use strict";
r.__r = o,
r.__d = function(r,i,n) {
if(null != e[i])
return;
e[i] = {
dependencyMap:n,factory:r,hasError:!1,importedAll:t,importedDefault:t,isInitialized:!1,publicModule:{exports:{}}
}
},
r.__c = n;
.... 代码省略
})();
__d(function(g,r,i,a,m,e,d){var n=r(d[0]),t=r(d[1]),o=n(r(d[2])),u=r(d[3]);t.AppRegistry.registerComponent(u.name,function(){return o.default})},0,[1,2,328,330]);
....省略其他 __d 代码
__d(function(g,r,i,a,m,e,d){m.exports=function(t){if(t&&t.__esModule)return t;var o={};if(null!=t)for(var n in t)if(Object.prototype.hasOwnProperty.call(t,n)){var c=Object.defineProperty&&Object.getOwnPropertyDescriptor?Object.getOwnPropertyDescriptor(t,n):{};c.get||c.set?Object.defineProperty(o,n,c):o[n]=t[n]}return o.default=t,o}},329,[]);
__d(function(e,s,t,a,n,N,d){n.exports={name:"RNTest",displayName:"RNTest"}},330,[]);
__r(79);
__r(0);
以最基础的RN项目的 bundle 为例,可以看到 bundle 文件中大致定义了四个模块:
(1)var 声明的变量,对当前运行环境的定义,bundle 的启动时间、Process进程环境相关信息
(2)(function() { })() 闭包中定义的代码块,其中定义了对 define(__d)、 require(__r)、clear(__c) 的支持,以及 module(react-native及第三方dependences依赖的module) 的加载逻辑
(3)__d 定义的代码块,包括RN框架源码 js 部分、自定义js代码部分、图片资源信息,供 require 引入使用
(4)__r 定义的代码块,找到 __d 定义的代码块 并执行
最终归纳出以下结构
polyfills : 预加载,最早执行的一些function,声明es语法新增的接口,定义模块声明方法等
module difinitations : 模块声明,以__d开头,一般为每一个js文件或资源文件,将其封装成一个module对象,并进行标号
require calls : bundle文件尾部指定入口文件,如如require(79),最后一行require(0);
ps:79可以找到是InitializeCore,这个加载了js-c+±java三层的通信注册类,通信临听类等
三.拆包方案
其他方案对比
- moles-packer
简介:携程大厂推出,稳定可靠,针对react native0.44时代的版本
优点:重写了react native自带的打包工具,重写就是为了分包,为分包而生的项目,肯定可靠
缺点:不持续维护更新,只适合rn老版本用户了,0.5以上的rn版本全部扑街
- 自己修改打包代码
简介:现在很多教程都是让你去修改打包的源码,在里面判断分包,58的0.44版本就是这个方案
优点:如果很懂打包源码,这个做法灵活,定制化强,100%没问题
缺点:上手难,需要完全理解打包源码,网上的教程比较古老
- diff patch
简介:大致的做法就是先打个正常的完整的jsbundle,然后再打个只包含了基础引用(react和第三方module)的基础包,比对一下patch,得出业务包,这样基础包和业务包都有了
优点:简单暴力,如果只是想简单做下分包的可以尝试下
缺点:1、不利于维护,由于module后面都是rn生成数字,依赖变了数字也变,导致基础包变了所有包都需要变2、图片没法分包,有的第三方库是有图片的,这个方法只处理jsbundle不处理图片
Metro
在执行 react-native bundle | unbundle 命令时,RN框架背后其实是依赖了 Metro-Bundler 来完成打包、加载任务。Metro 作为一个独立的打包工具,官方文档 对于它的定义如下:
The JavaScript bundler for React Native.
Fast:Metro aims for sub-second reload cycles, fast startup and quick bundling speeds.
快:Metro旨在实现亚秒级重载循环,快速启动和快速捆绑速度。
Scalable:Works with thousands of modules in a single application.
可扩展:在单个应用程序中使用数千个模块。
Integrated:Supports every React Native project out of the box.
集成:支持开箱即用的每个React Native项目。
Metro 的高度可扩展性,为我们提供了*配置的打包方式。我们可以根据实际的需要来控制打包过程中的一些需求。官方为我们提供了很多种可配置的方式,可以使用以下三种方式创建Metro配置(按优先级排序):
metro.config.js
metro.config.json
package.json中的 metro 字段
还可以通过在调用 CLI 时指定 --config <path / to / config> 来为配置提供自定义文件。
Metro中的常见配置结构如下所示:
module.exports = {
resolver: {
/* resolver options */
},
transformer: {
/* transformer options */
},
serializer: {
/* serializer options */
},
server: {
/* server options */
}
/* general options */
};
在打包过程中,Metro-Bundler 帮助我们完成了全部工作,解析加载的过程如下:
项目中,入口点文件(如 index.js)利用 import 依赖了其他组件。即组件间都是相互依赖的。
Resolution 代表 解析 的过程,负责梳理关联js文件间的相互依赖关系。
Transformation 代表 转换 的过程,负责将模块文件转换成平台可理解的格式。
Serialization 代表 序列化 的过程,负责在完成转换过程并将模块转换为可访问的格式后,将其序列化。序列化程序将模块组合在一起以生成一个或多个包。捆绑包实际上是一组模块,组合成一个JavaScript文件。
更多关于配置的详细信息可以查看(和谐*):
(2)Role of Metro Bundler in React native
核心修改项
拆包的核心思想就是将基础包和业务包拆分。那么我们只需要使用如下两个配置项即可:
createModuleIdFactory
用于生成 require 语句的模块ID,配置 createModuleIdFactory 让其每次打包的 module 使用固定的id(路径相关)。
参数是要打包的 module 文件的绝对路径,返回的是打包后的 module 的 id
processModuleFilter
起到过滤功能,用于从输出中丢弃特定模块。配置 processModuleFilter 过滤基础包,打出对应业务包。
参数是 Module 信息,返回值是 boolean 类型 ,如果是 false 就过滤掉不进行打包
Metro Config 配置文件
在打包过程中,我们需要依赖 createModuleIdFactory 、processModuleFilter 来帮助我们将JSBundle拆分为基础包和业务模块包。拆分的过程就需要我们通过配置 config 文件来完成。接下来我们来看看如何编写 config 配置文件。
在编写 config 配置文件之前,先来想个问题,为什么要固定基础包中的模块ID( __r(id) )呢?
在上面我们贴出的bundle文件中,可以看到最底部有两段代码:
__r(79);
__r(0);
不同文件打出的 bundle,最底部都为__r(0); 而上面的会随着顺序依次增加,例如以 index.js 文件打出的 bundle id 为 79,以 CustomComponent.js 打出的为 80。
基础包(common.bundle)
在打基础包的时候,我们会把RN的基础文件以及第三方的依赖打进去。当我们在打业务包的时候,可能会做修改,例如导入组件的顺序发生变化,或者依赖版本做了更新等等。都有可能导致ID发生变化,造成基础包中不能找到对应的模块ID,导致基础包失效。所以需要将ID固定。一种简单的方式就是以模块名称作为 require 即可。所以配置 createModuleIdFactory 让其每次打包的 module 使用固定的模块名称即可。
业务包 (bussiness.bundle)
在打业务包时,需要结合 createModuleIdFactory、processModuleFilter 同时进行。createModuleIdFactory负责固定 module 的ID。processModuleFilter 负责过滤掉基础包的内容模块。
createModuleIdFactory 源代码
//node_modules/metro/src/lib/createModuleIdFactory.js
"use strict";
function createModuleIdFactory() {
const fileToIdMap = new Map();
let nextId = 0;
return path => {
let id = fileToIdMap.get(path);
if (typeof id !== "number") {
id = nextId++;
fileToIdMap.set(path, id);
}
return id;
};
}
module.exports = createModuleIdFactory;
我们知道,createModuleIdFactory 用于生成 require 语句的模块ID,从上述源码也可以看出,系统使用整数型的方式,从0开始遍历所有模块,并依次使 Id 增加 1。所以我们可以修改此处逻辑,以模块路径名称的方式作为Id即可。
参考文档
https://blog.csdn.net/u013718120/article/details/84571326