使用React Native 混合开发,原生页面跳转到不同的RN页面

一: 需求背景

目前公司项目全部采用原生开发,现在想提升开发效率,把部分Android,iOS两端公共高复用的界面使用RN去做。就需要在原有的项目基础上加入RN并且单独跳转到不同的RN界面去。网上查了一下相关资料发现大部分都是从RN界面跳转到原生的介绍。RN不好处理的地方,直接调用原生界面去制作。很少有介绍有从原生跳转到RN某个单页面的介绍。最近研究了一下最终做出的效果如下。

按钮的界面为原生,跳转的两个是不同的RN界面

使用React Native 混合开发,原生页面跳转到不同的RN页面_第1张图片

二:思路分析

按照官网的介绍在原有项目集成RN后,只有一个RNActivity去打开RN中的index.js的入口文件。个人感觉这个RNActivity就像原生开发中的webview一样。只是一个承载打开Index.js的容器。那我们思路也很明显了。要是能在打开Index.js 的时候给RN页面传入不同的值,那我们就可以根据传入值的不同去渲染不同的界面。总体思路是这样,下面我们来一步步的去完成。

三: 代码编写

1.现有项目集成RN

按照官方文档一步步的集成详情参考集成到现有原生应用环境相关配置不在此介绍。

  1. 首先创建一个空目录用于存放 React Native 项目,然后在其中创建一个/android子目录,把你现有的 Android 项目拷贝到/android子目录中。
  2. 在项目根目录下创建一个名为package.json的空文本文件,然后填入以下内容
{
  "name": "NativeWithRnTest",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start"
  }
}
  1. 使用 yarn 或 npm(两者都是 node 的包管理器)来安装 React 和 React Native 模块。请打开一个终端/命令提示行,进入到项目目录中(即包含有 package.json 文件的目录),然后运行下列命令来安装
$ yarn add react-native

这样默认会安装最新版本的 React Native,同时会打印出类似下面的警告信息,命令窗口上往上翻一下就能看到黄色的相关警告信息。这是正常现象,意味着我们还需要安装指定版本的 React

warning "[email protected]" has unmet peer dependency "[email protected]".
  1. 安装指定版本的 React
$ yarn add react@16.6.3  //这个版本号一定要根据你自己安装时候警告提醒的保持一致,不要直接复制上去了
  1. 在你的 app 中 build.gradle 文件中添加 React Native 依赖:
dependencies {
    implementation "com.facebook.react:react-native:+" // From node_modules
}
  1. 在项目的 build.gradle 文件中为 React Native 添加一个 maven 依赖的入口,必须写在 “allprojects” 代码块中:
allprojects {
    repositories {
        maven {
            // All of React Native (JS, Android binaries) is installed from npm
            url "$rootDir/../node_modules/react-native/android"
        }
        ...
    }
    ...
}

7.在 AndroidManifest.xml 清单文件中声明网络权限

<uses-permission android:name="android.permission.INTERNET" />

8.果需要访问 DevSettingsActivity 界面(即开发者菜单),则还需要在 AndroidManifest.xml 中声明:

<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

9.RN方面首先在项目根目录中创建一个空的index.js文件。(注意在 0.49 版本之前是 index.android.js 文件)

index.js是 React Native 应用在 Android 上的入口文件。而且它是不可或缺的!

完成以后RN的环境集成基本就完了,最后集成好的完整的目录结构如下图
使用React Native 混合开发,原生页面跳转到不同的RN页面_第2张图片

10.添加一些原生代码来启动 React Native 的运行时环境并让它开始渲染。首先需要在一个Activity中创建一个ReactRootView对象,然后在这个对象之中启动 React Native 应用,RNActivity的代码如下:

class RNActivity : AppCompatActivity() , DefaultHardwareBackBtnHandler {

    private var mReactInstanceManager: ReactInstanceManager? = null

    private val OVERLAY_PERMISSION_REQ_CODE = 1  // 任写一个值

    private lateinit var mReactRootView: ReactRootView


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val type = intent.getStringExtra("type")
        Log.e("TAG","收到的type-----"+ type)

