1. 程式人生 > >React-Native之IOS本地模組的應用實踐分享(僅此一篇足以...)

React-Native之IOS本地模組的應用實踐分享(僅此一篇足以...)

這裡寫圖片描述

前言

React-Native從誕生至今,火熱程度已經不言而喻,在不斷的框架迭代過程中,RN也提供了豐富的元件,以供開發者使用,但是在實際應用中,我們可能需要更為豐富的互動元件,但是RN中又沒有及時提供,這時候我們就需要使用RN的本地模組,本地模組即可以使用JS呼叫Native,也可以使用Native呼叫JS, 並傳遞各種引數,實現完整功能,接下來我們看看具體的使用方法,方便大家參考學習,如果想了解更多,我們可以查閱官方的文件。

下面文章中,我會經常用到RN,即為React Native的縮寫,請見諒。

類庫及模組元件

1.RCTBridgeModule

  • RCTBridgeModule

在React Native中,如果實現一個原生模組,需要實現RCTBridgeModule”協議,其中RCT就是ReaCT的縮寫。

  • RCT_EXPORT_MODULE()

如果我們實現了RCTBridgeModule協議,我們的類需要包含RCT_EXPORT_MODULE()巨集。這個巨集也可以新增一個引數用來指定在Javascript中訪問這個模組的名字。如果你不指定,預設就會使用這個Objective-C類的名字。

  • RCT_EXPORT_METHOD()

與此同時我們需要宣告RCT_EXPORT_METHOD()巨集來實現要給Javascript匯出的方法,否則React Native不會匯出任何方法。

  • RCT_REMAP_METHOD()

React Native還定義了一個RCT_REMAP_METHOD()巨集,它可以指定Javascript方法名, 當許多方法的第一部分相同的時候用它來避免在Javascript端的名字衝突。

2.NativeModules

在Javascript中如果想呼叫Native的方法,需要使用RN提供的NativeModules模組,具體程式碼例項中會給出。

3.NativeAppEventEmitter

這是一個事件監聽處理的方法,應用在JS中,可以監聽到native呼叫的事件以及引數。

起步(Hello World…)

現在開始我們簡單來寫一個例子,就是利用JS呼叫Native方法,並列印Hello World。

建立native檔案(OC)

HelloWorld.h

#import <React/RCTBridgeModule.h>

@interface HelloWorld : NSObject<RCTBridgeModule>

@end

這裡主要實現一個協議RCTBridgeModule, IOS中的協議和Java中的介面概念非常相似。這個協議由RN提供。在RN 0.40版本以後,引入方式發生了改變

#import "HelloWorld.h"

@implementation HelloWorld
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(sayHello:(NSString *) msg)
{
  NSLog(@"列印Hello World%@",msg);
}
@end

.m檔案中實現了兩個巨集,用於宣告javascript呼叫的方法。RCT_EXPORT_MODULE()這個檔案中,我們可以不指定名字,會預設使用class名字。

JS檔案:

import React, { Component } from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    View,
        Button,
        NativeModules
} from 'react-native';

export default class NativeModuleDemo extends Component {
        onPress(){
                let HelloWorld = NativeModules.HelloWorld;
                HelloWorld.sayHello('Hello World你好');
        }
    render() {
            return (
          <View style={{marginTop:40}}>
            <Button onPress={this.onPress.bind(this)} title="Say Hello" />
          </View>
            );
    }
}

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

此檔案是JS檔案,首先要引入NativeModules模組,用於呼叫native方法。
核心程式碼主要是下面部分,獲取native模組,然後呼叫sayHello方法,傳遞引數。

let HelloWorld = NativeModules.HelloWorld;
HelloWorld.sayHello('Hello World你好');

特殊引數的傳遞

上面的例子中我們傳遞了一個簡單的string型別,但是實際應用中會有多種複雜的型別,比如列舉,比如日期時間型別等等,為此我們看看RN Bridge給我們提供了哪些型別引數:

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

奇怪,沒有時間和列舉型別,那該怎麼辦?

還好,在RN中提供了一個RCTConvert庫,幫助我們進行型別轉換,具體來看看程式碼:

建立MyDate.h

#import <React/RCTBridgeModule.h>
#import <React/RCTConvert.h>
@interface MyDate : NSObject<RCTBridgeModule>

