ReactNative之iOS原生和JavaScript的交互

js-ios

ReactNative开发中, 在JavaScript语法无法实现的时候会涉及到一些原生开发, 既然是混合开发就会涉及到一些iOSReactNative之间通讯的问题, 这里就涉及到两种方式:

  • RN调用原生的方法, 给原生发送数据
  • 原生给RN回传数据, 或者给RN发送通知
  • 下面就简单记录下这两种方式的实现

JS调用原生

  • 这里要讲的交互场景是JS调用原生方法,最后由原生方法将结果回调到JS里面
  • react-native是在原生的基础上,将接口调用统一为js
  • 也就是说,react-native调起原生的能力非常重要

js调用模块

在原生需要创建一个继承自NSObject的类(模块)

1
2
3
4
5
6
7
8
#import <Foundation/Foundation.h>
// 需要导入头文件
#import <React/RCTBridgeModule.h>

// 必须遵循RCTBridgeModule协议
@interface AppEventMoudle : NSObject <RCTBridgeModule>

@end

AppEventMoudle.m文件件中需要导出改模块, 并将创建的方法导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#import "AppEventMoudle.h"
#import <React/RCTBridge.h>

@implementation AppEventMoudle


// 导出桥接模块, 参数传空或者当前class的类名
// 参数若为空, 默认模块名为当前class类名即AppEventMoudle
RCT_EXPORT_MODULE(AppEventMoudle);

// 带有参数
RCT_EXPORT_METHOD(OpenView:(NSDictionary *)params){

// 因为是显示页面,所以让原生接口运行在主线程
dispatch_async(dispatch_get_main_queue(), ^{

// 在这里可以写需要原生处理的UI或者逻辑
NSLog(@"params = %@", params);
});
}

/// 带有回调
RCT_EXPORT_METHOD(OpenView:(NSDictionary *)params, callback:(RCTResponseSenderBlock)callback){

// 因为是显示页面,所以让原生接口运行在主线程
dispatch_async(dispatch_get_main_queue(), ^{

// 在这里可以写需要原生处理的UI或者逻辑
NSLog(@"params = %@", params);
if (callback) {
callback(@[params]);
}
});
}

上面代码需要注意的是

  • 桥接到Javascript的方法返回值类型必须是void
  • React Native的桥接操作是异步的,在queue里面异步执行,所以如果要返回结果给Javascript,就必须通过回调或者触发事件来进行
  • 这里的回调对应于iOS端就是通过block来回调的

RCTBridge

  • RCTBridge可以说是一个封装类,封装了RCTCxxBridge
  • 我们先看这个文件提供的一些变量和方法
  • RCTModuleClasses: 主要储存的是我们注册的module, 所有用宏RCT_EXPORT_MODULE()注册的module都会存入这个变量.
  • RCTGetModuleClasses: 获取RCTModuleClasses里面所有注册的module
  • RCTBridgeModuleNameForClass: 从一个类获取这个类的名字
  • RCTVerifyAllModulesExported: 验证我们所写的所有遵守RCTBridgeModule协议的类是否都在我们的管理中

RCTResponseSenderBlock

1
typedef void (^RCTResponseSenderBlock)(NSArray *response);
  • RCTResponseSenderBlockRCTBridgeModule里面提供的block
  • 这个block接受一个数组参数, 代表原生方法的返回结果

线程问题

  • js代码的执行是在js线程里面,原生模块的执行默认是在一个串行的queue里面异步执行的
  • 对于原生模块的执行来说,默认一个串行的queue是不够的,我们有时候需要指定模块所有任务执行所在的queue
1
2
3
4
5
6
7
8
9
RCT_EXPORT_METHOD(OpenView:(NSDictionary *)params){

// 因为是显示页面,所以让原生接口运行在主线程
dispatch_async(dispatch_get_main_queue(), ^{

// 在这里可以写需要原生处理的UI或者逻辑
NSLog(@"params = %@", params);
});
}

原生向RN发送监听

  • 例如: 项目中的H5页面, 通过原生的Webview实现, 并且监听url的变化, 并通知js做相关操作
  • 这样我们就要在url变化的时候, 给JavaScript发送监听通知
  • 并且不能使用RCTResponseSenderBlock进行回调, block回调只能执行一次, 并不能不断的执行

第一步

  • 我们需要创建一个WebViewController的控制器, 在该控制器内添加UIWebView的UI和逻辑的实现
  • UIWebViewDelegate的协议方法中监听webviewurl的变化, 并发送通知
1
2
3
4
5
6
7
8
9
10
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSString *url= request.URL.absoluteString;
if (url && ![url isEqualToString:@""]) {
// 发送通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"urlChange" object:url];
}
[self clickAction:url];

return YES;
}

第二步

需要在js调用的方法中接受上述代码中发送的通知, 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
RCT_EXPORT_METHOD(OpenWebView:(NSDictionary *)params){
dispatch_async(dispatch_get_main_queue(), ^{
// 接受通知监听
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(urlChange:) name:@"urlChange" object:nil];

WebViewController *webView = [WebViewController new];
webView.params = params;
UINavigationController *navi = [[UINavigationController alloc] initWithRootViewController:webView];

UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
[rootVC presentViewController:navi animated:YES completion:nil];
});
}

第三步

