美文网首页React-Native开发大全React Nativereactnative
React Native路由理解和react-navigatio

React Native路由理解和react-navigatio

作者: 乐帅 | 来源:发表于2017-06-10 22:16 被阅读2354次

    从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 使用详解

    相关文章

      网友评论

      • out曼曼:你好,请问你为 navigation 对象额外封装的方法是如何传递到每个页面的?我按照你的方式把额外封装的 resetRouteTo、resetActiveRouteTo 等方法直接放到 addNavigationHelpers 里,但是我到具体的页面去打印 this.props.navigation 的时候只有原本的方法,没有额外封装进去的方法。
        所以,想请问一下,是需要额外做什么处理吗?
        out曼曼:@乐帅 你好,很抱歉再次打扰。我看了一遍 V2 版本的文档,https://reactnavigation.org/docs/en/navigation-prop.html API 文档中,navigation 对象还是没有你当初封装的方法哎。请问你说的「React-Navigation已经提供了全部我当初封装的这些方法,也没有必要这样封装了,navigation现在自带这些方法」是在哪里看到的?
        out曼曼:@乐帅 噢噢,好的~谢谢:smiley:
        乐帅:@fa3f565428ff 额,这个React-Navigation更新太快了嗯,现在是V2版本,已经不能这样封装了,抱歉,我没有去更新文章,而且我当初这样封装是因为库没有提供这样的直接调用方法,但是现在我封装的这些方法,React-Navigation已经提供了全部我当初封装的这些方法,也没有必要这样封装了,navigation现在自带这些方法,你可以看看官网,而且addNavigationHelpers这个好像都没有再被使用了,那是以前版本1的东西,你应该去看看最新版本2的东西,变化很大,和以前不太一样
      • 乐帅: @小黑_4b7c

        如上面代码那样把路由当作AppNavigator封装成组件以后,然后可以看到
        loading独立封装,并且设置是一个绝对定位的loading,接下来只需要在你的app里面控制isVisible是true或false就可以控制loading是显示还是隐藏,所以增加一个值来在redux中控制就好了,代码:
        <Loading isVisible={screenProps.showLoading} mode="alipay" />

        navReducer:

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

        const initialState = AppNavigator.router.getStateForAction(AppNavigator.router.getActionForPathAndParams('Login'));

        const navigatorReducer = (state = initialState, action) => {
        switch (action.type) {
        case ActionType.SHOW_LOADING:
        return { ...state, screenProps: { showLoading: true } };
        case ActionType.DISMISS_LOADING:
        return { ...state, screenProps: { showLoading: false } };
        default:
        return { ...AppNavigator.router.getStateForAction(action, state), screenProps: { showLoading: false } };
        }
        };

        在各个地方disaptch发出显示还是隐藏的通知就可以了,如请求里面
        const _doLogin = (url, param) => dispatch => {
        dispatch(CommonAction.showLoading());
        return Fetcher.postQsBodyFetch(url, param)
        .then((response) => {
        dispatch(CommonAction.dismissLoading());
        dispatch(_loginSuccess(param, response));
        }).catch((error) => {
        dispatch(CommonAction.dismissLoading());
        dispatch(_loginFailed(error));
        });
        };

        commonAction.js:
        const _showLoading = () => {
        return {
        type: ActionTypes.SHOW_LOADING
        };
        };
        const _dismissLoading = () => {
        return {
        type: ActionTypes.DISMISS_LOADING
        };
        };
        你还可以看看我的另一篇文章,看我Redux是如何封装的,我这里就是把那个值也加在redux中控制而已http://www.jianshu.com/p/712f5d934875
        小黑_4b7c:@乐帅 多谢兄弟了,我也研究出来啦😬😬
      • 小黑_4b7c:兄弟请教下,你的那个loading是怎么来全局控制的?
      • 清焰如风:作者计算机专业的吗😂
        乐帅: @石上双 是的,嗯
      • ThinkNuo:如何自定义react-navigation的StackNavigator样式 或者说只需要其中的路由功能该怎么使用
        乐帅:@ThinkNuo 不会啊,你看Screen Navigation Options下的header属性:
        React Element or a function that given HeaderProps returns a React Element, to display as a header. Setting to null hides header.

        返回React组件或给定一个以HeaderProps为参,返回React组件的方法,设置为null时即隐藏该页header,
        这个属性我自己也还没有用过,或者你也可以像以前做navigator时那样,完全把它当路由,自己完全做一个Header放在页面上方。
        ThinkNuo:@乐帅 :smiley: 使用headerMode:none是能满足需求,就是觉得用起来奇怪,看官方例子好像没有使用自定义Component的用法
        乐帅:@ThinkNuo 我不知道你是这个意思吗?你说的应该是重新定义它自带的Header样式吧,你可以在设置RouteConfigs的时候使用headerMode:none,这样整个容器不会出现header了,或者你也可以在某个特定的页面,设置navigationOptions的时候使用header属性,返回一个header的组件,或者返回null隐藏当前页面的header,去重新定义自己的header,看一下官网的属性介绍https://reactnavigation.org/docs/navigators/stack
      • 哈喽蜗的:支持作者,受益匪浅
      • ForScanf:太有用了呀.作者好牛掰:+1::+1:
      • 张囧瑞:好牛逼啊!!
      • ChenEZ:作者写得很用心,对RN的理解也很不错,mark一下
        summerhe:荣神好久不见啊

      本文标题:React Native路由理解和react-navigatio

      本文链接:https://www.haomeiwen.com/subject/osmcqxtx.html