React native拆包之 原生加载多bundle(iOS&Android)

在这之前已经描述了0.57版本的react native如何拆包:https://blog.csdn.net/tyro_smallnew/article/details/83088216(React native拆包的正确姿势),对于原生如何加载多个js包的方法只是简单的描述下,现在开始补坑。

0、rn之jsbundle

    当然我们都知道jsbundle是什么(就是我们打包出来的js包,~~等于没说),直接look look

名字长这样:React native拆包之 原生加载多bundle(iOS&Android)_第1张图片

其实这里面是个文本,打开长这样:

React native拆包之 原生加载多bundle(iOS&Android)_第2张图片

WHAT?这都是些什么?

第一行是对当前运行环境的定义,是不是开发环境?启动的时间、还有进程环境

第二行是对require、define的支持,还有我们module加载的逻辑也是在第二行实现的

当然还有第三行第四行。。。。。

可以打测试包来展现这些代码原来的样子:(--dev指定为false就不会混淆了)

react-native bundle --platform android --dev false --entry-file index.js --bundle-output ./android/app/src/main/assets/index.android.bundle

var __DEV__=false,__BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now(),process=this.process||{};process.env=process.env||{};process.env.NODE_ENV="production";
(function (global) {
  "use strict";

  global.__r = metroRequire;
  global.__d = define;
  global.__c = clear;
  var modules = clear();
  var EMPTY = {};
  var _ref = {},
      hasOwnProperty = _ref.hasOwnProperty;

  function clear() {
    modules = typeof __NUM_MODULES__ === "number" ? Array(__NUM_MODULES__ | 0) : Object.create(null);
    return modules;
  }

  function define(factory, moduleId, dependencyMap) {
    if (modules[moduleId] != null) {
      return;
    }

    modules[moduleId] = {
      dependencyMap: dependencyMap,
      factory: factory,
      hasError: false,
      importedAll: EMPTY,
      importedDefault: EMPTY,
      isInitialized: false,
      publicModule: {
        exports: {}
      }
    };
  }

  function metroRequire(moduleId) {
    var moduleIdReallyIsNumber = moduleId;
    var module = modules[moduleIdReallyIsNumber];
    return module && module.isInitialized ? module.publicModule.exports : guardedLoadModule(moduleIdReallyIsNumber, module);
  }
 。。。。。。
  function unknownModuleError(id) {
    var message = 'Requiring unknown module "' + id + '".';
    return Error(message);
  }
  function moduleThrewError(id, error) {
    var displayName = id;
    return Error('Requiring module "' + displayName + '", which threw an exception: ' + error);
  }
})(typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this);

这里截取的只是第一行和第二行未混淆的代码,第二行是定义了一个函数并立即执行,把window或者this作为参数global,其实就是大家经常用的全局变量global。函数中定义了__r __d __c函数,分别对应require define clear。require大家都经常用,就是引入一个module并执行里面的代码;define开发中很少用,是定义一个模块,让require能找到该模块;clear就是清除所有定义的模块了。

其他的代码就不一一描述了,依次都是:

1、运行环境定义

2、js模块管理支持

3、ES6特性支持:比如一些Object类型的api

4、声明模块(就是__d开头的那些)

5、代码执行(__r开头)

如下:

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=functio///module系统支持
。。。。。。
这中间有一些polyfill,中文含义为涂料的意思,我们现在js运行环境不支持一些es6的新特性,
就像一堵没粉刷的墙,这些涂料粉刷到这个墙上面,让整个墙更好看优雅,
以支持我们在代码中使用es6特性
。。。。。。
__d(function(g,r,i,a,m,e,d){var t=r(d[0]),n=r(d[1]),u=t(r(d[2]));n.AppRegistry.registerComponent("reactnative_multibundler2",function(){return u.default})},"index2",["node_modules_@babel_runtime_helpers_interopRequireDefault","react-native-implementation","App2"]);//这个是定义的module
。。。。。
当然还有各式各样的module,其中包括了react native的module,
比如(Text、View、Image等等),也包括引入的第三方module
。。。。。
__r("InitializeCore");//执行初始化代码,里面包括了setTimeOut、fetch、WebSocket等一些基本函数的定义
__r("index2");//这里就是执行我们自己的代码了

1、js和原生视图的联系

     不用分包的情况下,使用rn官方的加载js 原生代码需要定义什么???

第一、定义jsbundle的名字或者位置(getJSMainModuleName)

第二、定义组件名(getMainComponentName)

js端需要做的是:注册组件:   AppRegistry.registerComponent

大致的流程是:js端先运行js代码注册组件---->原生端找到这个组件并关联

引用上面的代码:__r("index2")这里就包含了AppRegistry.registerComponent,这行代码执行完就就完成了组件的注册

而在原生端,android为例,see see ReactRootView里面的startReactApplication函数:

  public void startReactApplication(
      ReactInstanceManager reactInstanceManager,
      String moduleName,
      @Nullable Bundle initialProperties) {
    Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "startReactApplication");
    try {
      UiThreadUtil.assertOnUiThread();
      mReactInstanceManager = reactInstanceManager;
      mJSModuleName = moduleName;//这里将组件名作为成员变量
      mAppProperties = initialProperties;
      if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
        mReactInstanceManager.createReactContextInBackground();//先运行js代码
      }
      attachToReactInstanceManager();//绑定组件和视图
    } finally {
      Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
    }
  }

而iOS则简单很多:

  RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL
                                            moduleProvider:nil
                                             launchOptions:launchOptions];//执行js代码
  return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];//绑定视图

2、分而治之

  加载多包的可行主要还要归功于reac native的加载js代码逻辑、绑定视图的逻辑都可以分开。

