写在此文章之前
最近一直在研究Android
,我开始学习Android
是有原因的,前段时间写过一个类似于一元夺宝的跨平台APP,里面的好多功能是ReactNative
没有提供的,好在现在ReactNative
开源社区里面已经有好多优秀的开源库了,就直接拿来用了,但是,这并不能满足开发中一些特殊的需求,这个时候就要写js和原生之间的桥接了,因为我的老本就是iOS
,所以写iOS
的桥接很容易。但是写Adnroid
就有点费力了,因为我对Android
一窍不通。但是想学好ReactNative
,只会iOS
是不行的,所以我就开始了我的Android
之旅。还有,假如ReactNative
在以后死掉了,我也新郑了Android
开发这一新技能,一本万利。
Android
的博客也写了几篇了,我要回过头来说一些ReactNative
的东西,所以,如果你看到我今天在说Android
,明天又说ReactNative
,你不要奇怪,这很正常。
那今天就先来说说关于js和原生的桥接。
初始化项目 首先,本项目是在Mac
下开发。这里,我要向大家推荐一个我用着非常方便的IDE
:WebStorm
,相信前端的同学一定对他不陌生,如果想让WebStorm
智能提示代码,大家可以看这篇文章:点我 。最近WebStorm
刚更新了2017
版本,我感觉优化了好多,起码内存方面小了很多。当然,WebStorm
是收费的,但是在天朝,一定有办法的。
先来描述一下我们要做的功能:这里我们模拟一下去调用QQ
第三方的登录。当点击登录
按钮时,调用原生的QQ
登录(原生代码使用2秒延迟模拟登录),然后把登录结果回调到js
,并且在屏幕上添加一个下线
的原生按钮。当点击下线
按钮时,再次调用js的代码,强制下线。
注意:界面上的登录按钮
和文字
都是ReactNative
生成的,下线
按钮是iOS
或者Android
代码生成的。
iOS运行效果如下:
Android运行效果如下:
iOS通信实现 js
与iOS
原生之间的通信主要靠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
就行,当然,如果你不需要iOS
向js
通信,那么你直接继承NSObject
就行。否则就必须继承RCTEventEmitter
类,这个功能我们等会再说。
LocalModule.m
@implementation LocalModule
RCT_EXPORT_MODULE();
RCT_EXPORT_MODULE(MMMMMM);
@end
注意,无论你是否是只用默认的导出名,还是自定义的导出名,将来在js
中都很重要,所以,你最好导出一个有实际意义的名字。
导出方法 导出方法使用的也是宏,有两种宏:RCT_EXPORT_METHOD
和RCT_REMAP_METHOD
,我们来看看具体的用法:
使用RCT_EXPORT_METHOD
@implementation LocalModule
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
回调:RCTResponseSenderBlock
和RCTResponseErrorBlock
。前者一般处理正常的数据回调,后者会处理错误回调。
还有一点需要特别注意,所有的方法都是在子线程里进行的,如果你想在方法内操作一些UI,必须回调主线程,当然,ReactNative
也为我们提供了定义线程的方法,只要重写methodQueue
的get
方法就可以定义自己的队列。此属性定义在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
,你需要这么做:
RCT_REMAP_METHOD(asyncLoginWithqq,qqkey:(NSString *)qqkey resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){
}
这里必须传的两个参数是:RCTPromiseResolveBlock
和RCTPromiseRejectBlock
,我们使用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;
}
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(loginWithqq:(NSString *)qqkey callback:(RCTResponseSenderBlock)callback) {
NSLog (@"%@" ,[NSThread currentThread]);
self .callBack = callback;
NSString *code = [NSString stringWithFormat:@"%@%@" ,qqkey,@"---qq登陆成功---iOS" ];
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]);
}
});
}
RCT_REMAP_METHOD(asyncLoginWithqq,qqkey:(NSString *)qqkey resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){
NSLog (@"%@" ,[NSThread currentThread]);
NSString *code = [NSString stringWithFormat:@"%@%@" ,qqkey,@"---qq登陆成功---iOS" ];
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 {
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
js
与iOS
通信的基本步骤是:
使用宏RCT_EXPORT_MODULE()
导出当前类
使用宏RCT_EXPORT_METHOD()
或者RCT_REMAP_METHOD()
导出方法
使用一系列的block
进行回调
在js
中注册事件监听,使用sendEventWithName
方法发送事件
关于iOS
的原生我们已经说完,接下来看Android
的。
Android通信实现 相对于iOS
,Android
会更简单。
导出类 首先,导出的类必须继承ReactContextBaseJavaModule
,并且重新构造方法和返回模块的方法:
public class LocalModuleAndroid extends ReactContextBaseJavaModule {
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<>();
}
}
然后在MainApplication
的getPackages
方法中加入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) {
}
}
@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 {
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 () {
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 );
mContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("QQLoginOut" ,result);
}
});
}
});
}
}
js
与Android
通信的基本步骤:
导出类首先要继承ReactContextBaseJavaModule
,并重写getName()
返回导出模块的名字
新建一个类,实现ReactPackage
接口的三个方法,并且在createNativeModules
方法中去注册要导出的类,并在MainApplication
的getPackages
方法中导出package
管理器。
使用关键字@ReactMethod
导出方法,使用Callback
或者Promise
进行回调。
在js
中注册监听事件,使用RCTDeviceEventEmitter
可以直接发送事件到js
,
好了,现在我们的原生都写完了,接下来我们看看在js
中如何调用。
js调用 为了代码的复用性,一般都会创建一个js
文件,单独封装一类功能。所以我创建一个localModule
的js
文件。如下:
import React, { Component } from 'react' ;
import {
Platform,
NativeModules,
NativeEventEmitter,
DeviceEventEmitter
} from 'react-native' ;
var LocalModuleiOS = NativeModules.LocalModule;
var LocalModuleAndroid = NativeModules.LocalModuleAndroid;
const localModuleEmitter = new NativeEventEmitter(LocalModuleiOS);
var LocalModule = {
addLoginOutCallBack(callBack) {
if (Platform.OS == 'ios' ) {
localModuleEmitter.addListener('QQLoginOut' ,(result)=>{
if (callBack) {
callBack(result);
}
})
}else {
DeviceEventEmitter.addListener('QQLoginOut' ,(result)=>{
if (callBack) {
callBack(result);
}
})
}
},
loginWithqq(appkey,callBack) {
if (Platform.OS == 'ios' ) {
return LocalModuleiOS.asyncLoginWithqq(appkey);
}else {
LocalModuleAndroid.loginWithqq(appkey,(result)=>{
if (callBack) {
callBack(result);
}
});
}
}
};
module .exports = LocalModule;
首先要导入原生模块,这里使用NativeModules
直接导入,因为我们在iOS
中导出的模块名是类名,在Android
中导出的模块名为LocalModuleAndroid
,所以这里分别对应iOS
和Android
写开。然后又注册了iOS
的事件监听。
在loginWithqq
中,我们即可以使用block
回调,也可以使用Promise
。 这样,一个登陆功能就封装好了。来看看如何用。
为了保证iOS
和Android
的通用性,这里新定义一个界面home
,作为iOS
和Android
的主页:
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() {
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
与原始通信已经了解的差不多了,当然,还有更多的知识点需要我们去官网查看。推荐一个翻译至官网的中文网站:点这里