React Native的開發思路是經過組合各類組件來組織整個App,在大部分狀況下經過組合View、Image等幾個基礎的組件,能夠很是方便的實現各類複雜的跨平臺組件,不過在須要原生功能支持、對性能有要求的狀況下仍是須要進行必定的原生的開發,合理的組件實現方式能夠下降使用和跨平臺的成本。javascript
(底層實現分析參見:React-Native 渲染實現分析,本文僅討論組件開發方法)html
RN的組件開發有幾種方式,JS組件、Native功能組件、NativeUI組件。JS組件是僅使用React Native自帶的組件進行組合實現的組件,優點是跨平臺方便,但受限於RN實現的效果,一些複雜需求沒法實現。而Native組件要強大許多,不過須要考慮平臺差別,提供統一的接口難度要大一些。java
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
getInitialState
componentWillMount
render
componentDidMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate
componentWillUnmount
適用場景:
性能要求不高、不須要複雜端上能力的組件。網絡
那麼在須要端上能力、或性能要求高的時候怎麼辦呢?這就須要開發Native組件了,Native組件又能夠簡單的分爲功能性組件和UI組件兩種。app
通常來講,Native的組件開發分爲兩部分,一部分是Native代碼,一部分是與之配合的JS代碼(JS端代碼開發相似JS組件開發,開放接口,屏蔽實現細節、平臺差別),這兩部分共同構成一個Native組件。ide
功能性組件相似服務開發,以模塊的形式提供功能接口(單例),並暴露給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}];
*數據類型:
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} />
定義組件名稱:
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]; } }]; }
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; } }
實現你想實現的,他會出如今JS端使用的DOM位置上。
(底層實現分析參見:React-Native 渲染實現分析)