繼上一篇文章的React Native 與原生之間的通信(iOS),我們知道RN與原生通信主要通過(guò)屬性、原生模塊、封裝原生UI組件三種方式,上篇文章主要講了前面兩種方式,這篇文章補(bǔ)充下第三種方式。
由于剛?cè)腴TReact Native,知識(shí)水平有限,看了官方文檔(一臉懵b),找了好多博客、源碼去研究怎么封裝iOS端的原生控件,結(jié)果嘗試了幾天依舊只能對(duì)原生UI有簡(jiǎn)單的封裝,使得js能調(diào)用其屬性以及事件(不完整),所以這篇文章并不完整,希望RN的高手能給些意見(jiàn)或者博客引導(dǎo),寫得不對(duì)的地方歡迎留言和討論。。。

原生開(kāi)發(fā),發(fā)展到今天已經(jīng)非常成熟完善,已有組件成千上萬(wàn),極大的提高了開(kāi)發(fā)效率。而React Native 在Facebook的React.js conf 2015上提出,至今一年多,組件數(shù)目肯定沒(méi)得和原生的相比。
因此,在使用React Native開(kāi)發(fā)App的過(guò)程中,我們可能需要調(diào)用RN沒(méi)有實(shí)現(xiàn)的原生視圖組件或第三方組件。甚至,我們可以把本地模塊構(gòu)造成一個(gè)React Native組件,提供給別人使用。
本文的demo基于SDCycleScrollView,即banner,因?yàn)橄氩坏绞裁春玫睦樱跃桶言谧龅捻?xiàng)目用到的SDCycleScrollView封裝下,直接給js調(diào)用。
SDCycleScrollView為github開(kāi)源的無(wú)限循環(huán)自動(dòng)圖片輪播器。
地址為:https://github.com/gsdios/SDCycleScrollView
里面會(huì)用SDWebImage,如果項(xiàng)目已用到SDWebImage,則建議直接把SDCycleScrollView相關(guān)代碼拉進(jìn)項(xiàng)目就OK了。
一、對(duì)原生視圖進(jìn)行進(jìn)一步封裝
參考其他人對(duì)原生視圖的封裝,大多都會(huì)新建一個(gè)視圖,繼承(或者子視圖包含)原生視圖,里面可能含有事件的調(diào)用(這里簡(jiǎn)單demo,就沒(méi)用到)。
#import "UIView+React.h",對(duì)原生視圖進(jìn)行擴(kuò)展(這里有個(gè)重要的屬性reactTag,后面會(huì)用到,作為區(qū)分用途)。
TestScrollView.h
#import "SDCycleScrollView.h"
#import "RCTComponent.h"
#import "UIView+React.h"
@interface TestScrollView : SDCycleScrollView
@property (nonatomic, copy) RCTBubblingEventBlock onClickBanner;
@end
在封裝的UIView中聲明RCTBubblingEventBlock或RCTBubblingEventBlock類型的block屬性,才可以被當(dāng)做事件導(dǎo)出。(新的事件導(dǎo)出方式,后面會(huì)用到哦)
注意:聲明block屬性名稱要以on開(kāi)頭(不確定為什么,在不做其它配置的情況下,只有on開(kāi)頭能成功)
TestScrollView.m
#import "TestScrollView.h"
@implementation TestScrollView
/**
* 挺多封裝原生的第三方組件都會(huì)這么寫,這里還沒(méi)研究透徹,就沒(méi)按著去實(shí)現(xiàn)
- (instancetype)initWithBridge:(RCTBridge *)bridge {
if ((self = [super initWithFrame:CGRectZero])) {
_eventDispatcher = bridge.eventDispatcher;
_bridge = bridge;
......
}
return self;
}
*/
@end
二、創(chuàng)建RCTViewManager子類來(lái)創(chuàng)建和管理原生視圖
原生視圖都需要被一個(gè)RCTViewManager的子類來(lái)創(chuàng)建和管理。
這些管理器在功能上有些類似“視圖控制器”,但它們本質(zhì)上都是單例 - React Native只會(huì)為每個(gè)管理器創(chuàng)建一個(gè)實(shí)例。
它們創(chuàng)建原生的視圖并提供給RCTUIManager,RCTUIManager則會(huì)反過(guò)來(lái)委托它們?cè)谛枰臅r(shí)候去設(shè)置和更新視圖的屬性。RCTViewManager還會(huì)代理視圖的所有委托,并給JavaScript發(fā)回對(duì)應(yīng)的事件。
提供原生視圖步驟如下:
- 首先創(chuàng)建一個(gè)子類 —— 命名規(guī)范為“視圖名稱+Manager”. 視圖名稱可以加上自己的前綴,這里最好避免使用RCT前綴,除非你想給官方pull request
- 添加RCT_EXPORT_MODULE()標(biāo)記宏 —— 讓模塊接口暴露給JavaScript
- *實(shí)現(xiàn)-(UIView )view方法 —— 創(chuàng)建并返回組件視圖
- 封裝屬性及傳遞事件
下面先貼出完整的代碼,然后會(huì)對(duì)屬性和事件進(jìn)行進(jìn)一步的解說(shuō)。
TestScrollViewManager.h
#import "RCTViewManager.h"
@interface TestScrollViewManager : RCTViewManager
@end
TestScrollViewManager.m
#import "TestScrollViewManager.h"
#import "TestScrollView.h" //第三方組件的頭文件
#import "RCTBridge.h" //進(jìn)行通信的頭文件
#import "RCTEventDispatcher.h" //事件派發(fā),不導(dǎo)入會(huì)引起Xcode警告
@interface TestScrollViewManager() <SDCycleScrollViewDelegate>
@end
@implementation TestScrollViewManager
// 標(biāo)記宏(必要)
RCT_EXPORT_MODULE()
// 事件的導(dǎo)出,onClickBanner對(duì)應(yīng)view中擴(kuò)展的屬性
RCT_EXPORT_VIEW_PROPERTY(onClickBanner, RCTBubblingEventBlock)
// 通過(guò)宏RCT_EXPORT_VIEW_PROPERTY完成屬性的映射和導(dǎo)出
RCT_EXPORT_VIEW_PROPERTY(autoScrollTimeInterval, CGFloat);
RCT_EXPORT_VIEW_PROPERTY(imageURLStringsGroup, NSArray);
RCT_EXPORT_VIEW_PROPERTY(autoScroll, BOOL);
- (UIView *)view
{
// 實(shí)際組件的具體大小位置由js控制
TestScrollView *testScrollView = [TestScrollView cycleScrollViewWithFrame:CGRectZero delegate:self placeholderImage:nil];
// 初始化時(shí)將delegate指向了self
testScrollView.pageControlStyle = SDCycleScrollViewPageContolStyleClassic;
testScrollView.pageControlAliment = SDCycleScrollViewPageContolAlimentCenter;
return testScrollView;
}
/**
* 當(dāng)事件導(dǎo)出用到 sendInputEventWithName 的方式時(shí),會(huì)用到
- (NSArray *) customDirectEventTypes {
return @[@"onClickBanner"];
}
*/
#pragma mark SDCycleScrollViewDelegate
/**
* banner點(diǎn)擊
*/
- (void)cycleScrollView:(TestScrollView *)cycleScrollView didSelectItemAtIndex:(NSInteger)index
{
// 這也是導(dǎo)出事件的方式,不過(guò)好像是舊方法了,會(huì)有警告
// [self.bridge.eventDispatcher sendInputEventWithName:@"onClickBanner"
// body:@{@"target": cycleScrollView.reactTag,
// @"value": [NSNumber numberWithInteger:index+1]
// }];
if (!cycleScrollView.onClickBanner) {
return;
}
NSLog(@"oc did click %li", [cycleScrollView.reactTag integerValue]);
// 導(dǎo)出事件
cycleScrollView.onClickBanner(@{@"target": cycleScrollView.reactTag,
@"value": [NSNumber numberWithInteger:index+1]});
}
// 導(dǎo)出枚舉常量,給js定義樣式用
- (NSDictionary *)constantsToExport
{
return @{
@"SDCycleScrollViewPageContolAliment": @{
@"right": @(SDCycleScrollViewPageContolAlimentRight),
@"center": @(SDCycleScrollViewPageContolAlimentCenter)
}
};
}
// 因?yàn)檫@個(gè)類繼承RCTViewManager,實(shí)現(xiàn)RCTBridgeModule,因此可以使用原生模塊所有特性
// 這個(gè)方法暫時(shí)沒(méi)用到
RCT_EXPORT_METHOD(testResetTime:(RCTResponseSenderBlock)callback) {
callback(@[@(234)]);
}
@end
屬性
RCT_EXPORT_VIEW_PROPERTY(autoScrollTimeInterval, CGFloat);
通過(guò)宏RCT_EXPORT_VIEW_PROPERTY完成屬性的映射和導(dǎo)出。
CGFloat為autoScrollTimeInterval的OC數(shù)據(jù)類型,轉(zhuǎn)化成js則對(duì)應(yīng)number。
React Native用RCTConvert來(lái)在JavaScript和原生代碼之間完成類型轉(zhuǎn)換。
支持的默認(rèn)轉(zhuǎn)換類型(部分)如下:
- string (NSString)
- number (NSInteger, float, double, CGFloat, NSNumber)
- boolean (BOOL, NSNumber)
- array (NSArray) 包含本列表中任意類型
- map (NSDictionary) 包含string類型的鍵和本列表中任意類型的值
如果轉(zhuǎn)換無(wú)法完成,會(huì)產(chǎn)生一個(gè)“紅屏”的報(bào)錯(cuò)提示,這樣你就能立即知道代碼中出現(xiàn)了問(wèn)題。如果一切進(jìn)展順利,上面這個(gè)宏就已經(jīng)包含了導(dǎo)出屬性的全部實(shí)現(xiàn)。
ps:更復(fù)雜的類型轉(zhuǎn)換,則涉及到MKCoordinateRegion類型,本文沒(méi)做應(yīng)用,具體可參考官方文檔例子。
事件
js和原生之間需要有事件的交互,例如,在原生實(shí)現(xiàn)的代理或者點(diǎn)擊事件,js也需要實(shí)時(shí)獲取到此類事件時(shí),就需要利用事件進(jìn)行交互。
事件的實(shí)現(xiàn)方式有以下兩種:
- 通過(guò)sendInputEventWithName實(shí)現(xiàn)
- 實(shí)現(xiàn)customDirectEventTypes,返回自定義的事件名數(shù)組(on開(kāi)頭才有效)
- (NSArray *) customDirectEventTypes {
return @[@"onClickBanner"];
}
- sendInputEventWithName實(shí)現(xiàn)事件調(diào)用(reactTag用于實(shí)例的區(qū)分)
[self.bridge.eventDispatcher sendInputEventWithName:@"onClickBanner"
body:@{@"target": cycleScrollView.reactTag,
@"value": [NSNumber numberWithInteger:index+1]
}];
- 通過(guò)RCTBubblingEventBlock實(shí)現(xiàn)
- 在封裝的View中添加RCTBubblingEventBlock的block屬性(on開(kāi)頭才有效)
@property (nonatomic, copy) RCTBubblingEventBlock onClickBanner;
- 在Manager類中通過(guò)宏RCT_EXPORT_VIEW_PROPERTY完成Block屬性的映射和導(dǎo)出
RCT_EXPORT_VIEW_PROPERTY(onClickBanner, RCTBubblingEventBlock)
- 實(shí)現(xiàn)事件調(diào)用(reactTag用于實(shí)例的區(qū)分)
cycleScrollView.onClickBanner(@{@"target": cycleScrollView.reactTag,
@"value": [NSNumber numberWithInteger:index+1]});
通過(guò)上面兩種方式封裝好的事件,在js中可以直接利用同名函數(shù)調(diào)用即可(后面會(huì)展示)。
不過(guò)關(guān)于事件這塊,挺多都沒(méi)完全弄懂,希望有大神引導(dǎo)引導(dǎo),比如為什么只能定義on開(kāi)頭、如何自定義、如何事件數(shù)據(jù)源的回調(diào)等等。。。
樣式
因?yàn)槲覀兯械囊晥D都是UIView的子類,大部分的樣式屬性應(yīng)該直接就可以生效。有些屬性定義,需要用到枚舉,則可以利用通過(guò)原生傳遞來(lái)的常數(shù)方式來(lái)實(shí)現(xiàn),具體實(shí)現(xiàn)如下:
// 導(dǎo)出枚舉常量,給js定義樣式用
- (NSDictionary *)constantsToExport
{
return @{
@"SDCycleScrollViewPageContolAliment": @{
@"right": @(SDCycleScrollViewPageContolAlimentRight),
@"center": @(SDCycleScrollViewPageContolAlimentCenter)
}
};
}
在js中調(diào)用則如下:
// 首先獲取到常量
var TestScrollViewConsts = require('react-native').UIManager.TestScrollView.Constants;
// 調(diào)用
<TestScrollView style={styles.container}
pageControlAliment = {TestScrollViewConsts.SDCycleScrollViewPageContolAliment.right}
/>
ps: 一部分組件會(huì)希望使用自己定義的默認(rèn)樣式,例如UIDatePicker希望自己的大小是固定的。比如大小用原生默認(rèn)大小,這個(gè)例子具體可以參考官方文檔的樣式模塊。
三、在JS中進(jìn)行調(diào)用
在js中調(diào)用,可以有兩種方式,一為直接作為擴(kuò)展React組件調(diào)用,二為新建一個(gè)組件封裝好,再進(jìn)行調(diào)用。
下文用第二種方式,官方推薦,邏輯比較清晰。
1.先倒入原生組件,新建TestScrollView.js文件,在里面對(duì)TestScrollView導(dǎo)入,進(jìn)行屬性類型聲明等。具體代碼和解釋如下:
TestScrollView.js
// TestScrollView.js
import React, { Component, PropTypes } from 'react';
import { requireNativeComponent } from 'react-native';
// requireNativeComponent 自動(dòng)把這個(gè)組件提供給 "RCTScrollView"
var RCTScrollView = requireNativeComponent('TestScrollView', TestScrollView);
export default class TestScrollView extends Component {
render() {
return <RCTScrollView {...this.props} />;
}
}
TestScrollView.propTypes = {
/**
* 屬性類型,其實(shí)不寫也可以,js會(huì)自動(dòng)轉(zhuǎn)換類型
*/
autoScrollTimeInterval: PropTypes.number,
imageURLStringsGroup: PropTypes.array,
autoScroll: PropTypes.bool,
onClickBanner: PropTypes.func
};
module.exports = TestScrollView;
2.在index.ios.js中進(jìn)行調(diào)用
index.ios.js
var TestScrollView = require('./TestScrollView');
// requireNativeComponent 自動(dòng)把這個(gè)組件提供給 "TestScrollView"
// 如果不新建TestScrollView.js對(duì)原生組件封裝聲明,則直接用這句導(dǎo)入即可
// var TestScrollView = requireNativeComponent('TestScrollView', null);
// 導(dǎo)入常量
var TestScrollViewConsts = require('react-native').UIManager.TestScrollView.Constants;
var bannerImgs = [
'http://upload-images.jianshu.io/upload_images/2321678-ba5bf97ec3462662.png?imageMogr2/auto-orient/strip%7CimageView2/2',
'http://upload-images.jianshu.io/upload_images/1487291-2aec9e634117c24b.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/480/q/100',
'http://f.hiphotos.baidu.com/zhidao/pic/item/e7cd7b899e510fb37a4f2df3db33c895d1430c7b.jpg'
];
class NativeUIModule extends Component {
constructor(props){
super(props);
this.state={
bannerNum:0
}
}
render() {
return (
<ScrollView style = {{marginTop:64}}>
<View>
<TestScrollView style={styles.container}
autoScrollTimeInterval = {2}
imageURLStringsGroup = {bannerImgs}
pageControlAliment = {TestScrollViewConsts.SDCycleScrollViewPageContolAliment.right}
onClickBanner={(e) => {
console.log('test' + e.nativeEvent.value);
this.setState({bannerNum:e.nativeEvent.value});
}}
/>
<Text style={{fontSize: 15, margin: 10, textAlign:'center'}}>
點(diǎn)擊banner -> {this.state.bannerNum}
</Text>
</View>
</ScrollView>
);
}
}
// 實(shí)際組件的具體大小位置由js控制
const styles = StyleSheet.create({
container:{
padding:30,
borderColor:'#e7e7e7',
marginTop:10,
height:200,
},
});
AppRegistry.registerComponent('NativeTest2', () => NativeUIModule);
若使用第一種方式,即使用下面語(yǔ)句進(jìn)行組件的引用:
var TestScrollView = requireNativeComponent('TestScrollView', null);
則會(huì)存在的這樣的問(wèn)題:
雖然很方便簡(jiǎn)單,但這樣并不能很好的說(shuō)明這個(gè)組件的用法——用戶要想知道我們的組件有哪些屬性可以用,以及可以取什么樣的值,他不得不一路翻到Objective-C的代碼。要解決這個(gè)問(wèn)題,我們可以創(chuàng)建一個(gè)封裝組件,并且通過(guò)PropTypes來(lái)說(shuō)明這個(gè)組件的接口。
注意:我們現(xiàn)在把requireNativeComponent的第二個(gè)參數(shù)從null變成了用于封裝的組件TestScrollView。這使得React Native的底層框架可以檢查原生屬性和包裝類的屬性是否一致,來(lái)減少出現(xiàn)問(wèn)題的可能。
關(guān)于屬性、事件的調(diào)用,則是如下直接調(diào)用:
<TestScrollView style={styles.container}
autoScrollTimeInterval = {2}
imageURLStringsGroup = {bannerImgs}
pageControlAliment = {TestScrollViewConsts.SDCycleScrollViewPageContolAliment.right}
onClickBanner={(e) => {
console.log('test' + e.nativeEvent.value);
this.setState({bannerNum:e.nativeEvent.value});
}}
/>
關(guān)于事件,需要注意的是,事件事件默認(rèn)傳遞的是字典數(shù)據(jù)類型,即json,在js中調(diào)用需要利用e.nativeEvent才能將字典取出,在具體調(diào)用里面的值。(這里也還未研究透徹、需要指導(dǎo))
四、成果
到這里為止,應(yīng)該能對(duì)原生UI控件進(jìn)行簡(jiǎn)單的封裝和調(diào)用了,如果不用到數(shù)據(jù)源,只是實(shí)現(xiàn)代理的第三方控件的話,封裝來(lái)讓RN模塊調(diào)用是沒(méi)用問(wèn)題的+_+
下面推薦下有用的相關(guān)文章:
1.官方文檔——原生UI組件(這是肯定的)
2.React Native構(gòu)建本地視圖組件
3.React-Native之復(fù)用原生UI組件
4.React Native - How to Bridge an Objective-C View Component
還有對(duì)第三方對(duì)原生tableview的封裝代碼:
https://github.com/aksonov/react-native-tableview
另外,關(guān)于交互原理的文章則推薦以下幾篇:
[iOS] 干貨 | 速收藏 | React Native iOS 源碼解析篇 (二)
淺析ReactNative之通信機(jī)制(一)
bang's blog : React Native通信機(jī)制詳解
demo還是先暫時(shí)放到百度云中:
下面是demo的演示效果:

因?yàn)闆](méi)繼續(xù)這方面的工作所以好久沒(méi)更新了,可能代碼因?yàn)閞n的更新會(huì)有些問(wèn)題,最好更新下pod的版本,看看官方文檔,看到評(píng)論里有相應(yīng)的討論,出現(xiàn)問(wèn)題的朋友最好也看看評(píng)論哈哈,可能有解決辦法?───O(≧?≦)O────?
已有的成果如下:
1) React Native 簡(jiǎn)介與入門
2) React Native 環(huán)境搭建和創(chuàng)建項(xiàng)目(Mac)
3) React Native 開(kāi)發(fā)之IDE
4) React Native 入門項(xiàng)目與解析
5) React Native 相關(guān)JS和React基礎(chǔ)
6) React Native 組件生命周期(ES6)
7) React Native 集成到原生項(xiàng)目(iOS)
8) React Native 與原生之間的通信(iOS)
- React Native 封裝原生UI組件(iOS)