1. 程式人生 > >React Native iOS原生模組開發實戰|教程|心得|如何建立React Native iOS原生模組

React Native iOS原生模組開發實戰|教程|心得|如何建立React Native iOS原生模組

尊重版權,未經授權不得轉載
本文出自:賈鵬輝的技術部落格(http://blog.csdn.net/fengyuzhengfan/article/details/54691432)

告訴大家一個好訊息,為大家精心準備的React Native視訊教程釋出了,大家現可以看視訊學React Native了。

前言

一直想寫一下我在React Native原生模組封裝方面的一些經驗和心得,來分享給大家,但實在抽不開身,今天看了一下日曆發現馬上就春節了,所以就趕在春節之前將這篇博文寫好併發布(其實是兩篇:要看Android篇的點這裡《React Native Android原生模組開發》

)。

我平時在用React Native開發App時會用到一些原生模組,比如:在做社會化分享、第三方登入、掃描、通訊錄,日曆等等,想必大家也是一樣。

關於在React Native中使用原生模組,在這裡引用React Native官方文件的一段話:

有時候App需要訪問平臺API,但在React Native可能還沒有相應的模組。或者你需要複用一些Java程式碼,而不想用JavaScript再重新實現一遍;又或者你需要實現某些高效能的、多執行緒的程式碼,譬如圖片處理、資料庫、或者一些高階擴充套件等等。
我們把React Native設計為可以在其基礎上編寫真正的原生程式碼,並且可以訪問平臺所有的能力。這是一個相對高階的特性,我們並不期望它應當在日常開發的過程中經常出現,但它確實必不可少,而且是存在的。如果React Native還不支援某個你需要的原生特性,你應當可以自己實現對該特性的封裝。

上面是我翻譯React Native官方文件上的一段話,大家如果想看英文版可以點這裡:Native Modules
在這篇文章中呢,我會帶著大家來開發一個從相簿獲取照片並裁切照片的專案,並結合這個專案來具體講解一下如何一步步開發React Native iOS原生模組的。

React Native iOS原生模組開發實戰-教程-心得-如何建立一個React Native iOS原生模組

提示:告訴大家一個好訊息,React Native視訊教程釋出了,大家現可以看視訊學React Native了。

首先,讓我們先看一下,開發iOS原生模組的主要流程。

開發iOS原生模組的主要流程

在這裡我把構建React Native iOS原生模組的流程概括為以下三大步:

  1. 編寫原生模組的相關iOS程式碼;
  2. 暴露介面與資料互動;
  3. 匯出React Native原生模組;

接下來讓我們一起來看一下每一步所需要做的一些事情。

原生模組開發實戰

在這裡我們就以開發一個從相簿獲取照片並裁切照片的實戰專案,來具體講解一下如何開發React Native iOS原生模組的。

編寫原生模組的相關iOS程式碼

這一步我們需要用到XCode。
首先我們用XCode開啟React Native專案根目錄下的iOS專案,如圖:

open-react-native-iOS-native-project

接下來呢,我們就可以編寫iOS程式碼了。

首先呢,我們先來實現一個Crop介面:

@interface Crop:NSObject<UIImagePickerControllerDelegate,UINavigationControllerDelegate>
-(instancetype)initWithViewController:(UIViewController *)vc;
typedef void(^PickSuccess)(NSDictionary *resultDic);
typedef void(^PickFailure)(NSString* message);
@property (nonatomic, retain) NSMutableDictionary *response;
@property (nonatomic,copy)PickSuccess pickSuccess;
@property (nonatomic,copy)PickFailure pickFailure;
@property(nonatomic,strong)UIViewController *viewController;

-(void)selectImage:(NSDictionary*)option successs:(PickSuccess)success failure:(PickFailure)failure;
@end

檢視視訊教程

我們建立一個Crop.m,在這個類中呢,我們實現了從相簿選擇照片以及裁切照片的功能:

/**
 * React Native iOS原生模組開發
 * Author: CrazyCodeBoy
 * 技術博文:http://www.devio.org
 * GitHub:https://github.com/crazycodeboy
 * Email:[email protected]
 */

#import "Crop.h"
#import <AssetsLibrary/AssetsLibrary.h>
@interface Crop ()
@property(strong,nonatomic)NSDictionary*option;
@end

@implementation Crop
-(instancetype)initWithViewController:(UIViewController *)vc{
  self=[super init];
  self.viewController=vc;
  return self;
}

-(void)selectImage:(NSDictionary*)option successs:(PickSuccess)success failure:(PickFailure)failure{
  self.pickSuccess=success;
  self.pickFailure=failure;
  self.option=option;
  UIImagePickerController *pickerController = [[UIImagePickerController alloc] init];
  pickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
  pickerController.delegate = self;
  pickerController.allowsEditing = YES;
  [self.viewController presentViewController:pickerController animated:YES completion:nil];
}

//省略部分程式碼...

#pragma mark 獲取臨時檔案路徑
-(NSString*)getTempFile:(NSString*)fileName{
  NSString *imageContent=[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingString:@"/temp"];
  NSFileManager * fileManager = [NSFileManager defaultManager];
  if (![fileManager fileExistsAtPath:imageContent]) {
    [fileManager createDirectoryAtPath:imageContent withIntermediateDirectories:YES attributes:nil error:nil];
  }
  return [imageContent stringByAppendingPathComponent:fileName];
}
-(NSString*)getFileName:(NSDictionary*)info{
  NSString *fileName;
  NSString *tempFileName = [[NSUUID UUID] UUIDString];
  fileName = [tempFileName stringByAppendingString:@".jpg"];
  return fileName;
}
@end

檢視視訊教程

實現了從相簿選擇照片以及裁切照片的功能之後呢,接下來我們需要將iOS原生模組暴露給React Native,以供js呼叫。

暴露介面與資料互動

接下了我們就向React Native暴露介面以及做一些資料互動部分的操作。為了暴露介面以及進行資料互動我們需要藉助React Native的React/RCTBridgeModule.h,在這裡我們建立一個ImageCrop類讓它實現RCTBridgeModule協議。

建立一個ImageCrop

ImageCrop.h

/**
 * React Native iOS原生模組開發
 * Author: CrazyCodeBoy
 * 技術博文:http://www.devio.org
 * GitHub:https://github.com/crazycodeboy
 * Email:[email protected]
 */


#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
@interface  ImageCrop: NSObject <RCTBridgeModule>

@end

檢視視訊教程

ImageCrop.m

/**
 * React Native iOS原生模組開發
 * Author: CrazyCodeBoy
 * 技術博文:http://www.devio.org
 * GitHub:https://github.com/crazycodeboy
 * Email:[email protected]
 */


#import "ImageCrop.h"
#import "Crop.h"

@interface ImageCrop ()
@property(strong,nonatomic)Crop *crop;
@end

@implementation ImageCrop


RCT_EXPORT_MODULE();

- (dispatch_queue_t)methodQueue 
{
  return dispatch_get_main_queue();
}
RCT_EXPORT_METHOD(selectWithCrop:(NSInteger)aspectX aspectY:(NSInteger)aspectY resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
    UIViewController *root = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
    while (root.presentedViewController != nil) {
      root = root.presentedViewController;
    }
  NSString*aspectXStr=[NSString stringWithFormat: @"%ld",aspectX];
  NSString*aspectYStr=[NSString stringWithFormat: @"%ld",aspectY];
  [[self _crop:root] selectImage:@{@"aspectX":aspectXStr,@"aspectY":aspectYStr} successs:^(NSDictionary *resultDic) {
    resolve(resultDic);
  } failure:^(NSString *message) {
    reject(@"fail",message,nil);
  }];
}

//省略部分程式碼...

@end

檢視視訊教程

ImageCrop類中,我們呼叫了Crop類來實現從iOS相簿中獲取圖片並裁切圖片的功能,在呼叫Crop的時候我們用的是懶載入的方式。為什麼要用懶載入呢?這是為了避免當我們多次呼叫原生模組從相簿選擇照片的時候建立多個Crop例項情況的發生。

另外,需要特別提到的是,我們對Crop例項設定了強引用,這是為了防止在我們呼叫相簿的時候Crop被回收,如果Crop被回收我們就無法收到選擇照片之後的回調了,也就無法獲取到照片。

暴露介面

在上述程式碼中我們通過RCT_EXPORT_METHOD巨集來宣告向React Native暴露的介面,這樣以來我們就可以在js檔案中通過ImageCrop.selectWithCrop來呼叫我們所暴露給React Native的介面了。

接下來呢,我們來看一下原生模組和JS模組是如何進行資料互動的?

原生模組和JS進行資料互動

在我們要實現的從相簿選擇照片並裁切的專案中,JS模組需要告訴原生模組照片裁切的比例,等照片裁切完成後,原生模組需要對JS模組進行回撥來告訴JS模組照片裁切的結果,在這裡我們需要將照片裁切後生成的圖片的路徑告訴JS模組。

提示:在所有的情況下js和原生模組之前進行通訊都是在非同步的情況下進行的。

接下來我們就來看下一JS是如何向原生模組傳遞資料的?

JS向原生模組傳遞資料:

為了實現JS向原生模組進行傳遞資料,我們可以直接通過呼叫原生模組所暴露出來的介面,來為介面方法設定引數。這樣以來我們就可以將資料通過介面引數傳遞到原生模組中,如:

RCT_EXPORT_METHOD(selectWithCrop:(NSInteger)aspectX aspectY:(NSInteger)aspectY resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

通過上述程式碼我們可以看出,JS模組可以通過selectWithCrop方法來告訴原生模組要裁切照片的寬高比,最後兩個引數是一個Promise,照片裁剪完成之後呢,原生模組可以通過Promise來對JS模組進行回撥,來告訴裁切結果。

既然是js和Object-c進行資料傳遞,那麼他們兩者之間是如何進行型別轉換的呢:
在上述例子中我們通過RCT_EXPORT_METHOD巨集來宣告需要暴露的介面,被 RCT_EXPORT_METHOD標註的方法支援如下幾種資料型別。

RCT_EXPORT_METHOD標註的方法支援如下幾種資料型別的引數:

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

原生模組向JS傳遞資料:

原生模組向JS傳遞資料我們可以藉助Callbacks與Promises,接下來就講一下如何通過他們兩個進行資料傳遞的。

Callbacks

原生模組支援一個特殊型別的引數-Callbacks,我們可以通過它來對js進行回撥,以告訴js呼叫原生模組方法的結果。
將我們selectWithCrop的引數改為Callbacks之後:

RCT_EXPORT_METHOD(selectWithCrop:(NSInteger)aspectX aspectY:(NSInteger)aspectY success:(RCTResponseSenderBlock)success failure:(RCTResponseErrorBlock)failure)
{
    UIViewController *root = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
    while (root.presentedViewController != nil) {
      root = root.presentedViewController;
    }
  NSString*aspectXStr=[NSString stringWithFormat: @"%ld",aspectX];
  NSString*aspectYStr=[NSString stringWithFormat: @"%ld",aspectY];
  [[self _crop:root] selectImage:@{@"aspectX":aspectXStr,@"aspectY":aspectYStr} successs:^(NSDictionary *resultDic) {
    success(@[[NSNull null],@[@"imageUrl",[resultDic objectForKey:@"imageUrl"]]]);
  } failure:^(NSString *message) {
    failure(nil);
  }];
}

檢視視訊教程

在上述程式碼中我們實現了通過Callback來對js進行回撥。

接下來呢,我們在js中就可以這樣來呼叫我們所暴露的介面:

ImageCrop.selectWithCrop(parseInt(x),parseInt(y),(error, result)=>{
 if (error) {
    console.error(error);
  } else {
    console.log(result);
  }
})

提示:另外要告訴大家的是,無論是Callback還是我接下來要講的Promise,我們只能呼叫一次,也就是"you call me once,I can only call you once"。

Promises

除了上文所講的Callback之外React Native還為了我們提供了另外一種回撥js的方式叫-Promise。如果我們暴露的介面方法的最後一個引數是Promise時,如:

RCT_EXPORT_METHOD(selectWithCrop:(NSInteger)aspectX aspectY:(NSInteger)aspectY resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

那麼當js呼叫它的時候將會返回一個Promsie:

ImageCrop.selectWithCrop(parseInt(x),parseInt(y)).then(result=> {
    this.setState({
        result: result
    })
}).catch(e=> {
    this.setState({
        result: e
    })
});

另外,我們也可以使用ES2016的 async/await語法,來簡化我們的程式碼:

async onSelectCrop() {
    var result=await ImageCrop.selectWithCrop(parseInt(x),parseInt(y));
}

這樣以來程式碼就簡化了很多。

因為,基於回撥的資料傳遞無論是Callback還是Promise,都只能呼叫一次。但,在實際專案開發中我們有時會向js多次傳遞資料,比如二維碼掃描原生模組,針對這種多次資料傳遞的情況我們該怎麼實現呢?

接下來我就為大家介紹一種原生模組可以向js多次傳遞資料的方式:

向js傳送事件

在原生模組中我們可以向js傳送多次事件,即使原生模組沒有被直接的呼叫。為了向js傳遞事件我們需要用到RCTEventDispatcher,它是原生模組和js之間的一個事件排程管理器。

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

@implementation CalendarManager

@synthesize bridge = _bridge;

- (void)calendarEventReminderReceived:(NSNotification *)notification
{
  NSString *eventName = notification.userInfo[@"name"];
  [self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder"
                                               body:@{@"name": eventName}];
}

在上述方法中我們可以向JS模組傳送任意次數的事件,其中eventName是我們要傳送事件的事件名,params是此次事件所攜帶的資料,接下來呢我們就可以在JS模組中監聽這個事件了:

import { NativeAppEventEmitter } from 'react-native';

var subscription = NativeAppEventEmitter.addListener(
  'EventReminder',
  (reminder) => console.log(reminder.name)
);
...

另外,不要忘記在元件被解除安裝的時候移除監聽:

componentWillUnmount(){
    subscription.remove();
}

到現在呢,暴露介面以及資料傳遞已經進行完了,接下來呢,我們就需要匯出React Native原生模組了。

匯出React Native原生模組

為了方面我們使用剛才建立的原生模組,我們需要為它匯出一個相應的JS模組。

我們建立一個ImageCrop.js檔案,然後新增如下程式碼:

import { NativeModules } from 'react-native';
export default NativeModules.ImageCrop;

這樣以來呢,我們就可以在其他地方通過下面方式來使用我們所匯出的這個模組了:

import ImageCrop from './ImageCrop' //匯入ImageCrop.js
//...省略部分程式碼

    onSelectCrop() {
        let x=this.aspectX?this.aspectX:ASPECT_X;
        let y=this.aspectY?this.aspectY:ASPECT_Y;
        ImageCrop.selectWithCrop(parseInt(x),parseInt(y)).then(result=> {
            this.setState({
                result: result
            })
        }).catch(e=> {
            this.setState({
                result: e
            })
        });
    }
//...省略部分程式碼
}

檢視視訊教程

現在呢,我們這個原生模組就開發好了,而且我們也使用了我們的這個原生模組。

關於執行緒

React Native在一個獨立的序列GCD佇列中呼叫原生模組的方法。在我們為React Native開發原生模組的時候,如果有耗時的操作比如:檔案讀寫、網路操作等,我們需要新開闢一個執行緒,不然的話,這些耗時的操作會阻塞JS執行緒。通過實現方法- (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方法來在另一個佇列執行,而不影響其他方法:

 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    if (url) {
      [assetLibrary assetForURL:url resultBlock:^(ALAsset *asset) {
        ALAssetRepresentation *rep = [asset defaultRepresentation];
        Byte *buffer = (Byte*)malloc((unsigned long)rep.size);
        NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:((unsigned long)rep.size) error:nil];
        NSData *data = [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES];
        NSString * imagePath = [imageContent stringByAppendingPathComponent:fileName];
        [data writeToFile:imagePath atomically:YES];
        handler(imagePath);
      } failureBlock:^(NSError *error) {
        handler(@"獲取圖片失敗");
      }];
    }
  });

檢視視訊教程

在上述程式碼中我們將檔案寫入操作放到了一個獨立的執行緒佇列中,這樣以來即使檔案寫入的時間再長也不會阻塞其他執行緒。

還有一個需要告訴大家的是,如果原生模組中需要更新UI,我們需要獲取主執行緒,然後在主執行緒中更新UI,如:

    dispatch_async(dispatch_get_main_queue(), ^{
        [picker dismissViewControllerAnimated:YES completion:dismissCompletionBlock];
    });

關於React Native iOS原生模組的多執行緒無外乎這些東西。

告訴大家一個好訊息,為大家精心準備的React Native視訊教程釋出了,大家現可以看視訊學React Native了。

如果,大家在開發原生模組中遇到問題可以在本文的下方進行留言,我看到了後會及時回覆的哦。
另外也可以關注我的新浪微博,或者關注我的Github來獲取更多有關React Native開發的技術乾貨

推薦學習:視訊教程《React Native開發跨平臺GitHub App》