8) React Native 與原生之間的通信(iOS)

js and native.png

本文將講述下在原生和React Native之間的通信方式。方式和邏輯綜合了自己的思維方式,主要參考了React Native中文官方文檔,因為感覺它講的方式有些不妥,所以就按自己思路組織了下文。
雖然發覺一遍文章要把所有通信方式講清楚不太科學,不過把思路講講倒是可以,總體思路是,原生和React Native之間的通信方式主要包括三大部分:

  • 屬性
  • 原生模塊
  • 原生UI組件封裝

(下文主要詳細講了前兩部分,最后一部分還在研究,等待更新中)

一、屬性

React Native是從React中得到的靈感,因此基本的信息流是類似的。在React中信息是單向的。我們維護了組件層次,在其中每個組件都僅依賴于它父組件和自己的狀態。通過屬性(properties)我們將信息從上而下的從父組件傳遞到子元素。如果一個祖先組件需要自己子孫的狀態,推薦的方法是傳遞一個回調函數給對應的子元素。
屬性是最簡單的跨組件通信。因此我們需要一個方法從原生組件傳遞屬性到React Native或者從React Native到原生組件。

原生給JS傳數據,主要依靠屬性。
通過initialProperties,這個RCTRootView的初始化函數的參數來完成。
RCTRootView還有一個appProperties屬性,修改這個屬性,JS端會調用相應的渲染方法。

我們使用RCTRootView將React Natvie視圖封裝到原生組件中。RCTRootView是一個UIView容器,承載著React Native應用。同時它也提供了一個聯通原生端和被托管端的接口。

1. 從原生組件傳遞屬性到React Native(原生->rn)

通過RCTRootView的初始化函數你可以將任意屬性傳遞給React Native應用。參數initialProperties必須是NSDictionary的一個實例。這一字典參數會在內部被轉化為一個可供JS組件調用的JSON對象。
原生oc代碼:

NSArray *imageList = @[@"http://foo.com/bar1.png",
                  @"http://foo.com/bar2.png"];

NSDictionary *props = @{@"images" : imageList};

RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                      moduleName:@"ImageBrowserApp"
                                     initialProperties:props];

js處理代碼:

'use strict';
import React, { Component } from 'react';
import {
  AppRegistry,
  View,
  Image,
} from 'react-native';

class ImageBrowserApp extends Component {
  renderImage(imgURI) {
    return (
      <Image source={{uri: imgURI}} />
    );
  }
  render() {
    return (
      <View>
        {this.props.images.map(this.renderImage)}
      </View>
    );
  }
}

AppRegistry.registerComponent('ImageBrowserApp', () => ImageBrowserApp);

2. 從原生組件更新屬性到React Native(原生->rn)

RCTRootView同樣提供了一個可讀寫的屬性appProperties。在appProperties設置之后,React Native應用將會根據新的屬性重新渲染。當然,只有在新屬性和之前的屬性有區別時更新才會被觸發。

NSArray *imageList = @[@"http://foo.com/bar3.png",
                   @"http://foo.com/bar4.png"];
rootView.appProperties = @{@"images" : imageList};

你可以隨時更新屬性,但是更新必須在主線程中進行,讀取則可以在任何線程中進行。
更新屬性時并不能做到只更新一部分屬性。我們建議你自己封裝一個函數來構造屬性。
注意:目前,最頂層的RN組件(即registerComponent方法中調用的那個)的componentWillReceiveProps和componentWillUpdateProps方法在屬性更新后不會觸發。但是,你可以通過componentWillMount訪問新的屬性值。

3. 從React Native傳遞屬性到原生組件(rn->原生)

在你自定義的原生組件中通過RCT_CUSTOM_VIEW_PROPERTY宏導出屬性,就可以直接在React Native中使用,就好像它們是普通的React Native組件一樣。
更詳細的“原生UI組件封裝”部分會講到。

二、原生模塊

原生模塊是JS中也可以使用的Objective-C類。一般來說這樣的每一個模塊的實例都是在每一次通過JS bridge通信時創建的。他們可以導出任意的函數和常量給React Native。相關細節可以參閱這篇文章。

