React-Native 组件开发方法

作者: SuperEVO | 来源:发表于2016-10-28 01:22 被阅读962次

    前言

    React Native的开发思路是通过组合各种组件来组织整个App,在大部分情况下通过组合View、Image等几个基础的组件,可以非常方便的实现各种复杂的跨平台组件,不过在需要原生功能支持、对性能有要求的情况下还是需要进行一定的原生的开发,合理的组件实现方式可以降低使用和跨平台的成本。

    (底层实现分析参见:React-Native 渲染实现分析,本文仅讨论组件开发方法)

    React Native组件开发

    RN的组件开发有几种方式,JS组件、Native功能组件、NativeUI组件。JS组件是仅使用React Native自带的组件进行组合实现的组件,优势是跨平台方便,但受限于RN实现的效果,一些复杂需求无法实现。而Native组件要强大许多,不过需要考虑平台差异,提供统一的接口难度要大一些。

    JS组件(熟悉React可略过)

    JS组件是指仅靠RN本身自带的组件开发的组件,是RN下最基础的开发方式,大部分的组件都是JS组件。最大的好处是可以低成本的跨平台。JS组件的开发体验跟React(Web)一致,区别在基础标签不一样,以及样式定义方式不一样。

    一个例子,无网络提示组件:

    无网络提示组件

    (例子语言Typescript)

    // 组件的属性定义
    interface PropsDefine {
        // 组件宽度
        width: number
        // 组件高度
        height: number
        // 点击刷新按钮回调,可选
        onClickRefresh?: () => void
    }
    export class NoNetwork extends React.Component<PropsDefine, {}> { // 组件无状态,定义为空:{}
        // 组件的默认属性定义,单例,实例间共享
        static defaultProps = {
            onClickRefresh: () => { }
        }
    
        constructor(props: PropsDefine) {
            super(props)
        }
    
        render() {
            let {width, height} = this.props
    
            return (
                <View style={[Styles.panel, {
                    width: width,
                    height: height,
                }]}>
                    <View style={Styles.picBlock}>
                        <Image source={Styles.picUrl}/>
                    </View>
                    <View style={Styles.textBlock}>
                        <Text style={Styles.text}>你的网络好像不给力</Text>
                        <Text style={Styles.text}>点击按钮刷新</Text>
                    </View>
                    <TouchableOpacity style={Styles.button} onPress={this.props.onClickRefresh}>
                        <Text style={Styles.buttonText}>刷新</Text>
                    </TouchableOpacity>
                </View>
            )
        }
    }
    

    跟端上组件开发一样,React组件也定义了组件的生命周期:

    实例化

    • getDefaultProps
      组件类型首次实例化时初始化默认props属性,多实例共享
    • getInitialState
      实例化时初始化默认state属性
    • componentWillMount
      在渲染之前触发一次
    • render
      渲染函数,返回DOM结构
    • componentDidMount
      在渲染之后触发一次

    有需要重新渲染(props变更或者setState改变state时)

    • componentWillReceiveProps
      组件接收到新的props时调用,并将其作为参数nextProps使用,可在此更改组件state
    • shouldComponentUpdate
      判断是否需要更新组件(在首次渲染期间或者调用了forceUpdate方法后,该方法不会被调用)
    • componentWillUpdate
      更新渲染前调用
    • render
      渲染函数,返回DOM结构
    • componentDidUpdate
      更新渲染后调用

    销毁

    • componentWillUnmount
      组件移除之前调用

    适用场景:
    性能要求不高、不需要复杂端上能力的组件。

    Native组件

    那么在需要端上能力、或性能要求高的时候怎么办呢?这就需要开发Native组件了,Native组件又可以简单的分为功能性组件和UI组件两种。

    一般来说,Native的组件开发分为两部分,一部分是Native代码,一部分是与之配合的JS代码(JS端代码开发类似JS组件开发,开放接口,屏蔽实现细节、平台差异),这两部分共同构成一个Native组件。

    Native功能组件

    功能性组件类似服务开发,以模块的形式提供功能接口(单例),并暴露给JS端。

    网络信息模块(NetInfo) 为例:

    模块的JS端代码:

    const RCTNetInfo = NativeModules.NetInfo;
    // 已Promise的形式主动调用Native模块
    RCTNetInfo.getCurrentConnectivity().then(resp => resp.network_info);
    
    const NetInfoEventEmitter = new NativeEventEmitter(RCTNetInfo);
    // 注册Native模块的事件
    NetInfoEventEmitter.addListener(
          'networkStatusDidChange',
          (appStateData) => {
            handler(appStateData.network_info);
          }
        );
    

    模块的Native端代码:

    Android端:

      // 定义暴露的方法,以Promise的形式返回结果
      @ReactMethod
      public void getCurrentConnectivity(Promise promise) {
        promise.resolve(createConnectivityEventMap());
      }
      
      // 发送网络状态变更事件
      private void sendConnectivityChangedEvent() {
        getReactApplicationContext().getJSModule(RCTDeviceEventEmitter.class)
            .emit("networkStatusDidChange", createConnectivityEventMap());
      }
      
      // 创建网络状态变更事件数据
      private WritableMap createConnectivityEventMap() {
        WritableMap event = new WritableNativeMap();
        event.putString("network_info", mConnectivity);
        return event;
      }
    

    iOS端:

    // 定义暴露的方法,以Promise的形式返回结果
    RCT_EXPORT_METHOD(getCurrentConnectivity:(RCTPromiseResolveBlock)resolve
                      reject:(__unused RCTPromiseRejectBlock)reject)
    {
      resolve(@{@"network_info": _status ?: RCTReachabilityStateUnknown});
    }
    
    // 发送网络状态变更事件
    [self sendEventWithName:@"networkStatusDidChange" body:@{@"network_info": status}];
    

    *数据类型:

    • iOS下因为有id类型,所以限制不大,跟JS对象一致
    • Android下需要手动读取对象值
      • Null
      • Boolean
      • Number
      • String
      • Map
      • Array

    NativeUI组件

    UI组件要复杂一些,需要定义ViewManager和具体View的实现,ViewManager用来创建/管理View实例,是View实例和React之间沟通的桥梁。

    Android端ViewManager可继承自SimpleViewManager、ViewGroupManager

    以图片组件Image为例:

    组件的JS端代码:

    // 引用Native组件
    const RCTImageView = requireNativeComponent('RCTImageView', Image);
    
    // 组件代码
    <RCTImageView
      {...this.props}
      style={style}
      resizeMode={resizeMode}
      tintColor={tintColor}
      source={sources}
    />
    

    定义组件名称:

    ViewManager:

    Android端:

    // 定义组件名称
    public static final String REACT_CLASS = "RCTImageView";
    
    @Override
    public String getName() {
      return REACT_CLASS;
    }
    
    // 创建组件实例
    @Override
    public ReactImageView createViewInstance(ThemedReactContext context) {
    return new ReactImageView(
        context,
        getDraweeControllerBuilder(),
        getCallerContext());
    }
    

    iOS端:

    RCT_EXPORT_MODULE()
    
    // RCT_EXPORT_MODULE的宏定义
    #define RCT_EXPORT_MODULE(js_name) \
    RCT_EXTERN void RCTRegisterModule(Class); \
    + (NSString *)moduleName { return @#js_name; } \
    + (void)load { RCTRegisterModule(self); }
    
    // Implemented by RCT_EXPORT_MODULE
    + (NSString *)moduleName;
    
    - (UIView *)view
    {
      return [[RCTImageView alloc] initWithBridge:self.bridge];
    }
    

    定义组件属性:

    JS端传递属性的形式是:

    <RCTImageView
      {...this.props}
      // 属性
      style={style}
      resizeMode={resizeMode}
      tintColor={tintColor}
      source={sources}
    />
    

    Android端:

    @ReactProp(name = "src")
    public void setSource(ReactImageView view, @Nullable ReadableArray sources) {
      view.setSource(sources);
    }
    

    iOS端:

    RCT_EXPORT_VIEW_PROPERTY(blurRadius, CGFloat)
    RCT_REMAP_VIEW_PROPERTY(defaultSource, defaultImage, UIImage)
    RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTImageView)
    {
      view.tintColor = [RCTConvert UIColor:json] ?: defaultView.tintColor;
      view.renderingMode = json ? UIImageRenderingModeAlwaysTemplate : defaultView.renderingMode;
    }
    

    定义组件方法:

    在JS端可以通过UIManager调用UI组件的方法:

    UIManager.dispatchViewManagerCommand(
        findNodeHandle(this), // 找到与NativeUI组件对应的JS组件实例
        UIManager.[UI组件名].Commands.[方法],
        [] // 参数
    )
    

    Android端:

    // 定义命令号
    @Override
    public @Nullable Map<String, Integer> getCommandsMap() {
      return MapBuilder.of("focusTextInput", FOCUS_TEXT_INPUT, "blurTextInput", BLUR_TEXT_INPUT);
    }
    // 处理命令
    public void receiveCommand(T root, int commandId, @Nullable ReadableArray args) {
        // 根据命令号处理,root为对应View的实例
        switch(commandId) {
            
        }
    }
    

    iOS端:

    RCT_EXPORT_METHOD(start:(nonnull NSNumber *)reactTag
                      data:data)
    {
        [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, TBNAnimationView *> *viewRegistry) {
            // 找到目标View实例
            TBNAnimationView *view = viewRegistry[reactTag];
            if (![view isKindOfClass:[TBNAnimationView class]]) {
                RCTLogError(@"Invalid view returned from registry, expecting TBNAnimationView, got: %@", view);
            } else {
                // 调用View的方法
                [view start:data];
            }
        }];
    }
    

    Native事件回调JS端:

    Android端使用的是类似JS端调用Native的方式,使用了事件机制,不过事件的接收者是从JS端映射过来的,React下ReactNativeEventEmitter.receiveEvent(tag, topLevelType, nativeEventParam),所以需要先实现一个Event:(Switch的onValueChange事件)

    class ReactSwitchEvent extends Event<ReactSwitchEvent> {
        public static final String EVENT_NAME = "topChange"; // topChange会被映射成onChange,具体映射关系参见 UIManagerModuleConstants.java
    
        public ReactSwitchEvent(int viewId, boolean isChecked) {
            super(viewId);
            mIsChecked = isChecked;
        }
    
        public boolean getIsChecked() {
            return mIsChecked;
        }
    
        @Override
        public String getEventName() {
            return EVENT_NAME;
        }
    
        @Override
        public short getCoalescingKey() {
            // All switch events for a given view can be coalesced.
            return 0;
        }
    
        @Override
        public void dispatch(RCTEventEmitter rctEventEmitter) {
            rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
        }
    
        private WritableMap serializeEventData() {
            WritableMap eventData = Arguments.createMap();
            eventData.putInt("target", getViewTag());
            eventData.putBoolean("value", getIsChecked());
            return eventData;
        }
    }
    

    然后在ViewManager或View中进行事件派发:

    ReactContext reactContext = (ReactContext) buttonView.getContext();
    reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent(
        new ReactSwitchEvent(
            buttonView.getId(),
            isChecked));
    

    iOS端实现有所区别,iOS端将JS函数直接映射到Native,所以可以直接调用(可多次调用):(View为RCTSwitch)

    // ViewManager中声明事件为RCTBubblingEventBlock或RCTDirectEventBlock
    RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock);
    
    // View中声明
    @property (nonatomic, copy) RCTBubblingEventBlock onChange;
    
    // view实例化时监听onChange
    - (void)onChange:(RCTSwitch *)sender
    {
      if (sender.wasOn != sender.on) {
        if (sender.onChange) {
          sender.onChange(@{ @"value": @(sender.on) });
        }
        sender.wasOn = sender.on;
      }
    }
    

    View:

    实现你想实现的,他会出现在JS端使用的DOM位置上。

    (底层实现分析参见:React-Native 渲染实现分析

    相关文章

      网友评论

        本文标题:React-Native 组件开发方法

        本文链接:https://www.haomeiwen.com/subject/ceetyttx.html