写在此文章之前

最近一直在研究Android,我开始学习Android是有原因的,前段时间写过一个类似于一元夺宝的跨平台APP,里面的好多功能是ReactNative没有提供的,好在现在ReactNative开源社区里面已经有好多优秀的开源库了,就直接拿来用了,但是,这并不能满足开发中一些特殊的需求,这个时候就要写js和原生之间的桥接了,因为我的老本就是iOS,所以写iOS的桥接很容易。但是写Adnroid就有点费力了,因为我对Android一窍不通。但是想学好ReactNative,只会iOS是不行的,所以我就开始了我的Android之旅。还有,假如ReactNative在以后死掉了,我也新郑了Android开发这一新技能,一本万利。

Android的博客也写了几篇了,我要回过头来说一些ReactNative的东西,所以,如果你看到我今天在说Android,明天又说ReactNative,你不要奇怪,这很正常。

那今天就先来说说关于js和原生的桥接。

初始化项目

首先,本项目是在Mac下开发。这里,我要向大家推荐一个我用着非常方便的IDEWebStorm,相信前端的同学一定对他不陌生,如果想让WebStorm智能提示代码,大家可以看这篇文章:点我。最近WebStorm刚更新了2017版本,我感觉优化了好多,起码内存方面小了很多。当然,WebStorm是收费的,但是在天朝,一定有办法的。

先来描述一下我们要做的功能:这里我们模拟一下去调用QQ第三方的登录。当点击登录按钮时,调用原生的QQ登录(原生代码使用2秒延迟模拟登录),然后把登录结果回调到js,并且在屏幕上添加一个下线的原生按钮。当点击下线按钮时,再次调用js的代码,强制下线。

注意:界面上的登录按钮文字都是ReactNative生成的,下线按钮是iOS或者Android代码生成的。

iOS运行效果如下:



Android运行效果如下:



iOS通信实现

jsiOS原生之间的通信主要靠ReactNative定义的一些宏来实现的。能让js来调用的无非是类和类的方法,那么下面就来详细的说一下如何导出类和方法。本例是导出名为LocalModule的类和它的某些方法。

导出类

如果你想让你的某个类可以实现js调用必须实现RCTBridgeModule协议,我们先来看RCTBridgeModule协议必须实现的方法有哪些方法:

@protocol RCTBridgeModule <NSObject>
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }
+ (NSString *)moduleName;
@optional

我们看到,RCTBridgeModule协议只有一个需要实现的方法,这个方法就是告诉js需要导出的类,但是,通常情况下,我们并不直接实现这个方法,而是使用RCT_EXPORT_MODULE这个宏,因为这个宏还帮我们实现了load方法,即在类加载的时候就去注册当前类到js。当然,这个宏还接收一个参数,如果我们不传,默认导出当前类的类名为js的调用模块,当然,你可以自定义导出模块的名字。我们来看看具体如何导出类:

LocalModule.h

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface LocalModule : RCTEventEmitter <RCTBridgeModule>
@end

我们在LocalModule.h里面直接实现RCTBridgeModule就行,当然,如果你不需要iOSjs通信,那么你直接继承NSObject就行。否则就必须继承RCTEventEmitter类,这个功能我们等会再说。

LocalModule.m

@implementation LocalModule
//导出这个类,不然js不能使用 默认导出当前类名
RCT_EXPORT_MODULE();
//当然也可以自定义 名字你随便起,切记不是NSString类型的参数。
RCT_EXPORT_MODULE(MMMMMM);
@end

注意,无论你是否是只用默认的导出名,还是自定义的导出名,将来在js中都很重要,所以,你最好导出一个有实际意义的名字。

导出方法

导出方法使用的也是宏,有两种宏:RCT_EXPORT_METHODRCT_REMAP_METHOD,我们来看看具体的用法:

使用RCT_EXPORT_METHOD

@implementation LocalModule
//导出一个方法 RCTResponseSenderBlock 设置回调函数 当然 ReactNative为我们提供了多种回到block
RCT_EXPORT_METHOD(loginWithqq:(NSString *)qqkey callback:(RCTResponseSenderBlock)callback) {
//业务逻辑
}
@end

