React Native开发React Native开发经验集React Native开发技巧

React Native - 踩坑纪录

2019-04-23  本文已影响7人  aJIEw

记录下自己在 RN 开发中遇到的一些问题。

RN 组件相关

TextInput

  1. Android 文字对齐问题

当 TextInput 高度超过一行文本时,发现文字显示在 iOS 上是顶端对齐,而在 Android 上则是垂直居中,如下图:

text_input_differ

解决方法是通过为 TextInput 设置 textAlignVertical: "top" 属性,相关 issue 见:Render Multiline Text at start instead of center

  1. Android 上 TextInput 接收获得焦点之后键盘无法自动收起

这时候我们可以给根布局设置接收触摸事件:

onStartShouldSetResponder={() => true}
onResponderRelease={() => Keyboard.dismiss()}

这样当输入焦点之外区域触摸后,通过调用 Keyboard.dismiss() 方法强制收起键盘,曲线救国。

KeyboardAvoidingView

  1. 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

  1. imageStyle 属性

有没有发现给 ImageBackground 设置 style 的时候,其中某些属性似乎不起作用?比如设置 border 似乎没有效果。其实看下源码就可以发现,原来 style 属性里面还有个 imageStyle 属性,类似 borderborderRadius 这样的属性要设置到 imageStyle 上才有效。

FlatList/SectionList

  1. contentContainerStyle 属性

与 ImageBackground 类似,在给 FlatList 设置 paddingBottom 的时候,发现不起作用,后来在这个 issue 下找到了解决办法:FlastList/SectionList 中有个 contentContainerStyle 属性,代表 list 中的 content 容器的 style 属性。所以如果想要为 list 设置 paddingBottom,在这个属性上设置才能起作用。

  1. 加载时的性能问题

我们知道在 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

  1. onPanResponderMoveAnimated.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

  1. 实现 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 提醒。

  1. 在 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-fluxreact-redux,则可以定义一个返回按钮的组件,该组件通过全局 state 树接收点击事件的 function,然后设置到 ScenerenderLeftButton 属性中,最后,在需要处理 WebView 的地方设置返回事件到返回按钮中即可(注意:react-redux 是可以接收 function 作为属性的,不然就没法设置事件了)。

react-native-pushy

  1. 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 问题记录

上一篇下一篇

猜你喜欢

热点阅读