代码中可以先加载基础包而不绑定视图,在执行业务包中的registerComponent后再去绑定视图。

把这些流程分成三步:1、实现加载基础包达到懒加载效果 2、执行业务js代码 3、绑定视图

1、基础包加载

  这一步我们先加载基础包而不展示视图,可以看看分包后的基础包,是没有registerComponent代码的,加载基础包是为了把项目中公有的函数、模块、库、rn代码先执行,后面加载业务代码的时候就可以秒开。而且加载基础包之前还需要初始化javascript的运行环境,不过这些在rn上很简单,一步到位。

 android的做法:1、重写ReactApplication返回基础包的位置 2、执行createReactContextInBackground

public class MainApplication extends Application implements ReactApplication {
 @Override
    protected String getBundleAssetName() {
      return "platform.android.bundle";
    }
。。。
}

//在需要加载基础包的时候执行createReactContextInBackground
ReactInstanceManager reactInstanceManager = ((ReactApplication)getApplication()).getReactNativeHost().getReactInstanceManager();
        reactInstanceManager.createReactContextInBackground();//这里会先加载基础包platform.android.bundle

createReactContextInBackground会帮我们初始化react native的运行环境,
创建js线程,注册原生模块,
最重要的是:
这一步会获取ReactApplication中返回的jsbundle路径并执行其中的代码,
这样我们的js运行环境创建完了,基础包的代码也执行了

 iOS更简单了:

   NSURL *jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"platform.ios" withExtension:@"bundle"];
  bridge = [[RCTBridge alloc] initWithBundleURL:jsCodeLocation
                                 moduleProvider:nil
                                  launchOptions:launchOptions];
//直接使用基础包初始化js桥

现在基础包加载完成

2、业务包加载

  通过上一步的基础包加载,js桥环境也初始化完成了,而react native官方支持加载一次js代码,跟js桥的初始化绑定写死了,不过我们可以用反射(iOS不是用反射,用扩展)来暴露js桥执行js代码的方法。

  android的做法:

首先得找出哪个方法直接可以执行js代码,通过查阅发现com.facebook.react.bridge.CatalystInstanceImpl#loadScriptFromAssets这个函数可以。

而这个方法是package范围可访问的,那索性不用反射了,直接跟你一个package暴露你的接口:

package com.facebook.react.bridge;//这里直接就和CatalystInstanceImpl一个包下的
import android.content.Context;
public class BridgeUtil {
    public static void loadScriptFromAsset(Context context,
                                    CatalystInstance instance,
                                    String assetName,boolean loadSynchronously) {
        String source = assetName;
        if(!assetName.startsWith("assets://")) {
            source = "assets://" + assetName;
        }
        ((CatalystInstanceImpl)instance).loadScriptFromAssets(context.getAssets(), source,loadSynchronously);//不用反射,美滋滋
    }
    public static void loadScriptFromFile(String fileName,
                                    CatalystInstance instance,
                                    String sourceUrl,boolean loadSynchronously) {
        ((CatalystInstanceImpl)instance).loadScriptFromFile(fileName, sourceUrl,loadSynchronously);
    }
}

这样的做法有几点好处:1、减少反射代码2、如果当前的rn版本不支持这个方法可以立马发现3、不怕混淆

iOS的做法:

暴露RCTBridge的executeSourceCode方法

#import 
@interface RCTBridge (RnLoadJS) // RN私有类 ,这里暴露他的接口

- (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync;

@end

在需要用的地方import这个头文件 #import "RCTBridge.h"

最后就是加载业务包了

android:

ScriptLoadUtil.loadScriptFromAsset(getApplicationContext(),instance,scriptPath,false);

iOS

NSURL *jsCodeLocationBuz = [[NSBundle mainBundle] URLForResource:bundleName withExtension:@"bundle"];
      NSError *error = nil;
      NSData *sourceBuz = [NSData dataWithContentsOfFile:jsCodeLocationBuz.path
                                             options:NSDataReadingMappedIfSafe
                                               error:&error];
      [bridge.batchedBridge executeSourceCode:sourceBuz sync:NO];

3、绑定视图

   经过基础包、业务包的加载,现在js环境已经把component注册在js环境中了(AppRegistry.registerComponent),当然我们可以在一个业务包中注册许多component,也就是一个业务对应多个RCTRootView,所以一个js环境中是可以对应多个RCTRootView。

  绑定视图之android

android的绑定视图方法为:

    mReactRootView.startReactApplication(
      getReactNativeHost().getReactInstanceManager(),
      appKey,//这里的appkey就是组件名
      getLaunchOptions());

而在react-native-multibundler开源代码中不是这么做,里面定义了AsyncReactActivity保证能继承所有ReactActivity的生命周期和逻辑。如果不用activity而是直接生成自己的rootview时,需要注意生命周期的维护。

绑定视图之iOS则很简单:

RCTRootView* view = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:nil];//bridge和module传入

 

3、总结

    整个的加载分包流程还是要归功于javascript的灵活性,可以随时注入新的js代码。react native能把js加载和视图绑定设计得非常巧妙,而且提供的接口刚好也适合分包。整个拆包和拆包后的加载并不一定完全是这种流程,看你自己需要什么。有的人直接把基础包和业务包直接合并成一个文件后再加载、有的人使用unbundle来加快加载速度、有的人把所有业务加载到一个jsbridge对一个多个view,还可能有unbundle和拆包相结合的!所以 只要思想不滑坡,方法总比困难多。

开源地址: 

https://github.com/smallnew/react-native-multibundler

你可能感兴趣的:(react,naitve)