目前公司项目全部采用原生开发,现在想提升开发效率,把部分Android,iOS两端公共高复用的界面使用RN去做。就需要在原有的项目基础上加入RN并且单独跳转到不同的RN界面去。网上查了一下相关资料发现大部分都是从RN界面跳转到原生的介绍。RN不好处理的地方,直接调用原生界面去制作。很少有介绍有从原生跳转到RN某个单页面的介绍。最近研究了一下最终做出的效果如下。
按照官网的介绍在原有项目集成RN后,只有一个RNActivity去打开RN中的index.js的入口文件。个人感觉这个RNActivity就像原生开发中的webview一样。只是一个承载打开Index.js的容器。那我们思路也很明显了。要是能在打开Index.js 的时候给RN页面传入不同的值,那我们就可以根据传入值的不同去渲染不同的界面。总体思路是这样,下面我们来一步步的去完成。
1.现有项目集成RN
按照官方文档一步步的集成详情参考集成到现有原生应用环境相关配置不在此介绍。
{
"name": "NativeWithRnTest",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start"
}
}
$ yarn add react-native
这样默认会安装最新版本的 React Native,同时会打印出类似下面的警告信息,命令窗口上往上翻一下就能看到黄色的相关警告信息。这是正常现象,意味着我们还需要安装指定版本的 React
warning "[email protected]" has unmet peer dependency "[email protected]".
$ yarn add react@16.6.3 //这个版本号一定要根据你自己安装时候警告提醒的保持一致,不要直接复制上去了
dependencies {
implementation "com.facebook.react:react-native:+" // From node_modules
}
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的环境集成基本就完了,最后集成好的完整的目录结构如下图
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调用。详细介绍请看官方文档原生模块
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);
}
}
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()
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"))
}
}
}
目前笔者对RN还在学习中是个新手,对RN了解还不是很深入,对混合开发也只是调研试验阶段。文中要是有错误或者不足之处,大家可以提出一起学习探讨下。接下来还需要继续研究的是iOS端怎么和Android端统一起来。共同开发的话,js代码怎么和原生Android,iOS代码共同git管理等。
文中完整项目下载demo