@end

這裡主要引用了RCTConvert類庫,進行轉換。

實現MyDate.m

#import "MyDate.h"
@implementation MyDate
RCT_EXPORT_MODULE();
RCT_REMAP_METHOD(printDate, date1:(nonnull NSNumber *)d1 date2:(nonnull NSNumber *)d2)
{
  NSDate* dt1 = [RCTConvert NSDate:d1];
  NSDate* dt2 = [RCTConvert NSDate:d2];
  NSComparisonResult result = [dt1 compare:dt2];
  switch(result){
    case NSOrderedAscending:
    {
       NSLog(@"比較結果%@",@"開始時間小於結束時間");
    }
    case NSOrderedDescending:
    {
      NSLog(@"比較結果%@",@"開始時間大於結束時間");
    }
  }

}
@end

JS檔案實現:


import React, { Component } from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    View,
        Button,
    DatePickerIOS,
        NativeModules
} from 'react-native';

export default class NativeModuleDemo extends Component {
    constructor(){
      super();
      this.state = {startDate: new Date(), endDate: new Date()};
    }
        onPress(){
                let HelloWorld = NativeModules.HelloWorld;
                HelloWorld.sayHello('Hello World你好');
        }
        onPressDateValidation() {
          var myDate = NativeModules.MyDate;
          myDate.printDate(this.state.startDate.getTime(), this.state.endDate.getTime());
    }
        onStartDateChange(date) {
          this.setState({startDate: date});
    }
        onEndDateChange(date) {
          this.setState({endDate: date});
    }
    render() {
            return (
          <View style={{marginTop:40}}>
            <DatePickerIOS
              date={this.state.startDate}
              mode='date'
              onDateChange={this.onStartDateChange.bind(this)} />
            <DatePickerIOS
              date={this.state.endDate}
              mode='date'
              onDateChange={this.onEndDateChange.bind(this)} />
            <Button onPress={this.onPressDateValidation.bind(this)} title="Say Hello" />

          </View>
            );
    }
}

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

這裡RCTConvert可以轉換很多型別,比如字典型別:

NSString* str = [RCTConvert NSString:details[@"key"]];

回撥函式的使用

套用上邊的例子,我們只需要更改.m實現檔案即可:

#import "MyDate.h"
@implementation MyDate
RCT_EXPORT_MODULE();
RCT_REMAP_METHOD(printDate, date1:(nonnull NSNumber *)d1 date2:(nonnull NSNumber *)d2  event:(RCTResponseSenderBlock)callback)
{
  NSDate* dt1 = [RCTConvert NSDate:d1];
  NSDate* dt2 = [RCTConvert NSDate:d2];
  NSComparisonResult result = [dt1 compare:dt2];
  switch(result){
    case NSOrderedAscending:
    {
       NSLog(@"比較結果%@",@"開始時間小於結束時間");
    }
    case NSOrderedDescending:
    {
      NSLog(@"比較結果%@",@"開始時間大於結束時間");
    }
  }
  NSArray *events = [NSArray arrayWithObjects:@"測試結果",nil];
  callback(@[[NSNull null], events]);
}
@end

這裡主要應用了:

event:(RCTResponseSenderBlock)callback
....
callback(@[[NSNull null], events]);

callback返回的是一個數組,這裡要切記,第一個為錯誤資訊。

JS實現:

onPressDateValidation() {
          var myDate = NativeModules.MyDate;
          myDate.printDate(this.state.startDate.getTime(), this.state.endDate.getTime(), (err, result) => {
            alert(result);
      });
    }

效果:

這裡寫圖片描述

Promises回撥處理

原生模組還可以使用promise來簡化程式碼,搭配ES2016(ES7)標準的async/await語法則效果會更好理解,而且更為簡單。

主要原理為最後兩個引數是RCTPromiseResolveBlock和RCTPromiseRejectBlock,則對應的JS方法就會返回一個Promise物件。

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

套上上邊程式碼,我們更改MyDate.m檔案:

