React Native使用指南-原生UI组件
在如今的App中,已經(jīng)有成千上萬(wàn)的原生UI部件了——其中的一些是平臺(tái)的一部分,另一些可能來(lái)自于一些第三方庫(kù),而且可能你自己還收藏了很多。React Native已經(jīng)封裝了大部分最常見的組件,譬如ScrollView和TextInput,但不可能封裝全部組件。而且,說(shuō)不定你曾經(jīng)為自己以前的App還封裝過(guò)一些組件,React Native肯定沒(méi)法包含它們。幸運(yùn)的是,在React Naitve應(yīng)用程序中封裝和植入已有的組件非常簡(jiǎn)單。
和原生模塊向?qū)б粯?#xff0c;本向?qū)б彩且粋€(gè)相對(duì)高級(jí)的向?qū)?#xff0c;我們假設(shè)你已經(jīng)對(duì)iOS編程頗有經(jīng)驗(yàn)。本向?qū)?huì)引導(dǎo)你如何構(gòu)建一個(gè)原生UI組件,帶領(lǐng)你了解React Native核心庫(kù)中MapView組件的具體實(shí)現(xiàn)。
iOS MapView樣例
假設(shè)我們要把地圖組件植入到我們的App中——我們用到的是MKMapView,而現(xiàn)在只需要讓它可以被Javascript重用。
原生視圖都需要被一個(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)的事件。
提供原生視圖很簡(jiǎn)單:
- 首先創(chuàng)建一個(gè)子類
- 添加RCT_EXPORT_MODULE()標(biāo)記宏
- 實(shí)現(xiàn)-(UIView *)view方法
接下來(lái)你需要一些Javascript代碼來(lái)讓這個(gè)視圖變成一個(gè)可用的React組件:
// MapView.jsvar { requireNativeComponent } = require('react-native');// requireNativeComponent 自動(dòng)把這個(gè)組件提供給 "RCTMapManager" module.exports = requireNativeComponent('RCTMap', null);現(xiàn)在我們就已經(jīng)實(shí)現(xiàn)了一個(gè)完整功能的地圖組件了,諸如捏放和其它的手勢(shì)都已經(jīng)完整支持。但是現(xiàn)在我們還不能真正的從Javascript端控制它。(╯﹏╰)
屬性
我們能讓這個(gè)組件變得更強(qiáng)大的第一件事情就是要能夠封裝一些原生屬性供Javascript使用。舉例來(lái)說(shuō),我們希望能夠禁用手指捏放操作,然后指定一個(gè)初始的地圖可見區(qū)域。禁用捏放操作只需要一個(gè)布爾值類型的屬性就行了,所以我們添加這么一行:
// RCTMapManager.m RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL)注意我們現(xiàn)在把類型聲明為BOOL類型——React Native用RCTConvert來(lái)在JavaScript和原生代碼之間完成類型轉(zhuǎn)換。如果轉(zhuǎn)換無(wú)法完成,會(huì)產(chǎn)生一個(gè)“紅屏”的報(bào)錯(cuò)提示,這樣你就能立即知道代碼中出現(xiàn)了問(wèn)題。如果一切進(jìn)展順利,上面這個(gè)宏就已經(jīng)包含了導(dǎo)出屬性的全部實(shí)現(xiàn)。
現(xiàn)在要想禁用捏放操作,我們只需要在JS里設(shè)置對(duì)應(yīng)的屬性:
// MyApp.js <MapView pitchEnabled={false} />但這樣并不能很好的說(shuō)明這個(gè)組件的用法——用戶要想知道我們的組件有哪些屬性可以用,以及可以取什么樣的值,他不得不一路翻到Objective-C的代碼。要解決這個(gè)問(wèn)題,我們可以創(chuàng)建一個(gè)封裝組件,并且通過(guò)PropTypes來(lái)說(shuō)明這個(gè)組件的接口。
// MapView.js var React = require('react-native'); var { requireNativeComponent } = React;class MapView extends React.Component {render() {return <RCTMap {...this.props} />;} }MapView.propTypes = {/*** 當(dāng)這個(gè)屬性被設(shè)置為true,并且地圖上綁定了一個(gè)有效的可視區(qū)域的情況下,* 可以通過(guò)捏放操作來(lái)改變攝像頭的偏轉(zhuǎn)角度。* 當(dāng)這個(gè)屬性被設(shè)置成false時(shí),攝像頭的角度會(huì)被忽略,地圖會(huì)一直顯示為俯視狀態(tài)。*/pitchEnabled: React.PropTypes.bool, };var RCTMap = requireNativeComponent('RCTMap', MapView);module.exports = MapView;譯注:使用了封裝組件之后,你還需要注意到module.exports導(dǎo)出的不再是requireNativeComponent的返回值,而是所創(chuàng)建的包裝組件。
現(xiàn)在我們有了一個(gè)封裝好的組件,還有了一些注釋文檔,用戶使用起來(lái)也更方便了。注意我們現(xiàn)在把requireNativeComponent的第二個(gè)參數(shù)從null變成了用于封裝的組件MapView。這使得React Native的底層框架可以檢查原生屬性和包裝類的屬性是否一致,來(lái)減少出現(xiàn)問(wèn)題的可能。
現(xiàn)在,讓我們添加一個(gè)更復(fù)雜些的region屬性。我們首先添加原生代碼:
// RCTMapManager.m RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap) {[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES]; }這段代碼比剛才的一個(gè)簡(jiǎn)單的BOOL要復(fù)雜的多了。現(xiàn)在我們多了一個(gè)需要做類型轉(zhuǎn)換的MKCoordinateRegion類型,還添加了一部分自定義的代碼,這樣當(dāng)我們?cè)贘S里改變地圖的可視區(qū)域的時(shí)候,視角會(huì)平滑地移動(dòng)過(guò)去。在我們提供的函數(shù)體內(nèi),json代表了JS中傳遞的尚未解析的原始值。函數(shù)里還有一個(gè)view變量,使得我們可以訪問(wèn)到對(duì)應(yīng)的視圖實(shí)例。最后,還有一個(gè)defaultView對(duì)象,這樣當(dāng)JS給我們發(fā)送null的時(shí)候,可以把視圖的這個(gè)屬性重置回默認(rèn)值。
你可以為視圖編寫任何你所需要的轉(zhuǎn)換函數(shù)——下面就是MKCoordinateRegion的轉(zhuǎn)換實(shí)現(xiàn),它通過(guò)兩個(gè)RCTConvert的擴(kuò)展來(lái)完成:
@implementation RCTConvert(CoreLocation)RCT_CONVERTER(CLLocationDegrees, CLLocationDegrees, doubleValue); RCT_CONVERTER(CLLocationDistance, CLLocationDistance, doubleValue);+ (CLLocationCoordinate2D)CLLocationCoordinate2D:(id)json {json = [self NSDictionary:json];return (CLLocationCoordinate2D){[self CLLocationDegrees:json[@"latitude"]],[self CLLocationDegrees:json[@"longitude"]]}; }@end@implementation RCTConvert(MapKit)+ (MKCoordinateSpan)MKCoordinateSpan:(id)json {json = [self NSDictionary:json];return (MKCoordinateSpan){[self CLLocationDegrees:json[@"latitudeDelta"]],[self CLLocationDegrees:json[@"longitudeDelta"]]}; }+ (MKCoordinateRegion)MKCoordinateRegion:(id)json {return (MKCoordinateRegion){[self CLLocationCoordinate2D:json],[self MKCoordinateSpan:json]}; }這些轉(zhuǎn)換函數(shù)被設(shè)計(jì)為可以安全的處理任何JS扔過(guò)來(lái)的JSON:當(dāng)有任何缺少的鍵或者其它問(wèn)題發(fā)生的時(shí)候,顯示一個(gè)“紅屏”的錯(cuò)誤提示。
為了完成region屬性的支持,我們還需要在propTypes里添加相應(yīng)的說(shuō)明(否則我們會(huì)立刻收到一個(gè)錯(cuò)誤提示),然后就可以像使用其他屬性一樣使用了:
// MapView.jsMapView.propTypes = {/*** 當(dāng)這個(gè)屬性被設(shè)置為true,并且地圖上綁定了一個(gè)有效的可視區(qū)域的情況下,* 可以通過(guò)捏放操作來(lái)改變攝像頭的偏轉(zhuǎn)角度。* 當(dāng)這個(gè)屬性被設(shè)置成false時(shí),攝像頭的角度會(huì)被忽略,地圖會(huì)一直顯示為俯視狀態(tài)。*/pitchEnabled: React.PropTypes.bool,/*** 地圖要顯示的區(qū)域。** 區(qū)域由中心點(diǎn)坐標(biāo)和區(qū)域范圍坐標(biāo)來(lái)定義。* */region: React.PropTypes.shape({/*** 地圖中心點(diǎn)的坐標(biāo)。*/latitude: React.PropTypes.number.isRequired,longitude: React.PropTypes.number.isRequired,/*** 最小/最大經(jīng)、緯度間的距離。*/latitudeDelta: React.PropTypes.number.isRequired,longitudeDelta: React.PropTypes.number.isRequired,}), };// MyApp.jsrender() {var region = {latitude: 37.48,longitude: -122.16,latitudeDelta: 0.1,longitudeDelta: 0.1,};return <MapView region={region} />;}現(xiàn)在你可以看到region屬性的整個(gè)結(jié)構(gòu)已經(jīng)加上了文檔說(shuō)明——將來(lái)可能我們會(huì)自動(dòng)生成一些類似的代碼,但目前還沒(méi)有這樣的手段。
有時(shí)候你的原生組件有一些特殊的屬性希望導(dǎo)出,但并不希望它成為公開的接口。舉個(gè)例子,Switch組件可能會(huì)有一個(gè)onChange屬性用來(lái)傳遞原始的原生事件,然后導(dǎo)出一個(gè)onValueChange屬性,這個(gè)屬性在調(diào)用的時(shí)候會(huì)帶上Switch的狀態(tài)作為參數(shù)之一。這樣的話你可能不希望原生專用的屬性出現(xiàn)在API之中,也就不希望把它放到propTypes里。可是如果你不放的話,又會(huì)出現(xiàn)一個(gè)報(bào)錯(cuò)。解決方案就是帶上額外的nativeOnly參數(shù),像這樣:
var RCTSwitch = requireNativeComponent('RCTSwitch', Switch, {nativeOnly: { onChange: true } });事件
現(xiàn)在我們已經(jīng)有了一個(gè)原生地圖組件,并且從JS可以很容易的控制它了。不過(guò)我們?cè)趺床拍芴幚韥?lái)自用戶的事件,譬如縮放操作或者拖動(dòng)來(lái)改變可視區(qū)域?關(guān)鍵的步驟就在于讓RCTMapManager來(lái)委托我們提供的所有視圖,然后把事件通過(guò)分發(fā)器傳遞給JavaScript。最終的代碼看起來(lái)類似這樣(比起完整的實(shí)現(xiàn)有所簡(jiǎn)化):
// RCTMapManager.m#import "RCTMapManager.h"#import <MapKit/MapKit.h>#import "RCTBridge.h" #import "RCTEventDispatcher.h" #import "UIView+React.h"@interface RCTMapManager() <MKMapViewDelegate> @end@implementation RCTMapManagerRCT_EXPORT_MODULE()- (UIView *)view {MKMapView *map = [[MKMapView alloc] init];map.delegate = self;return map; }#pragma mark MKMapViewDelegate- (void)mapView:(RCTMap *)mapView regionDidChangeAnimated:(BOOL)animated {MKCoordinateRegion region = mapView.region;NSDictionary *event = @{@"target": mapView.reactTag,@"region": @{@"latitude": @(region.center.latitude),@"longitude": @(region.center.longitude),@"latitudeDelta": @(region.span.latitudeDelta),@"longitudeDelta": @(region.span.longitudeDelta),}};[self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:event]; }如你所見,我們剛才配置了管理器,委托它代理創(chuàng)建的所有視圖,并且在委托方法-mapView:regionDidChangeAnimated:中,把地圖目前的區(qū)域以及reactTag目標(biāo)封裝成了一個(gè)事件,這樣我們的事件就可以通過(guò)sendInputEventWithName:body:發(fā)送到正確的React組件實(shí)例上。事件名@"topChange"對(duì)應(yīng)的是JavaScript端的onChange回調(diào)屬性。這個(gè)回調(diào)會(huì)被原生事件執(zhí)行,然后我們通常都會(huì)在封裝組件里做一些處理,來(lái)使得API更簡(jiǎn)明:
// MapView.jsclass MapView extends React.Component {constructor() {this._onChange = this._onChange.bind(this);}_onChange(event: Event) {if (!this.props.onRegionChange) {return;}this.props.onRegionChange(event.nativeEvent.region);}render() {return <RCTMap {...this.props} onChange={this._onChange} />;} } MapView.propTypes = {/*** Callback that is called continuously when the user is dragging the map.*/onRegionChange: React.PropTypes.func,... };樣式
因?yàn)槲覀兯械囊晥D都是UIView的子類,大部分的樣式屬性應(yīng)該直接就可以生效。但有一部分組件會(huì)希望使用自己定義的默認(rèn)樣式,例如UIDatePicker希望自己的大小是固定的。這個(gè)默認(rèn)屬性對(duì)于布局算法的正常工作來(lái)說(shuō)很重要,但我們也希望在使用這個(gè)組件的時(shí)候可以覆蓋這些默認(rèn)的樣式。DatePickerIOS實(shí)現(xiàn)這個(gè)功能的辦法是通過(guò)封裝一個(gè)擁有彈性樣式的額外視圖,然后在內(nèi)層的視圖上應(yīng)用一個(gè)固定樣式(通過(guò)原生傳遞來(lái)的常數(shù)生成):
// DatePickerIOS.ios.jsvar RCTDatePickerIOSConsts = require('react-native').UIManager.RCTDatePicker.Constants; ...render: function() {return (<View style={this.props.style}><RCTDatePickerIOSref={DATEPICKER}style={styles.rkDatePickerIOS}.../></View>);} });var styles = StyleSheet.create({rkDatePickerIOS: {height: RCTDatePickerIOSConsts.ComponentHeight,width: RCTDatePickerIOSConsts.ComponentWidth,}, });常量RCTDatePickerIOSConsts在原生代碼中導(dǎo)出,從一個(gè)組件的實(shí)際布局上獲取到:
// RCTDatePickerManager.m- (NSDictionary *)constantsToExport {UIDatePicker *dp = [[UIDatePicker alloc] init];[dp layoutIfNeeded];return @{@"ComponentHeight": @(CGRectGetHeight(dp.frame)),@"ComponentWidth": @(CGRectGetWidth(dp.frame)),@"DatePickerModes": @{@"time": @(UIDatePickerModeTime),@"date": @(UIDatePickerModeDate),@"datetime": @(UIDatePickerModeDateAndTime),}}; }本向?qū)Ц采w了包裝原生組件所需了解的許多方面,不過(guò)你可能還有很多知識(shí)需要了解,譬如特殊的方式來(lái)插入和布局子視圖。如果你想更深入了解,可以閱讀RCTMapManager和其它的組件的源代碼。
總結(jié)
以上是生活随笔為你收集整理的React Native使用指南-原生UI组件的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 关于iOS7里的JavaScriptCo
- 下一篇: web处理高并发措施