事實上原生模塊的單實例模式限制了嵌入。假設我們有一個React Native組件被嵌入了一個原生視圖,并且我們希望更新原生的父視圖。使用原生模塊機制,我們可以導出一個函數,不僅要接收預設參數,還要接收父視圖的標識。這個標識將會用來獲得父視圖的引用以更新父視圖。那樣的話,我們需要維持模塊中標識到原生模塊的映射。 雖然這個解決辦法很復雜,它仍被用在了管理所有React Native視圖的RCTUIManager類中,原生模塊同樣可以暴露已有的原生庫給JS,地理定位庫就是一個現成的例子。

警告:所有原生模塊共享同一個命名空間。創建新模塊時注意命名沖突。

在React Native中,一個“原生模塊”就是一個實現了“RCTBridgeModule”協議的Objective-C類,其中RCT是ReaCT的縮寫。

// CalendarManager.h
#import "RCTBridgeModule.h"

@interface CalendarManager : NSObject <RCTBridgeModule>
@end
為了實現RCTBridgeModule協議,你的類需要包含RCT_EXPORT_MODULE()宏。這個宏也可以添加一個參數用來指定在Javascript中訪問這個模塊的名字。如果你不指定,默認就會使用這個Objective-C類的名字。

// CalendarManager.m
@implementation CalendarManager

//  必須實現
RCT_EXPORT_MODULE();

@end

必須明確的聲明要給Javascript導出的方法,否則React Native不會導出任何方法。

1. 普通調用

OC中聲明要給Javascript導出的方法,通過RCT_EXPORT_METHOD()宏來實現:

//  對外提供調用方法(testNormalEvent為方法名,后面為參數,按順序和對應數據類型在js進行傳遞)
RCT_EXPORT_METHOD(testNormalEvent:(NSString *)name forSomething:(NSString *)thing){
    NSString *info = [NSString stringWithFormat:@"Test: %@\nFor: %@", name, thing];
    NSLog(@"%@", info);
}

現在從Javascript里可以這樣調用
首先,先導入和聲明原生模塊:

//  導入NativeModules
import { NativeModules } from 'react-native';
//  聲明CalendarManager
var CalendarManager = NativeModules.CalendarManager;

然后,再進行調用:

//  調用原生方法
CalendarManager.addEvent('調用testNormalEvent方法', '測試普通調用')

**注意: **
導出到Javascript的方法名是Objective-C的方法名的第一個部分。React Native還定義了一個RCT_REMAP_METHOD()宏,它可以指定Javascript方法名。當許多方法的第一部分相同的時候用它來避免在Javascript端的名字沖突。
橋接到Javascript的方法返回值類型必須是void。React Native的橋接操作是異步的,所以要返回結果給Javascript,你必須通過回調或者觸發事件來進行。

參數類型

RCT_EXPORT_METHOD 支持所有標準JSON類型,包括:

  • string (NSString)
  • number (NSInteger, float, double, CGFloat, NSNumber)
  • boolean (BOOL, NSNumber)
  • array (NSArray) 包含本列表中任意類型
  • object (NSDictionary) 包含string類型的鍵和本列表中任意類型的值
  • function (RCTResponseSenderBlock)

除此以外,任何RCTConvert類支持的的類型也都可以使用(參見RCTConvert了解更多信息)。RCTConvert還提供了一系列輔助函數,用來接收一個JSON值并轉換到原生Objective-C類型或類。

特殊參數類型處理(Date對象)

在我們的CalendarManager例子里,我們需要把事件的時間交給原生方法。我們不能在橋接通道里傳遞Date對象,所以需要把日期轉化成字符串或數字來傳遞。我們可以這么實現原生函數:

RCT_EXPORT_METHOD(testDateEventOne:(NSString *)name forSomething:(NSString *)thing data:(NSNumber*)secondsSinceUnixEpoch)
{
  NSDate *date = [RCTConvert NSDate:secondsSinceUnixEpoch];
}

或者這樣:

RCT_EXPORT_METHOD(testDateEventTwo:(NSString *)name forSomething:(NSString *)thing date:(NSString *)ISO8601DateString)
{
  NSDate *date = [RCTConvert NSDate:ISO8601DateString];
}

不過我們可以依靠自動類型轉換的特性,跳過手動的類型轉換,而直接這么寫:

RCT_EXPORT_METHOD(testDateEvent:(NSString *)name forSomething:(NSString *)thing date:(NSDate *)date)
{
  // Date is ready to use!
}

在Javascript既可以這樣:

var date = new Date();

 // 把日期以unix時間戳形式傳遞
