React Native開發(fā)小貼士

1. 屏幕適配

RN布局使用的單位是dp,而開發(fā)人員從設計稿最方便獲取的是px,所以需要一個工具類把px轉(zhuǎn)成dp,下面以寬度為375px的設計稿為例:

const deviceWidthDp = Dimensions.get('window').width;
const uiWidthPx = 375;
export default function pxtodp(uiElementPx) {
    return Platform.OS === 'ios' ? uiElementPx *  deviceWidthDp / uiWidthPx : Math.floor(uiElementPx *  deviceWidthDp / uiWidthPx)
}

在bug修復階段,發(fā)現(xiàn)一個常見的bug,多組件傳值時,出現(xiàn)了多次的p2dp嵌套,導致了值被轉(zhuǎn)換多次,不符預期,所以寫組件的時,應該規(guī)定好是最底層使用p2dp,還是傳入的參數(shù)使用p2dp。

2. 樣式管理

  • RN的樣式可以是數(shù)組,類似css中定義多個class;
style={[styles.A,styles.B]}
  • RN的樣式?jīng)]有繼承嵌套這類的功能,為了方便、高效使用樣式,我們使用了一個樣式的工具類,寫入常用的樣式、樣式組合,便于頁面調(diào)用。但每個頁面調(diào)用都要引入工具類太麻煩,考慮注冊到全局變量,這時候發(fā)現(xiàn)了一個問題,RN的global(全局變量)只能作用于Component,在StyleSheet無法識別,難道是根據(jù)某種上下文關系存在的?最后找到一個解決方案是我們寫一個函數(shù),在函數(shù)內(nèi)是就能訪問全局變量,然后把StyleSheet在函數(shù)中return出來,代碼片段像這樣:
