react-navigation 使用锦囊

2,066 阅读4分钟

前言

这里是针对日常使用react-navigation中的遇到的一些问题对其进行解决而总结出的小技巧。

TabNavigator 和 StackNavigator

简单了解一下 StackNavigator

如其名就是一个栈,遵循先进后出的原则,每打开一个screen,screen就会在页面最顶层的位置。

简单了解一下TabNavigator

在初始化TabNavigator的时候就会将TabNavigator上的所有screen都进行初始化。通过左右滑动/点击底部的TabBar对应的icon项目进行切换。

而一个TabNavigator也可以作为一个screen放入到StackNavigator中。

在了解了其简单使用原理后,进入到日常可能会遇到的一些问题。

TabNavigator的子screen缺少一些额外的钩子

由于TabNavigator会在第一次加载的时候实例化其子screen,所以其所以子screen的componentDidMount() 会随着TabNavigator实例的时候执行。 于是就有了这样的需要,子screen在屏幕上的时候才向服务器请求数据,或者是更新数据等逻辑。 目前react-navigation并没有提供相关的钩子去帮助我们(见#51),所以我们就有需要去使用一些高阶组件为我们的screen添加一些hook。 如https://github.com/pmachowski/react-navigation-is-focused-hoc 这里提供一个在集成redux后自己实现的一个方案

import React from 'react'
import { connect } from 'react-redux'

import PropTypes from 'prop-types'



function _getCurrentRouteName(navigationState) {
  if (!navigationState) return null
  const route = navigationState.routes[navigationState.index]
  if (route.routes) return _getCurrentRouteName(route)
  return route.routeName
}

/**
 * 给当前screen传递isFocused以判断是否在为当前路由
 * @param {Component} WrappedComponent 
 * @param {string} screenName 
 */
export default function withNavigationFocus(WrappedComponent, screenName) {
  class InnerComponent extends React.Component {
    static propTypes = {
      nav: PropTypes.object,
    }

    static navigationOptions = (props) => {
      if (typeof WrappedComponent.navigationOptions === 'function') {
        return WrappedComponent.navigationOptions(props)
      }
      return { ...WrappedComponent.navigationOptions }
    }

    constructor(props) {
      super(props)
      this.state = {
        isFocused: true,
      }
    }

    componentDidMount() {

    }

    componentWillReceiveProps(nextProps) {
      if (nextProps && nextProps.nav) {
        this._handleNavigationChange(_getCurrentRouteName(nextProps.nav))
      }
    }

    componentWillUnmount() {

    }

    _handleNavigationChange = (routeName) => {
      // update state only when isFocused changes
      if (this.state.isFocused !== (screenName === routeName)) {
        this.setState({
          isFocused: screenName === routeName,
        })
      }
    }

    render() {
      return <WrappedComponent isFocused={this.state.isFocused} {...this.props} />
    }
  }
  return connect(mapStateToProps)(InnerComponent)
}

/*将react-navigation集成到redux中*/
const mapStateToProps = (state) => ({
  nav: state.nav,
})

通过抛出引玉可以为相关的其他钩子实现提供思路

实现一个自定义的tabbar

为什么会有这个需求,因为设计师总会各种新(sao)的想(cao)法(zuo),例如issues上看到的这个图

这里之前实践的例子

import React, { Component } from 'react'
import {
  View,
  TouchableOpacity,
  Text,
  StyleSheet,
  Dimensions,
  Image,
} from 'react-native'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'

const { width } = Dimensions.get('window')

function _getCurrentRouteName(navigationState) {
  if (!navigationState) return null
  const route = navigationState.routes[navigationState.index]
  if (route.routes) return _getCurrentRouteName(route)
  return route.routeName
}

const extraRoutes = [{
  routeName: 'InsuranceTimeline',
  defaultIcon: require('../../assets/tabbar-icon/verify-icon/ic_circle_n.png'),
  selectIcon: require('../../assets/tabbar-icon/verify-icon/ic_circle_s.png'),
  title: 'screen2',
}, {
  routeName: 'InsuranceLesson',
  defaultIcon: require('../../assets/tabbar-icon/verify-icon/ic_umbrella_n.png'),
  selectIcon: require('../../assets/tabbar-icon/verify-icon/ic_umbrella_s.png'),
  title: 'sreen2',
}]

class TabBar extends Component {
  static defaultProps = {
    activeTintColor: '#3478f6', // Default active tint color in iOS 10
    activeBackgroundColor: 'transparent',
    inactiveTintColor: '#929292', // Default inactive tint color in iOS 10
    inactiveBackgroundColor: 'transparent',
    showLabel: true,
    showIcon: true,
  }

  static propTypes = {
    activeTintColor: PropTypes.string,
    inactiveTintColor: PropTypes.string,
    navigation: PropTypes.object,
    showPromoteModal: PropTypes.func,
    nav: PropTypes.object.isRequired,
  }

  renderExtraTabBtns = (props) => {
    const { navigation } = this.props
    const {
      activeTintColor,
      inactiveTintColor,
    } = this.props
    const imageType = isActive ? 'selectedIcon' : 'defaultIcon'
    const color = isActive ? activeTintColor : inactiveTintColor
    const isActive = _getCurrentRouteName(navigation.state) == props.name
    return <TouchableOpacity
      onPress={() => {
        navigation.navigate(props.routeName)
      }}
      style={styles.tab}
      key={props.routeName}
    >
      <Image
        source={props[imageType]}
        style={styles.icon}
      />
      <Text style={{ color, fontSize: 10 }}>{props.title}</Text>
    </TouchableOpacity>
  }

  render() {
    let navigation = this.props.navigation
    let images = [
      {
        default: require('../../assets/tabbar-icon/verify-icon/ic_home_n.png'),
        selected: require('../../assets/tabbar-icon/verify-icon/ic_home_s.png'),
      },
      {
        default: require('../../assets/tabbar-icon/verify-icon/ic_mine_n.png'),
        selected: require('../../assets/tabbar-icon/verify-icon/ic_mine_s.png'),
      },
    ]

    let titles = [
      'screen1',
      'screen2',
    ]
    const { routes, index } = navigation.state
    const {
      activeTintColor,
      inactiveTintColor,
    } = this.props

    const tabBtns = routes.map((route, idx) => {
      const color = (index === idx) ? activeTintColor : inactiveTintColor
      const isActive = index === idx
      const imageType = isActive ? 'selected' : 'default'
      return (
        <TouchableOpacity
          onPress={() => {
            navigation.navigate(route.routeName)
          }}
          style={styles.tab}
          key={route.routeName}
        >
          <Image
            source={images[idx][imageType]}
            style={styles.icon}
          />
          <Text style={{ color, fontSize: 10 }}>{titles[idx]}</Text>
        </TouchableOpacity>
      )
    })

    const extraBtns = extraRoutes.map(route => (
      this.renderExtraTabBtns(route)
    ))

    return (
      <View style={styles.tabContainer}>
        {
          [...tabBtns.slice(0, 1),
            ...extraBtns
            , ...tabBtns.slice(1)]
        }
      </View>
    )
  }
}


const styles = StyleSheet.create({
  tabContainer: {
    borderTopWidth: 1,
    borderTopColor: '#e6e6e6',
    position: 'relative',
    flexDirection: 'row',
    width,
    backgroundColor: '#fff',
    // borderTopColor: theme.primaryColor
  },
  tab: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    height: 55,
  },
  icon: {
    width: 30,
    height: 30,
  },
})