实现监听方法, 并给JavaScript发送消息通知

1
2
3
4
- (void)urlChange:(NSNotification *)notification{
[self.bridge.eventDispatcher sendAppEventWithName:@"NativeWebView"
body:@{@"url":(NSString *)notification.object}];
}

要获取self.bridge属性, 需要遵循RCTBridgeModule协议, 并加上如下代码

1
@synthesize bridge = _bridge;

最后不要忘记移除该通知

1
2
3
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"urlChange" object:nil];
}

第四步

JavaScript中接受iOS原生发送的消息通知

1
2
3
4
5
6
7
8
this.webViewListener = NativeAppEventEmitter.addListener('NativeWebView', message => {
this.handleMessageFromNative(message)
})


// 并在对应的位置销毁即可
this.webViewListener && this.webViewListener.remove()
this.webViewListener = null

原生给js发送事件

  • 上面提到的那种方式都是js调用iOS原生代码后, 用iOS原生在给js发送事件监听
  • 那么如果需要iOS原生主动给js发送监听事件呢, 类似场景: 比如在AppDelegate中给js发送事件通知有改如何实现
  • 之前遇到过这样一个需求: 需要监听APP进入后台和APP从后台进入前台的事件, 并在JavaScript中做相关操作
  • 不能像之前那种, 定义一个_bridge, 并遵循RCTBridgeModule协议, 就可以使用下面代码发送监听事件了, 加断点可以发现, 下面获取的self.bridgenil
1
2
3
4
5
-(void)applicationDidEnterBackground:(UIApplication *)application {
// 这里的self.bridge为nil
[self.bridge.eventDispatcher sendAppEventWithName:@"NativeWebView"
body:@{@"url":(NSString *)notification.object}];
}

下面先介绍一个消息监听的实例类

RCTEventEmitter

RCTEventEmitter是一个基类, 用于发出JavaScript需要监听的事件, 提供了一下属性和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@interface RCTEventEmitter : NSObject <RCTBridgeModule>

@property (nonatomic, weak) RCTBridge *bridge;

// 返回你将要发送的消息的name, 如果有未添加的, 运行时将会报错
- (NSArray<NSString *> *)supportedEvents;

// 用于发送消息事件
- (void)sendEventWithName:(NSString *)name body:(id)body;

// 在子类中重写此方法, 用于发送/移除消息通知
- (void)startObserving;
- (void)stopObserving;

// 添加监听和移除监听
- (void)addListener:(NSString *)eventName;
- (void)removeListeners:(double)count;

@end

具体的使用示例, 可继续向下看

第一步

创建一个继承自RCTEventEmitter的类, 并遵循协议<RCTBridgeModule>

1
2
3
4
5
6
7
8
9
10
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

NS_ASSUME_NONNULL_BEGIN

@interface AppEventManager : RCTEventEmitter <RCTBridgeModule>

@end

NS_ASSUME_NONNULL_END

第二步

再具体的iOS原生代码中发送消息通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#import "AppEventManager.h"

@implementation AppEventManager

// 导出该模块
RCT_EXPORT_MODULE();

// 返回sendEventWithName中监听的name, 如果有监听, 但是为在该方法中添加的, 运行时会报错
- (NSArray<NSString *> *)supportedEvents {
return @[@"DidEnterBackground", @"DidBecomeActive"];
}

// 添加观察者事件, 重写该方法中, 并在该方法中接受消息通知
- (void)startObserving {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:@"DidEnterBackground" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:@"DidBecomeActive" object:nil];
}

// 移除观察者
- (void)stopObserving {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}


- (void)applicationDidEnterBackground:(NSNotification *)notification{
// 在此处向JavaScript发送监听事件
[self sendEventWithName:@"DidEnterBackground" body: notification.object];
}

- (void)applicationDidBecomeActive:(NSNotification *)notification{
// 在此处向JavaScript发送监听事件
[self sendEventWithName:@"DidBecomeActive" body: notification.object];
}

注意的是

  • 一旦RCT_EXPORT_MODULE()声明该类是EXPORT_MODULE, 那么该类的实例已经创建好了
  • 如果你在其他地方创建这个类的实例(allocnew), 会导致,ReactNative不能正确识别该类的实例

第三步

ReactNative中引用该模块, 并添加对对应事件的监听即可

先导出iOS原生定义的模块

1
2
// AppEventManager为原生中创建的类名
const appEventMan = new NativeEventEmitter(NativeModules.AppEventManager)

使用appEventMan在对应的地方添加监听即可

1
2
3
4
5
6
7
this.didEnterBackground = appEventMan.addListener('DidEnterBackground', () => {
console.log(`APP开始进入后台---------------`)
})

this.didBecomeActive = appEventMan.addListener('DidBecomeActive', () => {
console.log(`APP开始从后台进入前台----------`)
})

但是也不要忘记在对应的地方移除该监听

1
2
3
4
5
6
7
componentWillUnmount () {
this.didEnterBackground && this.didEnterBackground.remove()
this.didEnterBackground = null

this.didBecomeActive && this.didBecomeActive.remove()
this.didBecomeActive = null
}

O(∩_∩)O哈哈~

至此, 在ReactNativeJavaScriptiOS原生的交互基本就结束了, O(∩_∩)O哈哈~