ReactNative系列(五):react-natigatio

2019-07-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'}
/>

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,该属性包含便捷的方法用于触发导航操作,如下所示:

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

2、Navigator-dependent functions

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

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

三、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都可以用其他类型导航器代替,其中可以包含多个页面。

需要注意的属性有两个:

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
    }
  }
});

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

有个特殊属性可以对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);

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

七、结语

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

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

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

上一篇下一篇

猜你喜欢

热点阅读