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的追隨者, 希望今後能提供更為完善的介面元件。
接下來,就到你了,試試吧!