React Native 路由理解和 react-navigation 库封装学习

3,730

从0.44版本开始Facebook放弃了原来的Navigator接口控制RN应用的路由跳转,并推荐使用react-navigation库实现应用的导航和跳转等功能。本文不止会介绍react-navigation的学习和使用,并同时也会介绍曾经的Navigator接口使用并介绍它们如何在应用中实现路由跳转的集中管理。笔者不会过多的介绍Navigator和react-navigation各个属性和方法的使用,笔者旨在学习和理解在react native中的栈结构路由的使用。

栈概念理解


对于手机应用中单页面应用(SPA)的路由,你可以这样理解:

  • 应用(APP)== 整套扑克牌(包括牌盒)
  • 栈容器(Navigator或React-Navigation中的 StackNavigator)== 牌盒
  • 页面(路由) == 牌

现在我们往空牌盒里面放入牌J,这是你的初始化页面,你可以再放入一张牌Q,盖住了牌J,在你的应用中你看到的页面就变成了牌Q,这个操作就是PUSH,然后你又把牌Q从牌盒中拿出来,你可以返回到之前的牌J,这个操作就是POP,通常PUSH操作你只能按顺序一次次的放入一个个对象,这个对象也许是一张牌,但是也有可能是封装多个同级页面的容器,比如说你的Tab容器页面;这就是单页面最简单的跳转和返回路由操作,其他还有以下相关操作:

  • reset(重置): 在已经有牌J、Q、K的牌盒里面,把所有的牌全部一次性拿出,放入牌A,这个过程就是重置你的整个路由;
  • popTo(返回指定页面):对已经有牌J、Q、K的牌盒里面对各牌进行按顺序下标0,1,2,其实就是数组结构,当前情况下你可以看到牌K,你的pop()返回至Q其实相当于popTo(1),你还可以使用popTo(0),这样你就等于一次性移开了最上面的牌Q、K,而你的牌盒中只剩下了J,这样相当于一次性按顺序返回多个页面;
  • getCurrentRoutes(获取当前所有路由): 对已经有牌J、Q、K的牌盒里面对各牌进行按顺序下标0,1,2,你可以获取到这个牌盒概念的路由数组,你可以对当前里面的牌进行指定操作。

延伸: 你的App即是你的牌盒,你只能对你牌盒中已经有的牌进行操作,当然你也可以新拿一张牌放入牌盒中进行操作,但是如果你的牌本身不在你的牌盒中,你是无法进行操作的,所以有时候如果这个牌都不在你的牌盒中,你使用通知-观察等这样的概念去操作一个不存在的页面对象是不会成功的。

Navigator使用和封装


点击查看官方文档

0.44版本后Navigator已经从react-native库中移除,如需导入可按如下操作:

// install
$npm install React-native-deprecated-custom-components --save

// import API
import CustomerComponents, {Navigator} from 'react-native-deprecated-custom-components';

实际项目中对于单页面应用,我们可以把Navigator封装成一个组件,把各页面当作Navigator的一个个场景转换,在页面中实现跳转,返回,动画等的各种操作时只需要调用相应方法即可。

class APP extends Component {
  constructor(props) {
    super(props);
    this._renderScene = this._renderScene.bind(this);
    this.state = {};
  }

  /* eslint-disable */
  _renderScene(route, navigator) {
    let Component = route.component;
    return (
      <Component
        {...route}
        navigator={navigator}
        passProps={route.passProps}
        callback={route.callback}
      />
    );
  }

  render() {
    return (
      <View style={{ flex: 1 }}>
        <Navigator
          ref="navigator"
          renderScene={this._renderScene}
          configureScene={(route) => ({
            ...route.sceneConfig || Navigator.SceneConfigs.HorizontalSwipeJump,
            gestures: route.gestures
          })}
          initialRoute={{
            component: Login
          }}
        />
        <LoadingView isVisible={this.props.showLoading} />
      </View>
    )
  }
}

除了场景转换等操作,还可以在这个组件中集成控制App全局的一些操作,比如说,Loading的设置,网络状态检查等设置,在各页面就无须再单独设置。尽量在一个地方里面实现控制app的一些相近的默认操作

实际页面中跳转或其他操作:

_jumpPage() {
    const { navigator } = this.props;
    if (navigator) {
      navigator.push({
        component: TabBarList, //next route
        sceneConfig: Navigator.SceneConfigs.FloatFromBottomAndroid, // animated config
        callback: () => {}  //callback  
        passProps: {  //transfer parameters
          tabs: 'home',
          activeTab: 'home',
          onPressHandler: this.props.goToPage
        }
      });
    }
  }

React Navigation理解和使用


点击查看官方文档