#import "MyDate.h"
@implementation MyDate
RCT_EXPORT_MODULE();
RCT_REMAP_METHOD(printDate, date1:(nonnull NSNumber *)d1 date2:(nonnull NSNumber *)d2  resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  NSDate* dt1 = [RCTConvert NSDate:d1];
  NSDate* dt2 = [RCTConvert NSDate:d2];
  NSComparisonResult result = [dt1 compare:dt2];
  switch(result){
    case NSOrderedAscending:
    {
       NSLog(@"比較結果%@",@"開始時間小於結束時間");
    }
    case NSOrderedDescending:
    {
      NSLog(@"比較結果%@",@"開始時間大於結束時間");
    }
  }
  NSArray *events = [NSArray arrayWithObjects:@"測試結果",nil];
  if (events) {
    resolve(events);
  } else {
    reject(@"",@"",nil);
  }
}
@end

JS檔案更改:

async onPressDateValidation() {
          var myDate = NativeModules.MyDate;
          var result = await myDate.printDate(this.state.startDate.getTime(), this.state.endDate.getTime());
          alert(result);
    }

Native單向呼叫JS方法

新建MyCallBack.h MyCallBack.m檔案, 這裡引入RCTEventDispatcher 類庫用於處理事件回撥。

MyCallBack.m

#import "MyCallBack.h"
#import <React/RCTEventDispatcher.h>

@implementation MyCallBack

RCT_EXPORT_MODULE();
@synthesize bridge = _bridge;
RCT_REMAP_METHOD(checkCallBack, str:(NSString *)str)
{
  [self.bridge.eventDispatcher sendAppEventWithName:@"EventCallBack" body:@{@"name": @"sad"}];
}
@end

首先需要用到synthesize方法,同步一個引數變數_bridge,主要程式碼如下:

 [self.bridge.eventDispatcher sendAppEventWithName:@"EventCallBack" body:@{@"name": @"sad"}];

sendAppEventWithName必須和JS中保持一致。

JS中註冊監聽事件:

 this.state = {startDate: new Date(), endDate: new Date()};
            var subscription = NativeAppEventEmitter.addListener(
                'EventCallBack',
                (reminder) => alert(reminder.name)
            );

這裡我們需要從RN中引入NativeAppEventEmitter, 用於建立監聽事件。

千萬不要忘記忘記取消訂閱, 通常在componentWillUnmount函式中實現。

常量列舉型別的匯出事件

在Naive開發中我們應用到了豐富的變數型別,那麼RN中如何能應用到Native的部分資料型別呢?比如列舉,常量?

1.常量

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

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

上面constantsToExport為複寫的方法,名字不可更改,否則無法呼叫。

JS使用:

console.log(MyDate.firstDayOfTheWeek);

2.列舉:

用NS_ENUM定義的列舉型別。

#import "EnumConstants.h"

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

@implementation EnumConstants

RCT_EXPORT_MODULE();

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

@end

JS使用:

onPressForEnum() {
        let enumConstants = NativeModules.EnumConstants;
        alert(enumConstants.statusBarAnimationFade);
}

執行緒的應用

首先我們建立一個MyThread 類, 複寫methodQueue方法, 如果返回dispatch_get_main_queue,即為呼叫主執行緒。

#import "MyThread.h"

@implementation MyThread
RCT_EXPORT_MODULE()
- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}

RCT_REMAP_METHOD(doInThread, date1:(nonnull NSNumber *)d1 callback:(RCTResponseSenderBlock)callback)
{
  ...
}
@end

類似的,如果一個操作需要花費很長時間,原生模組不應該阻塞住,而是應當宣告一個用於執行操作的獨立佇列。

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

如果你的方法中“只有一個”是耗時較長的(或者是由於某種原因必須在不同的佇列中執行的),你可以在函式體內用dispatch_async方法來在另一個佇列執行,而不影響其他方法:

RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 在這裡執行長時間的操作
    ...
    // 你可以在任何執行緒/佇列中執行回撥函式
    callback(@[...]);
  });
}

上面程式碼示例,參考官方文件!更多資訊大家可以自行查閱!

總結

總體來講,RN的提供給開發者的橋介面比較完善,方便了呼叫,在實際專案中應用也比較流暢。

時代的高歌在唱響,科技的浪潮在邁進,移動的時代在變革,不論未來如何,作為RN的追隨者, 希望今後能提供更為完善的介面元件。

接下來,就到你了,試試吧!