学过iOS也许能看出个大概,此导出方法所对应的原生方法为:

- (void)loginWithqq:(NSString *)qq {
//业务逻辑
}

导出方法和原生方法相比,去掉了-(void),方法写在了RCT_EXPORT_METHOD宏的括号内。

切记,所有的方法都没有返回值,如果你想在iOS返回一些数据给js,你必须使用block回调,就像我们刚才所说的第一个导出方法一样。直接在你原有的方法后面追加一个Block参数就行,ReactNative为我们提供了两种Block回调:RCTResponseSenderBlockRCTResponseErrorBlock。前者一般处理正常的数据回调,后者会处理错误回调。

还有一点需要特别注意,所有的方法都是在子线程里进行的,如果你想在方法内操作一些UI,必须回调主线程,当然,ReactNative也为我们提供了定义线程的方法,只要重写methodQueueget方法就可以定义自己的队列。此属性定义在RCTBridgeModule协议中。

//决定此类的所有方法运行在哪个队列中
- (dispatch_queue_t)methodQueue {
return dispatch_get_main_queue();
}
//也可以自己创建队列
- (dispatch_queue_t)methodQueue {
return dispatch_queue_create("com.custom", DISPATCH_QUEUE_SERIAL);
}

如果重写这个方法,那么你所有的方法都会在这个队列中运行,当然,你也可以在方法内部重新开启其他的线程。

使用RCT_REMAP_METHOD

学过前端的可能都知道Promises,ES7以后新增了async/await语法,所以ReactNative也为我们提供了一个可以返回Promises对象的导出方法。

例如,我的原生方法为:

- (void)loginWithqq:(NSString *)qq {
//业务逻辑
}

那么,要是想让此方法支持Promises,你需要这么做:

//返回一个Promises对象 可以搭配ES7的async/await语法
RCT_REMAP_METHOD(asyncLoginWithqq,qqkey:(NSString *)qqkey resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){
//业务处理
}

这里必须传的两个参数是:RCTPromiseResolveBlockRCTPromiseRejectBlock,我们使用RCTPromiseResolveBlock来处理正常的数据,使用RCTPromiseRejectBlock来处理错误的数据。通常情况下,使用这个宏来导出方法,此方法一定会返回数据,不然就没有意义了。

现在我们已经知道如何导出类和方法让js来调用,可是,有时候,我们想主动的向js发送一些消息,这时候该怎么办呢?

RCTEventEmitter

如果导出类继承于RCTEventEmitter,那么我们只需要实现它的- (NSArray<NSString *> *)supportedEvents方法就可以,此方法返回一个字符串数组,代表可以发送的事件名称有哪几个。其实,这些事件名称就类似与通知名,我们在iOS中定义这些通知名,在js去注册观察这些通知名。

//这里注册一个退出登录的事件(通知名)
- (NSArray<NSString *> *)supportedEvents {
return @[@"QQLoginOut"];
}
- (void)buttonclick:(UIButton *)button {
[button removeFromSuperview];
//发送事件,可以携带数据
[self sendEventWithName:@"QQLoginOut" body:@{@"result":@1}];
}

如果我们在iOS中注册了事件,但是在js中并没有去监听这个事件,那么,程序会发生警告,但并不会崩溃,要解决这种情况,我们可以覆盖RCTEventEmitter类的startObserving放和stopObserving方法。

@implementation LocalModule {
bool hasListeners;
}
// 在添加第一个监听函数时触发
-(void)startObserving {
hasListeners = YES;
}
// 取消监听时触发
-(void)stopObserving {
hasListeners = NO;
}
- (void)buttonclick:(UIButton *)button {
[button removeFromSuperview];
if (hasListeners) {
//如果有监听,才发送事件
[self sendEventWithName:@"QQLoginOut" body:@{@"result":@1}];
}
}

好了,现在来看看开头所说的例子的具体实现:

LocalModule.h

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface LocalModule : RCTEventEmitter <RCTBridgeModule>
@end

