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
, TouchableOpacity
和 TouchableWithoutFeedback
。因為這幾個組件的功能和使用方法基本類似,只是 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 targets
和Create groups
,修改Info.plist
文件,添加屬性Fonts provided by application
;
安卓上要使用自定義的字體,必須要把字體文件放在[project root]/android/app/src/main/assets/fonts/
目錄下才能生效
16. icon解決方案
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
我們在使用ScrollView
的onScroll
方法的時候,有時會發(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
,推薦使用FlatList
或SectionList
,看來應該比較穩(wěn)定了。
21. ref
任何組件都用一個ref
的屬性,ref
是組件實例的引用,通過復制給this變量,可以在任意位置操作組件。
22. PureComponent
當props
或者state
改變的時候,會執(zhí)行shouldComponentUpdate
方法來判斷是否需要重新render
組建,我們平時在做頁面的性能優(yōu)化的時候,往往也是通過這一步來判斷的。Component
默認的shouldComponentUpdate
返回的是true,如下:
shouldComponentUpdate(nextProps, nextState) {
return true;
}
而PureComponent
的shouldComponentUpdate
是這樣的:
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
定義,absolute
和flex
之間無法使用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)createViewInstance
和getName
方法,然后使用@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
通過redux
的store
,簡單粗暴,沒啥好說 -
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);