const styles = () => {
  const {
    paddingLarge, paddingSmall, paddingMedium,
    fontSizeMedium, fontSizeSmall, fontWeightLight
  } = theme

  return StyleSheet.create({
    wrapper: {
      height: px2dp(106),
      marginHorizontal: paddingLarge,

3. 平臺差異

使用react-native的Platform庫來控制android和ios的差異

Platform.OS === 'ios' ? doios : doandroid

新版api還可以這么寫:

const instructions = Platform.select({
  ios: 'Press Cmd+R to reload,\n' +
    'Cmd+D or shake for dev menu',
  android: 'Double tap R on your keyboard to reload,\n' +
    'Shake or press menu button for dev menu',
});

另外如何需要根據(jù)平臺引入不同的組件,例如:

BigButton.ios.js
BigButton.android.js

你可以直接:

const BigButton = require('./BigButton');

React Native 會自動識別。

4. 點擊

RN上除了Text組件(自帶onPress方法),其他組件默認是不支持點擊事件。所以 RN 中提供了幾個直接處理響應事件的組件,基本上能夠滿大部分的點擊處理需求TouchableHighlight, TouchableNativeFeedback, TouchableOpacityTouchableWithoutFeedback。因為這幾個組件的功能和使用方法基本類似,只是 Touch 的反饋效果不一樣,所以根據(jù)需求選用合適的方法使用即可。

另外,如果在Touchable中onPress執(zhí)行了一個setState的操作,這個操作需要大量計算工作并且導致了掉幀,這時候可以將操作封裝到requestAnimationFrame中:

handleOnPress() {
// 謹記在使用requestAnimationFrame、setTimeout以及setInterval時
// 要使用TimerMixin(其作用是在組件unmount時,清除所有定時器)
this.requestAnimationFrame(() => {
this.doExpensiveAction();
});
}

5. 手勢識別

RN 提供了內(nèi)置的手勢識別庫PanResponder,我們只需要創(chuàng)建一個實例,然后搭載在任意的區(qū)域,就能監(jiān)聽到這塊區(qū)域的手勢變化,代碼片段如下:

componentWillMount: function() {
    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onPanResponderGrant: this._handlePanResponderGrant,
      onPanResponderMove: this._handlePanResponderMove,
      onPanResponderRelease: this._handlePanResponderEnd,
      onPanResponderTerminate: this._handlePanResponderEnd,
    });
}

<View  
  {...this._panResponder.panHandlers}
/>

但這個手勢庫PanResponder有個bug,會blockTouchableWithoutFeedback/Highlight等的點擊操作,解決方案是:

onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
    return Math.abs(gestureState.dx) > 5;
},

具體可以看這個issue

6. 文字行數(shù)控制

RN提供了numberOfLines方法實現(xiàn)行數(shù)控制,以及溢出部分的處理,同css的text-overflow

7. 樣式表

RN的布局主要使用flex,而且是閹割版的flex,樣式表大致只有如下屬性:

"alignItems",
"alignSelf",
"backfaceVisibility",
"backgroundColor",
"borderBottomColor",
"borderBottomLeftRadius",
"borderBottomRightRadius",
"borderBottomWidth",
"borderColor",
"borderLeftColor",
"borderLeftWidth",
"borderRadius",
"borderRightColor",
"borderRightWidth",
"borderStyle",
"borderTopColor",
"borderTopLeftRadius",
"borderTopRightRadius",
"borderTopWidth",
"borderWidth",
"bottom",
"color",
"flex",
"flexDirection",
"flexWrap",
"fontFamily",
"fontSize",
"fontStyle",
"fontWeight",
"height",
"justifyContent",
"left",
"letterSpacing",
"lineHeight",
"margin",
"marginBottom",
"marginHorizontal",
"marginLeft",
"marginRight",
"marginTop",
"marginVertical",
"opacity",
"overflow",
"padding",
"paddingBottom",
"paddingHorizontal",
"paddingLeft",
"paddingRight",
"paddingTop",
"paddingVertical",
"position",
"resizeMode",
"right",
"rotation",
"scaleX",
"scaleY",
"shadowColor",
"shadowOffset",
"shadowOpacity",
"shadowRadius",
"textAlign",
"textDecorationColor",
"textDecorationLine",
"textDecorationStyle",
"tintColor",
"top",
"transform",
"transformMatrix",
"translateX",
"translateY",
"width",
"writingDirection"

8. 警告信息

在開發(fā)過程中,如果我們需要在界面中打印出信息,可以借助console.warn打印出警告信息,而console.log的信息需要開啟debug模式,在控制臺可見。
另外最常見的一個警告信息是提示你加上key屬性,當我們遍歷輸出組件時,組件一定記得加上key屬性,這樣做能提高虛擬DOM Diff的效率。

9. 解決緩慢的導航器(Navigator)切換

Navigator的動畫是由JavaScript線程所控制的。想象一下“從右邊推入”這個場景的切換:每一幀中,新的場景從右向左移動,從屏幕右邊緣開始(不妨認為是320單位寬的的x軸偏移),最終移動到x軸偏移為0的屏幕位置。切換過程中的每一幀,JavaScript線程都需要發(fā)送一個新的x軸偏移量給主線程。如果JavaScript線程卡住了,它就無法處理這項事情,因而這一幀就無法更新,動畫就被卡住了。

長遠的解決方法,其中一部分是要允許基于JavaScript的動畫從主線程分離。同樣是上面的例子,我們可以在切換動畫開始的時候計算出一個列表,其中包含所有的新的場景需要的x軸偏移量,然后一次發(fā)送到主線程以某種優(yōu)化的方式執(zhí)行。由于JavaScript線程已經(jīng)從更新x軸偏移量給主線程這個職責中解脫了出來,因此JavaScript線程中的掉幀就不是什么大問題了 —— 用戶將基本上不會意識到這個問題,因為用戶的注意力會被流暢的切換動作所吸引。

不幸的是,這個方案還沒有被實現(xiàn)。所以當前的解決方案是,在動畫的進行過程中,利用InteractionManager來選擇性的渲染新場景所需的最小限度的內(nèi)容。

InteractionManager.runAfterInteractions的參數(shù)中包含一個回調(diào),這個回調(diào)會在navigator切換動畫結(jié)束的時候被觸發(fā)。

componentDidMount() {
    InteractionManager.runAfterInteractions(() => {
      this.setState({renderPlaceholderOnly: false});
    });
  }

10. 全局變量

RN可以通過global來設置全局變量,例如我們要把本地存儲的方法掛載到全局:

global.storage = storage

之后直接使用storage即可。

11. 調(diào)試

RN在開發(fā)菜單里提供了Debug JS Remotely的選項,點擊后會打開chrome,可以查看日志,斷點調(diào)試。
另外還可以安裝react-devtools進行樣式調(diào)試。
更詳細的調(diào)試文檔,可以看這里

12. WebView

RN自帶了WebView的支持,我們可以通過簡單的封裝,讓它更易用,另外它除了支持url,還支持自定義的html。

13. 鏈接原生庫

有一些庫基于一些原生代碼實現(xiàn),你必須把這些文件添加到你的應用,否則應用會在你使用這些庫的時候產(chǎn)生報錯。

我們無需手動添加,通過react-native link命令即可完成鏈接原生庫。

14. Component命名

react聲明組件時,第一個字母必須大寫。

15. 字體引入

IOS上要使用自定義的字體,必須把字體文件拖到對應的Xcode工程里面,勾選Add to targetsCreate groups,修改Info.plist文件,添加屬性Fonts provided by application;
安卓上要使用自定義的字體,必須要把字體文件放在[project root]/android/app/src/main/assets/fonts/目錄下才能生效

16. icon解決方案

我們使用iconfont,然后進行了簡單的封裝,詳細見

17. 使用ListView的正確姿勢

我們在一次使用ListView過程中,發(fā)現(xiàn)state不會改變,在GitHub上找到了同樣問題的issues:this.state does't work at listView's renderRow。進而獲得了一些使用ListView的正確姿勢:適合動態(tài)列表數(shù)據(jù),固化數(shù)據(jù)盡量不用,renderRow里盡量傳數(shù)據(jù),避免state判斷,如需state,應該付給參數(shù)傳入。

18. ScrollView

我們在使用ScrollViewonScroll方法的時候,有時會發(fā)現(xiàn)獲取的值和我們的預期不一致,是因為ScrollView默認每幀最多調(diào)用一次此回調(diào)函數(shù),如果要增大調(diào)用的頻率,可以用scrollEventThrottle屬性來控制。

19. 陰影

iOS上的陰影使用以下的屬性:

shadowColor Sets the drop shadow color
shadowOffset {width: number, height: number}Sets the drop shadow offset
shadowOpacity numberSets the drop shadow opacity (multiplied by the color's alpha component)
shadowRadius numberSets the drop shadow blur radius

但注意如果給Image組件添加陰影,不能把樣式寫在Image的style,而需要包裹一層View來添加陰影樣式。
Android上則不支持shadow*的樣式,只有elevation仰角的屬性來替代,但效果不太好,如果需要實現(xiàn)一致的效果,需要自己實現(xiàn)或者引入相關的庫。

20. FlatList

FlatList號稱是ListView的升級版,會有更好的體驗、更高的效率,但目前這個組件還不穩(wěn)定。使用過程有很多問題,例如首次加載會觸發(fā)兩次onEndReached、必須設置height屬性,不然onEndReached無法觸發(fā)、下拉到底仍可下拉,并出現(xiàn)大片白屏等。

注:官方在0.48版本開始廢棄ListView,推薦使用FlatListSectionList,看來應該比較穩(wěn)定了。

21. ref

任何組件都用一個ref的屬性,ref是組件實例的引用,通過復制給this變量,可以在任意位置操作組件。

22. PureComponent

props或者state改變的時候,會執(zhí)行shouldComponentUpdate方法來判斷是否需要重新render組建,我們平時在做頁面的性能優(yōu)化的時候,往往也是通過這一步來判斷的。Component默認的shouldComponentUpdate返回的是true,如下:

shouldComponentUpdate(nextProps, nextState) {
  return true;
}

PureComponentshouldComponentUpdate是這樣的:

if (this._compositeType === CompositeTypes.PureClass) {
  shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState);
}

相當于PureComponent幫我們判斷如果props或者state沒有改變的時候,就不重復render,這對于純展示組件,能節(jié)省不少比較的工作。

23. babelHelpers.objectDestructuringEmpty is not a function

在某些機子上遇到過一個如題的報錯,具體看issue
原因是使用了如下的語法:

const {} = result 

開發(fā)時盡量規(guī)范語法,避免寫些無意義的語法。

24. IOS模擬器卡頓

別當心,很可能是你按到了快捷鍵,打開了慢動畫的選項,關掉它就行了。


25. 快捷方式

IOS喚起調(diào)試菜單是? + D,刷新是? + R
Android喚起調(diào)試菜單是? + M ,刷新是R+ R
在真機上可以通過搖一搖喚起調(diào)試菜單。

26. LayoutAnimation

Animated的接口一般會在JavaScript線程中計算出所需要的每一個關鍵幀,而LayoutAnimation則利用了Core Animation,使動畫不會被JS線程和主線程的掉幀所影響。

注意:LayoutAnimation只工作在“一次性”的動畫上("靜態(tài)"動畫) -- 如果動畫可能會被中途取消,你還是需要使用Animated

27.本地存儲的使用

這個問題琢磨了一段時間,還沒有找到我想要的答案。情景大致是這樣,一次訪問某頁面,通過AsyncStorage保存了數(shù)據(jù),第二次進入頁面肯定希望render中直接用AsyncStorage中的本地數(shù)據(jù),無需二次render。但是AsyncStorage是個異步函數(shù),所以你即便在componentWillMount調(diào)用,還是需要在render后才能拿到數(shù)據(jù),所以就會出現(xiàn)二次render,即便componentWillMount中用await也無效,認真看了遍官方生命周期的文檔,但并沒有什么收獲。目前的解決方案是用一個標志位控制,標志位為false時出loading,只有當拿到數(shù)據(jù)標志位為true時才切真正的render,但這種方案其實還是執(zhí)行了兩次render,不過意外的是效果不錯,看不出有閃動,甚至看不出有loading過程。但如果你把關于本地存儲的一系列判斷邏輯是寫在InteractionManager.runAfterInteractions中,就會明顯的看到loading,打斷點看了下,發(fā)現(xiàn)即便是兩次render,都發(fā)生在頁面過場前,也就是屏幕還在上一頁面的時候就在render,而寫在InteractionManager.runAfterInteractions里,正是在執(zhí)行過場或者過場執(zhí)行完時發(fā)現(xiàn),這里面的 state變化反應到render中就會在屏幕中被看到。當然這個問題我還是想繼續(xù)關注下去,react-native也有不少類似的issue,最終還是希望能找到只需要一次render的辦法。
注:思路1(Redux是無視生命周期的)

28.從原生頁面如何跳轉(zhuǎn)到指定RN頁面

這里用到方法就是發(fā)送事件到JavaScript,然后根據(jù)獲取的參數(shù),跳轉(zhuǎn)相應的路由。
原生模塊可以在沒有被調(diào)用的情況下往JavaScript發(fā)送事件通知。最簡單的辦法就是通過RCTDeviceEventEmitter,這可以通過ReactContext來獲得對應的引用,像這樣:

    @ReactMethod
    public void goPage(int pageid) {
        System.out.println("########"+pageid+"########");
        // failedCallback.invoke();
        WritableMap params = Arguments.createMap();
        params.putInt("name", pageid);
        reactApplicationContextAction
                .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit("test", params);
    }

Javascript通過DeviceEventEmitter模塊來監(jiān)聽事件,獲取跳轉(zhuǎn)信息,跳轉(zhuǎn)至相應路由:

import {DeviceEventEmitter} from 'react-native'
componentWillMount(){
    DeviceEventEmitter.addListener("test", (result) => {
            let mainComponent = require(result.name);
            this.setState({
                content:mainComponent,
                showModule:true
            })
     })
}

render(){
    if(this.state.content){
        route.push(this.state.content)
        return null
    }else{
        return null
    }
}
    

29.字體背景色

字體的背景色是會繼承它父級的backgroundColor,通常我們沒有在意。但當你如果需要在字體上疊加一層蒙版也好、漸變也好,它們的顏色又恰好與你之前的背景色不一致,你就會發(fā)現(xiàn)字體的背景色凸顯出來了,這時需要把字體的backgroundColor設置為transparent,這樣才不會影響蓋在它上面的層,當然遇到這樣的問題還可能和你布局的先后順序有關,通常使用absolute應該排在后面,避免被后面的元素覆蓋,zIndex好像是只作用于同樣是absolute定義,absoluteflex之間無法使用zIndex

30.擴展性

使用原生方法(NativeModules)

  • IOS

想要創(chuàng)建一個iOS模塊,只需要創(chuàng)建一個接口,實現(xiàn)RCTBridgeModule協(xié)議,然后把你想在Javascript中使用的任何方法用RCT_EXPORT_METHOD包裝。最后,再用RCT_EXPORT_MODULE導出整個模塊即可。

// Objective-C

#import "RCTBridgeModule.h"

@interface MyCustomModule : NSObject <RCTBridgeModule>
@end

@implementation MyCustomModule

RCT_EXPORT_MODULE();

// Available as NativeModules.MyCustomModule.processString
RCT_EXPORT_METHOD(processString:(NSString *)input callback:(RCTResponseSenderBlock)callback)
{
  callback(@[[input stringByReplacingOccurrencesOfString:@"Goodbye" withString:@"Hello"]]);
}
@end
// JavaScript

import React, {
  Component,
} from 'react';
import {
  NativeModules,
  Text
} from 'react-native';

class Message extends Component {
  constructor(props) {
    super(props);
    this.state = { text: 'Goodbye World.' };
  }
  componentDidMount() {
    NativeModules.MyCustomModule.processString(this.state.text, (text) => {
      this.setState({text});
    });
  }
  render() {
    return (
      <Text>{this.state.text}</Text>
    );
  }
}
  • Android

同樣的,Android也支持自定義擴展。僅僅是方法略有差異。
創(chuàng)建一個基礎的安卓模塊,需要先創(chuàng)建一個繼承自ReactContentBaseJavaModule的類,然后使用@ReactMethod標注(Annotation)來標記那些你希望通過Javascript來訪問的方法。最后,需要在ReactPackage中注冊這個模塊。

// Java

public class MyCustomModule extends ReactContextBaseJavaModule {

// Available as NativeModules.MyCustomModule.processString
  @ReactMethod
  public void processString(String input, Callback callback) {
    callback.invoke(input.replace("Goodbye", "Hello"));
  }
}
// JavaScript

import React, {
  Component,
} from 'react';
import {
  NativeModules,
  Text
} from 'react-native';
class Message extends Component {
  constructor(props) {
    super(props);
    this.state = { text: 'Goodbye World.' };
  },
  componentDidMount() {
    NativeModules.MyCustomModule.processString(this.state.text, (text) => {
      this.setState({text});
    });
  }
  render() {
    return (
      <Text>{this.state.text}</Text>
    );
  }
}

使用原生頁面(requireNativeComponent)

  • IOS

若想自定義iOS View,可以這樣來做:首先繼承RCTViewManager類,然后實現(xiàn)一個-(UIView *)view方法,并且使用RCT_EXPORT_VIEW_PROPERTY宏導出屬性。最后用一個Javascript文件連接并進行包裝。

// Objective-C

#import "RCTViewManager.h"

@interface MyCustomViewManager : RCTViewManager
@end

@implementation MyCustomViewManager

RCT_EXPORT_MODULE()

- (UIView *)view
{
  return [[MyCustomView alloc] init];
}

RCT_EXPORT_VIEW_PROPERTY(myCustomProperty, NSString);
@end
// JavaScript

import React, { 
  Component,
} from 'react';
import PropTypes from 'prop-types';
import { requireNativeComponent } from 'react-native';

var NativeMyCustomView = requireNativeComponent('MyCustomView', MyCustomView);

export default class MyCustomView extends Component {
  static propTypes = {
    myCustomProperty: PropTypes.oneOf(['a', 'b']),
  };
  render() {
    return <NativeMyCustomView {...this.props} />;
  }
}
  • Android

創(chuàng)建自定義的Android View,首先定義一個繼承自SimpleViewManager的類,并實現(xiàn)createViewInstancegetName方法,然后使用@ReactProp標注導出屬性,最后用一個Javascript文件連接并進行包裝。

// Java

public class MyCustomViewManager extends SimpleViewManager<MyCustomView> {
  @Override
  public String getName() {
    return "MyCustomView";
  }

  @Override
  protected MyCustomView createViewInstance(ThemedReactContext reactContext) {
    return new MyCustomView(reactContext);
  }

  @ReactProp(name = "myCustomProperty")
  public void setMyCustomProperty(MyCustomView view, String value) {
    view.setMyCustomProperty(value);
  }
}
// JavaScript

import React, {
  Component,
  requireNativeComponent 
} from 'react-native';

var NativeMyCustomView = requireNativeComponent('MyCustomView', MyCustomView);

export default class MyCustomView extends Component {
  static propTypes = {
    myCustomProperty: React.PropTypes.oneOf(['a', 'b']),
  };
  render() {
    return <NativeMyCustomView {...this.props} />;
  }
}

更多(使用原生UI、同個頁面RN與Native的相互嵌套等)

更多和原生通信的內(nèi)容可以看官網(wǎng)文檔:英文中文

31.IOS真機打包

如何沒有IOS開發(fā)者賬號,一個項目只允許最多在三臺設備上打包,而且過期時間只有7天,另外無法移除打包過的機子的mac地址,意思就是我在A機子裝過,就用掉一個名額,沒法把這個名額讓出來了。這樣導致我們在鋪開測試、給大家體驗時遇到了瓶頸。這時候最好的方案是有一個企業(yè)賬號,可以打出一個企業(yè)包,在任何機子安裝,如果只有開發(fā)者賬號,那也只能在100臺設備安裝,開啟和關閉權(quán)限都需要到開發(fā)者網(wǎng)站操作,收回權(quán)限還需要給Apple發(fā)郵件。打完包,我推薦用fir平臺托管應用,只要把生成的頁面或者二維碼發(fā)給大家即可,方便、快捷,另外還支持權(quán)限、密碼的設置,實名制后每天有一百次的下載額度,其實也是足夠用了。

32.如何實現(xiàn)回退后刷新上個頁面

刷新上個頁面,說白了就是傳參。目前用的navigator,只有push能傳參,pop并沒有,這樣如何做到頁面回退能讓上一個頁面感知呢?我嘗試了幾個辦法:

  • Redux
    通過reduxstore,簡單粗暴,沒啥好說
  • DeviceEventEmitter
    第一個頁面監(jiān)聽,回退的時候觸發(fā),其實就是個簡單的觀察者模式,代碼大致如下:
//A頁面
import {
     AppRegistry,
     StyleSheet,
     Text,
     View,
     DeviceEventEmitter
 } form 'react-native';
componentDidMount() {
     this.subscription = DeviceEventEmitter.addListener('userNameDidChange',(userName) =>{
          console.warn(userName);
     })
}
componentWillUnmount() {
    // 移除
    this.subscription.remove();
}
//B頁面,在回退前
DeviceEventEmitter.emit('userNameDidChange', '通知來了');
  • callback
    在A界面跳到B界面時,帶上回調(diào)參數(shù),如:
this.props.navigator.push({‘id’:’b’,’callback’:this.refreshAAvatar}

然后在你回退前執(zhí)行callback即可

33. 在初始化bundle時如何傳參

在注冊bundle時傳參有什么用呢?可以實現(xiàn)跳轉(zhuǎn)到特定頁面。
來看看IOS和Android分別是怎么實現(xiàn):

//IOS initialProps就是給RN的參數(shù)
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"MyAwesomeApp"
                                               initialProperties:initialProps
                                                   launchOptions:launchOptions];
//Android
Bundle initialProps = new Bundle();
initialProps.putString("myKey", "myValue");

mReactRootView.startReactApplication(mReactInstanceManager, "MyAwesomeApp", initialProps);

34. 使用React Navigation實現(xiàn)App喚醒功能

詳見官網(wǎng)文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容