React Native实现多业务热部署
刚好最近在研究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