CalendarManager.testDateEvent('調用testDateEvent方法', '測試date格式', date.getTime());

也可以這樣:
// 把日期以ISO-8601的字符串形式傳遞
CalendarManager.testDateEvent('調用testDateEvent方法', '測試date格式', date.toISOString()); 

兩個值都會被轉換為正確的NSDate類型。但如果提供一個不合法的值,譬如一個Array,則會產生一個“紅屏”報錯信息。

dictionary參數

隨著CalendarManager.addEvent方法變得越來越復雜,參數的個數越來越多,其中有一些可能是可選的參數。在這種情況下我們應該考慮修改我們的API,用一個dictionary來存放所有的事件參數,像這樣:

//  對外提供調用方法,為了演示事件傳入屬性字段
RCT_EXPORT_METHOD(testDictionaryEvent:(NSString *)name details:(NSDictionary *) dictionary)
{
    NSString *location = [RCTConvert NSString:dictionary[@"thing"]];
    NSDate *time = [RCTConvert NSDate:dictionary[@"time"]];
    NSString *description=[RCTConvert NSString:dictionary[@"description"]];
    
    NSString *info = [NSString stringWithFormat:@"Test: %@\nFor: %@\nTestTime: %@\nDescription: %@",name,location,time,description];
    NSLog(@"%@", info);
}

然后在JS里這樣調用:

CalendarManager.testDictionaryEvent('調用addEventMoreDetails方法', {
              thing:'測試字典(字段)格式',
              time:date.getTime(),
              description:'就是這么簡單~'
            })

注意: 關于數組和映射
Objective-C并沒有提供確保這些結構體內部值的類型的方式。你的原生模塊可能希望收到一個字符串數組,但如果JavaScript在調用的時候提供了一個混合number和string的數組,你會收到一個NSArray,里面既有NSNumber也有NSString。對于數組來說,RCTConvert提供了一些類型化的集合,譬如NSStringArray或者UIColorArray,你可以用在你的函數聲明中。對于映射而言,開發者有責任自己調用RCTConvert的輔助方法來檢測和轉換值的類型。

2. 回調函數

原生模塊還支持一種特殊的參數——回調函數。它提供了一個函數來把返回值傳回給JavaScript。

//  對外提供調用方法,演示Callback
RCT_EXPORT_METHOD(testCallbackEvent:(RCTResponseSenderBlock)callback)
{
    NSArray *events=@[@"callback ", @"test ", @" array"];
    callback(@[[NSNull null],events]);
}

RCTResponseSenderBlock只接受一個參數——傳遞給JavaScript回調函數的參數數組。在上面這個例子里我們用Node.js的常用習慣:第一個參數是一個錯誤對象(沒有發生錯誤的時候為null),而剩下的部分是函數的返回值。

CalendarManager.testCallbackEvent((error, events) => {
  if (error) {
    console.error(error);
  } else {
    this.setState({events: events});
  }
})

原生模塊通常只應調用回調函數一次。但是,它可以保存callback并在將來調用。這在封裝那些通過“委托函數”來獲得返回值的iOS API時最為常見。RCTAlertManager中就屬于這種情況。
如果你想傳遞一個更接近Error類型的對象給Javascript,可以用RCTUtils.h提供的RCTMakeError函數。現在它僅僅是發送了一個和Error結構一樣的dictionary給Javascript,但我們考慮在將來版本里讓它產生一個真正的Error對象。

3. Promises

(譯注:這一部分涉及到較新的js語法和特性,不熟悉的讀者建議先閱讀ES6的相關書籍和文檔。)

原生模塊還可以使用promise來簡化代碼,搭配ES2016(ES7)標準的async/await語法則效果更佳。如果橋接原生方法的最后兩個參數是RCTPromiseResolveBlock和RCTPromiseRejectBlock,則對應的JS方法就會返回一個Promise對象。

我們把上面的代碼用promise來代替回調進行重構:

//  對外提供調用方法,演示Promise使用
RCT_REMAP_METHOD(testPromiseEvent,
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
    NSArray *events =@[@"Promise ",@"test ",@" array"];
    if (events) {
        resolve(events);
    } else {
        NSError *error=[NSError errorWithDomain:@"我是Promise回調錯誤信息..." code:101 userInfo:nil];
        reject(@"no_events", @"There were no events", error);
    }
}

