美文网首页
ReactNative系列(五):react-natigatio

ReactNative系列(五):react-natigatio

作者: 猿海一粟 | 来源:发表于2019-07-05 19:05 被阅读0次
    ReactNative.jpg

    ReactNative整理:《ReactNative系列》

    内容目录

    1、navigationOptionsAppContainer
    2、导航器属性参数
    3、StackNavigator用法详解
    4、SwitchNavigator用法详解
    5、BottomTabNavigator用法详解
    6、DrawerNavigator用法详解
    7、结语


    一、navigationOptionsAppContainer

    AppContainer:负责管理应用的state并将顶层的navigator链接到整个应用环境。创建各种navigator时,已经将要用到的页面整合到一起,并生成了一个导航组件,但是该组件并没有接入到应用中,所以需要AppContainer将导航组件包裹,同时链接到整个应用环境,这样生成的导航器就可以使用了。

    1、关于createAppContainer的简单示例:

    import { createAppContainer, createStackNavigator } from 'react-navigation';
    
    const StackControllers = createStackNavigator({
      // 路由配置对象
    }, {
      // 导航属性配置
    });
    
    // 将生成的导航器组件 StackControllers 包裹到 Container 中
    const StackContainer = createAppContainer(StackControllers);
    
    // 现在 StackContainer 变成了 React 渲染的主要组件
    export default StackContainer;
    

    2、React Native 中的 createAppContainer prop:

    <StackContainer
       onNavigationStateChange={this.handleNavigationChange()}
       uriPrefix={'/app'}
    />
    
    • onNavigationStateChange(prevState, newState, action)
      每当导航器管理的navigation state 发生变化时,都会调用该函数。它接收之前的 state、navigation 的新 state 以及发布状态更改的 action。 默认情况下,它将 state 的更改打印到控制台。
    • uriPrefix
      应用可能会处理的URI前缀,在处理深度链接以提取传递给路由器的路径时使用。

    navigationOptions:导航器内部页面的选项配置。可以在导航器RouteConfigsNavigatorConfig中配置;也可以在页面中配置。优先级为:RouteConfigs配置 > 页面中navigationOptions配置 > NavigatorConfig配置。
    以StackNavigator为例:

    // 导航器中配置
    const StackControllers = createStackNavigator({
      stack1: {
        screen: StackController1,
        navigationOptions: () => ({
          title: 'controller1'
        })
      },
      stack2: {
        screen: StackController2
      }
    }, {
      initialRouteName: 'stack1',
    });
    
    // 页面中配置 navigationOptions
    export default class StackController1 extends Component {
      static navigationOptions = {
        title: 'StackController1'
      };
      ...
    }
    

    如果RouteConfigs中和页面中都存在navigationOptions,则以RouteConfigs中的配置为准。
    navigationOptions是用来配置页面头部或者手势等属性的,RouteConfigs和页面中静态配置,是针对单个页面的;而在XXNavigatorConfig中配置,则是针对导航内所有screen生效。

    二、 导航器属性参数

    1、Navigation prop reference

    应用中的每个页面组件都会自动提供navigation prop,该属性包含便捷的方法用于触发导航操作,如下所示:

    • this.props.navigation
      • navigate - 跳转到另一个屏幕,计算出需要执行的操作
      • goBack - 关闭活动屏幕并在堆栈中向后移动
      • addListener - 订阅导航生命周期的更新
      • isFocused - 如果屏幕获取焦点,函数返回true,否则返回false
      • state - 当前state,路由状态
      • setParams - 更改路由的参数
      • getParam - 获取具有回退功能的特定参数
      • dispatch - 向路由发送action
      • dangerouslyGetParent - 返回父级navigator的函数

    重点是要强调navigation属性不会传递给所有组件;只有screen页面组件会自动收到此属性。

    2、Navigator-dependent functions

    this.props.navigation上有些取决于当前navigator的附加函数
    如果是StackNavigator,除了navigategoBack,还提供了如下方法:

    • this.props.navigation
      • push - 推一个新的路由到堆栈
      • pop - 返回堆栈中的上一个页面
      • popToTop - 跳转到堆栈中最顶层的页面
      • replace - 用新路由替换当前路由
      • reset - 擦除整个导航状态,并将其替换为多个操作的结果
      • dismiss - 关闭当前堆栈

    如果是DrawerNavigator,则还可以使用以下选项:

    • this.props.navigation
      • openDrawer - 打开
      • closeDrawer - 关闭
      • toggleDrawer - 切换,如果是打开则关闭,反之亦然

    三、StackNavigator用法详解

    堆栈式导航:提供一种在每个新屏幕放置在堆栈顶部的屏幕之间转换的方法。该导航器是以栈的形式管理页面,每新建一个页面都会压入栈中,最新创建的页面在栈顶。默认情况下的配置具有熟悉的Android和iOS外观&效果:iOS上从右侧滑入,Android上从底部淡入。
    StackNavigator配置代码示例:

    /**
     * 堆栈导航:
     * 将页面配置到导航器中,不能跳转到导航没有配置的页面
     * @type {NavigationContainer}
     */
    const StackControllers = createStackNavigator({
      // RouteConfig 配置
      stack1: {
        screen: StackController1,
        navigationOptions: {
          title: 'Controller1',
          headerStyle: {
            backgroundColor: '#ffffff'
          }
        }
      },
      stack2: {
        screen: StackController2,
        navigationOptions: {
          title: '页面2'
        }
      },
      stack3: {
        screen: StackController3
      },
      stack4: {
        screen: StackController4
      }
    }, { // 
      initialRouteName: 'stack1',
      defaultNavigationOptions: {
        headerStyle: {
          backgroundColor: 'grey',
        },
        headerTintColor: 'blue',
        headerTitleStyle: {
          fontSize: 20,
        },
      }
    });
    
    const StackContainer = createAppContainer(StackControllers);
    
    export default StackContainer;
    

    RouteConfig - 配置的页面必须含有screen属性值,用来定义页面标识;navigationOptions用来初始化页面的一些配置,例如:Header样式,手势等。
    需要注意的是:
    StackNavigatorConfig - 配置中用的是defaultNavigationOptions控制导航内所有页面Header展示;用navigationOptions配置没有效果,react-navigation版本号是3.8.1,各位同学可以自己尝试下。
    initialRouteName属性值是配置导航器的默认页面;在没有设置的情况下,默认为RouteConfig中配置的第一个页面。
    页面代码:

    /**
     * 展示页面
     * 跳转方法:handleOnPress
     * 返回方法:backPress
     */
    export default class StackController2 extends Component {
      constructor(props) {
        super(props);
        this.handleOnPress = this.handleOnPress.bind(this);
      }
    
      componentDidMount() {
        console.log('-did-mount-stack2--');
      }
    
      componentWillUnmount() {
        console.log('-un-mount-stack2--');
      }
    
      /* 点击跳转到第三个页面 stack3 */
      handleOnPress() {
        this.props.navigation.navigate('stack3');
      }
    
      /* 点击返回上层页面 */
      backPress() {
        this.props.navigation.goBack();
      }
      
      render() {
        return(
          <View style={pageStyle.container}>
            <Text
              style={pageStyle.contentText}
              onPress={this.handleOnPress}
            >
              Controller2 To Controller3
            </Text>
            <Text
              style={pageStyle.backText}
              onPress={this.backPress}
            >
              返回
            </Text>
          </View>
        );
      }
    }
    

    看过ReactNative官方文档的同学应该知道:
    常用的点击事件组件有TouchableOpacityButtonText,它们都包含onPress属性,可以调用点击方法。我这里用的是Text组件实现点击切换页面和页面返回。
    StackNavigator的页面创建与跳转相对比较简单,比较麻烦的是多层页面的关闭。这里给出几种多层页面退出的解决办法:
    例如:从A -> B -> C -> D页面,要从D返回到A
    1、利用页面key
    导航器中每个页面都包含navigation属性值,可以通过this.props.navigation取到,该属性中有许多方法和数据,其中包括state。在state中包含keyrouteNameparamskey - 是页面在导航器中的唯一标识ID,根据这个标识能找到对应页面;routeName - 是当前页面在导航器中配置的路由名称;params - 传递的参数,是由上一个页面传入。
    注意:从D返回到A,用到的是B页面的key值,而不是A的。

     /**
       * 点击跳转到第三个页面 stack3
       * 其中 B: navigate.state.key 为传递的参数
       */
      handleOnPress() {
        const navigate = this.props.navigation;
        navigate.navigate('stack3', {
          B: navigate.state.key
        });
      }
    

    可以用类似的代码结构做出四个页面,测试跳转和返回。

    /**
       * 最后一个页面的点击事件
       * 点击返回到A页面
       */
      handleOnPress() {
        const navigate = this.props.navigation;
        navigate.goBack(navigate.state.params.B);
      }
    

    这样,在点击最后一个页面的文本时,就能返回到A页面,而且没有多余的退栈动画。
    2、拦截路由actionstate改变
    createStackNavigator关联到的源码:创建时传递两个参数生成的是NavigationContainer,而该NavigationContainer是个接口,包含router属性。继续向下查看源码发现router的value值是NavigationRouter,其中有导航调用的方法,getStateForAction就是我们需要用到的,它能监听交互的action和导航的state。下面摘出来源码:

    ...
    // 堆栈导航的创建
    export function createStackNavigator(
      routeConfigMap: NavigationRouteConfigMap,
      stackConfig?: StackNavigatorConfig
    ): NavigationContainer;
    ...
    
    // NavigationContainer接口及属性
    export interface NavigationContainer extends React.ComponentClass<
          NavigationContainerProps & NavigationNavigatorProps<any>
        > {
        new (
          props: NavigationContainerProps & NavigationNavigatorProps<any>,
          context?: any
        ): NavigationContainerComponent;
    
        router: NavigationRouter<any, any>;
        screenProps: ScreenProps;
        navigationOptions: any;
        state: { nav: NavigationState | null };
    }
    
    // NavigationRouter -- 导航路由接口
    export interface NavigationRouter<State = NavigationState, Options = {}> {
        /**
         * The reducer that outputs the new navigation state for a given action, with
         * an optional previous state. When the action is considered handled but the
         * state is unchanged, the output state is null.
         */
        getStateForAction: (
          action: NavigationAction,
          lastState?: State
        ) => State | null;
    
        /**
         * Maps a URI-like string to an action. This can be mapped to a state
         * using `getStateForAction`.
         */
        getActionForPathAndParams: (
          path: string,
          params?: NavigationParams
        ) => NavigationAction | null;
    
        getPathAndParamsForState: (
          state: State
        ) => {
          path: string;
          params?: NavigationParams;
        };
    
        getComponentForRouteName: (routeName: string) => NavigationComponent;
    
        getComponentForState: (state: State) => NavigationComponent;
    
        /**
         * Gets the screen navigation options for a given screen.
         *
         * For example, we could get the config for the 'Foo' screen when the
         * `navigation.state` is:
         *
         *  {routeName: 'Foo', key: '123'}
         */
        getScreenOptions: NavigationScreenOptionsGetter<Options>;
    }
    

    了解依据之后,来修改我们自己的代码:

    // 定义拦截器,用来将修改的action和state作为新的数据传入
    const StackInterceptor = StackControllers.router.getStateForAction;
    /**
     * 拦截思路:
     *  1、过滤action,只有是 action.type === 'Navigation/BACK' 时拦截处理
     *  2、根据拦截到的action中的key值,在state.routes中找到对应数据
     *  3、找到的对应的route数据下标index,取 index + 1 的数据的key值赋给 action.key
     *  4、把新修改的 action 和 state 传入定义好的拦截器中
     */
    StackControllers.router.getStateForAction = (action, state) => {
      console.log(action, '---action--');
      console.log(state, '---state--');
      let nextAction = action;
      if (state && action && action.type === 'Navigation/BACK') {
        const routeLength = state.routes.length;
        const isExist = state.routes.findIndex(route => action.key === route.routeName);
        if (isExist > -1 && isExist + 1 <= routeLength) {
          nextAction = {
            ...action,
            key: state.routes[isExist + 1].key
          };
        }
        console.log(nextAction, '--nextAction--');
        console.log(state, '--nextstate---');
      }
      return StackInterceptor(nextAction, state);
    };
    

    该方法中,我们用到的goBack方法能直接传递导航器中配置的路由名称为参数。例如:this.props.navigation.goBack('stack1')。同样的,返回A页面用到的还是B页面的Key值,所以在拦截查找位置的时候要取到stack1的下标index的下一个位置index + 1的数据才行。
    依据同样的方法,可以通过修改state.routes的数据来实现由D -> A,我们通过日志可以看到 B、C、D三个页面的componentWillUnmount方法都没有执行,虽然页面仍然能创建跳转和关闭,但是毕竟影响了组件的生命周期,所以不推荐大家使用。修改action.keystate.routes都能达到相同的效果,但建议使用前者更稳妥。

    另:其实还可以通过修改源码方法实现多层页面返回。3.x版本之前可以通过修改react-navigation源码的StackRouter.js中针对action.type === NavigationActions.BACK修改返回方式;但是3.x之后版本光修改该文件不生效了,还需要修改别的地方(还在摸索)。而且开发中会碰到有些问题需要删除node_modules文件夹重新npm install的情况,这时node_modules文件夹会重置,需要再重新修改react-navigation源码,很麻烦,所以建议大家不要修改源码。

    四、SwitchNavigator用法详解

    SwitchNavigator的用途是一次只显示一个页面。 默认情况下,它不处理返回操作,并在你切换时将路由重置为默认状态。项目中我们时常会碰到进入应用时展示启动页、广告页或者校验身份的需求,而且这些页面展示一次后就不再返回。此时就可以用SwitchNavigator来实现。

    const SwitchControllers = createSwitchNavigator({
      switch1: { // 广告页面或者身份验证页面
        screen: SwitchController1
      },
      switch2: { // 主页面
        screen: SwitchController2
      }
    }, {
      initialRouteName: 'switch1',
      resetOnBlur: true,
      backBehavior: 'none'
    });
    
    export default SwitchContainer = createAppContainer(SwitchControllers);
    

    SwitchNavigator单独使用局限性比较大,往往适合与别的导航器嵌套使用:比如,示例中的SwitchController1和SwitchController2都可以用其他类型导航器代替,其中可以包含多个页面。

    需要注意的属性有两个:

    • resetOnBlur - 用来标识切换离开屏幕时是否需要重置所有嵌套的导航器状态,默认值是true
    • backBehavior - 设置后退按钮是否会导致标签切换到初始路由,如果是,设置为initialRoute;否则为none。默认是none

    SwitchNavigator可以实现切换路由后,返回键不能回到上一个页面的功能,在某些特定情况下可以使用该特质。

    五、BottomTabNavigator用法详解

    TabNavigator标签导航是我们最常见的一种导航样式,在3.x中将TabNavigator被移除,改用BottomTabNavigatorMaterialTopTabNavigator,两者类似。这里只对前者进行讲解。
    还是和其他导航器创建一样,需要两个参数配置对象。

    const tab_home_select = require('../../../resource/tabbar_home_select.png');
    const tab_home = require('../../../resource/tabbar_home.png');
    const tab_list_select = require('../../../resource/tabbar_list_select.png');
    const tab_list = require('../../../resource/tabbar_list.png');
    const tab_self_select = require('../../../resource/tabbar_self_select.png');
    const tab_self = require('../../../resource/tabbar_self.png');
    
    const BottomTabControllers = createBottomTabNavigator({
      Home: {
        screen: TabHomeController,
        navigationOptions: {
          title: '首页',
          tabBarLabel: '首页',
          tabBarIcon: ({ focused, horizontal, tintColor }) => (
            <Image
              source={focused ? tab_home_select : tab_home}
              style={{ width: 20, height: 20 }}
              resizeMode={'contain'}
            />
          )
        }
      },
      List: {
        screen: TabListController,
        navigationOptions: {
          title: '书单',
          tabBarLabel: '书单',
          tabBarIcon: ({ focused, horizontal, tintColor }) => (
            <Image
              source={focused ? tab_list_select : tab_list}
              style={{ width: 20, height: 20 }}
              resizeMode={'contain'}
            />
          ),
          // tabBarOnPress: ({navigation, defaultHandler}) => {
          //   console.log(navigation, '--navigation--');
          //   console.log(defaultHandler, '--defaultHandler--');
          // }
        }
      },
      Self: {
        screen: TabMineController,
        navigationOptions: {
          title: '我的',
          tabBarLabel: '我的',
          tabBarIcon: ({ focused, horizontal, tintColor }) => (
            <Image
              source={focused ? tab_self_select : tab_self}
              style={{ width: 20, height: 20 }}
              resizeMode={'contain'}
            />
          )
        }
      }
    }, {
      lazy: true,
      initialRouteName: 'Home',
      order: ['Home', 'List', 'Self'],
      tabBarOptions: {
        activeTintColor: '#FF8800',
        inactiveTintColor: '#666666',
        showIcon: true,
        labelStyle: {
          fontSize: 12
        },
        style: {
          backgroundColor: 'white',
          height: 45
        }
      }
    });
    

    有几个比较重要的属性这里需要提一下:

    • tabBarIcon - 该方法中有三个参数:
      • focused - 当前Tab是否获取焦点,如果是我们一般都会设置当前Tab高亮;
      • horizontal - 当前是否横屏,如果横屏为true,否则为false;
      • tintColor - 对应activeTintColorinactiveTintColor,如果获取焦点则为activeTintColor设置的rgba色值字符串,否则为inactiveTintColor;如果没有设置则返回默认色值。
    • lazy - 是否懒加载。和原生一样,懒加载除了能提高渲染性能之外,还可以提升交互体验。
    • order - 底部Tab 的位置,是在左侧还是中间,都可以通过这个属性调整。
    • tabBarOptions - 设置TabBar的一些属性:激活与非激活状态下的颜色、是否显示图标或者图标文本样式等。

    有个特殊属性可以对Tab的点击进行监测:
    tabBarOnPress - 用来添加自定义逻辑处理,该方法在切换到下一个页面之前调用,包含的参数可以直接用一个event表示,或者可以拆分成navigationdefaultHandler
    方法中需要调用defaultHandler(),否则会页面切换失效。

    // 两个参数navigation、defaultHandler
    tabBarOnPress: ({navigation, defaultHandler}) => {
       defaultHandler();
    }
    
    // 一个event参数
    tabBarOnPress: (event) => {
       event.defaultHandler();
    }
    

    六、DrawerNavigator用法详解

    DrawerNavigator抽屉式导航也是常见导航类型之一,原生应用中经常会见到 -- 由侧滑菜单来控制页面跳转。但是一个应用肯定会有不少页面,如果都用侧滑菜单来控制的话,不仅混乱,而且体验也不是很好,所以抽屉式导航往往是与别的导航器嵌套使用的。

    const DrawerControllers = createDrawerNavigator({
      Main: {
        screen: DrawerMainController,
      },
      List: {
        screen: DrawerListController,
      },
      Self: {
        screen: DrawerSelfController,
      },
      Setting: {
        screen: DrawerSettingController,
      }
    }, {
      drawerWidth: 300,
      drawerPosition: 'left',
      initialRouteName: 'Main',
      order: ['Main', 'List', 'Self', 'Setting'],
      drawerLockMode: 'locked-closed',
      drawerType: 'slide',
      contentComponent: (props) => {
        console.log(props, '--props--');
        return (
          <ScrollView style={{flex: 1}}>
            <SafeAreaView forceInset={{ top: 'always', horizontal: 'never' }}>
              <DrawerItems {...props}/>
            </SafeAreaView>
          </ScrollView>
        );
      },
      contentOptions: {
        activeTintColor: '#FF8800',
        inactiveTintColor: '#666666',
      }
    });
    
    export default DrawerContainer = createAppContainer(DrawerControllers);
    

    其中有几个比较重要的属性需要注意:

    • drawerIcon - 侧滑item的图标,在页面的各自navigationOptions中配置。会回传两个参数:focused 状态是否选中标识;tintColor item选中时的色值。
    • drawerLockMode - 设置抽屉的锁定模式:unlocked,是默认值,用手势可以打开和关闭抽屉;locked-closed,锁定关闭,在抽屉保持关闭的状态下,用手势不能打开;locked-open,锁定打开,在抽屉打开的状态下,用手势不能关闭抽屉。
    • contentComponent - 该属性是用来设置侧滑内容组件的,可以自定义组件样式,默认情况下为DrawerItems(该组件可以从react-navigation中导入)。方法中会传递props属性给item组件,通过打印可以查看里面包含的值以及方法(选其中几个,大部分比较好理解):
      • activeItemKey - 是当前选中页面的key值标签
      • items - 抽屉的路由数组,可以修改或覆盖。其中元素为页面路由对象state值,包含keyrouteNameparams三个参数
      • descriptors - 我理解为描述元,里面包含抽屉页面的常用属性值,例如:keynavigationstateoptions(这个不知道具体用处)
    • contentOptions - 内容选项,用来设置item的属性值。
      • activeTintColor - 当前选项卡的标签和图标颜色
      • inactiveTintColor - 非当前选项卡的标签和图标颜色
      • onItemPress - 当item被点击时调用
      • itemStyle - 子组件item的样式

    七、结语

      以上是对几个导航器的拆分理解,其中的属性只是挑出了一部分,比较重要、难理解或者典型的,并不是全部。剩余的需要大家自己去对照上一篇尝试或者log日志输出对比,这里就不再挨个讲解了。相信自己动手尝试过的肯定要记忆更深,理解也更透彻。
      单个导航理解以后,它们的优缺点也就有了基本的认识,之后就是各种搭配组合使用了。多个导航嵌套能实现更复杂的业务需求,也能提高交互体验。

      下一篇:ReactNative系列(六):react-natigation 3.x全解(下)

    如果有不对的地方欢迎指出,大家互相讨论,如果喜欢请点赞关注

    相关文章

      网友评论

          本文标题:ReactNative系列(五):react-natigatio

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