1. 程式人生 > >React Native 與原生之間的通訊(iOS)

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

本文將講述下在原生和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(CalendarManager);

@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 *[email protected][@"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 [email protected][@"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 *[email protected][@"callback ", @"test ", @" array"];
    callback(@[[NSNull null],events]);
}


//  對外提供呼叫方法,演示Promise使用
RCT_REMAP_METHOD(testPromiseEvent,
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
    NSArray *events [email protected][@"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_a