        //这个是为了有调试的悬浮窗界面,正式环境可以去掉
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (!Settings.canDrawOverlays(this)) {
                val intent = Intent(
                    Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                    Uri.parse("package:$packageName")
                )
                startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE)
            }
        }
        mReactRootView = ReactRootView(this)
        mReactInstanceManager = ReactInstanceManager.builder()
            .setApplication(application)
            .setBundleAssetName("index.android.bundle")
            .setJSMainModulePath("index")
            .addPackage(MainReactPackage())
            .addPackage(RNBridgePackage(type))//!!!!!!!!!!!!这里一定要加自己的package,太坑了,官网都没介绍的
            .setUseDeveloperSupport(BuildConfig.DEBUG)
            .setInitialLifecycleState(LifecycleState.RESUMED)
            .build()
        // 注意这里的MyReactNativeApp必须对应“index.js”中的
        // “AppRegistry.registerComponent()”的第一个参数
        mReactRootView.startReactApplication(mReactInstanceManager, "NativeWithRnTest", null)

        setContentView(mReactRootView)
    }



    override fun invokeDefaultOnBackPressed() {
        super.onBackPressed()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                if (!Settings.canDrawOverlays(this)) {
                    // SYSTEM_ALERT_WINDOW permission not granted
                }
            }
        }
        mReactInstanceManager?.onActivityResult(this, requestCode, resultCode, data)
    }

    override fun onPause() {
        super.onPause()
        mReactInstanceManager?.onHostPause(this)
    }

    override fun onResume() {
        super.onResume()
        mReactInstanceManager?.onHostResume(this, this)
    }

    override fun onDestroy() {
        super.onDestroy()
        mReactInstanceManager?.onHostDestroy(this)
        mReactRootView.unmountReactApplication()
    }


    override fun onBackPressed() {
        if (mReactInstanceManager != null) {
            mReactInstanceManager!!.onBackPressed()
        } else {
            super.onBackPressed()
        }
    }

    override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
        if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) {
            mReactInstanceManager!!.showDevOptionsDialog()
            return true
        }
        return super.onKeyUp(keyCode, event)
    }

}

11.运行应用
运行应用首先需要启动开发服务器(Packager)。你只需在项目根目录中执行以下命令即可

$ yarn start

保持 packager 的窗口运行不要关闭,然后像往常一样编译运行你的 Android 应用(在命令行中执行./gradlew installDebug或是在 Android Studio 中编译运行)

我在集成好运行的时候,按照文档这样操作,运行不起来直接报错。然后我就用RN运行的方式,在根目录下执行

$ react-native run-android

然后就可以正常跑起来了,还是不行就要自己多百度一下了。

2.启动RNActivity给Index.js传值

做到这里的时候就感觉有点麻烦了,网上查了下原生向RN传递数据的方式。
具体这几种方式大家可以自己去查一查学习下,这里就不具体介绍了。可以参考React Native原生模块向JS模块传递数据的几种方式(Android)这篇文章,最后我选择的第三种方式,个人感觉是比较简单一些。

一:通过Callbacks的方式
二:通过Promises的方式
三:通过发送事件的方式

但是上面几种方式了解了以后我们发现,他们的模式都是js调用原生方法然后原生这边再给JS回调这种方式,没有直接给RN传值的方法。而且我们的RNActivity中也只是加载了RN的入口页面,没有直接给RN传值的方法。这时候我们就要转变思路了。这时候的解决思路就是在index.js RN界面启动的时候调用原生的方法,然后原生这边回调给js不同的值,RN这边收到回调后根据不同的值在render()函数中去做判断然后重新绘制界面。

但是想要RN能调用到原生方法,就需要我们写一些原生的代码供给RN调用。详细介绍请看官方文档原生模块

编写原生模块实现通信

  1. 首先来创建一个原生模块。一个原生模块是一个继承了ReactContextBaseJavaModule的 Java 类
  2. ReactContextBaseJavaModule要求派生类实现getName方法。这个函数用于返回一个字符串名字,这个名字在 JavaScript 端标记这个模块,这里我们把这个模块叫做RNBridgeMoudle,这样就可以在 JavaScript 中通过NativeModules.RNBridgeMoudle访问到这个模块。
  3. 要导出一个方法给 JavaScript 使用,Java 方法需要使用注解@ReactMethod。方法的返回类型必须为void。React Native 的跨语言访问是异步进行的,所以想要给 JavaScript 返回一个值的唯一办法是使用回调函数或者发送事件。

RNBridgeMoudle的具体代码

public class RNBridgeMoudle extends ReactContextBaseJavaModule {

    private ReactContext mReactContext;
    private String msg;

	//构造函数里面这个msg是为了能从外面传值
    public RNBridgeMoudle(String msg, ReactApplicationContext reactContext) {
        super(reactContext);
        this.mReactContext = reactContext;
        this.msg = msg;
    }

    @Override
    public String getName() {
        return "RNBridgeMoudle";
    }

    @ReactMethod
    public void getType() {
        WritableMap writableMap = new WritableNativeMap();
        writableMap.putString("key", msg);
        sendTransMisson(mReactContext, "EventName", writableMap);
    }

    /**
     * RCTDeviceEventEmitter方式
     *
     * @param reactContext
     * @param eventName    事件名
     * @param params       传惨
     */
    private void sendTransMisson(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
        reactContext
                .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit(eventName, params);

    }

}
  1. 还需要注册这个模块。我们需要在应用的 Package 类的createNativeModules方法中添加这个模块。如果模块没有被注册,它也无法在 JavaScript 中被访问到。需要我们写一个RNBridgePackage实现ReactPackage接口

RNBridgePackage的具体代码

public class RNBridgePackage implements ReactPackage {

    private String msg;

    public RNBridgePackage(String msg) {
        this.msg = msg;
    }

    public RNBridgePackage() {

    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }

    @Override
    public List<NativeModule> createNativeModules(
            ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new RNBridgeMoudle(msg,reactContext));
        return modules;
    }
}

5.按照官网的介绍,我们还需要在自定义的MainApplication中去注册我们刚实现的RNBridgePackage

/**
 * 测试了一下,从原生跳转到RN页面,在RNActivity中再进行相关参数的初始化,这里就不需要写了
 * 如果在RN中有的页面功能需要调原生的一些功能,还是需要在MainApplication注册的
 */

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(),
              new RNBridgePackage()//就是这里哦,所有自定的Package都要在这里注册
      );
    }
  };

  @Override
  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
  }
}

需要注意的是,我们是在原生中通过RNActivity这个入口去打开和初始化加载RN相关界面的,所以我们的相关配置会在onCreate中的ReactInstanceManager中去配置。MainApplication都可以不用写。所以千万要注意,我们的自定义的RNBridgePackage要在ReactInstanceManager.builder()中add进去。这里也是我们给Index.js传值的关键,通过RNBridgePackage(type)的构造函数把我们自定义的type值传过去,然后RN调用原生方法的时候通过RNBridgeMoudle把type值回调给RN,RN收到type值后根据type重绘界面,跳转到不同的界面去。

  mReactInstanceManager = ReactInstanceManager.builder()
            .setApplication(application)
            .setBundleAssetName("index.android.bundle")
            .setJSMainModulePath("index")
            .addPackage(MainReactPackage())
            .addPackage(RNBridgePackage(type))//这里一定要加自己的package!!!!
            .setUseDeveloperSupport(BuildConfig.DEBUG)
            .setInitialLifecycleState(LifecycleState.RESUMED)
            .build()
  1. 最后一步就是我们index.js的代码了。上面代码写好后我们就能在js中通过NativeModules.RNBridgeMoudle的方式调用我们原生的方法。具体请看代码及注释

index.js具体代码如下

import React from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    View,
    DeviceEventEmitter,
    ToastAndroid,
    NativeModules
    } from 'react-native';

//初始化一个值,没有接收到type的时候显示loading
var TYPE = "loading";

class HelloWorld extends React.Component {

  componentWillMount() {
    var outThis = this;
    //用来监听原生RNBridgeMoudle发送过来的消息,这里的EventName要和RNBridgeMoudle发送时候保持一致
    DeviceEventEmitter.addListener('EventName', function (msg) {
      console.log("接收到原生传过来的msg--->"+msg.key);
      TYPE = msg.key;
      console.log("重新赋值的type----->"+TYPE);
      // ToastAndroid.show("DeviceEventEmitter收到消息:" + "\n" + msg.key+TYPE, ToastAndroid.SHORT);
      //更新UI,重绘
      outThis.updataUI();
    });
    this.getDeviceEventType();
  }

  //重绘界面
  updataUI(){
    console.log("重绘界面--type的值--->"+TYPE);
    this.setState({
    })
  }

  //调用原生模块发送消息
  getDeviceEventType() {
    console.log('getDeviceEventType---调用原生模块发送消息');
    NativeModules.RNBridgeMoudle.getType();
  }

  render() {
    console.log('render()开始绘制界面了');
    console.log("render里面的type--->"+TYPE);
    //这里就可以根据type的值去渲染不同的页面了,可以在没获取到type值的时候写一个loading页面
    if (TYPE==="1"){
      return (
        <View style={styles.container}>
          <Text style={styles.hello}
          >我是第一个界面呦~</Text>
        </View>
      );
    } else if (TYPE === "2"){
      return (
        <View style={styles.container}>
          <Text style={styles.hello}>我是第二个界面喽~~~</Text>
        </View>
      );
   }else{
      return (
        <View style={styles.container}>
          <Text>
            正在加载……
        </Text>
        </View>
      );
   }
  }

}
var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
  },
  hello: {
    fontSize: 30,
    textAlign: 'center',
    margin: 10,
  },
});

//这里的NativeWithRnTest要和RNActivity中参数保持一致
AppRegistry.registerComponent('NativeWithRnTest', () => HelloWorld);

从MainActivity跳转到不同RN界面代码

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button.setOnClickListener {
            startActivity(Intent(this, RNActivity::class.java).putExtra("type","1"))
        }
        button2.setOnClickListener {
            startActivity(Intent(this, RNActivity::class.java).putExtra("type","2"))
        }
    }
}

根据上面代码运行出来的日志如下图
使用React Native 混合开发,原生页面跳转到不同的RN页面_第3张图片

后记

目前笔者对RN还在学习中是个新手,对RN了解还不是很深入,对混合开发也只是调研试验阶段。文中要是有错误或者不足之处,大家可以提出一起学习探讨下。接下来还需要继续研究的是iOS端怎么和Android端统一起来。共同开发的话,js代码怎么和原生Android,iOS代码共同git管理等。

文中完整项目下载demo

你可能感兴趣的:(使用React Native 混合开发,原生页面跳转到不同的RN页面)