LocalModule.m

#import "LocalModule.h"
#import <UIKit/UIKit.h>
@interface LocalModule ()
@property (nonatomic, copy) RCTResponseSenderBlock callBack;
@end
@implementation LocalModule {
bool hasListeners;
}
//导出这个类,不然js不能使用 默认导出当前类名
RCT_EXPORT_MODULE();
//当然也可以自定义 名字你随便起,切记不是NSString类型的参数。
//RCT_EXPORT_MODULE(MMMMMM);
//决定此类的所有方法运行在哪个队列中
//- (dispatch_queue_t)methodQueue {
// return dispatch_get_main_queue();
//}
//也可以自己创建队列
//- (dispatch_queue_t)methodQueue {
// return dispatch_queue_create("com.custom", DISPATCH_QUEUE_SERIAL);
//}
//导出一个方法 注意,当前线程为子线程 RCTResponseSenderBlock 设置回调函数 当然 ReactNative为我们提供了多种回到block
RCT_EXPORT_METHOD(loginWithqq:(NSString *)qqkey callback:(RCTResponseSenderBlock)callback) {
NSLog(@"%@",[NSThread currentThread]);
//记录回调 有可能会在另一个方法中完成业务逻辑。
self.callBack = callback;
//模拟执行本地操作
NSString *code = [NSString stringWithFormat:@"%@%@",qqkey,@"---qq登陆成功---iOS"];
//回调到js函数
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self addButton];
if (self.callBack) {
NSDictionary *info = @{@"result":code};
self.callBack(@[info]);
}
});
}
//使用ES7的async/await语法
RCT_REMAP_METHOD(asyncLoginWithqq,qqkey:(NSString *)qqkey resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){
NSLog(@"%@",[NSThread currentThread]);
//模拟登陆
NSString *code = [NSString stringWithFormat:@"%@%@",qqkey,@"---qq登陆成功---iOS"];
//回调到js函数
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self addButton];
if (resolve) {
NSDictionary *info = @{@"result":code};
resolve(info);
}
});
}
//注册事件
- (NSArray<NSString *> *)supportedEvents {
return @[@"QQLoginOut"];
}
//添加一个下线的按钮
- (void)addButton {
//所有UI的操作都要在主线程中
dispatch_async(dispatch_get_main_queue(), ^{
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
[button setTitle:@"ios的按钮——下线" forState:UIControlStateNormal];
button.frame = CGRectMake(100, 40, 130, 30);
[button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
button.backgroundColor = [UIColor redColor];
[[UIApplication sharedApplication].keyWindow addSubview:button];
[button addTarget:self action:@selector(buttonclick:) forControlEvents:UIControlEventTouchUpInside];
});
}
// 在添加第一个监听函数时触发
-(void)startObserving {
hasListeners = YES;
}
// 取消监听时触发
-(void)stopObserving {
hasListeners = NO;
}
//下线
- (void)buttonclick:(UIButton *)button {
[button removeFromSuperview];
if (hasListeners) {
//如果有监听,才发送事件
[self sendEventWithName:@"QQLoginOut" body:@{@"result":@1}];
}
}
@end

jsiOS通信的基本步骤是:

  1. 使用宏RCT_EXPORT_MODULE()导出当前类
  2. 使用宏RCT_EXPORT_METHOD()或者RCT_REMAP_METHOD()导出方法
  3. 使用一系列的block进行回调
  4. js中注册事件监听,使用sendEventWithName方法发送事件

关于iOS的原生我们已经说完,接下来看Android的。

Android通信实现

相对于iOSAndroid会更简单。

导出类

首先,导出的类必须继承ReactContextBaseJavaModule,并且重新构造方法和返回模块的方法:

public class LocalModuleAndroid extends ReactContextBaseJavaModule {
//这里记录下当前的Context
private ReactApplicationContext mContext;
public LocalModuleAndroid(ReactApplicationContext reactContext) {
super(reactContext);
mContext= reactContext;
}
@Override
public String getName() {
return "LocalModuleAndroid";
}
}

然后你还需要新建一个类,并且实现ReactPackage接口:

public class LocalModulePackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
//返回需要注册的类
modules.add(new LocalModuleAndroid(reactContext));
return modules;
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return new ArrayList<>();
}
}