react-native 0.44版本之前路由控制使用的Navigator虽然非常稳定,基本没出现过什么BUG,但是跳转效果一直被人诟病,跳转时候的动画和原生App的效果相比,非常明显差一等,在0.44版本后Facebook推荐使用react-navigation库来实现页面跳转,tab转换,侧边栏滑动等功能。

react-navigation主要包括导航,底部tab,顶部tab,侧滑等,功能很强大,而且体验接近原生。接下来会一一介绍:

  • 导航 -> StackNavigator
  • 底部或者顶部tab -> TabNavigator

关于侧滑DrawerNavigator的使用,笔者不在本文介绍,但可以看这篇附带Demo的推荐博客

StackNavigator

StackNavigator在功能上就是相当于原来使用Navigator,但是他有着不一样的实现和非常好的跳转体验,使用上也非常简单,其实也就是三部曲:

  • 路由配置(页面注册):
const routeConfigs = {
      Login: { screen: Login },
      TabBar: { screen: TabBarContainer },
      Feedback: { screen: Feedback },
};
  • 默认场景配置:
const stackNavigatorConfig = {
  initialRouteName: 'Login',
  navigationOptions: {
    headerBackTitle: null,
    headerTintColor: 'white',
    showIcon: true,
    swipeEnabled: false,
    animationEnabled: false,
    headerStyle: {
      backgroundColor: '#f2f2f2'
    }
  },
  mode: 'card',
  paths: 'rax/: Login',
  headerMode: 'float',
  transitionConfig: (() => ({
    screenInterpolator: CardStackStyleInterpolator.forHorizontal // android's config about jump to next page 
  })),
  onTransitionStart: () => {},
  onTransitionEnd: () => {}
};
  • 容器生成与初始化:
const Nav = StackNavigator(routeConfigs, stackNavigatorConfig);
export default class QQDrawerHome extends Component {
    render() {
        return(
            <Nav/>
        );
    }
}

这样就简单完成了路由的配置,开发时只需要把新页面添加到注册对象routeConfigs中,StackNavigator会对里面的的注册页面和注册时使用的KEY值形成对应关系,当你在页面时跳转时,只需要这样:

_jumpPage() {
    const { navigation } = this.props;
    if (navigation) {
      const { navigation } = this.props;
      navigation.navigate('TabBar');
    }
}

带参数跳转时:

_jumpPage() {
    const { navigation } = this.props;
    if (navigation) {
      const { navigation } = this.props;
      navigation.navigate('TabBar', { 
          visible: false,
          title: '首页'
      });
    }
}

在下个页面就可以拿到参数并设置头部或其他参数:

static navigationOptions = ({ navigation }) => {
    const { state } = navigation;
    const { title } = state.params;
    return {
      title: title,
    };
  };

其他reset,setParams等操作将可以学着本文后面封装到组件中去使用,当然你也可以直接在页面跳转函数中重置路由,就像这样:

const resetAction = NavigationActions.reset({
  index: 0,
  actions: [
    NavigationActions.navigate({ routeName: 'Login'})
  ]
})
this.props.navigation.dispatch(resetAction)

TabNavigator

0.44版本之前我们实现Tab页面通常都选择使用框架react-native-tab-navigator或者react-native-scrollable-tab-view,现在0.44版本后react-navigation库中推荐使用TabNavigator,同样的使用方式,类似StackNavigator三部曲:

const routeConfigs = {
       Message:{
            screen:QQMessage,
            navigationOptions: {
            tabBarLabel: '消息',
            tabBarIcon: ({ tintColor }) => (
              <Image
                source={require('./notif-icon.png')}
                style={[styles.icon, {tintColor: tintColor}]}
              />),
            }
        },
        Contact:{
            screen:QQContact,
            navigationOptions: {
            tabBarLabel: '联系人',
            tabBarIcon: ({ tintColor }) => (
              <Image
                source={require('./notif-icon.png')}
                style={[styles.icon, {tintColor: tintColor}]}
              />),
            }
        },
};

const  tabNavigatorConfig = {
        tabBarComponent:TabBarBottom,
        tabBarPosition:'bottom',
        swipeEnabled:false,
        animationEnabled:false,
        lazy:true,
        initialRouteName:'Message',
        backBehavior:'none',
        tabBarOptions:{
            activeTintColor:'rgb(78,187,251)',
            activeBackgroundColor:'white',
            inactiveTintColor:'rgb(127,131,146)',
            inactiveBackgroundColor:'white',
            labelStyle:{
                fontSize:12
            }
        }
    }

export default TabNavigator(routeConfigs, tabNavigatorConfig);

