刚好最近在研究APP接入第三方业务功能的需求,本文总结一下Android平台如何使用React Native实现多业务接入并能满足各个子业务独立更新维护的要求。
一、场景分析
比如我有一个APP(暂叫主APP),现在需要接入第三方的业务(业务A、业务B、业务C等…)。但是主APP跟第三方业务是完全独立的,我们不希望主APP中嵌入大量的第三方业务代码(当然第三方也不会愿意暴露代码给主APP~),也不希望子业务更新时太依赖主APP。那怎么办呢?现在主流的做法一般是使用H5页面跳转,但是这种体验较差。除了H5之外我们想到过的能解决问题并且体验还不错的方案就是React Native、Weex、LuaView、Cordova等框架。子业务只需要把自身热更新相关的代码和需要使用Native实现的代码打包成一个很小的SDK(aar)移交给主程序集成,剩下的业务页面展示功能全部通过在线更新加载来实现。React Native、Weex、LuaView、Cordova等框架更新资源包的原理大同小异,大概分如下几步:
- 携带本地资源包的版本号等信息调用接口
- 后台根据前端传过来的版本信息告诉前端是否有升级包
- 有升级包则下载升级包保存到SD卡上(增量升级包或全量升级包下载)
- 下次启动APP时读取新的资源包
后面有时间再记录下详细的热更新实现方案,这里主要记录下载多个资源包的问题。前面也说了,我们要实现接入多个业务的需求,这就存在一个问题:APP中存在多个业务入口,如何实现进入不同业务模块时下载并加载不同的业务数据呢?比如:进入业务模块A时更新业务A的资源包并且加载A的数据,进入业务模块B时更新业务B的资源包并加载B的数据。
React Native是使用getJSBundleFile方法来指定加载JSBundle路径的,但是0.29版本开始发生了变化。0.29之前getJSBundleFile方法在ReactActivity类中,这个时候要实现上面的需求比较简单,只需要在各个业务的主Activity中集成ReactActivity并重写getJSBundleFile方法即可。但是0.29及以后的版本中ReactActivity中移除了getJSBundleFile方法,该方法在ReactNativeHost中,并且是在ReactApplication中初始化,也就是说一个应用只能初始化一个JSBundle路径,那怎么办呢?
二、解决思路
我们先来看看React Native加载Bundle的大概流程:
1.ReactActivity类
加载页面是从自定义的Activity开始,该Activity需要继承ReactActivity,里面有2个很重要的方法getMainComponentName和createReactActivityDelegate。
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
* e.g. "MoviesApp"
*/
protected @Nullable String getMainComponentName() {
return null;
}
/**
* Called at construction time, override if you have a custom delegate implementation.
*/
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName());
}
getMainComponentName是用来指定加载的React Native组件的名称,需要在自定义Activity中重写改方法,指定组件名称,该名称需要和index.js中注册的组件名称一致。
import { AppRegistry } from 'react-native';
import App from './App';
AppRegistry.registerComponent('组件名称', () => App);
2.ReactActivityDelegate类
createReactActivityDelegate方法是用来创建ReactActivityDelegate对象,ReactActivityDelegate类中有一个获取前面提到过的ReactApplication中的ReactNativeHost对象。
/**
* Get the {@link ReactNativeHost} used by this app. By default, assumes
* {@link Activity#getApplication()} is an instance of {@link ReactApplication} and calls
* {@link ReactApplication#getReactNativeHost()}. Override this method if your application class
* does not implement {@code ReactApplication} or you simply have a different mechanism for
* storing a {@code ReactNativeHost}, e.g. as a static field somewhere.
*/
protected ReactNativeHost getReactNativeHost() {
return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}
从代码可以看到,app中使用的ReactNativeHost对象只有一份,也就是说默认情况下载Application中就已经定义好了JSBundle的加载路径,而且一次只能设置一个路径,更别说要实现不同子业务加载不同的JSBundle了。那我要实现不同子业务加载不同的JSBundle怎么办呢?我们可以自定义ReactActivityDelegate类,并且在自定义的Activity中重写父类中的createReactActivityDelegate方法,使用我们自定义的ReactActivityDelegate类。自定义ReactActivityDelegate有两种方法,一种方法是直接继承ReactActivityDelegate类,另一种方法是仿照ReactActivityDelegate类重写一个,但是后面那种方法个人觉得把整个类移植可能存在不可预测的风险。那就只能继承ReactActivityDelegate写一个了,那问题有来了,大家发现getReactNativeHost方法是protected类型的,直接继承的话不能重写该方法,我们可以变通一下,java中在相同的包名下其他类是可以访问protected方法的,所以我们可以在建一个和ReactActivityDelegate一样的包名,把自定义的类写在该包名下即可。
三、代码实现
1.环境
node.js:6.11.1
npm:6.1.0
react-native-cli:2.0.1
react-native:0.55.4
2.主APP
主APP主要是用来模拟实现个子业务入口,集成React Native基础框架和个子业务提供的SDK(aar)。
1) build.gradle
配置React Native环境,引入第三方业务的SDK
dependencies {
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:23.0.1"
compile "com.facebook.react:react-native:+" // From node_modules
compile(name:'modelone',ext:'aar')
compile(name:'modeltwo',ext:'aar')
compile(name:'modelthree',ext:'aar')
}
2)MainApplication类
该类集成ReactApplication,主要用来初始化so动态库以及定义ReactNativeHost对象,指定主JSBundle文件路径。
package com.luoxudong.app.rndynamicload;
import android.app.Application;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
@Override
protected String getJSMainModuleName() {
return "index";
}
@Nullable
@Override
protected String getJSBundleFile() {
return super.getJSBundleFile();
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
3.子业务SDK
我业务集成到主APP时,需要提供提供一个SDK,该SDK的作用是提供业务主入口供主APP调用,实现自身业务JSBundle等资源的升级更新以及封装Native端的代码(如:敏感数据处理,自定义插件等)。
1) 自定义ReactActivityDelegate类
该类是实现多业务热更新接入的关键,集成ReactActivityDelegate并重写getReactNativeHost方法。以其中一个子业务为例:
package com.facebook.react;
import android.app.Activity;
import android.support.v4.app.FragmentActivity;
import com.facebook.react.shell.MainReactPackage;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
/**
* Created by luoxudong on 2018/7/2.
*/
public class MOReactActivityDelegate extends ReactActivityDelegate {
private Activity mActivity = null;
public MOReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
super(activity, mainComponentName);
mActivity = activity;
}
public MOReactActivityDelegate(FragmentActivity fragmentActivity, @Nullable String mainComponentName) {
super(fragmentActivity, mainComponentName);
mActivity = fragmentActivity;
}
@Override
protected ReactNativeHost getReactNativeHost() {
return new ReactNativeHost(mActivity.getApplication()) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
@Override
protected String getJSMainModuleName() {
return "index";
}
@Nullable
@Override
protected String getJSBundleFile() {
/**
* 为了简单,这个路径先写死
* 实际开发中这个地址一般是不写死的,路径可能会变化,比如不同版本的bundle资源放在不同的目录中。
* 由于是写死在SD卡路径,记得6.0+系统要授权访问。
*/
String jsBundeFile = "/sdcard/rn/modelone/index.android.bundle";
return jsBundeFile;
}
};
}
}
在getJSBundleFile方法中指定加载JSBundle文件的路径,其中index.android.bundle是生成的bundle的js文件。实际开发过程中这里需要结合动态更新的流程来,这里简单处理先写死路径。要生成bundle文件,可以先在package.json中修改如下配置:
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "jest",
"android-bundle": "react-native bundle --platform android --dev false --entry-file index.js --bundle-output bundle/android/index.android.bundle --assets-dest bundle/android/assets",
"ios-bundle": "react-native bundle --platform ios --dev false --entry-file index.js --bundle-output bundle/ios/index.android.bundle --assets-dest bundle/ios/assets"
},
配置完成以后需要手动创建 bundle/android和bundle/ios目录,然后在控制台运行npm run android-bundle即可。
2)子业务主界面
ReactNativeActivity为业务的主界面,继承ReactActivity并且实现getMainComponentName方法,返回js中的组件名称,名称需要跟index.js中的组件名称一致。另外一个很重要的工作就是重写createReactActivityDelegate方法,使用自定义的MOReactActivityDelegate对象,这样程序进入该界面时就会加载在业务的资源,于主程序及其他业务完全独立。注意,实际开发啊过程中一般来说这个Activity不是子业务的入口,子业务的入口应该是实现热更新相关的业务逻辑,热更新检测处理完以后再进入该页面。
package com.luoxudong.app.modelone;
import com.facebook.react.MOReactActivityDelegate;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
/**
* Created by luoxudong on 2018/7/4.
*/
public class ReactNativeActivity extends ReactActivity {
@Override
protected String getMainComponentName() {
/**
* 这个组件名称需要跟js入口中的组件名称保持一致
*/
String componentName = "modelone";
return componentName;
}
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
/**
* 进入这个界面时会初始化加载bundle的路径。
* 这里的demo是实现把bundle文件放在了sd卡中,没有写动态更新升级相关业务代码,
* 实际项目开发过程中需要考虑更新机制,可能事前需要做一些更新资源文件等前置工作
*/
ReactActivityDelegate delegate = new MOReactActivityDelegate(this, getMainComponentName());
return delegate;
}
}
以上工作完成以后就可以打包成aar,提供给主程序集成。接下来就是使用React Native语法实现界面布局以及业务实现,然后通过bundle命令打包放在服务器上供各业务更新。这样整个流程基本上就完成了。
四、实现效果
以下是简单的实现效果:
五、其他
以上哪里写的不对或者有待改进,欢迎大家提意见,谢谢!
源码地址:https://github.com/rohsuton/RnDynamicLoad
转载请注明出处:http://www.luoxudong.com/?p=393