然后在MainApplicationgetPackages方法中加入LocalModulePackage:

@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new LocalModulePackage()//自己的本地导出类管理器
);
}

这样,就可以把LocalModuleAndroid导出来了。

导出方法

导出方法更简单,只需要声明@ReactMethod就可以了:

//使用回调函数回调结果
@ReactMethod
public void loginWithqq(String appkey, Callback callback) {
try {
//模拟登陆操作
String code = appkey+"---qq登陆成功--Android";
if (callback != null) {
WritableMap result = Arguments.createMap();
result.putString("result",code);
callback.invoke(result);
}
}catch (Exception e) {
}
}
//使用Promise回调数据
@ReactMethod
public void asyncLoginWithqq(String appkey, Promise promise) {
//模拟登陆操作
String code = appkey+"---qq登陆成功--Android";
WritableMap result = Arguments.createMap();
result.putString("result",code);
promise.resolve(result);
}

这里提供了两种回调方法,你可以任意的选择。

RCTDeviceEventEmitter

如果我们想向js发送事件,直接使用RCTDeviceEventEmitter来发送就行:

public void onClick(View v) {
((ViewGroup)layout.getParent()).removeView(layout);
WritableMap result = Arguments.createMap();
result.putInt("result",1);
getCurrentActivity().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("QQLoginOut",result);
}

关于上面的例子,全部的代码如下:

LocalModuleAndroid类

public class LocalModuleAndroid extends ReactContextBaseJavaModule {
//这里记录下当前的Context
private ReactApplicationContext mContext;
public LocalModuleAndroid(ReactApplicationContext reactContext) {
super(reactContext);
mContext= reactContext;
}
@Override
public String getName() {
return "LocalModuleAndroid";
}
@ReactMethod
public void loginWithqq(String appkey, Callback callback) {
try {
//模拟登陆操作
String code = appkey+"---qq登陆成功--Android";
addButton();
if (callback != null) {
WritableMap result = Arguments.createMap();
result.putString("result",code);
callback.invoke(result);
}
}catch (Exception e) {
}
}
@ReactMethod
public void asyncLoginWithqq(String appkey, Promise promise) {
addButton();
//模拟登陆操作
String code = appkey+"---qq登陆成功--Android";
WritableMap result = Arguments.createMap();
result.putString("result",code);
promise.resolve(result);
}
//使用原生代码添加按钮
private void addButton() {
getCurrentActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
//在主线程中创建UI
final LinearLayout layout = (LinearLayout) LinearLayout.inflate(mContext,R.layout.local_button,null);
LinearLayout.LayoutParams mLayoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
mLayoutParams.setMargins(400,80,0,0);
getCurrentActivity().addContentView(layout,mLayoutParams);
final Button button = (Button) layout.findViewById(R.id.loginOut);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
((ViewGroup)layout.getParent()).removeView(layout);
WritableMap result = Arguments.createMap();
result.putInt("result",1);
//向js发送事件
mContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("QQLoginOut",result);
}
});
}
});
}
}

jsAndroid通信的基本步骤:

  • 导出类首先要继承ReactContextBaseJavaModule,并重写getName()返回导出模块的名字
  • 新建一个类,实现ReactPackage接口的三个方法,并且在createNativeModules方法中去注册要导出的类,并在MainApplicationgetPackages方法中导出package管理器。
  • 使用关键字@ReactMethod导出方法,使用Callback或者Promise进行回调。
  • js中注册监听事件,使用RCTDeviceEventEmitter可以直接发送事件到js

好了,现在我们的原生都写完了,接下来我们看看在js中如何调用。

js调用

为了代码的复用性,一般都会创建一个js文件,单独封装一类功能。所以我创建一个localModulejs文件。如下:

