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

有69人对 “React Native实现多业务热部署”留言了

  1. Does your site have a contact page? I’m having
    a tough time locating it but, I’d like to send you an e-mail.

    I’ve got some creative ideas for your blog you might be interested in hearing.
    Either way, great website and I look forward to seeing it develop over time.

  2. Thanks for sharing your thoughts. I really appreciate your efforts and
    I will be waiting for your next post thanks once again.

  3. Pretty section of content. I just stumbled upon your
    site and in accession capital to assert that I acquire in fact enjoyed account your blog posts.
    Any way I will be subscribing to your feeds and even I
    achievement you access consistently quickly.

  4. Most notably, in Pennsylvania, you cannot wager actual dollars on events like the NFL Draft or the Academy Awards.

  5. I am not sure where you’re getting your information, but good topic.
    I needs to spend some time learning much more or understanding more.
    Thanks for magnificent info I was looking for this information for my mission.

  6. Hey! I could have sworn I’ve been to this website before but after checking through some of the post
    I realized it’s new to me. Nonetheless, I’m definitely happy I found
    it and I’ll be book-marking and checking back often!

  7. If some one wishes to be updated with most recent technologies then he
    must be visit this web page and be up to date everyday.

  8. Hi there everyone, it’s my first go to see at this website, and
    piece of writing is truly fruitful designed for me, keep up posting these types of posts.

  9. I am extremely inspired together with your writing
    talents and also with the layout for your blog. Is that this a paid topic or did you
    customize it your self? Either way keep up the nice high quality writing,
    it is rare to look a nice blog like this one today..

  10. Howdy, i read your blog from time to time and i own a
    similar one and i was just curious if you get a lot of spam comments?
    If so how do you protect against it, any plugin or anything you
    can suggest? I get so much lately it’s driving me crazy so any support is very much appreciated.

  11. Good day! I could have sworn I’ve been to this blog before but after browsing through some of the post I realized it’s
    new to me. Nonetheless, I’m definitely glad I found it and I’ll
    be bookmarking and checking back frequently!

  12. An outstanding share! I’ve just forwarded this onto a
    colleague who has been conducting a little homework on this.
    And he actually ordered me breakfast simply because I found it for him…
    lol. So allow me to reword this…. Thanks for
    the meal!! But yeah, thanx for spending some time to discuss this issue here on your
    blog.

  13. Its like you read my thoughts! You appear to know a lot about this,
    like you wrote the e book in it or something. I believe that you simply
    could do with some percent to power the message home a bit, however other than that, this is excellent blog.
    A fantastic read. I will certainly be back.

  14. If you wish for to increase your know-how only keep
    visiting this website and be updated with the most recent news
    update posted here.

  15. Hey! I realize this is kind of off-topic but I had to ask.
    Does running a well-established blog such as yours require a massive amount
    work? I’m brand new to blogging however I do write in my
    diary on a daily basis. I’d like to start a blog so I will be able to share my own experience and thoughts online.
    Please let me know if you have any ideas or tips for brand
    new aspiring blog owners. Appreciate it!

发表评论

邮箱地址不会被公开。