ReactNative: 构建原生的Native UI组件(此文以系统内置视图为例,自定义视图的构建原理相同,自定义一个view导出即可)
一、前言
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---------地图加载结束