前言
React Native的開發(fā)思路是通過組合各種組件來組織整個(gè)App,在大部分情況下通過組合View、Image等幾個(gè)基礎(chǔ)的組件,可以非常方便的實(shí)現(xiàn)各種復(fù)雜的跨平臺(tái)組件,不過在需要原生功能支持、對(duì)性能有要求的情況下還是需要進(jìn)行一定的原生的開發(fā),合理的組件實(shí)現(xiàn)方式可以降低使用和跨平臺(tái)的成本。
(底層實(shí)現(xiàn)分析參見:React-Native 渲染實(shí)現(xiàn)分析,本文僅討論組件開發(fā)方法)
React Native組件開發(fā)
RN的組件開發(fā)有幾種方式,JS組件、Native功能組件、NativeUI組件。JS組件是僅使用React Native自帶的組件進(jìn)行組合實(shí)現(xiàn)的組件,優(yōu)勢(shì)是跨平臺(tái)方便,但受限于RN實(shí)現(xiàn)的效果,一些復(fù)雜需求無法實(shí)現(xiàn)。而Native組件要強(qiáng)大許多,不過需要考慮平臺(tái)差異,提供統(tǒng)一的接口難度要大一些。
JS組件(熟悉React可略過)
JS組件是指僅靠RN本身自帶的組件開發(fā)的組件,是RN下最基礎(chǔ)的開發(fā)方式,大部分的組件都是JS組件。最大的好處是可以低成本的跨平臺(tái)。JS組件的開發(fā)體驗(yàn)跟React(Web)一致,區(qū)別在基礎(chǔ)標(biāo)簽不一樣,以及樣式定義方式不一樣。
一個(gè)例子,無網(wǎng)絡(luò)提示組件:

(例子語言Typescript)
// 組件的屬性定義
interface PropsDefine {
// 組件寬度
width: number
// 組件高度
height: number
// 點(diǎn)擊刷新按鈕回調(diào),可選
onClickRefresh?: () => void
}
export class NoNetwork extends React.Component<PropsDefine, {}> { // 組件無狀態(tài),定義為空:{}
// 組件的默認(rèn)屬性定義,單例,實(shí)例間共享
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}>你的網(wǎng)絡(luò)好像不給力</Text>
<Text style={Styles.text}>點(diǎn)擊按鈕刷新</Text>
</View>
<TouchableOpacity style={Styles.button} onPress={this.props.onClickRefresh}>
<Text style={Styles.buttonText}>刷新</Text>
</TouchableOpacity>
</View>
)
}
}
跟端上組件開發(fā)一樣,React組件也定義了組件的生命周期:
實(shí)例化
-
getDefaultProps
組件類型首次實(shí)例化時(shí)初始化默認(rèn)props屬性,多實(shí)例共享 -
getInitialState
實(shí)例化時(shí)初始化默認(rèn)state屬性 -
componentWillMount
在渲染之前觸發(fā)一次 -
render
渲染函數(shù),返回DOM結(jié)構(gòu) -
componentDidMount
在渲染之后觸發(fā)一次
有需要重新渲染(props變更或者setState改變state時(shí))
-
componentWillReceiveProps
組件接收到新的props時(shí)調(diào)用,并將其作為參數(shù)nextProps使用,可在此更改組件state -
shouldComponentUpdate
判斷是否需要更新組件(在首次渲染期間或者調(diào)用了forceUpdate方法后,該方法不會(huì)被調(diào)用) -
componentWillUpdate
更新渲染前調(diào)用 -
render
渲染函數(shù),返回DOM結(jié)構(gòu) -
componentDidUpdate
更新渲染后調(diào)用
銷毀
-
componentWillUnmount
組件移除之前調(diào)用
適用場(chǎng)景:
性能要求不高、不需要復(fù)雜端上能力的組件。
Native組件
那么在需要端上能力、或性能要求高的時(shí)候怎么辦呢?這就需要開發(fā)Native組件了,Native組件又可以簡(jiǎn)單的分為功能性組件和UI組件兩種。
一般來說,Native的組件開發(fā)分為兩部分,一部分是Native代碼,一部分是與之配合的JS代碼(JS端代碼開發(fā)類似JS組件開發(fā),開放接口,屏蔽實(shí)現(xiàn)細(xì)節(jié)、平臺(tái)差異),這兩部分共同構(gòu)成一個(gè)Native組件。
Native功能組件
功能性組件類似服務(wù)開發(fā),以模塊的形式提供功能接口(單例),并暴露給JS端。
以 網(wǎng)絡(luò)信息模塊(NetInfo) 為例:
模塊的JS端代碼:
const RCTNetInfo = NativeModules.NetInfo;
// 已Promise的形式主動(dòng)調(diào)用Native模塊
RCTNetInfo.getCurrentConnectivity().then(resp => resp.network_info);
const NetInfoEventEmitter = new NativeEventEmitter(RCTNetInfo);
// 注冊(cè)Native模塊的事件
NetInfoEventEmitter.addListener(
'networkStatusDidChange',
(appStateData) => {
handler(appStateData.network_info);
}
);
模塊的Native端代碼:
Android端:
// 定義暴露的方法,以Promise的形式返回結(jié)果
@ReactMethod
public void getCurrentConnectivity(Promise promise) {
promise.resolve(createConnectivityEventMap());
}
// 發(fā)送網(wǎng)絡(luò)狀態(tài)變更事件
private void sendConnectivityChangedEvent() {
getReactApplicationContext().getJSModule(RCTDeviceEventEmitter.class)
.emit("networkStatusDidChange", createConnectivityEventMap());
}
// 創(chuàng)建網(wǎng)絡(luò)狀態(tài)變更事件數(shù)據(jù)
private WritableMap createConnectivityEventMap() {
WritableMap event = new WritableNativeMap();
event.putString("network_info", mConnectivity);
return event;
}
iOS端:
// 定義暴露的方法,以Promise的形式返回結(jié)果
RCT_EXPORT_METHOD(getCurrentConnectivity:(RCTPromiseResolveBlock)resolve
reject:(__unused RCTPromiseRejectBlock)reject)
{
resolve(@{@"network_info": _status ?: RCTReachabilityStateUnknown});
}
// 發(fā)送網(wǎng)絡(luò)狀態(tài)變更事件
[self sendEventWithName:@"networkStatusDidChange" body:@{@"network_info": status}];
*數(shù)據(jù)類型:
- iOS下因?yàn)橛衖d類型,所以限制不大,跟JS對(duì)象一致
- Android下需要手動(dòng)讀取對(duì)象值
- Null
- Boolean
- Number
- String
- Map
- Array
NativeUI組件
UI組件要復(fù)雜一些,需要定義ViewManager和具體View的實(shí)現(xiàn),ViewManager用來創(chuàng)建/管理View實(shí)例,是View實(shí)例和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;
}
// 創(chuàng)建組件實(shí)例
@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調(diào)用UI組件的方法:
UIManager.dispatchViewManagerCommand(
findNodeHandle(this), // 找到與NativeUI組件對(duì)應(yīng)的JS組件實(shí)例
UIManager.[UI組件名].Commands.[方法],
[] // 參數(shù)
)
Android端:
// 定義命令號(hào)
@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) {
// 根據(jù)命令號(hào)處理,root為對(duì)應(yīng)View的實(shí)例
switch(commandId) {
}
}
iOS端:
RCT_EXPORT_METHOD(start:(nonnull NSNumber *)reactTag
data:data)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, TBNAnimationView *> *viewRegistry) {
// 找到目標(biāo)View實(shí)例
TBNAnimationView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[TBNAnimationView class]]) {
RCTLogError(@"Invalid view returned from registry, expecting TBNAnimationView, got: %@", view);
} else {
// 調(diào)用View的方法
[view start:data];
}
}];
}
Native事件回調(diào)JS端:
Android端使用的是類似JS端調(diào)用Native的方式,使用了事件機(jī)制,不過事件的接收者是從JS端映射過來的,React下ReactNativeEventEmitter.receiveEvent(tag, topLevelType, nativeEventParam),所以需要先實(shí)現(xiàn)一個(gè)Event:(Switch的onValueChange事件)
class ReactSwitchEvent extends Event<ReactSwitchEvent> {
public static final String EVENT_NAME = "topChange"; // topChange會(huì)被映射成onChange,具體映射關(guān)系參見 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中進(jìn)行事件派發(fā):
ReactContext reactContext = (ReactContext) buttonView.getContext();
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent(
new ReactSwitchEvent(
buttonView.getId(),
isChecked));
iOS端實(shí)現(xiàn)有所區(qū)別,iOS端將JS函數(shù)直接映射到Native,所以可以直接調(diào)用(可多次調(diào)用):(View為RCTSwitch)
// ViewManager中聲明事件為RCTBubblingEventBlock或RCTDirectEventBlock
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock);
// View中聲明
@property (nonatomic, copy) RCTBubblingEventBlock onChange;
// view實(shí)例化時(shí)監(jiān)聽onChange
- (void)onChange:(RCTSwitch *)sender
{
if (sender.wasOn != sender.on) {
if (sender.onChange) {
sender.onChange(@{ @"value": @(sender.on) });
}
sender.wasOn = sender.on;
}
}
View:
實(shí)現(xiàn)你想實(shí)現(xiàn)的,他會(huì)出現(xiàn)在JS端使用的DOM位置上。
(底層實(shí)現(xiàn)分析參見:React-Native 渲染實(shí)現(xiàn)分析)