import React, { Component } from 'react';
import {
Platform,
NativeModules,
NativeEventEmitter,
DeviceEventEmitter
} from 'react-native';
//导入iOS原生模块
var LocalModuleiOS = NativeModules.LocalModule;
//导入Android原生模块
var LocalModuleAndroid = NativeModules.LocalModuleAndroid;
//iOS事件监听
const localModuleEmitter = new NativeEventEmitter(LocalModuleiOS);
var LocalModule = {
//添加事件监听
addLoginOutCallBack(callBack) {
if (Platform.OS == 'ios') {
//监听iOS的QQLoginOut事件
localModuleEmitter.addListener('QQLoginOut',(result)=>{
if (callBack) {
callBack(result);
}
})
}else {
//监听Android的QQLoginOut事件
DeviceEventEmitter.addListener('QQLoginOut',(result)=>{
if (callBack) {
callBack(result);
}
})
}
},
//调用原生QQ登录
loginWithqq(appkey,callBack) {
if (Platform.OS == 'ios') {
// LocalModuleiOS.loginWithqq(appkey,(result) => {
// if (callBack) {
// callBack(result);
// }
// });
//使用ES7新特性 async/await
return LocalModuleiOS.asyncLoginWithqq(appkey);
}else {
//使用Callback回调
LocalModuleAndroid.loginWithqq(appkey,(result)=>{
if (callBack) {
callBack(result);
}
});
// return LocalModuleAndroid.asyncLoginWithqq(appkey);
}
}
};
//导出模块
module.exports = LocalModule;

首先要导入原生模块,这里使用NativeModules直接导入,因为我们在iOS中导出的模块名是类名,在Android中导出的模块名为LocalModuleAndroid,所以这里分别对应iOSAndroid写开。然后又注册了iOS的事件监听。

loginWithqq中,我们即可以使用block回调,也可以使用Promise
这样,一个登陆功能就封装好了。来看看如何用。

为了保证iOSAndroid的通用性,这里新定义一个界面home,作为iOSAndroid的主页:

index.ios.js

import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
import Home from './home';
export default class moduleDemo extends Component {
render() {
return (
<Home style={styles.container}/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
}
});
AppRegistry.registerComponent('moduleDemo', () => moduleDemo);

index.android.js

import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
import Home from './home';
export default class moduleDemo extends Component {
render() {
return (
<Home style={styles.container}/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
}
});
AppRegistry.registerComponent('moduleDemo', () => moduleDemo);

现在,我们只需要关注home界面的业务逻辑就行了,看home界面:

import LocalModule from './localModule';
export default class home extends Component {
// 构造
constructor(props) {
super(props);
// 初始状态
this.state = {
text:'hello word',
isLogin:false
};
}
render() {
return (
<View>
<TouchableHighlight style={styles.clikeButton} onPress={this.onclick.bind(this)}>
<Text style={styles.text}>{this.state.isLogin?'已登录':'登录'}</Text>
</TouchableHighlight>
<Text style={styles.contentText}>{this.state.text}</Text>
</View>
);
}
async onclick() {
//qq登录
// LocalModule.loginWithqq('123456789',(result)=>{
// var code = result['result'];
// this.setState({
// text:code,
// isLogin:true
// })
// });
//这里使用async/await的方法
var result = await LocalModule.loginWithqq('123456789');
var code = result['result'];
this.setState({
text:code,
isLogin:true
});
//添加退出登录的响应
LocalModule.addLoginOutCallBack((result)=>{
var code = result['result'];
if (code == 1) {
this.setState({
text:'hello word',
isLogin:false
});
}
});
}
}
const styles = StyleSheet.create({
clikeButton:{
marginLeft:20,
marginTop:40,
width:60,
height:30,
backgroundColor:'red',
alignItems:'center',
justifyContent:'center'
},
text:{
color:'white'
},
contentText:{
fontSize:30,
alignSelf:'center',
marginTop:30
}
});

至此,ReactNative与原始通信已经了解的差不多了,当然,还有更多的知识点需要我们去官网查看。推荐一个翻译至官网的中文网站:点这里