美文网首页React NativeReact NativeJS
react-navigation路由篇之StackRouter

react-navigation路由篇之StackRouter

作者: 羽纱 | 来源:发表于2017-07-12 18:26 被阅读2012次

    react-navigation的基础认识:

    react-navigation是官方正在推的导航,但是没有放入到react-native包中,而是单独开了一个库,它的核心概念是Router,Router的概念图如下所示:

    routers-concept-map.png

    最上方的文字:
    上面这个图清晰的表达出了Router能干什么,最重要的一句话是:Router经常组合使用,并且能代理子路由,这句话的意思我待会分析源码来深入了解。

    上图的右部分:
    由Action而引起State的变化,是不是很像Redux?后面我会写篇文章专门写如何配合Redux自定义行为。在不配合Redux使用时,它自己内部其实也通过createNavigationContainer(后边源码分析会说到)来作为主容器维护这类似Redux形式的数据流。用户在使用App的过程中触发导航Action,例如StackRouter的Navigate,goBack等,这些Action被Dispatch出去后会被router.getStateForAction(action, lastState)处理,getStateForAction通过Action和之前的State获得新的State。这就是一个完整的数据流过程。

    上图的左部分:
    除了在App运行过程中用户主动触发goBack,navigate这些Action外,当App不在运行时,也可以通过推送,通知等唤醒App来派发Action,router.getActionForPathAndParams通过path和params可以取得Action,然后就可以走图右部分的流程了。

    由上面的分析可以知道,router是用来管理导航状态,它的原理就是通过派发Action来获得新的State,从而维护和管理导航的状态,导航的State的结构如下:

    //MainTabState:
    {
      index: 0,
      routes: [
        {
          key: 'key-1',
          routeName: 'Home',
          ...
        },
        {
          key: 'key-2',
          routeName: 'SomeTab',
          index: 2,
          routes:[...]
        },
        {
          key: 'key-3',
          routeName: 'Mine',
          ...
        }
      ]
    }
    

    MainTab有3个Tab,分别是Home,SomeTab, Mine,当前MainTab停留在Home上,SomeTab也是一个TabNavigator,它停留在第3个Tab上(index从0开始)。index表示导航的当前位置,routes表示导航中已经存在的screen,每个screen都有唯一的key标识,screen也可以是Navigator,比如SomeTab。

    上面对Router有了大概的了解,注意是Router,因为后面会说到route,经常会混淆。Router是用来管理导航的State,是导航的Core,StackNavigator的navigate,back操作的逻辑都是由StackRouter处理的,TabNavigator的navigate,back操作的逻辑都是由TabRouter处理的。用Router的State来渲染当前Screen,管理手势和动画的任务,就要配合Navigation View和Transitioner了。
    Navigation View
    Transitioner


    说在前边的话

    这篇文章是分析导航路由的实现原理,而不是讲解react-navigation的基础用法,需要了解react-navigation的基础用法,请移步官网:https://reactnavigation.org/


    深入浅出Router

    什么是Router?Router是Navigator的核心,有了Router就可以自定义Navigator了:

    class MyNavigator extends React.Component {
        static router = StackRouter(routes, config);
        ...
    }
    

    react-navigation有两个内建的Router,分别是StackRouterTabRouter,它们都提供了一套基础的Router API:

    • getStateForAction(action, state)
    • getComponentForRouteName(routeName)
    • getComponentForState(state)
    • getActionForPathAndParams(path, params)
    • getPathAndParamsForState(state)
    • getScreenOptions(navigation, screenProps)

    StackNavigator:

    用法:

    const ModalStack = StackNavigator({
      Home: {
        screen: MyHomeScreen,
      },
      Profile: {
        screen: MyProfileScreen,
      },
    });
    

    StackNavigator的代码实现:
    https://github.com/react-community/react-navigation/blob/master/src/navigators/StackNavigator.js

    //摘录StackNavigator.js
    export default (
      routeConfigMap: NavigationRouteConfigMap,
      stackConfig: StackNavigatorConfig = {}
    ) => {
      ...
      const router = StackRouter(routeConfigMap, stackRouterConfig);
    
      const navigator = createNavigator(
        router,
        routeConfigMap,
        stackConfig,
        NavigatorTypes.STACK
      )((props: *) => (
        <CardStackTransitioner
          {...props}
          ...
        />
      ));
    
      return createNavigationContainer(navigator, stackConfig.containerOptions);
    };
    

    使用StackRouter工厂方法生成router,传入createNavigator

    createNavigator的实现:
    https://github.com/react-community/react-navigation/blob/master/src/navigators/createNavigator.js

    const createNavigator = (
      router: NavigationRouter<*, *, *>,
      routeConfigs: NavigationRouteConfigMap,
      navigatorConfig: any,
      navigatorType: NavigatorType
    ) => (View: NavigationNavigator<*, *, *, *>) => {
      class Navigator extends React.Component {
        props: NavigationNavigatorProps<*>;
    
        static router = router;
        ...
        render() {
          return <View {...this.props} router={router} />;
        }
      }
    
      return Navigator;
    };
    

    createNavigator会返回一个函数,这个函数需要传入View,这个View就是

    (props: *) => (
        <CardStackTransitioner
          {...props}
          ...
        />
      )
    

    router作为属性传入到VIew中,用来渲染界面,关于使用router渲染screen的分析下篇再说。CardStackTransitioner管理了CardStack的转场动画,手势返回,createNavigator函数返回的函数传入VIew参数后还是返回一个Navigator(拥有router静态属性就把它当做Navigator)。

    createNavigationContainer:的实现
    https://github.com/react-community/react-navigation/blob/master/src/createNavigationContainer.js

    export default function createNavigationContainer<T: *>(
      Component: ReactClass<NavigationNavigatorProps<T>>,
      containerOptions?: {}
    ) {
       ...
      class NavigationContainer extends React.Component<void, Props<T>, State> {
        state: State;
        props: Props<T>;
    
        //NavigationConatainer也是一个路由
        static router = Component.router;
    
        constructor(props: Props<T>) {
          super(props);
          ...
          this.state = {
            nav: this._isStateful()
              ? Component.router.getStateForAction(NavigationActions.init())
              : null,
          };
        }
    
        //当外部组件没有传入navigation属性时,自己处理状态
        _isStateful(): boolean {
          return !this.props.navigation;
        }
        ...
    
        //与Redex的dispatch同名,方便接入Redux,用意也相同,派发Action,通过getStateForAction改变State,从而刷新组件
        dispatch = (action: NavigationAction) => {
          const { state } = this;
          if (!this._isStateful()) {
            return false;
          }
          const nav = Component.router.getStateForAction(action, state.nav);
          if (nav && nav !== state.nav) {
            this.setState({ nav }, () =>
              ...
            );
            ...
          }
          ...
        };
    
        //关于android back的处理
        ....
    
        _navigation: ?NavigationScreenProp<NavigationRoute, NavigationAction>;
    
        render() {
          let navigation = this.props.navigation;
          //只有外部组件没有传入navigation时才自己创建navigation
          if (this._isStateful()) {
            //不存在navigation或者state.nav发生变化,重新获取navigation
            if (!this._navigation || this._navigation.state !== this.state.nav) {
              this._navigation = addNavigationHelpers({
                dispatch: this.dispatch,
                state: this.state.nav,
              });
            }
            navigation = this._navigation;
          }
          //将navigtion作为属性传给组件,这就是Container名称的来意
          return <Component {...this.props} navigation={navigation} />;
        }
      }
    
      return NavigationContainer;
    }
    

    所以如果外部传入了navigation属性,NavigationContainer就不做任何事情,就要直接渲染出Component并把属性往下传递,如果没有navigation属性,则自己充当container,派发Action,管理State,刷新Navigator。

    总结:
    由routeConfig创建router,由router创建navigator,然后由navigator创建了createNavigationContainer,在NavigationContainer中使用isStateful判断是否作为container使用(自己充当container,派发Action,管理State,刷新Navigator)。

    StackRouter:

    StackRouter的代码实现:
    https://github.com/react-community/react-navigation/blob/master/src/routers/StackRouter.js
    由上面的代码分析可以看出,通过StackRouter(routeConfigMap, stackRouterConfig)创建的router最终作为了NavigationContainer的静态属性。那么StackRouter(routeConfigMap, stackRouterConfig)创建的router是什么样的呢?

    第一步:解析路由配置

    const ModalStack = StackNavigator({
      Home: {
        screen: MyHomeScreen,
      },
      Profile: {
        screen: MyProfileScreen,
      },
    });
    

    调用StackNavigator工厂方法的第一个参数就是routeConfigs

      const childRouters = {};
      const routeNames = Object.keys(routeConfigs);
      console.log('开始解析路由配置...');
      routeNames.forEach((routeName: string) => {
        const screen = getScreenForRouteName(routeConfigs, routeName);
        //前面说过,通过router来判断是否为Navigator
        if (screen && screen.router) {
          // If it has a router it's a navigator.
          //这对后面路由的嵌套处理特别关键
          childRouters[routeName] = screen.router;
        } else {
          // If it doesn't have router it's an ordinary React component.
          childRouters[routeName] = null;
        }
      console.log('路由配置解析结果:');
      console.log(JSON.stringify(childRouters))
      });
    

    由路由配置生成相应的childRoutes,注意这里有三种状态在后面会用到,分别是null, router, undefined,为null则代表这个routeName配置过,但是不是子路由,router则代表是子路由,undefined代表没有在路由配置中。

    第二步:初始化路由栈

          // Set up the initial state if needed
          if (!state) {
            //当state不存在时,初始化路由栈
            console.log('开始初始化初始路由为' + initialRouteName + '的路由状态...');
            let route = {};
            if (
              action.type === NavigationActions.NAVIGATE &&
              childRouters[action.routeName] !== undefined
            ) {
              //这是一种配置导航首页的写法,首页有三种写法,第一种是routeConfig的第一项,第二种是stackConfig中指定initialRouteName,第三种则是routeName与在父路由中注册的routeName一致,则为首页。
              //这也是navigate是使用action.action会被调用的地方,后边会提到
              return {
                index: 0,
                routes: [
                  {
                    ...action,
                    type: undefined,
                    key: `Init-${_getUuid()}`,
                  },
                ],
              };
            }
            if (initialChildRouter) {
              //如果初始化路由为子路由,则默认以initialRouteName为首页初始化子路由状态
              console.log('初始化路由为子路由时,获取子路由的初始路由');
              route = initialChildRouter.getStateForAction(
                NavigationActions.navigate({
                  routeName: initialRouteName,
                  params: initialRouteParams,
                })
              );
              console.log(initialRouteName + '的初始路由为:' + JSON.stringify(route))
            }
            //装配params和route
            const params = (route.params ||
              action.params ||
              initialRouteParams) && {
              ...(route.params || {}),
              ...(action.params || {}),
              ...(initialRouteParams || {}),
            };
            route = {
              ...route, //将子路由嵌入进来
              routeName: initialRouteName,
              key: `Init-${_getUuid()}`,
              ...(params ? { params } : {}),
            };
            // eslint-disable-next-line no-param-reassign
            //装配state
            state = {
              index: 0,
              routes: [route],
            };
            console.log('初始路由为' + initialRouteName + '的路由状态为:' + JSON.stringify(state));
          }
    
    

    这里举个例子,大家慢慢领悟,路由配置为:

    const InvestScreen = TabRouter({
        WanjiaJX: {
            screen:WanjiaJX,
        },
        WanjiaY: {
            screen: WanjiaYing,
        },
    }, {
        initialRouteName: 'WanjiaY',
    })
    
    const MainTabNavigator = TabNavigator({
        Home: {
            screen: HomeScreen,
        },
        Invest: {
            screen: InvestScreen,
        },
        Find: {
            screen: FindScreen,
        },
        My: {
            screen: MyScreen,
        },
    })
    
    const CardStackNavigator = StackNavigator({
        MainTab: {
            screen: MainTabNavigator,
        },
        transferDetail: {
            screen: TransferDetailScreen,
        }
    });
    
    const ModelStackNavigator = StackNavigator({
        mainStackNavigator: {
            screen: CardStackNavigator,
        },
        investSuccess: {
            screen: InvestSuccessScreen,
        },
        rechargeGetVcode: {
            screen: RechargeGetVcodeScreen,
        },
    })
    
    export default ModelStackNavigator
    

    打印结果为:

    开始解析路由配置...
    StackRouter.js:42 {"MainTab":{},"transferDetail":{}}
    StackRouter.js:55 路由配置解析结果:
    StackRouter.js:56 {"MainTab":{},"transferDetail":null}
    StackRouter.js:41 开始解析路由配置...
    StackRouter.js:42 {"mainStackNavigator":{},"investSuccess":{},"rechargeGetVcode":{}}
    StackRouter.js:55 路由配置解析结果:
    StackRouter.js:56 {"mainStackNavigator":{},"investSuccess":null,"rechargeGetVcode":null}
    StackRouter.js:105 开始初始化初始路由为mainStackNavigator的路由状态...
    StackRouter.js:125 初始化路由为子路由时,获取子路由的初始路由
    StackRouter.js:105 开始初始化初始路由为MainTab的路由状态...
    StackRouter.js:125 初始化路由为子路由时,获取子路由的初始路由
    StackRouter.js:132 MainTab的初始路由为:{"routes":[{"key":"Home","routeName":"Home"},{"routes":[{"key":"WanjiaJX","routeName":"WanjiaJX"},{"key":"WanjiaY","routeName":"WanjiaY"}],"index":1,"key":"Invest","routeName":"Invest"},{"key":"Find","routeName":"Find"},{"key":"My","routeName":"My"}],"index":1}
    StackRouter.js:152 初始路由为MainTab的路由状态为:{"index":0,"routes":[{"routes":[{"key":"Home","routeName":"Home"},{"routes":[{"key":"WanjiaJX","routeName":"WanjiaJX"},{"key":"WanjiaY","routeName":"WanjiaY"}],"index":1,"key":"Invest","routeName":"Invest"},{"key":"Find","routeName":"Find"},{"key":"My","routeName":"My"}],"index":1,"routeName":"MainTab","key":"Init-id-1499828145378-0"}]}
    StackRouter.js:132 mainStackNavigator的初始路由为:{"index":0,"routes":[{"routes":[{"key":"Home","routeName":"Home"},{"routes":[{"key":"WanjiaJX","routeName":"WanjiaJX"},{"key":"WanjiaY","routeName":"WanjiaY"}],"index":1,"key":"Invest","routeName":"Invest"},{"key":"Find","routeName":"Find"},{"key":"My","routeName":"My"}],"index":1,"routeName":"MainTab","key":"Init-id-1499828145378-0"}]}
    StackRouter.js:152 初始路由为mainStackNavigator的路由状态为:{"index":0,"routes":[{"index":0,"routes":[{"routes":[{"key":"Home","routeName":"Home"},{"routes":[{"key":"WanjiaJX","routeName":"WanjiaJX"},{"key":"WanjiaY","routeName":"WanjiaY"}],"index":1,"key":"Invest","routeName":"Invest"},{"key":"Find","routeName":"Find"},{"key":"My","routeName":"My"}],"index":1,"routeName":"MainTab","key":"Init-id-1499828145378-0"}],"routeName":"mainStackNavigator","key":"Init-id-1499828145378-1"}]}
    

    用递归的方法从外到内获取路由状态,然后从内到外组装路由状态。

    第三步:路由行为
    前面分析了StackRoute的初始化,下面将依次来分析navigate,setParams,reset,goBack的行为。
    react-navigation的Router的强大之处,也是难以理解之处就是路由的组合(composable)使用和相互嵌套。我们经常会使用navigate来push,使用goBack来pop,使用reset来重置路由栈,使用setParams来重置params。

    关于路由嵌套和navigate action

    在上面路由配置的示例中,我们提出两个问题:
    1、假如当前的页面为:mainStackNavigator->mainTab->Invest->WanjiaY,那在WanjiaYing的screen中,调用this.props.navigation.navigate('investSuccess'),或者调用this.props.navigation.navigate('transferDetail')会发生什么?为什么会有这种效果?
    2、假如当前的页面为:investSuccess,在InvestSuccessScreen中调用this.props.navigation.navigate('My')会有什么效果?为什么会有这种效果?

    如果能回答以上两个问题,那对于导航的嵌套和组合使用就了解了。我们先来分析代码,然后再给出答案。

    第一段代码:

    // Check if a child scene wants to handle the action as long as it is not a reset to the root stack
    //只要不是对root stack的reset操作,都先检查当前指定的子路由(使用key指定)或者activited状态的子路由是否想处理改Action。
          if (action.type !== NavigationActions.RESET || action.key !== null) {
            //如果指定了key,则找到该key在state中的index,否则index为-1
            const keyIndex = action.key
              ? StateUtils.indexOf(state, action.key)
              : -1;
            //当index < 0 时,则用activited状态的index为childIndex,否则用keyIndex作为childIndex
            const childIndex = keyIndex >= 0 ? keyIndex : state.index;
            //通过childIndex找到childRoute
            const childRoute = state.routes[childIndex];
            //通过childRoute的routeNam在childRouter中查询是否为子路由
            const childRouter = childRouters[childRoute.routeName];
            if (childRouter) {
              //如果存在子路由,则让子路由去处理这个Action,并且传入对应的childRoute
              const route = childRouter.getStateForAction(action, childRoute);
              //如果route为null则返回当前state
              if (route === null) {
                return state;
              }
              //如果route不等于之前的childRoute,也就是route不等于preRoute
              if (route && route !== childRoute) {
                //在state的对应的key中做replace操作
                return StateUtils.replaceAt(state, childRoute.key, route);
              }
            }
          }
    

    在上面代码中要好好理解两个字段,一个是childRouter,一个是childRoute,它们在字面上只有一个r的区别,但是实际的用处是相差甚远的,childRouter是一个Router,它就像StackRouter或者TabRouter一样,拥有getStateForAction这些方法,childRoute是一个Route,它是存在于State中的,它的结构类似于:

    {
      key: ***,
      routeName: mainTab,
      ...
    }
    

    因此,在State中通过key找到Route,通过Route中的routeName在childRouter中找到是否有响应的router,来判断它是否为子路由,这个逻辑就说的很通了。
    所以这段代码的意思可以通俗的描述为:在非root stack的reset的action中,指定(通过key或者activited route)一个childRouter来处理action。

    第二段代码:

    // Handle explicit push navigation action
    //处理确切的navigate action,所谓确切,就是说在routeConfig中有*直接*注册过
          if (
            action.type === NavigationActions.NAVIGATE &&
            childRouters[action.routeName] !== undefined
          ) {
            const childRouter = childRouters[action.routeName];
            let route;
            if (childRouter) {
              //如果navigate的是一个子路由,并且存在子action(action.action)则让子路由执行这个子action
              //如果没有子action则使用init为action
              const childAction =
                action.action || NavigationActions.init({ params: action.params });
              route = {
                params: action.params,
                ...childRouter.getStateForAction(childAction), //注意,getStateForAction的第二个参数没有传,证明它是用来初始化状态的。
                key: _getUuid(),
                routeName: action.routeName,
              };
            } else {
              //没有子路由则直接构建route
              route = {
                params: action.params,
                key: _getUuid(),
                routeName: action.routeName,
              };
            }
            //直接push route
            return StateUtils.push(state, route);
          }
    

    这段代码就比较简单了,在当前(不会去递归到子路由)路由配置中找是否有注册过routeName的screen,如果有注册,则分两种情况,第一种,这个screen就是一个普通screen,直接构建route即可,第二种是,这个screen是一个navigator,初始化子路由状态(使用action.action或者init)然后组装route。最后都要将route push到state中去。

    这里有个问题,在...childRouter.getStateForAction(childAction)这句代码中,如果childRouter为StackRouter,则会调用到第二步:初始化路由栈中的下面这段代码来:

    if (
              action.type === NavigationActions.NAVIGATE &&
              childRouters[action.routeName] !== undefined
            ) {
              //这是一种配置导航首页的写法,首页有三种写法,第一种是routeConfig的第一项,第二种是stackConfig中指定initialRouteName,第三种则是routeName与在父路由中注册的routeName一致,则为首页。
              return {
                index: 0,
                routes: [
                  {
                    ...action,
                    type: undefined,
                    key: `Init-${_getUuid()}`,
                  },
                ],
              };
            }
    

    在这段代码中,如果action.routeName的childRouter依然是一个子路由,即childRouters[action.routeName] !== null则会报错,因为CardStack要渲染的screen为一个navigator,但是state中却没有相对应的routes和index。报错信息:
    Expect nav state to have routes and index, {"routeName": "MainTab", "key": "Init-id-123322334-3"}
    改成如下即可:

    if (
              action.type === NavigationActions.NAVIGATE &&
              childRouters[action.routeName] !== undefined
            ) {
              if(childRouters[action.routeName]) {
                  const childRouter = childRouters[action.routeName];
                  state = {
                      index: 0,
                      routes: [
                          {
                              ...action,
                              ...childRouter.getStateForAction(action),
                              type: undefined,
                              key: `Init-${_getUuid()}`,
                          },
                      ],
                  };
                  console.log('返回状态:' + JSON.stringify(state));
                  return state;
              }
              state = {
                index: 0,
                routes: [
                  {
                    ...action,
                    type: undefined,
                    key: `Init-${_getUuid()}`,
                  },
                ],
              };
              console.log('返回状态:' + JSON.stringify(state));
              return state;
            }
    

    第三段代码:

    //当指定的子路由没有处理,路由配置中没有配置响应的routeName时,遍历*所有*子路由,一旦有子路由愿意处理该action,则将处理结果push返回。
          if (action.type === NavigationActions.NAVIGATE) {
            const childRouterNames = Object.keys(childRouters);
            for (let i = 0; i < childRouterNames.length; i++) {
              const childRouterName = childRouterNames[i];
              const childRouter = childRouters[childRouterName];
              if (childRouter) {
                //遍历子路由,从初始状态开始,处理Action
                const initChildRoute = childRouter.getStateForAction(
                  NavigationActions.init()
                );
                //检查子路由是否想处理action
                const navigatedChildRoute = childRouter.getStateForAction(
                  action,
                  initChildRoute
                );
                let routeToPush = null;
                if (navigatedChildRoute === null) {
                  // Push the route if the router has 'handled' the action and returned null
                  //如果子路由处理了这个action,并且返回null,则push子路由的初始状态
                  routeToPush = initChildRoute;
                } else if (navigatedChildRoute !== initChildRoute) {
                  //如果子路由处理了这个action,并改变了初始状态,则push这个新的路由状态
                  routeToPush = navigatedChildRoute;
                }
                if (routeToPush) {
                  return StateUtils.push(state, {
                    ...routeToPush,
                    key: _getUuid(),
                    routeName: childRouterName,
                  });
                }
              }
            }
          }
    

    总结:
    以上分析了路由是如何管理子路由的,在处理action时会先判断该action不是root stack的reset操作时(action.type !== NavigationActions.RESET || action.key !== null),找到指定的router(找到action.key对应的router,当action.key无效时找到activited router)去处理该action,如果指定的router处理了这个action(返回null或者route!==childRoute)则在state中替换对应的route。
    如果指定(通过key或者activited router)的router不处理action,则判断action.routeName有没有在routeConfig中注册过,对于这种直接注册的,就直接push就好。
    如果action.routeName没有被注册过,则遍历所有子路由去尝试处理action,一旦有子路由去处理了,则直接push这个处理结果。

    所以,你能回答上面两个问题了吗?

    navigation action: setParams、reset、goBack

    setParams:
    代码:

    if (action.type === NavigationActions.SET_PARAMS) {
            //通过action.key在state中找到对应的route
            const lastRoute = state.routes.find(
              /* $FlowFixMe */
              (route: *) => route.key === action.key
            );
            if (lastRoute) {
              //如果route存在,将参数合并
              const params = {
                ...lastRoute.params,
                ...action.params,
              };
              //做这一步是为了改变route的引用
              const routes = [...state.routes];
              routes[state.routes.indexOf(lastRoute)] = {
                ...lastRoute,
                params,
              };
              //返回一个全新的state
              return {
                ...state,
                routes,
              };
            }
          }
    

    用法:

    import { NavigationActions } from 'react-navigation'
    
    const setParamsAction = NavigationActions.setParams({
      params: { title: 'Hello' },
      key: 'screen-123',
    })
    this.props.navigation.dispatch(setParamsAction)
    

    reset:
    代码:

          if (action.type === NavigationActions.RESET) {
            const resetAction: NavigationResetAction = action;
    
            return {
              ...state,
              routes: resetAction.actions.map( //遍历action.actions
                (childAction: NavigationNavigateAction) => {
                  const router = childRouters[childAction.routeName];
                  if (router) {
                    //当childAction.routerName为子路由时,获取子路由的初始状态
                    return {
                      ...childAction,
                      ...router.getStateForAction(childAction), //这里没传第二个参数,是去获取初始状态
                      routeName: childAction.routeName,
                      key: _getUuid(),
                    };
                  }
                  //直接创建route
                  const route = {
                    ...childAction,
                    key: _getUuid(),
                  };
                  delete route.type;
                  return route;
                }
              ),
              index: action.index,
            };
          }
    

    用法:

    import { NavigationActions } from 'react-navigation'
    
    const resetAction = NavigationActions.reset({
      index: 0,  //确定route的index
      actions: [ //确定routes
        NavigationActions.navigate({ routeName: 'Profile'})
      ]
    })
    this.props.navigation.dispatch(resetAction)
    

    goBack:
    代码:

          if (action.type === NavigationActions.BACK) {
            //当前要pop的index
            let backRouteIndex = null;
            if (action.key) {
              //通过key找到route,通过route找到index
              const backRoute = state.routes.find(
                /* $FlowFixMe */
                (route: *) => route.key === action.key
              );
              /* $FlowFixMe */
              //赋值当前要pop的index
              backRouteIndex = state.routes.indexOf(backRoute);
            }
            if (backRouteIndex == null) {
              //当index不存在时,直接pop最上面的route
              return StateUtils.pop(state);
            }
            if (backRouteIndex > 0) { 
              //pop route到index为backRouteIndex - 1
              return {
                ...state,
                routes: state.routes.slice(0, backRouteIndex),
                index: backRouteIndex - 1, 
              };
            }
          }
          return state;
        },
    

    用法:

    import { NavigationActions } from 'react-navigation'
    
    const backAction = NavigationActions.back({
      key: 'Profile'
    })
    this.props.navigation.dispatch(backAction)
    
    
    

    注意:setParams、reset、goBack都是可以通过key来使用的,可以自动在一个conatiner navigtor中指定router来处理这些action。在StackRouter中key是使用_getUuid直接生成的,可以用过this.props.navigation.state.key获取到。

    总结

    项目中使用了redux + react-navigation,还是觉得很好用的,但是刚开始学习时感觉与以前使用过的navigation在思想上有种种不同,很多时候想自定义或者修改时常常找不到地方,比如防止push同样的screen,指定routeName来back等,但是多看文档和源码后,发现它的自由度是非常高的,可以重写或拦截router,自定义NavigatorView,灵活的配置Transition等,配合redux也是非常好用的。值得推。
    其实这些官方文档都有所描述,只是之前看的云里雾里,现在终有所理解,希望对和我一样在使用react-navigation时有疑问的同学有所帮助。

    避免重复跳转和指定routeName返回

    react-navigation配合redux使用

    //@flow
    
    import AppNavigator from '../AppNavigator'
    import { NavigationActions, StateUtils } from 'react-navigation'
    
    function getCurrentRoute(state: Object) {
        if ( state && state.routes) return getCurrentRoute(state.routes[state.index]);
    
        return state;
    }
    
    function routeIsInCurrentState(state: Object, routeName: string) {
        if(state && state.routeName === routeName) {
            return true
        }
    
        if(state && state.routes) {
            return routeIsInCurrentState(state.routes[state.index], routeName)
        }
    
        return false
    }
    
    function goBackToName(state, name) {
        //使用routeName返回
        const routes = state.routes
        let newState = null
        routes.forEach((route, index) => {
            if(newState === null && route.routeName === name) {
                newState = {
                    ...state,
                    routes: routes.slice(0, index + 1),
                    index: index,
                };
            }
        })
        if(newState !== null) {
            return newState
        }
    
        routes.forEach((route, index) => {
            if(newState === null && route.routes) {
                const newRoute = goBackToName(route, name)
                if(newRoute) {
                    newState = StateUtils.replaceAt(state, route.key, newRoute);
                }
            }
        })
        if(newState !== null) {
            return newState
        }
    
        return state
    }
    
    function MainNavReducer(state: Object, action: Object) {
    
        if(action.type === NavigationActions.NAVIGATE) {
            if(action.routeName && routeIsInCurrentState(state, action.routeName)) {
                //避免重复跳转
                return state
            }
        }
        if(action.type === NavigationActions.BACK) {
            if (state && action.routeName) {
                //使用routeName返回
                return goBackToName(state, action.routeName)
            }
        }
    
        if(!action.key) {
            action.key = null
        }
        const nextState = AppNavigator.router.getStateForAction(action, state);
        // Simply return the original `state` if `nextState` is null or undefined.
        return nextState || state;
    }
    
    export default MainNavReducer
    

    相关文章

      网友评论

      • LinMeiQi:this.props.navigation.state 取到的路由栈都是一个,navigate到第二页,取到的路由栈还是同一个,而且也就只有一个?不知道为什么会这样?
      • Skyling:大佬 我的项目中stackrouter嵌套了tab 那如何从stackroute中回退goback()或者resetAction 到tabnavigator中的某个子路由呢?都会报there's no route key for Cart,其中cart为tab路由.我要实现的功能就是登陆前在购物车登陆完成后再返回购物车的功能,但是直接this.props.navigaton.navigte('cart')是OK的,期待您的指教
        羽纱:@Skyling 你登陆的路由呢?
        Skyling:const Tab = TabNavigator({
        Home: {
        screen: Home,
        navigationOptions: ({
        navigation
        }) => ({
        tabBarLabel: '首页',
        header: null,
        tabBarIcon: ({
        focused,
        tintColor
        }) => (
        <TabBarItem
        tintColor={tintColor}
        focused={focused}
        normalImage={require('./src/image/BottomTab/Home.png')}
        selectedImage={require('./src/image/BottomTab/HomeCopy.png')}
        />
        )
        }),
        },
        Category: {
        screen: Category,
        navigationOptions: ({
        navigation
        }) => ({
        tabBarLabel: '分类',
        header: null,
        tabBarIcon: ({
        focused,
        tintColor
        }) => (
        <TabBarItem
        tintColor={tintColor}
        focused={focused}
        normalImage={require('./src/image/BottomTab/Category.png')}
        selectedImage={require('./src/image/BottomTab/CategoryCopy.png')}
        />
        )
        }),
        },
        Cart: {
        screen: Cart,
        navigationOptions: ({
        navigation
        }) => ({
        tabBarLabel: '购物车',
        tabBarIcon: ({
        focused,
        tintColor
        }) => (
        <TabBarItem
        tintColor={tintColor}
        focused={focused}
        normalImage={require('./src/image/BottomTab/Cart.png')}
        selectedImage={require('./src/image/BottomTab/CartCopy.png')}
        />
        )
        }),
        },
        User: {
        screen: User,
        navigationOptions: ({
        navigation
        }) => ({
        tabBarLabel: '我的',
        header: null,
        tabBarIcon: ({
        focused,
        tintColor
        }) => (
        <TabBarItem
        tintColor={tintColor}
        focused={focused}
        normalImage={require('./src/image/BottomTab/User.png')}
        selectedImage={require('./src/image/BottomTab/UserCopy.png')}
        />
        )
        }),
        },
        const Navigator = StackNavigator({
        Tab: {
        screen: Tab
        }, //tab,期待你回复
        羽纱:麻烦发一下你相应路由的路由配置:blush:
      • 程序猿的呐喊:楼主你好,要如何获取当前路由的最新的state呢?我通过getStateForAction获取到的一直都是index为0,routers数组只有一个的路由状态,但是我看reactnavigation源码打印的路由状态New State已经到index为2, routers有3个元素了。我想问一下,要怎么样才能获取源码打印的数据?
        羽纱:@程序猿的呐喊 设置onNavigationStateChange属性,利用方法this.props.onNavigationStateChange(prevNav, nav, action)保存nav
        程序猿的呐喊:@羽纱 没有用redux,现在就是用this.props.navigation.state,但是我想获取的是当前的routers,而不是仅仅是当前的路由,我想根据routers的个数来判断安卓的物理返回键是否显示退出应用。
        羽纱:1、如果你使用redux,那就很简单了,直接state.navReduer就行了。
        2、或者使用this.props.navigation.state
        在官网有提到:https://reactnavigation.org/docs/navigation-prop.html
      • Rocky_92d5:楼主好,如果我想实现点击底部导航栏的按钮后,弹出一组浮动的按钮。该如何实现呢?谢谢。
      • walter211:有几个问题不是看的很清楚:
        1 三个router之间嵌套的时候是如何做到平滑过渡的,如果是一样的作者为啥不抽象为一个
        2 goBack的逻辑是怎么样的,如何实现根据view的出入栈来pop界面,而不是直接跳转到一层的navigator的root路由
        walter211:@羽纱 果然还是要自己去看源代码,对上下文不是很清楚:disappointed_relieved:
        羽纱:1、第一个问题的答案:Check if a child scene wants to handle the action as long as it is not a reset to the root stack,看`if (action.type !== NavigationActions.RESET || action.key !== null)`这一段代码,只有子路由放弃处理时才有父路由处理。
        2、这个问题的答案是基于第一个问题的答案的,通过递归找到当前页面(展示在用户面前的页面)的路由,对这个路由直接进行pop操作,然后再递归回去replace它在父路由中的位置。
      • Lucifer_Lin:请问一下作者,如果使用StackNavigation跳转的页面层数较深,当我想要goBack到较早的页面时,如何维护key比较好?
        羽纱:我更新了文章 ,最后有写
        羽纱:是要按name跳转吗?
      • 李简书:写的很好赞
        羽纱:@fangkyi03 :scream:或许导航嵌套不能这么玩 一般都是Drawer套Stack套Stack套Tab套Tab 所以没遇到你的那些为题
        fangkyi03:如果你的结构为
        stack {
        tab:{
        stack:{
        tab
        这个时候 最里面的这个tab会出现无法显示的情况

        还有 因为没有左侧tab的关系
        所以如果你自己手动实现
        左侧buttonGroup右侧一个stack
        会出现onlayout获取不正常的情况

        其次有些时候需要把底部的tabbar给隐藏掉
        但是 这个时候就会有一个问题 如果你在这个tab中跳转的时候 没有发送setParams将底部的tabbar重新显示出来的话 就会直接报错

        假设现在有5个tab页面 其中有2个页面用了drawer
        如果你在第5个页面使用draweropen打开这个菜单的时候 你会发现
        打开的是第一个tab中的drawer

        在drawer中也会出现onlayout获取位置不正常的情况

        以上 都是我遇到过的问题
        羽纱:@est7 谢谢:blush:
      • liviuscn:想嵌套两个tab能实现吗?
        羽纱:@苦苷蔗 我有这么实现 不卡啊 你是实现了 但是很卡吗?试一下懒加载吧 设置lazy属性
        liviuscn:@lyxia_ios 一个tab作为底部导航,tab第一个页面再嵌套一个顶部tab类似今日头条的顶部tab,都是会变的很卡顿,要怎么实现呢?
        羽纱:@苦苷蔗 可以

      本文标题:react-navigation路由篇之StackRouter

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