美文网首页
原生嵌RN面板Metro拆包

原生嵌RN面板Metro拆包

作者: 深圳王思聪 | 来源:发表于2024-06-30 10:49 被阅读0次

    原生嵌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.png

    processModuleFilter:按照给定的规则,过滤掉一些特定的 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";

    }

    相关文章

      网友评论

          本文标题:原生嵌RN面板Metro拆包

          本文链接:https://www.haomeiwen.com/subject/jdbkyltx.html