React Native - 踩坑纪录
记录下自己在 RN 开发中遇到的一些问题。
RN 组件相关
TextInput
- Android 文字对齐问题
当 TextInput 高度超过一行文本时,发现文字显示在 iOS 上是顶端对齐,而在 Android 上则是垂直居中,如下图:
text_input_differ解决方法是通过为 TextInput 设置 textAlignVertical: "top"
属性,相关 issue 见:Render Multiline Text at start instead of center
- Android 上 TextInput 接收获得焦点之后键盘无法自动收起
这时候我们可以给根布局设置接收触摸事件:
onStartShouldSetResponder={() => true}
onResponderRelease={() => Keyboard.dismiss()}
这样当输入焦点之外区域触摸后,通过调用 Keyboard.dismiss() 方法强制收起键盘,曲线救国。
KeyboardAvoidingView
-
behavior
相关
该组件在 Android 和 iOS 上的表现有区别,所以我们会区分平台使用不同的 behavior,比如下面这样:
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'android' ? null : 'padding'}
keyboardVerticalOffset={64}>
...
</KeyboardAvoidingView>
padding
模式下,当键盘弹起的时候,你的 view 会向上弹起并被压缩。使用 padding
作为 behavior 的时候,在 iOS 上表现比较好,而在 Android 上则不设置 behavior 比较好。
position
模式下,view 整体会向上滑动。这种模式 Android 和 iOS 上表现一致,但是前提是此时 KeyboardAvoidingView
是根 view。同时这也会造成一个问题,那就是键盘弹出后,输入组件会一直占有焦点,这在安卓上还好,可以通过返回键关闭键盘,而在 iOS 设备上就会造成键盘无法被关闭的尴尬。解决这一问题的方法是通过在 KeyboardAvoidingView
设置接收触摸事件,当在输入焦点之外获得点击时收起键盘:
<KeyboardAvoidingView
style={styles.container}
behavior={'position'}
onStartShouldSetResponder={(evt) => true}
onResponderRelease={() => Keyboard.dismiss()}
/>
ImageBackground
-
imageStyle
属性
有没有发现给 ImageBackground 设置 style
的时候,其中某些属性似乎不起作用?比如设置 border
似乎没有效果。其实看下源码就可以发现,原来 style
属性里面还有个 imageStyle
属性,类似 border
和 borderRadius
这样的属性要设置到 imageStyle
上才有效。
FlatList/SectionList
-
contentContainerStyle
属性
与 ImageBackground 类似,在给 FlatList 设置 paddingBottom 的时候,发现不起作用,后来在这个 issue 下找到了解决办法:FlastList/SectionList 中有个 contentContainerStyle
属性,代表 list 中的 content 容器的 style 属性。所以如果想要为 list 设置 paddingBottom,在这个属性上设置才能起作用。
- 加载时的性能问题
我们知道在 Android 中加载大量列表数据时,RecyclerView 的性能是比较好的,因为它可以复用 view,而在 RN 中如果你用 FlatList 直接加载成百上千的数据的时候,你会发现整个界面会变得非常卡,所以这种情况下我们就需要懒加载。FlatList 本身是支持增量加载的,只不过需要一些额外的处理。
首先,FlatList 中有一个 initialNumToRender
属性,用于指定初始加载的数据,我们可以设置为 10,这个看你的需求了,一般根据 item 的高度来定。然后 FlatList 还有一个 onEndReached
属性,我们可以在这里定义一个方法,用于指定当列表滑动到底部的时候触发的事件。有了这两个属性,我们就可以对 FlatList 中的数据进行懒加载了。
<FlatList
style={styles.productListStyle}
data={this.state.productList}
renderItem={({item, index}) => <ProductItem />}
initialNumToRender={10}
numColumns={2}
onEndReached={() => this.lazyLoadProducts()}
onEndReachedThreshold={0.5}
ListHeaderComponent={this.props.ListHeaderComponent}
ListFooterComponent={(this.state.productList.length !== this.props.productList.length) ? <ActivityIndicator style={styles.activityIndicator} size='large'/> : undefined}
/>
可以看到,FlatList 中数据源来自 this.state.productList
,然后在 onEndReached
中调用了一个 lazyLoadProducts
方法:
lazyLoadProducts() {
if (this.state.productList.length === this.props.productList.length) {
return;
}
this.setState(state => ({
productList: state.productList.concat(
this.props.productList.slice(state.productList.length, this.state.productList.length + 10)
.map(item => ({...item, key: item.storeProductId})))
}));
}
我们首先将完整的数据保存在 props 中,然后在 onEndReached
中每次多加载 10 条新数据。
可以看到上面的 FlatList
中还定义了一个 onEndReachedThreshold
属性,表示 FlatList
可见部分离底部多远的时候会触发 onEndReached
方法。比如我们定义为 0.5,则如果可见部分为 10 条数据,那么当我们向下滑动 5 条数据的时候,就会去加载另外 5 条新数据。
PanResponder
-
onPanResponderMove
和Animated.event()
的结合使用
利用 PanResponder 做了一个拖动调节图标位置的功能,网上找的方法是在 onPanResponderMove
中使用 Animated.event()
来对 View 进行移动。实现效果不错,但是发现一旦在 onPanResponderMove
中使用了 lambda 表达式后,就不起作用了。后来网上找到这个 issue,发现原来 Animated.event()
会返回一个方法,并且接收 event 和 gestureState 作为参数,所以我们只要去调用一下这个方法即可:
onPanResponderMove: (evt, gestureState) => {
return Animated.event([null, {
dx: this.state.pan.x,
dy: this.state.pan.y,
}])(evt, gestureState)
}
UIManager
我们可以使用 UIManager 来测量某个 view 的位置,这个在一些特殊的场合非常有用。
测量某个 view 的位置前,我们首先需要获得该 view 的引用:
<YourView
ref={component => this.myView = component}/>
获得 view 的引用后,就可以通过 view 获得 nodeHandle
去测量 view 的位置了:
measureViewPosition() {
let nodeHandle = findNodeHandle(this.myView);
if (nodeHandle) {
UIManager.measure(nodeHandle, (x, y, width, height, pageX, pageY) => {
// measure success, do something with the data
}
}
从示例代码中可以看到,在测量方法中,我们定义了一个测量成功的回调,我们可以在这里获得测量到的当前 view 的中心点坐标,高度,宽度,距离页面顶端的 x 坐标,y 坐标。
三方库相关
react-native-router-flux
- 实现 Android 上连续点击两次返回键退出
这个在安卓上是比较常见的操作,但是在 RN 中结合 react-native-router-flux 使用却折腾了好长时间,这里记录下自己的实现方式。
首先,react-native-router-flux 原生就支持,我们不需要通过自己去添加 BackHandler 监听器来实现。当我们使用 react-native-router-flux 时,我们一般用 Router
作为根节点,所以我们通过 Router
为其设置 backAndroidHandler
属性即可。
let lastBackPressed = Date.now();
const onExitApp = () => {
if (Actions.currentScene !== 'home') {
Actions.pop();
return true
}
if (lastBackPressed && Date.now() < lastBackPressed + 2000) {
BackHandler.exitApp();
return false;
}
ToastAndroid.show('再按一次退出应用', ToastAndroid.SHORT);
lastBackPressed = Date.now();
return true;
};
class AppRouter extends Component {
render() {
return <Router backAndroidHandler={onExitApp}>
<Scene key='home' ...>
{/* other code */}
</Router>
}
}
可以看到,这里定义了一个 onExitApp
方法并且设置到了 backAndroidHandler
属性上,该属性会根据返回值来决定是否退出应用(false 时退出应用)。
在 onExitApp
中,我们首先判断当前应用是否处于根页面(可以通过 react-native-router-flux 中的 Actions.currentScene
获取当前 Scene),如果不是根页面则作为普通的返回键处理(弹出一页),否则判断是否在指定时间内连续点击,连续点击才退出应用,否则弹出 Toast 提醒。
- 在 Android 上点击返回键时使 WebView 后退一页
假如有这样一个需求,某个页面下有一个 WebView 组件,我们需要控制当在该页面按下返回键时后退一页(相当于从网页的历史记录中后退),而如果没有历史记录时则直接退出。这个要怎么实现呢?
注意到,WebView 中有一个 onNavigationStateChange
方法,当新的页面加载或退出时该方法会被调用。因此一种可行的方法是,在该方法中监听页面变化并读取页面加载后的数据。以下是该方法中的部分数据:
canGoBack: true
canGoForward: true
loading: false
navigationType: "other"
target: 187
title: ""
url: ""
因此,可以通过事件中传回的 canGoBack
值判断此时 WebView 是否可以返回,如果可以则使用 WebView 的 ref 去调用 goBack()
返回上一页,否则使用 Actions.pop()
退出当前页面。
基本代码如下:
goBack = () => {
if (this.state.canGoBack) {
this.refs.webView.goBack();
} else {
Actions.pop();
}
};
当然由于返回按钮可能不在当前组件下,如果你使用的是 react-native-router-flux
和 react-redux
,则可以定义一个返回按钮的组件,该组件通过全局 state 树接收点击事件的 function,然后设置到 Scene
的 renderLeftButton
属性中,最后,在需要处理 WebView 的地方设置返回事件到返回按钮中即可(注意:react-redux
是可以接收 function
作为属性的,不然就没法设置事件了)。
react-native-pushy
- iOS 上传报错
iOS 上传 ipa 包时,报错 TypeError [ERR_INVALID_CALLBACK]:
TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
at maybeCallback (fs.js:133:9)
at Object.exists (fs.js:201:3)
at CertDownloader.pem (/Users/Cerie/Workspace/JSProject/huge2.0_reactNative/node_modules/cert-downloader/index.js:96:18)
at CertDownloader.verify (/Users/Cerie/Workspace/JSProject/huge2.0_reactNative/node_modules/cert-downloader/index.js:131:11)
at module.exports (/Users/Cerie/Workspace/JSProject/huge2.0_reactNative/node_modules/provisioning/index.js:7:10)
at /Users/Cerie/Workspace/JSProject/huge2.0_reactNative/node_modules/ipa-metadata/node_modules/async/lib/async.js:731:23
at /Users/Cerie/Workspace/JSProject/huge2.0_reactNative/node_modules/ipa-metadata/node_modules/async/lib/async.js:673:13
at /Users/Cerie/Workspace/JSProject/huge2.0_reactNative/node_modules/ipa-metadata/node_modules/async/lib/async.js:230:13
at _arrayEach (/Users/Cerie/Workspace/JSProject/huge2.0_reactNative/node_modules/ipa-metadata/node_modules/async/lib/async.js:81:9)
at _each (/Users/Cerie/Workspace/JSProject/huge2.0_reactNative/node_modules/ipa-metadata/node_modules/async/lib/async.js:72:13)
发现是由于 node 版本过高,相关 issue: pushy uploadIpa TypeError #209,可以通过下载 n
切换到较低版本来解决:
npm install -g n
sudo n 8.11.1
比如这里我把 node 从 10.7.0 降到 8.11.1 问题就解决了,参考:React Native 问题记录