原生開發,發展到今天已經非常成熟完善,已有組件成千上萬,極大的提高了開發效率。而React Native 在Facebook的React.js conf 2015上提出,至今一年多,組件數目肯定沒得和原生的相比。因此,在使用React Native開發App的過程中,我們可能需要調用RN沒有實現的原生視圖組件或第三方組件。甚至,我們可以把本地模塊構造成一個React Native組件,提供給別人使用。
本文的demo基于SDCycleScrollView,即banner,因為想不到什么好的例子,所以就把在做的項目用到的SDCycleScrollView封裝下,直接給js調用。
SDCycleScrollView為github開源的無限循環自動圖片輪播器。地址為:https://github.com/gsdios/SDCycleScrollView里面會用SDWebImage,如果項目已用到SDWebImage,則建議直接把SDCycleScrollView相關代碼拉進項目就OK了。
一、對原生視圖進行進一步封裝
參考其他人對原生視圖的封裝,大多都會新建一個視圖,繼承(或者子視圖包含)原生視圖,里面可能含有事件的調用(這里簡單demo,就沒用到)。#import "UIView+React.h",對原生視圖進行擴展(這里有個重要的屬性reactTag,后面會用到,作為區分用途)。
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屬性,才可以被當做事件導出。(新的事件導出方式,后面會用到哦)注意:聲明block屬性名稱要以on開頭(不確定為什么,在不做其它配置的情況下,只有on開頭能成功)
TestScrollView.m
#import "TestScrollView.h"@implementation TestScrollView/** * 挺多封裝原生的第三方組件都會這么寫,這里還沒研究透徹,就沒按著去實現- (instancetype)initWithBridge:(RCTBridge *)bridge { if ((self = [super initWithFrame:CGRectZero])) { _eventDispatcher = bridge.eventDispatcher; _bridge = bridge; ...... } return self;} */@end二、創建RCTViewManager子類來創建和管理原生視圖
原生視圖都需要被一個RCTViewManager的子類來創建和管理。這些管理器在功能上有些類似“視圖控制器”,但它們本質上都是單例 - React Native只會為每個管理器創建一個實例。它們創建原生的視圖并提供給RCTUIManager,RCTUIManager則會反過來委托它們在需要的時候去設置和更新視圖的屬性。RCTViewManager還會代理視圖的所有委托,并給javaScript發回對應的事件。
提供原生視圖步驟如下:
首先創建一個子類 —— 命名規范為“視圖名稱+Manager”. 視圖名稱可以加上自己的前綴,這里最好避免使用RCT前綴,除非你想給官方pull request添加RCT_EXPORT_MODULE()標記宏 —— 讓模塊接口暴露給Javascript實現-(UIView *)view方法 —— 創建并返回組件視圖封裝屬性及傳遞事件下面先貼出完整的代碼,然后會對屬性和事件進行進一步的解說。
TestScrollViewManager.h
#import "RCTViewManager.h"@interface TestScrollViewManager : RCTViewManager@endTestScrollViewManager.m
#import "TestScrollViewManager.h"#import "TestScrollView.h" //第三方組件的頭文件#import "RCTBridge.h" //進行通信的頭文件#import "RCTEventDispatcher.h" //事件派發,不導入會引起Xcode警告@interface TestScrollViewManager() <SDCycleScrollViewDelegate>@end@implementation TestScrollViewManager// 標記宏(必要)RCT_EXPORT_MODULE()// 事件的導出,onClickBanner對應view中擴展的屬性RCT_EXPORT_VIEW_PROPERTY(onClickBanner, RCTBubblingEventBlock)// 通過宏RCT_EXPORT_VIEW_PROPERTY完成屬性的映射和導出RCT_EXPORT_VIEW_PROPERTY(autoScrollTimeInterval, CGFloat);RCT_EXPORT_VIEW_PROPERTY(imageURLStringsGroup, NSArray);RCT_EXPORT_VIEW_PROPERTY(autoScroll, BOOL);- (UIView *)view{ // 實際組件的具體大小位置由js控制 TestScrollView *testScrollView = [TestScrollView cycleScrollViewWithFrame:CGRectZero delegate:self placeholderImage:nil]; // 初始化時將delegate指向了self testScrollView.pageControlStyle = SDCycleScrollViewPageContolStyleClassic; testScrollView.pageControlAliment = SDCycleScrollViewPageContolAlimentCenter; return testScrollView;}/** * 當事件導出用到 sendInputEventWithName 的方式時,會用到- (NSArray *) customDirectEventTypes { return @[@"onClickBanner"];} */#pragma mark SDCycleScrollViewDelegate/** * banner點擊 */- (void)cycleScrollView:(TestScrollView *)cycleScrollView didSelectItemAtIndex:(NSInteger)index{// 這也是導出事件的方式,不過好像是舊方法了,會有警告// [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]); // 導出事件 cycleScrollView.onClickBanner(@{@"target": cycleScrollView.reactTag, @"value": [NSNumber numberWithInteger:index+1]});}// 導出枚舉常量,給js定義樣式用- (NSDictionary *)constantsToExport{ return @{ @"SDCycleScrollViewPageContolAliment": @{ @"right": @(SDCycleScrollViewPageContolAlimentRight), @"center": @(SDCycleScrollViewPageContolAlimentCenter) } };}// 因為這個類繼承RCTViewManager,實現RCTBridgeModule,因此可以使用原生模塊所有特性// 這個方法暫時沒用到RCT_EXPORT_METHOD(testResetTime:(RCTResponseSenderBlock)callback) { callback(@[@(234)]);}@end屬性
RCT_EXPORT_VIEW_PROPERTY(autoScrollTimeInterval, CGFloat);
通過宏RCT_EXPORT_VIEW_PROPERTY完成屬性的映射和導出。CGFloat為autoScrollTimeInterval的OC數據類型,轉化成js則對應number。
React Native用RCTConvert來在JavaScript和原生代碼之間完成類型轉換。支持的默認轉換類型(部分)如下:
string (NSString)number (NSInteger, float, double, CGFloat, NSNumber)boolean (BOOL, NSNumber)array (NSArray) 包含本列表中任意類型map (NSDictionary) 包含string類型的鍵和本列表中任意類型的值如果轉換無法完成,會產生一個“紅屏”的報錯提示,這樣你就能立即知道代碼中出現了問題。如果一切進展順利,上面這個宏就已經包含了導出屬性的全部實現。
ps:更復雜的類型轉換,則涉及到MKCoordinateRegion類型,本文沒做應用,具體可參考官方文檔例子。
事件
js和原生之間需要有事件的交互,例如,在原生實現的代理或者點擊事件,js也需要實時獲取到此類事件時,就需要利用事件進行交互。事件的實現方式有以下兩種:
通過sendInputEventWithName實現1) 實現customDirectEventTypes,返回自定義的事件名數組(on開頭才有效)
- (NSArray *) customDirectEventTypes { return @[@"onClickBanner"];}2) sendInputEventWithName實現事件調用(reactTag用于實例的區分)
[self.bridge.eventDispatcher sendInputEventWithName:@"onClickBanner" body:@{@"target": cycleScrollView.reactTag, @"value": [NSNumber numberWithInteger:index+1] }];通過RCTBubblingEventBlock實現1) 在封裝的View中添加RCTBubblingEventBlock的block屬性(on開頭才有效)
@property (nonatomic, copy) RCTBubblingEventBlock onClickBanner;2) 在Manager類中通過宏RCT_EXPORT_VIEW_PROPERTY完成Block屬性的映射和導出
RCT_EXPORT_VIEW_PROPERTY(onClickBanner, RCTBubblingEventBlock)3) 實現事件調用(reactTag用于實例的區分)
cycleScrollView.onClickBanner(@{@"target": cycleScrollView.reactTag, @"value": [NSNumber numberWithInteger:index+1]});通過上面兩種方式封裝好的事件,在js中可以直接利用同名函數調用即可(后面會展示)。不過關于事件這塊,挺多都沒完全弄懂,希望有大神引導引導,比如為什么只能定義on開頭、如何自定義、如何事件數據源的回調等等。。。
樣式
因為我們所有的視圖都是UIView的子類,大部分的樣式屬性應該直接就可以生效。有些屬性定義,需要用到枚舉,則可以利用通過原生傳遞來的常數方式來實現,具體實現如下:
// 導出枚舉常量,給js定義樣式用- (NSDictionary *)constantsToExport{ return @{ @"SDCycleScrollViewPageContolAliment": @{ @"right": @(SDCycleScrollViewPageContolAlimentRight), @"center": @(SDCycleScrollViewPageContolAlimentCenter) } };}在js中調用則如下:
// 首先獲取到常量var TestScrollViewConsts = require('react-native').UIManager.TestScrollView.Constants;// 調用<TestScrollView style={styles.container} pageControlAliment = {TestScrollViewConsts.SDCycleScrollViewPageContolAliment.right} />ps: 一部分組件會希望使用自己定義的默認樣式,例如UIDatePicker希望自己的大小是固定的。比如大小用原生默認大小,這個例子具體可以參考官方文檔的樣式模塊。
三、在JS中進行調用
在js中調用,可以有兩種方式,一為直接作為擴展React組件調用,二為新建一個組件封裝好,再進行調用。下文用第二種方式,官方推薦,邏輯比較清晰。
1.先倒入原生組件,新建TestScrollView.js文件,在里面對TestScrollView導入,進行屬性類型聲明等。具體代碼和解釋如下:
TestScrollView.js
// TestScrollView.jsimport React, { Component, PropTypes } from 'react';import { requireNativeComponent } from 'react-native';// requireNativeComponent 自動把這個組件提供給 "RCTScrollView"var RCTScrollView = requireNativeComponent('TestScrollView', TestScrollView);export default class TestScrollView extends Component { render() { return <RCTScrollView {...this.props} />; }}TestScrollView.propTypes = { /** * 屬性類型,其實不寫也可以,js會自動轉換類型 */ autoScrollTimeInterval: PropTypes.number, imageURLStringsGroup: PropTypes.array, autoScroll: PropTypes.bool, onClickBanner: PropTypes.func};module.exports = TestScrollView;2.在index.ios.js中進行調用
index.ios.js
var TestScrollView = require('./TestScrollView');// requireNativeComponent 自動把這個組件提供給 "TestScrollView"// 如果不新建TestScrollView.js對原生組件封裝聲明,則直接用這句導入即可// var TestScrollView = requireNativeComponent('TestScrollView', null);// 導入常量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'}}> 點擊banner -> {this.state.bannerNum} </Text> </View> </ScrollView> ); }}// 實際組件的具體大小位置由js控制const styles = StyleSheet.create({ container:{ padding:30, borderColor:'#e7e7e7', marginTop:10, height:200, },});AppRegistry.registerComponent('NativeTest2', () => NativeUIModule);若使用第一種方式,即使用下面語句進行組件的引用:
var TestScrollView = requireNativeComponent('TestScrollView', null);則會存在的這樣的問題:雖然很方便簡單,但這樣并不能很好的說明這個組件的用法——用戶要想知道我們的組件有哪些屬性可以用,以及可以取什么樣的值,他不得不一路翻到Objective-C的代碼。要解決這個問題,我們可以創建一個封裝組件,并且通過PropTypes來說明這個組件的接口。
注意:我們現在把requireNativeComponent的第二個參數從null變成了用于封裝的組件TestScrollView。這使得React Native的底層框架可以檢查原生屬性和包裝類的屬性是否一致,來減少出現問題的可能。
關于屬性、事件的調用,則是如下直接調用:
<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}); }}/>關于事件,需要注意的是,事件事件默認傳遞的是字典數據類型,即json,在js中調用需要利用e.nativeEvent才能將字典取出,在具體調用里面的值。(這里也還未研究透徹、需要指導)
React Native 與原生代碼之間混合互相調用。經典項目如下:
鏈接: https://pan.baidu.com/s/1kVwRnYB 密碼: vuwz
新聞熱點
疑難解答