原生嵌RN面板Metro拆包
概览
背景:
在 React Native 中,我们打包生成的包只有一个jsbundle,里面包含了我们的业务代码、RN 源码及依赖的第三方库。通常为了更好的性能,我们需要将这个jsbundle文件进行拆分,得到一个基础包和多个业务包。
问题:尽管拆包可以带来诸多好处,如减少页面首次加载时间,降低内存资源消耗,减少更新内容包的大小等,但如何进行有效的拆包呢?
策略:
我们采用基于 Metro 进行拆包的方法,Metro 是 React Native 官方提供的打包工具,我们基于 Metro 二次开发,实现了 jsbundle 拆分为一个基础包和多个业务包。拆包步骤如下:
● Metro提供了两个配置项createModuleIdFactory和processModuleFilter,前者用于生成require语句的模块ID,后者用于过滤掉一些特定的模块。
● 公司基于这两个配置项进行了拆包的实现,首先配置createModuleIdFactory让它每次打包生成的module都使用固定的id,然后配置processModuleFilter过滤基础包,打出对应业务包。
● 为了避免基础包内的第三方库重复打入,公司在生成基础包时,把所有依赖的模块name放到一个数组并写入到一个本地文件中,这个文件保存了基础包中的依赖信息。在打业务包时,读取这个文件的内容,就可以识别基础包已存在的依赖库,不再重复打入。
● 在打包过程中,公司将基础包中包含的RN源码、第三方依赖库、内部公共组件等,通过import方式引入,然后使用react-native的bundle命令执行打包。
● 在加载过程中,公司让APP在启动时先加载基础包,然后再按需加载业务包。同时,公司在iOS和Android上分别实现了基础包和业务包的加载方式。
效果:
通过这种方式,我们可以在 APP 启动时提前加载基础包,在需要进入 RN 页面时,再动态加载该页面所在的业务模块文件,实现按需加载。在热更新时只更新有变化的业务包,再配合 bsdiff 差分算法,大大减少更新内容包的大小。拆包后能更好地支持动态下发业务包,动态加载,从而让我们更灵活地部署、上线。
拆包方案简介
在 React Native 中,我们打包生成的包只有一个jsbundle,里面包含了我们的业务代码、RN 源码及依赖的第三方库,通常为了更好的性能,我们会拆分这个jsbundle文件,得到一个基础包和多个业务包。
基础包:将重复的React Native代码与第三方依赖库打包成一个文件。
业务包:按照应用内的不同业务单元,拆分出一个或多个包。
拆包后,让基础包在 APP 启动时提前加载到内存中,在需要进入 RN 页面时,再动态加载该页面所在的业务模块文件,按需加载。
拆包给我们带来了很多好处,如下:
● 提前加载 js 框架,这样在进入RN页面时,只需要加载业务js代码,从而减少RN页面首次加载时间;
● 打开哪个页面加载哪个业务包,避免一次性加载全部js代码,降低内存资源消耗;
● 在热更新时只更新有变化的业务包,再配合 bsdiff 差分算法,大大减少更新内容包的大小;
● 拆包后能更好地支持动态下发业务包,动态加载,从而让我们更灵活地部署、上线。
现有的几种拆包方案:
1,diff patch
首先生成基础包,只引用RN源码和第三方依赖库,然后现生成完成的jsbundle,通过diff比对基础包和完整的jsbundle,得出业务包。
优点:简单
缺点:只能拆分包,对性能没有提升,反而增加了合包带来的时间消耗
2,CRN
携程最近开源的拆包方案,包含了拆包、框架代码预加载、两端一套产物、懒require等。
优点:性能好,两端一套产物
缺点:成本高,对RN源码、打包工具改动较大,难升级、难维护
3,Metro
官方出的打包工具,从 0.57 开始,已经支持拆包了。
优点:稳定可靠,无需改动RN源码
缺点:性能没有CRN好
我们的业务规模还不大,哪个方案下页面加载速度和内存问题都不会很严重,出于成本和稳定性考虑,最终选择了 Metro 方案。
下面介绍如何基于 Metro 进行拆包的原理和实现过程。
拆包
Metro 是 React Native 官方提供的打包工具,它将我们的业务代码及依赖的第三方库打包生成一个jsbundle文件。我们基于 Metro 二次开发,实现了 jsbundle 拆分为一个基础包和多个业务包。
其中有两个配置项(更改 metro.config.js 文件):
createModuleIdFactory:用于生成 require 语句的模块ID,配置 createModuleIdFactory 让它每次打包生成的 module 都使用固定的id。它的返回值是一个函数,参数 path 是各个 module 的绝对路径,返回的是打包后的 module 的 id。
image.pngprocessModuleFilter:按照给定的规则,过滤掉一些特定的 module,配置processModuleFilter 过滤基础包,打出对应业务包。它返回一个 boolean 类型,输入参数为 module 信息,如果返回 false,就过滤掉,不打入 bundle。
image.png打包
通常基础包中包含RN源码、第三方依赖库、内部公共组件等,通过 import 方式引入进来,common.js代码如下:
image.png基础包打包命令:
"commonBundle": "react-native bundle --platform android --dev false --entry-file shell/common.js --bundle-output Desktop/index.bundle --assets-dest Desktop/android --config common.config.js"
业务包打包命令:
"bussinessBundle": "react-native bundle --platform android --dev false --entry-file shell/business.js --bundle-output Desktop/bussinessBundle.bundle --assets-dest Desktop/android --config business.config.js"
image.png打包效果图:
基础包
image.png业务包
image.png基础包、业务包加载
iOS
基础包预加载
APP 启动时,先加载基础包,不展示视图。
//直接使用基础包初始化js框架 �NSURL *jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"common.ios" withExtension:@"jsbundle"];
self.bridge= [[RCTBridge alloc] initWithBundleURL:jsCodeLocation moduleProvider:nil launchOptions:launchOptions];
业务包加载
暴露RCTBridge的executeSourceCode方法
NSURL *jsCodeLocationBuz = [[NSBundle mainBundle] URLForResource:bundleName withExtension:@"jsbundle"];
NSError *error = nil;�NSData *sourceBus = [NSData dataWithContentsOfFile:jsCodeLocationBuz.path options:NSDataReadingMappedIfSafe error:&error];�[bridge.batchedBridge executeSourceCode:sourceBus sync:NO];
RCTRootView* view = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:nil]; //bridge和module传入
Android
基础包预加载以及HomePage业务包预加载
private void preLoadBundle(){
ReactInstanceManager reactInstanceManager = getReactInstanceManager();
//这里会先加载基础包index.bundle
if (reactInstanceManager != null && !reactInstanceManager.hasStartedCreatingInitialContext()) {
getReactInstanceManager().addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
@Override
public void onReactContextInitialized(ReactContext context) {
//加载完成预加载HomePage.bundle
ScriptLoadUtil.loadScript(getReactInstanceManager(), BridgeUtil.getScriptPathType("Me"), BridgeUtil.getScriptPath("Me"));
if (getReactInstanceManager() != null) {
getReactInstanceManager().removeReactInstanceEventListener(this);
}
}
});
reactInstanceManager.createReactContextInBackground();
}
}
业务包加载
通过传入业务包的类型和路径加载
public static void loadScript(ReactInstanceManager instanceManager, RNUpdateConfig.ScriptType pathType, String scriptPath){
// 当设置成debug模式时,所有需要的业务代码已经都加载好了
if (DevKitConfig.DEBUG && ReactUtil.isFromServer(instanceManager)){
return;
}
if (instanceManager != null && instanceManager.getCurrentReactContext() != null){
CatalystInstance instance = instanceManager.getCurrentReactContext().getCatalystInstance();
if(pathType== RNUpdateConfig.ScriptType.ASSET) {
ScriptLoadUtil.loadScriptFromAsset(WYCoreUtils.getApp(), instance, scriptPath,false);
}else {
File scriptFile = new File(scriptPath);
scriptPath = scriptFile.getAbsolutePath();
ScriptLoadUtil.loadScriptFromFile(scriptPath, instance, scriptPath,false);
}
}
}
业务包类型分为ScriptType.ASSET和ScriptType.FILE,通过当前手机是否存在比内置包更高版本的RN包进行判断。
业务包路径则通过页面传输字段PageName来判断加载哪个业务包,如:HomePage/Me,则就是加载HomePage.bundle
int index = pageName.indexOf("/");
if (index != -1){
return pageName.substring(0, index)+".bundle";
}
网友评论