在这之前已经描述了0.57版本的react native如何拆包:https://blog.csdn.net/tyro_smallnew/article/details/83088216(React native拆包的正确姿势),对于原生如何加载多个js包的方法只是简单的描述下,现在开始补坑。
当然我们都知道jsbundle是什么(就是我们打包出来的js包,~~等于没说),直接look look
其实这里面是个文本,打开长这样:
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");//这里就是执行我们自己的代码了
不用分包的情况下,使用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];//绑定视图
加载多包的可行主要还要归功于reac native的加载js代码逻辑、绑定视图的逻辑都可以分开。
代码中可以先加载基础包而不绑定视图,在执行业务包中的registerComponent后再去绑定视图。
把这些流程分成三步:1、实现加载基础包达到懒加载效果 2、执行业务js代码 3、绑定视图
这一步我们先加载基础包而不展示视图,可以看看分包后的基础包,是没有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桥
现在基础包加载完成
通过上一步的基础包加载,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];
经过基础包、业务包的加载,现在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传入
整个的加载分包流程还是要归功于javascript的灵活性,可以随时注入新的js代码。react native能把js加载和视图绑定设计得非常巧妙,而且提供的接口刚好也适合分包。整个拆包和拆包后的加载并不一定完全是这种流程,看你自己需要什么。有的人直接把基础包和业务包直接合并成一个文件后再加载、有的人使用unbundle来加快加载速度、有的人把所有业务加载到一个jsbridge对一个多个view,还可能有unbundle和拆包相结合的!所以 只要思想不滑坡,方法总比困难多。
开源地址:
https://github.com/smallnew/react-native-multibundler