現在JavaScript端的方法會返回一個Promise。這樣你就可以在一個聲明了async的異步函數內使用await關鍵字來調用,并等待其結果返回。(雖然這樣寫著看起來像同步操作,但實際仍然是異步的,并不會阻塞執行來等待)。

//獲取Promise對象處理
async updateEvents(){
    console.log('updateEvents');
    try{
        var events=await CalendarManager.testPromiseEvent();
        this.setState({events});
    }catch(e){
        console.error(e);
    }
}

//  在對應位置調用
this.updateEvents;

4. 多線程

原生模塊不應對自己被調用時所處的線程做任何假設。React Native在一個獨立的串行GCD隊列中調用原生模塊的方法,但這屬于實現的細節,并且可能會在將來的版本中改變。
通過實現方法- (dispatch_queue_t)methodQueue,原生模塊可以指定自己想在哪個隊列中被執行。具體來說,如果模塊需要調用一些必須在主線程才能使用的API,那應當這樣指定:

- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}

類似的,如果一個操作需要花費很長時間,原生模塊不應該阻塞住,而是應當聲明一個用于執行操作的獨立隊列。舉個例子,RCTAsyncLocalStorage模塊創建了自己的一個queue,這樣它在做一些較慢的磁盤操作的時候就不會阻塞住React本身的消息隊列:

- (dispatch_queue_t)methodQueue
{
  return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}

指定的methodQueue會被你模塊里的所有方法共享。如果你的方法中“只有一個”是耗時較長的(或者是由于某種原因必須在不同的隊列中運行的),你可以在函數體內用dispatch_async方法來在另一個隊列執行,而不影響其他方法:

RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 在這里執行長時間的操作
    ...
    // 你可以在任何線程/隊列中執行回調函數
    callback(@[...]);
  });
}

注意: 在模塊之間共享分發隊列
methodQueue方法會在模塊被初始化的時候被執行一次,然后會被React Native的橋接機制保存下來,所以你不需要自己保存隊列的引用,除非你希望在模塊的其它地方使用它。但是,如果你希望在若干個模塊中共享同一個隊列,則需要自己保存并返回相同的隊列實例;僅僅是返回相同名字的隊列是不行的。

這里有一點需要注意,若是要對原生的UI進行操作,則必須在主線程中進行,即影響原生UI的方法要調用:
dispatch_async(dispatch_get_main_queue(), ^{
//Update UI in UI thread here
});
也可實現methodQueue方法,將全部方法在主線程中執行(不推薦)。

5. 導出常量

原生模塊可以導出一些常量,這些常量在JavaScript端隨時都可以訪問。用這種方法來傳遞一些靜態數據,可以避免通過bridge進行一次來回交互。

- (NSDictionary *)constantsToExport
{
  return @{ @"firstDayOfTheWeek": @"Monday" };
}

Javascript端可以隨時同步地訪問這個數據:

console.log(CalendarManager.firstDayOfTheWeek);

但是注意這個常量僅僅在初始化的時候導出了一次,所以即使你在運行期間改變constantToExport返回的值,也不會影響到JavaScript環境下所得到的結果。

6. 枚舉常量

用NS_ENUM定義的枚舉類型必須要先擴展對應的RCTConvert方法才可以作為函數參數傳遞。

假設我們要導出如下的NS_ENUM定義:

typedef NS_ENUM(NSInteger, UIStatusBarAnimation) {
    UIStatusBarAnimationNone,
    UIStatusBarAnimationFade,
    UIStatusBarAnimationSlide,
};

你需要這樣來擴展RCTConvert類:

@implementation RCTConvert (StatusBarAnimation)
  RCT_ENUM_CONVERTER(UIStatusBarAnimation, (@{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
                                               @"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
                                               @"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide)}),
                      UIStatusBarAnimationNone, integerValue)
@end

接著你可以這樣定義方法并且導出enum值作為常量:

- (NSDictionary *)constantsToExport
{
  return @{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
            @"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
            @"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide) }
};

RCT_EXPORT_METHOD(updateStatusBarAnimation:(UIStatusBarAnimation)animation
                                completion:(RCTResponseSenderBlock)callback)

你的枚舉現在會用上面提供的選擇器進行轉換(上面的例子中是integerValue),然后再傳遞給你導出的函數。

7. 給Javascript發送事件

即使沒有被JavaScript調用,本地模塊也可以給JavaScript發送事件通知。最直接的方式是使用eventDispatcher:

#import "RCTBridge.h"
#import "RCTEventDispatcher.h"

@implementation CalendarManager

@synthesize bridge = _bridge;

//  進行設置發送事件通知給JavaScript端
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
    NSString *name = [notification userInfo][@"name"];
    [self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder"
                                                 body:@{@"name": name}];
}

@end

在JavaScript中可以這樣訂閱事件:

import { NativeAppEventEmitter } from 'react-native';

var subscription = NativeAppEventEmitter.addListener(
  'EventReminder',
  (reminder) => console.log(reminder.name)
);
...
// 千萬不要忘記忘記取消訂閱, 通常在componentWillUnmount函數中實現。
subscription.remove();

8. 從Swift導出

Swift不支持宏,所以從Swift向React Native導出類和函數需要多做一些設置,但是大致與Objective-C是相同的。

假設我們已經有了一個一樣的CalendarManager,不過是用Swift實現的類:

// CalendarManager.swift

@objc(CalendarManager)
class CalendarManager: NSObject {

  @objc func addEvent(name: String, location: String, date: NSNumber) -> Void {
    // Date is ready to use!
  }

}

注意: 你必須使用@objc標記來確保類和函數對Objective-C公開。
接著,創建一個私有的實現文件,并將必要的信息注冊到React Native中。

// CalendarManagerBridge.m
#import "RCTBridgeModule.h"

@interface RCT_EXTERN_MODULE(CalendarManager, NSObject)

RCT_EXTERN_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)date)

@end

請注意,一旦你在IOS中混用2種語言, 你還需要一個額外的橋接頭文件,稱作“bridging header”,用來導出Objective-C文件給Swift。如果你是通過Xcode菜單中的File>New File來創建的Swift文件,Xcode會自動為你創建這個頭文件。在這個頭文件中,你需要引入RCTBridgeModule.h。

// CalendarManager-Bridging-Header.h
#import "RCTBridgeModule.h"

也可以使用RCT_EXTERN_REMAP_MODULE和RCT_EXTERN_REMAP_METHOD來改變導出模塊和方法的JavaScript調用名稱。


這一part的編寫主要參考自官方中文文檔:原生模塊,因為覺得官方的講述不太清楚,所以自己進行了部分修改,結合了江清清的demo,為了更清晰的展示原生模塊和RN之間的交互,將RN部分和原生部分在同一頁面中展示,demo如下(上部分為RN頁面,下部分為原生view):
demo暫時上傳至百度云:https://pan.baidu.com/s/1hrMVNta

rn&原生test.gif

去掉原生UI,部分(可運行)代碼如下:
原生OC代碼:

#import "CalendarManager.h"
#import "RCTConvert.h"
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"

@implementation CalendarManager

@synthesize bridge=_bridge;

//  默認名稱
RCT_EXPORT_MODULE()

/**
//  指定執行模塊里的方法所在的隊列
- (dispatch_queue_t)methodQueue
{
    return dispatch_get_main_queue();
}
*/

//  在完整demo中才有用到,用于更新原生UI
- (void)showInfo:(NSString *)info
{
    //  更新UI操作在主線程中執行
    dispatch_async(dispatch_get_main_queue(), ^{
        //Update UI in UI thread here
        
        [[NSNotificationCenter defaultCenter] postNotificationName:@"react_native_test" object:nil userInfo:@{@"info": info}];
    });
}

- (void)showDate:(NSDate *)date withName:(NSString *)name forSomething:(NSString *)thing
{
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init] ;
    [formatter setDateFormat:@"yyyy-MM-dd"];
    
    NSString *info = [NSString stringWithFormat:@"Test: %@\nFor: %@\nTestTime: %@", name, thing,[formatter stringFromDate:date]];
    NSLog(@"%@", info);
}


//  對外提供調用方法
RCT_EXPORT_METHOD(testNormalEvent:(NSString *)name forSomething:(NSString *)thing)
{
    NSString *info = [NSString stringWithFormat:@"Test: %@\nFor: %@", name, thing];
    NSLog(@"%@", info);
}

