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等框架更新资源包的原理大同小异,大概分如下几步:

  1. 携带本地资源包的版本号等信息调用接口
  2. 后台根据前端传过来的版本信息告诉前端是否有升级包
  3. 有升级包则下载升级包保存到SD卡上(增量升级包或全量升级包下载)
  4. 下次启动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

发表评论

电子邮件地址不会被公开。