关于使用TabNavigator的一些注意点和当前问题:

  • 如你甚至未使用StackNavigator,而想直接使用TabNavigator,还是用其他第三方框架吧,他和StackNavigator是配套使用的,你必须保证TabNavigator存在于StackNavigator中,TabNavigator才能良好工作。
  • 当你当前页面使用了TabNavigator,那么TabNavigator所形成的容器组件应该是当前页面的顶层组件,否则报错,将会无法获取到tab中的router数组。
  • 关于嵌套使用TabNavigator,即在TabNavigator的一个screen中再次使用了TabNavigator形成页面,安卓平台下无法渲染子组件,页面空白,且内层Tab基本失效,或者你的内层Tab容器使用其他第三方框架如react-native-tab-view等类似框架,问题依然存在,关于此问题可关注公关BUG#1796

StackNavigator路由的集中封装

此部分集成了一部分Redux知识,建议可以看一下redux官方文档了解一下redux。StackNavigator本身就集成了Redux来进行路由数据的管理,如你想要将你自己的redux管理集成到StackNavigator中,官方同样提供接口addNavigationHelpers,这里我们关注的是如何把reset,setParams等Navigator中的Action直接封装到组件中形成页面调用接口。

以下是笔者的封装组件,类似之前封装Navigator组件封装集中管理组件的思路代码,我们把StackNavigator同样封装为一个组件作为管理中心

......

const AppNavigator = StackNavigator(RouteConfigs, stackNavigatorConfig);// eslint-disable-line

class MainContainer extends Component {
  constructor(props) {
    super(props);
    this.resetRouteTo = this.resetRouteTo.bind(this);
    this.resetActiveRouteTo = this.resetActiveRouteTo.bind(this);
    this.backTo = this.backTo.bind(this);
    this.setParamsWrapper = this.setParamsWrapper.bind(this);
    this.state = {};
  }

  resetRouteTo(route, params) {
    const { dispatch } = this.props;
    if (dispatch) {
      dispatch(
        NavigationActions.reset({
          index: 0,
          actions: [NavigationActions.navigate({ routeName: route, params: params })],
        })
      );
    }
  }

  resetActiveRouteTo(routeArray, activeIndex) {
    const { dispatch } = this.props;
    if (dispatch) {
      const actionsArray = [];
      for (let i = 0; i < routeArray.length; i++) {
        actionsArray.push(NavigationActions.navigate({ routeName: routeArray[i] }));
      }

      const resetAction = NavigationActions.reset({
        index: activeIndex,
        actions: actionsArray,
      });
      dispatch(resetAction);
    }
  }

  backTo(key) {
    const { dispatch } = this.props;
    if (dispatch) {
      dispatch(
        NavigationActions.reset({
          key: key
        })
      );
    }
  }

  setParamsWrapper(params, key) {
    const { dispatch } = this.props;
    if (dispatch) {
      const setParamsAction = NavigationActions.setParams({
        params: params,
        key: key,
      });
      dispatch(setParamsAction);
    }
  }

  render() {
    const { dispatch, navigationState, screenProps } = this.props;
    return (
      <View
        style={{ flex: 1 }}
        onStartShouldSetResponder={() => dismissKeyboard()}
      >
        <StatusBar barStyle="light-content" />
        <AppNavigator
          navigation={addNavigationHelpers({
            dispatch: dispatch,
            state: navigationState,
            resetRouteTo: (route, params) => this.resetRouteTo(route, params),
            resetActiveRouteTo: (routeArray, activeIndex) => this.resetActiveRouteTo(routeArray, activeIndex),
            backTo: (key) => this.backTo(key),
            setParamsWrapper: (params, key) => this.setParamsWrapper(params, key)
          })}
          screenProps={screenProps}
        />
        <Loading isVisible={true} mode="alipay" />
      </View>
    );
  }
}

const mapStateToProps = (state) => {
  const newNavigationState = state.navReducer;
  if (state.screenProps) {
    newNavigationState.params = {
      ...state.params,
      ...state.screenProps
    };
  }
  return {
    navigationState: newNavigationState,
    screenProps: state.screenProps
  };
};

export default connect(mapStateToProps)(MainContainer);

......

其中绑定navReducer文件的数据,可参考redux和react-navigation官网文档,此文不再列出

这样封装后,各页面使用reset,setParams等操作时,就可以像以前一样直接使用相关操作,如重置路由:

_jumpPage() {
    const { navigation } = this.props;
    if (navigation) {
        navigation.resetRouteTo('TabBar', { title: '首页', selectedTab: 'home' });
    }
}

写在最后


笔者第一次写博客,如果有什么不足之处,或者上面的一些问题有什么不对的,欢迎大家批评与指正,一起学习和进步。

相关文章可参考:

ReactNative导航新宠儿react-navigation

React Native未来导航者:react-navigation 使用详解