React-Native 組件開發方法

前言

React Native的開發思路是經過組合各類組件來組織整個App,在大部分狀況下經過組合View、Image等幾個基礎的組件,能夠很是方便的實現各類複雜的跨平臺組件,不過在須要原生功能支持、對性能有要求的狀況下仍是須要進行必定的原生的開發,合理的組件實現方式能夠下降使用和跨平臺的成本。javascript

(底層實現分析參見:React-Native 渲染實現分析,本文僅討論組件開發方法)html

React Native組件開發

RN的組件開發有幾種方式,JS組件、Native功能組件、NativeUI組件。JS組件是僅使用React Native自帶的組件進行組合實現的組件,優點是跨平臺方便,但受限於RN實現的效果,一些複雜需求沒法實現。而Native組件要強大許多,不過須要考慮平臺差別,提供統一的接口難度要大一些。java

JS組件(熟悉React可略過)

JS組件是指僅靠RN自己自帶的組件開發的組件,是RN下最基礎的開發方式,大部分的組件都是JS組件。最大的好處是能夠低成本的跨平臺。JS組件的開發體驗跟React(Web)一致,區別在基礎標籤不同,以及樣式定義方式不同。react

一個例子,無網絡提示組件:json

無網絡提示組件

(例子語言Typescript)react-native

// 組件的屬性定義
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組件也定義了組件的生命週期:promise

實例化

  • 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組件兩種。app

通常來講,Native的組件開發分爲兩部分,一部分是Native代碼,一部分是與之配合的JS代碼(JS端代碼開發相似JS組件開發,開放接口,屏蔽實現細節、平臺差別),這兩部分共同構成一個Native組件。ide

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 渲染實現分析

相關文章
相關標籤/搜索