一、前言
2020,一個不平凡的悲痛的庚子年,新年伊始,湖北武漢就發生了令人始料不及的疫情。一夜間,全國都停止了腳步,隔離在家,共同抗疫。中華民族的凝聚力歷來強大,幾個月的努力,上萬人的付出,如今春暖花開,疫情基本快要控制住了。這場戰役中,犧牲了和去世了太多人,令人痛惜,為活着的人祝福,為逝去的人禱告。哪來的歲月靜好,只不過是有人替我們負重前行。學習依舊,回歸博客。
二、簡介
在ReactNative開發中,ReactNative提供了很多已經封裝好的基礎組件,在前面的文章中有很多實踐。雖然這些基礎的組件可以通過組合成復合組件來實現復雜的功能,但是在性能上稍有不足。原生組件經過長時間的積累和更新,很多優秀的原生UI組件可以極大地提升性能和開發效率,ReactNative可以將它們抽象成ReactJS的組件對象提供給JavaScript端使用,也即構建原生的Native UI組件。Native UI組件實質是就是一個Native模塊,跟構建的Native API組件類似,它還需要被抽象出提供給React使用的標簽,如標簽屬性、響應用戶行為等。在React中創建UI組件時,都會生成reactTag來作為唯一標識。JavaScript UI與Native UI都將通過reactTag進行關聯。JavaScript UI的更新會通過調用RCTUIManager模塊的方法來映射成Native UI的更新。當Native UI被通知改變時,會通過reactTag來定位UI實例來進行更新操作,所有的UI更新並不會馬上執行,而是被緩存到一個UIBlocks中,每次通信完畢后,再由主線程統一執行UIBlocks中的更新。在幀級別的通信頻率下,讓Native UI無縫響應JavaScript的改變。
三、構建
1、UI組件定義
要構建Native UI組件,必須要先創建Native UI組件的管理類,這個管理類繼承自RCTViewManager類,遵循RCTBridgeModule協議,導出模塊類,重寫-(UIView *)view接口返回Native UI實例。注意,Native UI組件的樣式完全是由JavaScript來控制的,所以在這個接口內部設置UI的任何樣式都會被JavaScript的樣式覆蓋。一般不需要對返回的Native UI實例設置frame,如果該組件內部的UI或者圖層不支持自適應,則需要在UI組件的-(void)layoutSubviews方法中自適應布局。構建如下:
OC:
// ReactNativeCustomUIDemo // Created by 夏遠全 on 2020/3/9. // Copyright © 2020 Facebook. All rights reserved. #import <React/RCTViewManager.h> NS_ASSUME_NONNULL_BEGIN @interface RCTMapViewManager : RCTViewManager @end NS_ASSUME_NONNULL_END
// ReactNativeCustomUIDemo // Created by 夏遠全 on 2020/3/9. // Copyright © 2020 Facebook. All rights reserved. #import "RCTMapViewManager.h" #import <MapKit/MapKit.h> @implementation RCTMapViewManager //導出模塊類 RCT_EXPORT_MODULE(); //返回Native UI -(UIView *)view { //地圖 MKMapView *mapView = [[MKMapView alloc] init]; //樣式 mapView.mapType = MKMapTypeStandard; return mapView; } @end
2、UI組件使用
參照系統的命名規范,擴展的Native UI組件模塊都是以Manager為后綴,在使用時只需要在JavaScript中導出對應的原生組件對象即可。組件名需要過濾類名后綴Manager,所有的組件對象導出后都可以使用組件標簽引用。在這里需要使用requireNativeComponent組件引入Native UI組件,如下所示:
JavaScript:
/** * Sample React Native App * https://github.com/facebook/react-native * @flow */ import React, { Component } from 'react'; import { requireNativeComponent, AppRegistry, StyleSheet, View } from 'react-native';/* function requireNativeComponent( viewName: string, //構建的原生UI組件名稱, 去掉manager后綴 componentInterface?: ?ComponentInterface, //封裝到哪個組件內部,可選值。一般將原生的UI組件二次封裝成新的React組件時填寫 extraConfig?: ?{nativeOnly?: Object}, //額外配置,可選值 ) */ const RCTMapView = requireNativeComponent('RCTMapView', null); export default class ReactNativeCustomUIDemo extends Component { render() { return ( <View style={styles.container}> <RCTMapView style={styles.mapView}/> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#F5FCFF', }, mapView: { flex: 1, justifyContent: 'center', alignItems: 'center', } }); AppRegistry.registerComponent('ReactNativeCustomUIDemo', () => ReactNativeCustomUIDemo);
3、UI組件屬性
原生組件的屬性橋接到JavaScript中使用,需要以標簽形式就行訪問。RN中提供了三個宏定義來橋接NativeUI的屬性,分別如下:
//1.導出Native UI Property //name:屬性名稱 type:該屬性對應的類型 #define RCT_EXPORT_VIEW_PROPERTY(name, type) //2.導出重映射的Native UI Property //name:屬性名稱 keyPath:重映射屬性名稱 type:該屬性對應的類型 #define RCT_REMAP_VIEW_PROPERTY(name, keyPath, type) //3.導出自定義的Native UI Property //name:自定義的屬性名稱 type:該屬性對應的類型 viewClass:該屬性對應的組件 #define RCT_CUSTOM_VIEW_PROPERTY(name, type, viewClass)
默認情況下,JavaScript標簽屬性和Native屬性相同,使用上述第1個宏導出屬性即可。如果屬性名稱需要另外定義,則需要使用上述第2個宏導出屬性。這兩種宏定義的使用都必須滿足JavaScript和OC之間的屬性類型是支持轉換的。同前面博文創建Native API組件的模塊方法一樣,屬性的類型也支持標准JSON對象,RCTConvert類能夠幫助實現類型的自動轉換。如果當前屬性的類型不支持轉換,那么此時就要使用上述第3個宏導出屬性。簡單示例如下:
OC:
// RCTMapViewManager.m // ReactNativeCustomUIDemo // Created by 夏遠全 on 2020/3/9. // Copyright © 2020 Facebook. All rights reserved. #import "RCTMapViewManager.h" #import <MapKit/MapKit.h> @implementation RCTMapViewManager //導出模塊類 RCT_EXPORT_MODULE(); //導出Native UI Property //#define RCT_EXPORT_VIEW_PROPERTY(name, type) //name:屬性名稱 type:該屬性對應的類型 RCT_EXPORT_VIEW_PROPERTY(showsCompass, BOOL); //是否顯示指南針 RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL); //是否顯示用戶位置 RCT_EXPORT_VIEW_PROPERTY(showsScale, BOOL); //是否顯示比例尺 //返回Native UI -(UIView *)view { //地圖 MKMapView *mapView = [[MKMapView alloc] init]; //樣式 mapView.mapType = MKMapTypeStandard; return mapView; } @end
JavaScript:
/** * Sample React Native App * https://github.com/facebook/react-native * @flow */ import React, { Component } from 'react'; import { requireNativeComponent, AppRegistry, StyleSheet, View } from 'react-native'; /* function requireNativeComponent( viewName: string, //原生的UI組件名稱 componentInterface?: ?ComponentInterface, //封裝到哪個組件內部,可選值。一般將原生的UI組件二次封裝成新的React組件時填寫 extraConfig?: ?{nativeOnly?: Object}, //額外配置,可選值 ) */ const RCTMapView = requireNativeComponent('RCTMapView', null); export default class ReactNativeCustomUIDemo extends Component { render() { return ( <View style={styles.container}> <RCTMapView style={styles.mapView} showsCompass={true} showsUserLocation={true} showsScale={true} /> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#F5FCFF', }, mapView: { flex: 1, justifyContent: 'center', alignItems: 'center', } }); AppRegistry.registerComponent('ReactNativeCustomUIDemo', () => ReactNativeCustomUIDemo);
4、UI組件方法
NativeUI組件同樣支持模塊方法,也是使用RCT_EXPORT_METHOD宏定義,其方法定義中必須包含由JS傳遞出來的reactTag,其實現邏輯需要封裝在RCTUIManager的addUIBlock接口的塊函數中執行。在塊函數中,可以通過RCTUIManager維護的ViewRegistry根據reactTag獲得調用方法的組件實例。在JS中,需要為組件設置引用ref,調用方法時通過引用ReactNative.findNodeHandle(ref)來獲取組件的reactTag,然后將其作為UI組件模塊方法對應的參數傳入。此處我將RCTMapView單獨封裝成一個獨立的js文件,具體示例如下:
OC:
// RCTMapViewManager.m // ReactNativeCustomUIDemo // Created by 夏遠全 on 2020/3/9. // Copyright © 2020 Facebook. All rights reserved. #import "RCTMapViewManager.h" #import <React/RCTUIManager.h> #import <MapKit/MapKit.h> @implementation RCTMapViewManager //導出模塊類 RCT_EXPORT_MODULE(); //導出Native UI Property //#define RCT_EXPORT_VIEW_PROPERTY(name, type) //name:屬性名稱 type:該屬性對應的類型 RCT_EXPORT_VIEW_PROPERTY(showsCompass, BOOL); //是否顯示指南針 RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL); //是否顯示用戶位置 RCT_EXPORT_VIEW_PROPERTY(showsScale, BOOL); //是否顯示比例尺 //導出方法 RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag){ [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) { //根據reactTag取出對應的目標視圖 id view = viewRegistry[reactTag]; if ([view isKindOfClass:MKMapView.class]) { //此處獲取到了系統的MKMapView組件,可以調用MKMapView的內置方法 // { code } printf("\n-----地圖刷新了----\n"); } }]; } //返回Native UI -(UIView *)view { //地圖 MKMapView *mapView = [[MKMapView alloc] init]; //樣式 mapView.mapType = MKMapTypeStandard; return mapView; } @end
JavaScript:
import React, { Component } from 'react'; import ReactNative, { requireNativeComponent, NativeModules ,StyleSheet } from 'react-native';//模塊類(需要去掉前綴RCT) const RCTMapViewManager = NativeModules.MapViewManager; //UI組件 const RCTMapView = requireNativeComponent('RCTMapView', CustomMapView); //引用 const RCT_UI_REF = "theMapView"; export default class CustomMapView extends Component{ //方法調用 componentDidMount(): void { //根據引用獲取組件的reactTag作為reload方法的參數傳入 RCTMapViewManager.reload( ReactNative.findNodeHandle(this.refs[RCT_UI_REF]) ); } render(){ return ( <RCTMapView ref={RCT_UI_REF} style={styles.container} showsCompass={true} showsUserLocation={true} showsScale={true} /> ); } } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', } });
打印結果如下:
2020-03-19 16:55:59.671 [info][tid:main][RCTBatchedBridge.m:77] Initializing <RCTBatchedBridge: 0x60000143c380> (parent: <RCTBridge: 0x600000631490>, executor: RCTJSCExecutor) 2020-03-19 16:55:59.725 [warn][tid:com.facebook.react.JavaScript][RCTModuleData.mm:220] RCTBridge required dispatch_sync to load RCTDevSettings. This may lead to deadlocks 2020-03-19 16:55:59.891 [info][tid:main][RCTRootView.m:295] Running application ReactNativeCustomUIDemo ({ initialProps = { }; rootTag = 1; }) 2020-03-19 16:55:59.893 [info][tid:com.facebook.react.JavaScript] Running application "ReactNativeCustomUIDemo" with appParams: {"rootTag":1,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF -----地圖刷新了----
5、UI組件事件
NativeUI組件也可以實現與JS進行事件交互,在ReactNative框架中,把原生的事件通知到JavaScript,最后由JavaScript端來完成事件的響應。在ReactNative中,還要在原生控件響應用戶事件的地方,通過事件派發器RCTEventDispatcher的sendInputEventWithName方法來將事件發送給JavaScript模塊。在ReactNative中,事件名會在Native模塊中進行格式化處理,例如帶有c/Change的事件名,會被自動轉為JavaScript的onChange事件屬性來響應。在RCTViewManager中,默認定義了一些事件,這些事件會自動與JavaScript標簽中的onEventName屬性進行綁定,如下所示:
//按壓事件 press //改變事件 change //獲得焦點事件 focus //失去焦點事件 blur //提交事件 submitEnding //結束編輯 endEnding //觸摸開始 touchStart //觸摸移動 touchMove //觸摸取消 touchCancel //觸摸結束 touchEnd
以上都是系統內置事件屬性,但是如果需要自定義的事件名,則需要在Manager類中重寫-(NSArray *)customBubblingEventTypes接口實現。然后在JavaScript與OC中保持事件名一致即可。具體示例如下:
OC:
// RCTMapViewManager.m // ReactNativeCustomUIDemo // Created by 夏遠全 on 2020/3/9. // Copyright © 2020 Facebook. All rights reserved. #import "RCTMapViewManager.h" #import <React/RCTUIManager.h> #import <MapKit/MapKit.h> @interface RCTMapViewManager() <MKMapViewDelegate> @end @implementation RCTMapViewManager //導出模塊類 RCT_EXPORT_MODULE(); //導出Native UI Property //#define RCT_EXPORT_VIEW_PROPERTY(name, type) //name:屬性名稱 type:該屬性對應的類型 RCT_EXPORT_VIEW_PROPERTY(showsCompass, BOOL); //是否顯示指南針 RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL); //是否顯示用戶位置 RCT_EXPORT_VIEW_PROPERTY(showsScale, BOOL); //是否顯示比例尺 //導出方法 RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag){ [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) { //根據reactTag取出對應的目標視圖 id view = viewRegistry[reactTag]; if ([view isKindOfClass:MKMapView.class]) { //此處獲取到了系統的MKMapView組件,可以調用MKMapView的內置方法 // { code } printf("\n-----地圖刷新了----\n"); } }]; } //返回Native UI -(UIView *)view { //地圖 MKMapView *mapView = [[MKMapView alloc] init]; //樣式 mapView.mapType = MKMapTypeStandard; //代理 mapView.delegate = self; return mapView; } //自定義事件名稱 -(NSArray *)customBubblingEventTypes { return @[ @"customEventHandler" ]; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" #pragma mark - delegate - (void)mapViewWillStartLoadingMap:(MKMapView *)mapView { NSDictionary *event = @{ @"target":mapView.reactTag,@"status":@"地圖開始加載"}; [self.bridge.eventDispatcher sendInputEventWithName:@"change" body:event]; //系統的change事件名稱 } - (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView { NSDictionary *event = @{ @"target":mapView.reactTag,@"status":@"地圖加載結束"}; [self.bridge.eventDispatcher sendInputEventWithName:@"change" body:event]; //系統的change事件名稱 } - (void)mapViewWillStartRenderingMap:(MKMapView *)mapView { NSDictionary *event = @{ @"target":mapView.reactTag,@"status":@"mapViewWillStartRenderingMap"}; [self.bridge.eventDispatcher sendInputEventWithName:@"customEventHandler" body:event]; //自定義的customEventHandler事件名稱 } #pragma clang diagnostic pop @end
JavaScript:
import React, { Component } from 'react'; import ReactNative, { requireNativeComponent, NativeModules ,StyleSheet } from 'react-native'; // let NativeModules = require('NativeModules'); //模塊類(需要去掉前綴RCT) const RCTMapViewManager = NativeModules.MapViewManager; //UI組件 const RCTMapView = requireNativeComponent('RCTMapView', CustomMapView); //引用 const RCT_UI_REF = "theMapView"; export default class CustomMapView extends Component{ //方法調用 componentDidMount(): void { //根據引用獲取組件的reactTag作為reload方法的參數傳入 RCTMapViewManager.reload( ReactNative.findNodeHandle(this.refs[RCT_UI_REF]) ); } //系統事件 systemEventComplete(body){ console.log("body---------" + body.nativeEvent.status) } //自定義事件 customEventComplete(body){ console.log("body---------" + body.nativeEvent.status) } render(){ return ( <RCTMapView ref={RCT_UI_REF} style={styles.container} showsCompass={true} showsUserLocation={true} showsScale={true} onChange={this.systemEventComplete.bind(this)} onCustomEventHandler={this.customEventComplete.bind(this)} /> ); } } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', } });
打印結果如下:
2020-03-20 11:13:49.770 [info][tid:main][RCTBatchedBridge.m:77] Initializing <RCTBatchedBridge: 0x600000558700> (parent: <RCTBridge: 0x60000175a220>, executor: RCTJSCExecutor) 2020-03-20 11:13:49.826 [warn][tid:com.facebook.react.JavaScript][RCTModuleData.mm:220] RCTBridge required dispatch_sync to load RCTDevSettings. This may lead to deadlocks 2020-03-20 11:13:50.461 [info][tid:main][RCTRootView.m:295] Running application ReactNativeCustomUIDemo ({ initialProps = { }; rootTag = 1; }) 2020-03-20 11:13:50.463 [info][tid:com.facebook.react.JavaScript] Running application "ReactNativeCustomUIDemo" with appParams: {"rootTag":1,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF -----地圖刷新了---- 2020-03-20 11:13:50.711 [info][tid:com.facebook.react.JavaScript] body---------mapViewWillStartRenderingMap 2020-03-20 11:13:50.792 [info][tid:com.facebook.react.JavaScript] body---------地圖開始加載 2020-03-20 11:13:50.885 [info][tid:com.facebook.react.JavaScript] body---------地圖加載結束