//  對外提供調用方法,為了演示事件時間格式化 secondsSinceUnixEpoch
RCT_EXPORT_METHOD(testDateEventOne:(NSString *)name forSomething:(NSString *)thing data:(NSNumber*)secondsSinceUnixEpoch)
{
    NSDate *date = [RCTConvert NSDate:secondsSinceUnixEpoch];
    [self showDate:date withName:name forSomething:thing];
}

//  對外提供調用方法,為了演示事件時間格式化 ISO8601DateString
RCT_EXPORT_METHOD(testDateEventTwo:(NSString *)name forSomething:(NSString *)thing date:(NSString *)ISO8601DateString)
{
    NSDate *date = [RCTConvert NSDate:ISO8601DateString];
    [self showDate:date withName:name forSomething:thing];
}

//  對外提供調用方法,為了演示事件時間格式化 自動類型轉換
RCT_EXPORT_METHOD(testDateEvent:(NSString *)name forSomething:(NSString *)thing date:(NSDate *)date)
{
    [self showDate:date withName:name forSomething:thing];
}

//  對外提供調用方法,為了演示事件傳入屬性字段
RCT_EXPORT_METHOD(testDictionaryEvent:(NSString *)name details:(NSDictionary *) dictionary)
{
    NSString *location = [RCTConvert NSString:dictionary[@"thing"]];
    NSDate *time = [RCTConvert NSDate:dictionary[@"time"]];
    NSString *description=[RCTConvert NSString:dictionary[@"description"]];
    
    NSString *info = [NSString stringWithFormat:@"Test: %@\nFor: %@\nTestTime: %@\nDescription: %@",name,location,time,description];
    NSLog(@"%@", info);
}


//  對外提供調用方法,演示Callback
RCT_EXPORT_METHOD(testCallbackEvent:(RCTResponseSenderBlock)callback)
{
    NSArray *events=@[@"callback ", @"test ", @" array"];
    callback(@[[NSNull null],events]);
}


//  對外提供調用方法,演示Promise使用
RCT_REMAP_METHOD(testPromiseEvent,
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
    NSArray *events =@[@"Promise ",@"test ",@" array"];
    if (events) {
        resolve(events);
    } else {
        NSError *error=[NSError errorWithDomain:@"我是Promise回調錯誤信息..." code:101 userInfo:nil];
        reject(@"no_events", @"There were no events", error);
    }
}


//  對外提供調用方法,演示Thread使用
RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{
 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 在后臺執行耗時操作
        // You can invoke callback from any thread/queue
        callback(@[[NSNull null],@"耗時操作執行完成..."]);
    });
}


//  進行設置封裝常量給JavaScript進行調用
-(NSDictionary *)constantsToExport
{
    // 此處定義的常量為js訂閱原生通知的通知名
    return @{@"receiveNotificationName":@"receive_notification_test"};
}



//  開始訂閱通知事件
RCT_EXPORT_METHOD(startReceiveNotification:(NSString *)name)
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(calendarEventReminderReceived:)
                                                 name:name
                                               object:nil];
}

//  進行設置發送事件通知給JavaScript端(這里需要注意,差一個觸發通知點,需自己想辦法加上,或者看完整demo)
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
    NSString *name = [notification userInfo][@"name"];
    [self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder"
                                                 body:@{@"name": name}];
}

@end

js代碼:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */
import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  ScrollView,
  TouchableHighlight
} from 'react-native';

import { 
  NativeModules,
  NativeAppEventEmitter 
} from 'react-native';

var subscription;
var CalendarManager = NativeModules.CalendarManager;

class CustomButton extends React.Component {
  render() {
    return (
      <TouchableHighlight
        style={{padding: 8, backgroundColor:this.props.backgroundColor}}
        underlayColor="#a5a5a5"
        onPress={this.props.onPress}>
        <Text>{this.props.text}</Text>
      </TouchableHighlight>
    );
  }
}

class ModulesDemo extends Component {

  constructor(props){
    super(props);
    this.state={
        events:'',
        notice:'',
    }
  }

  componentDidMount(){
    console.log('開始訂閱通知...');
    this.receiveNotification();

    subscription = NativeAppEventEmitter.addListener(
         'EventReminder',
          (reminder) => {
            console.log('通知信息:'+reminder.name)
            this.setState({notice:reminder.name});
          }
         );
  }

  receiveNotification(){
    //  CalendarManager.receiveNotificationName 為原生定義常量
    CalendarManager.startReceiveNotification(CalendarManager.receiveNotificationName);
  }