const mapStateToProps = (state) => ({
  nav: state.nav,
})

export default connect(mapStateToProps)(TabBar)


stackNavigator登录后通过状态刷新screen

由于登录后一般是将登录页reset即将上面的screen直接出栈,而下面的screen是直接呈现在页面上面,而在单一数据流,有时候没有触发其刷新的数据,此时就通过加一个高阶函数

import React, { Component } from 'react'
import { ScrollView, RefreshControl } from 'react-native'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'

/**
 * 
 * @param {Component} WrappedComponent 需要套的高阶组件
 * @param {Array}  extraKeys 需要更新数据的额外key
 * 连接了redux中的user
 * 通过受控组件的fetchData更新数据
 * (可以传入extraKeys)
 */
const AuthComponent = (WrappedComponent, extraKeys = [], scroll = false) => {
  class InnerComponent extends Component {
    static navigationOptions = (props) => {
      if (typeof WrappedComponent.navigationOptions === 'function') {
        return WrappedComponent.navigationOptions(props)
      }
      return { ...WrappedComponent.navigationOptions }
    }
    static propTypes = {
      fetchData: PropTypes.func,
      user: PropTypes.object,
    }

    constructor(props) {
      super(props)
      this.state = {
        refreshing: false,
      }
    }

    /**
     * 执行获取数据(子控件通过这个方法刷新数据)
     */
    fetchData = (nextProps) => {
      if (this.wrappedComponent.fetchData) {
        //传递下一次props
        this.wrappedComponent.fetchData(nextProps)
      }
    }

    componentDidMount = () => {
      this.fetchData()
    }

    componentWillReceiveProps(nextProps) {
      if (nextProps.user.isLogin !== this.props.user.isLogin) {
        if (nextProps) {
          this.fetchData(nextProps)
        } else {
          this.fetchData()
        }

      }

      for (let i = 0; i < extraKeys.length; i++) {
        let key = extraKeys[i]
        if (this.props.hasOwnProperty(key)) {
          //只支持浅比较
          if (nextProps[key] !== this.props[key]) {
            this.fetchData(nextProps)
            break
          }
        }
      }
    }

    /**
     * 比对前后属性
     */
    compare = (now, next) => {
      if (Array.isArray(now) && Array.isArray(next)) {
        if (now.length !== next.length) {
          return false
        }
        return now.every((element, index) => {
          return now[index] === next[index]
        })
      }
    }

    _onRefresh = () => {

    }

    render() {
      if (scroll) {
        return <ScrollView
          refreshControl={
            <RefreshControl
              refreshing={this.state.refreshing}
              onRefresh={this._onRefresh}
            />
          }
        >
          <WrappedComponent
            ref={(wrappedComponent) => this.wrappedComponent = wrappedComponent}
            {...this.props}
          />
        </ScrollView>
      }
      return <WrappedComponent
        ref={(wrappedComponent) => this.wrappedComponent = wrappedComponent}
        {...this.props}
      />

    }
  }
  return connect(mapStateToProps)(InnerComponent)
}


const mapStateToProps = (state) => ({
  user: state.user,
})

export default AuthComponent

重置 stack状态

在上面提到了重置stack状态

    const resetAction = NavigationActions.reset({
      index: 1,
      actions: [
        NavigationActions.navigate({ routeName: 'screen1' }),//重置后的第一层screen
        NavigationActions.navigate({ routeName: 'screen2' }),//重置后的第二层screen(此时是栈顶,即为当前屏幕显示页面)
      ],
    })
 this.props.navigation.dispatch(resetAction)

替换screen

      this.props.dispatch({
        key: 'NearMeMap',
        type: 'ReplaceCurrentScreen',
        routeName: routeName,
      })