ReactNative之iOS原生和JavaScript的交互

js-ios

原文博客地址: www.titanjun.top/ReactNative…html

ReactNative開發中, 在JavaScript語法沒法實現的時候會涉及到一些原生開發, 既然是混合開發就會涉及到一些iOSReactNative之間通信的問題, 這裏就涉及到兩種方式:react

  • RN調用原生的方法, 給原生髮送數據
  • 原生給RN回傳數據, 或者給RN發送通知
  • 下面就簡單記錄下這兩種方式的實現

JS調用原生

  • 這裏要講的交互場景是JS調用原生方法,最後由原生方法將結果回調到JS裏面
  • react-native是在原生的基礎上,將接口調用統一爲js
  • 也就是說,react-native調起原生的能力很是重要

js調用模塊

在原生須要建立一個繼承自NSObject的類(模塊)ios

#import <Foundation/Foundation.h>
// 須要導入頭文件
#import <React/RCTBridgeModule.h>

// 必須遵循RCTBridgeModule協議
@interface AppEventMoudle : NSObject <RCTBridgeModule>

@end
複製代碼

AppEventMoudle.m文件件中須要導出改模塊, 並將建立的方法導出web

#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]);
        }
    });
}
複製代碼

上面代碼須要注意的是react-native

  • 橋接到Javascript的方法返回值類型必須是void
  • React Native的橋接操做是異步的,在queue裏面異步執行,因此若是要返回結果給Javascript,就必須經過回調或者觸發事件來進行
  • 這裏的回調對應於iOS端就是經過block來回調的

RCTBridge

  • RCTBridge能夠說是一個封裝類,封裝了RCTCxxBridge
  • 咱們先看這個文件提供的一些變量和方法
  • RCTModuleClasses: 主要儲存的是咱們註冊的module, 全部用宏RCT_EXPORT_MODULE()註冊的module都會存入這個變量.
  • RCTGetModuleClasses: 獲取RCTModuleClasses裏面全部註冊的module
  • RCTBridgeModuleNameForClass: 從一個類獲取這個類的名字
  • RCTVerifyAllModulesExported: 驗證咱們所寫的全部遵照RCTBridgeModule協議的類是否都在咱們的管理中

RCTResponseSenderBlock

typedef void (^RCTResponseSenderBlock)(NSArray *response);
複製代碼
  • RCTResponseSenderBlockRCTBridgeModule裏面提供的block
  • 這個block接受一個數組參數, 表明原生方法的返回結果

線程問題

  • js代碼的執行是在js線程裏面,原生模塊的執行默認是在一個串行的queue裏面異步執行的
  • 對於原生模塊的執行來講,默認一個串行的queue是不夠的,咱們有時候須要指定模塊全部任務執行所在的queue
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的變化, 併發送通知
- (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調用的方法中接受上述代碼中發送的通知, 以下數組

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發送消息通知微信

- (void)urlChange:(NSNotification *)notification{
  [self.bridge.eventDispatcher sendAppEventWithName:@"NativeWebView"
                                               body:@{@"url":(NSString *)notification.object}];
}
複製代碼

要獲取self.bridge屬性, 須要遵循RCTBridgeModule協議, 並加上以下代碼併發

@synthesize bridge = _bridge;
複製代碼

最後不要忘記移除該通知app

- (void)dealloc {
  [[NSNotificationCenter defaultCenter] removeObserver:self name:@"urlChange" object:nil];
}
複製代碼

第四步

JavaScript中接受iOS原生髮送的消息通知異步

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
-(void)applicationDidEnterBackground:(UIApplication *)application {
    // 這裏的self.bridge爲nil
  [self.bridge.eventDispatcher sendAppEventWithName:@"NativeWebView"
                                               body:@{@"url":(NSString *)notification.object}];
}
複製代碼

下面先介紹一個消息監聽的實例類

RCTEventEmitter

RCTEventEmitter是一個基類, 用於發出JavaScript須要監聽的事件, 提供了一下屬性和方法

@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>

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

NS_ASSUME_NONNULL_BEGIN

@interface AppEventManager : RCTEventEmitter <RCTBridgeModule>

@end

NS_ASSUME_NONNULL_END
複製代碼

第二步

再具體的iOS原生代碼中發送消息通知

#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原生定義的模塊

// AppEventManager爲原生中建立的類名
const appEventMan = new NativeEventEmitter(NativeModules.AppEventManager)
複製代碼

使用appEventMan在對應的地方添加監聽便可

this.didEnterBackground = appEventMan.addListener('DidEnterBackground', () => {
	console.log(`APP開始進入後臺---------------`)
})

this.didBecomeActive = appEventMan.addListener('DidBecomeActive', () => {
	console.log(`APP開始從後臺進入前臺----------`)
})
複製代碼

可是也不要忘記在對應的地方移除該監聽

componentWillUnmount () {
	this.didEnterBackground && this.didEnterBackground.remove()
	this.didEnterBackground = null
	
	this.didBecomeActive && this.didBecomeActive.remove()
	this.didBecomeActive = null
}
複製代碼

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


歡迎您掃一掃下面的微信公衆號,訂閱個人博客!

微信公衆號
相關文章
相關標籤/搜索