  componentWillUnmount(){
     subscription.remove();
  }

  //獲取Promise對象處理
  async updateEvents(){
    console.log('updateEvents');
    try{
        var events=await CalendarManager.testPromiseEvent();
        this.setState({events});
    }catch(e){
        console.error(e);
    }
  }

  render() {

    var date = new Date();

    return (
      <ScrollView>
      <View>
        <Text style={{fontSize: 16, textAlign: 'center', margin: 10}}>
          RN模塊
        </Text>

        <View style={{borderWidth: 1,borderColor: '#000000'}}>
        <Text style={{fontSize: 15, margin: 10}}>
          普通調用原生模塊方法
        </Text>

        <CustomButton text="調用testNormalEvent方法-普通"
            backgroundColor= "#FF0000"
            onPress={()=>CalendarManager.testNormalEvent('調用testNormalEvent方法', '測試普通調用')}
        />

        <CustomButton text="調用testDateEvent方法-日期處理"
            backgroundColor= "#FF7F00"
            onPress={()=>CalendarManager.testDateEvent('調用testDateEvent方法', '測試date格式',date.getTime())}
        />
        <CustomButton text="調用testDictionaryEvent方法-字典"
            backgroundColor= "#FFFF00"
            onPress={()=>CalendarManager.testDictionaryEvent('調用testDictionaryEvent方法', {
              thing:'測試字典(字段)格式',
              time:date.getTime(),
              description:'就是這么簡單~'
            })}
        />
        </View>

        <View>
        <Text style={{fontSize: 15, margin: 10}}>
          'Callback返回數據:{this.state.events}
        </Text>

        <CustomButton text="調用原生模塊testCallbackEvent方法-Callback"
            backgroundColor= "#00FF00"
            onPress={()=>CalendarManager.testCallbackEvent((error,events)=>{
                if(error){
                  console.error(error);
                }else{
                  this.setState({events:events,});
                }
              }
            )}
        />
        <CustomButton text="調用原生模塊testPromiseEvent方法-Promise"
            backgroundColor= "#00FFFF"
            onPress={()=>this.updateEvents()}
        />
        </View>
        
        <View style={{borderWidth: 1,borderColor: '#000000'}}>
        <Text style={{fontSize: 15, margin: 10}}>
          原生調js,接收信息:{this.state.notice}
        </Text>
        </View>

      </View>
      </ScrollView>
    );
  }
}

AppRegistry.registerComponent('NativeTest', () => ModulesDemo);

三、原生UI組件封裝

在如今的App中,已經有成千上萬的原生UI部件了——其中的一些是平臺的一部分,另一些可能來自于一些第三方庫,而且可能你自己還收藏了很多。React Native已經封裝了大部分最常見的組件,譬如ScrollView和TextInput,但不可能封裝全部組件。而且,說不定你曾經為自己以前的App還封裝過一些組件,React Native肯定沒法包含它們。幸運的是,在React Naitve應用程序中封裝和植入已有的組件非常簡單。

簡單的說,就是React Native自身的組件肯定不夠我們用,我們需要學會把原生的控件進行封裝,以供RN模塊使用。

看著這篇東西的字數越來越多,覺得讀者應該很難讀的下篇幅如此長的文章,一口氣吃不了那么多,本人其實也寫得暈暈的(望體諒),所以這一部分就暫時放著,這部分更詳細的了解可以先參考以下兩篇文章:
中文官方:原生UI組件
React Native 原生UI組件--簡書


ps:React Native 與原生之間的通信其實也需要去了解下原理,下面推薦幾篇關于實現原理的文章:
淺析ReactNative之通信機制(一)
bang's blog : React Native通信機制詳解

因為沒繼續這方面的工作所以好久沒更新了,可能代碼因為rn的更新會有些問題,最好更新下pod的版本,看看官方文檔,看到評論里有相應的討論,出現問題的朋友最好也看看評論哈哈,可能有解決辦法?───O(≧?≦)O────?

已有的成果如下:
1) React Native 簡介與入門
2) React Native 環境搭建和創建項目(Mac)
3) React Native 開發之IDE
4) React Native 入門項目與解析
5) React Native 相關JS和React基礎

6) React Native 組件生命周期(ES6)
7) React Native 集成到原生項目(iOS)
8) React Native 與原生之間的通信(iOS)
9) React Native 封裝原生UI組件(iOS)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容