React Navigation: Android 和 iOS 设备上的路由工具,包括手势和动画。

零、环境篇

在使用 react-navigation 之前,我们需要创建一个 react-native 项目。(参考https://reactnative.cn/docs/getting-started

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

 

一、Navigator 的种类和创建

在 web 项目中的 react-router,只负责功能实现,样式是需要开发者自己去设计的。而 react-navigation 自带了几种常见的交互和样式。它共有四种常用的 Navigator:

  • Stack的功能与 react-router 类似,但是每一个页面有一个标题栏。

  • Switch(Switch / AnimatedSwitch) 没有样式,为鉴权场景而生。它每次只渲染一个页面,不处理返回操作,并在你切换时将路由重置为默认状态。

  • Drawer菜单被放在一个抽屉中,通过一个在屏幕最左边的右滑手势,来打开抽屉。

  • Tab(BottomTab / MaterialBottomTab / MaterialTopTab) 菜单被放在 Tabs 中,可以在屏幕的顶部或底部。 

1. 认识 create***Navigator

创建这些导航的语法都是类似的:

 1 import { createAppContainer, createSwitchNavigator } from 'react-navigation';
 2 import { createDrawerNavigator } from 'react-navigation-drawer';
 3 import { createStackNavigator }  from 'react-navigation-stack';
 4 import { createMaterialTopTabNavigator } from 'react-navigation-tabs';
 5 
 6 const navigator = create***Navigator(
 7   // routes
 8   {
 9       Home: { // 如果没有 navigation 等其他选项,也可以简写为:Home: Home
10         screen: Home // 加载的组件
11         navigationOptions: {}, // screen 配置
12         path: 'people/:name', // deep-link 或者 web应用 场景下使用
13       }
14     },
15   // configs
16   {
17       initialRouteName: '', // 初始路由
18         navigationOptions: {}, // navigator 的配置
19       defaultNavigationOptions: {}, // screens 的配置
20       paths: {} // deep-link 场景
21     ...
22     }
23 )
24 export default createAppContainer(navigator)

 

2. 认识 navigationOptions

 navigationOptions可以写在 route 中,可以写在 navigator 中(3.x 开始叫defaultNavigationOptions),也可以写在 screen 中。优先级是 route > screen > navigator。

1 ({ navigation, screenProps, navigationOptions }) => ({ // object | function
2       title: '标题', // ️默认情况下按照平台惯例设置,所以在 iOS 上标题居中,在 Android 上左对齐
3           headerTitle: <Title />, // 也可以设置一个组件,它可以通过 nativation.getParam、setParams 和页面通信,也可以使用 redux 等
4           headerRight: <Title />,
5           headerLeft: <Title />, // 会覆盖返回按钮
6           headerStyle: {}, // 整个标题栏
7           headerTintColor: '', // 标题和返回按钮的颜色
8           headerTitleStyle: {}, // 标题的样式
9     })

 React Navigation 学习 随笔 第1张React Navigation 学习 随笔 第2张

 

3. 认识 createAppContainer

createAppContainer将导航配置转变成 React 组件,这时它就可以放在项目的任何地方了。生成的组件可以接受两个属性:onNavigationStateChangeuriPrefix

1 const AppContainer = createAppContainer(navigator);
2 
3 <AppContainer
4   onNavigationStateChange={(prevState, newState, action) => {}} // 监听所有的路由状态变化
5   uriPrefix="/app" // deep-link 场景
6 />

 

二、Navigation Prop 基础功能

1. 通用导航API

  • navigate
    下图说明 stackNavigator 中的 navigate 行为。当栈内没有找到该路由对应的页面时,就推入一个新的页面,否则只是弹出到已有页面。
    Drawer、Tab 中,一个路由只能有一个组件存在——底层也是 stack 实现,但 this.props.navigation.state 永远都是所有路由的集合。
    React Navigation 学习 随笔 第3张 

  • goBack
    此图说明 stackNavigator 中的 goBack 行为,传入参数表示「以我为参考进行回退」
    Drawer、Tab 中,goBack 默认返回初始路由。
    React Navigation 学习 随笔 第4张

 

2. stack 专用导航API

  • push,推入页面(和 navigate 的区别是,push不会去查找栈中是否已经有该路由)

  • pop,弹出页面

  • popToTop,弹出到底部路页面

  • replace,替换

  • reset,重置当前 navigator

  • dismiss,退出当前 navigator,返回上层 navigator

React Navigation 学习 随笔 第5张React Navigation 学习 随笔 第6张React Navigation 学习 随笔 第7张

React Navigation 学习 随笔 第8张  React Navigation 学习 随笔 第9张React Navigation 学习 随笔 第10张

 

3. drawer 专用导航API

  • openDrawer

  • closeDrawer

  • toggleDrawer控制菜单显隐

 

4. 其他通用的属性

  • state

  • setParams(name, value)

  • getParams(name, defaultValue)

  • isfocused()  // 是否被聚焦

  • dangerouslyGetParent()  // 获取父导航

  • dispatch()  // 用 props.navigation.dispatch(action) 的方式去改变路由,如下图

  • addListener(eventName, ({ action, context, lastState, state, type }) => {}) 

 

5. 路由变化时组件生命周期

Stack 在路由出栈的时候,组件会被卸载。但是 Drawer、Tab 的组件不会被卸载,状态会一直保存。

 

三、不传属性系列

上面的这些属性都是在 screen 组件中,通过this.props.navigation调用的。这就意味着,如果有深层次的子组件想操作路由,screen 就需要将navigation作为子组件的属性传递下去。以下提供了一些不传属性也能操作路由的方法:

1. withNavigation

这是一个高阶组件,对内传递给子组件navigation属性,对外暴露onRef属性传递出子组件的引用

 1 import React from 'react';
 2 import { Button } from 'react-native';
 3 import { withNavigation } from 'react-navigation';
 4 
 5 class MyBackButton extends React.Component {
 6   render() {
 7     return (
 8       <Button
 9         title="Back"
10         onPress={() => {
11           this.props.navigation.goBack();
12         }}
13       />
14     );
15   }
16 }
17 export default withNavigation(MyBackButton);
18 
19 // 使用
20 <MyBackButton onRef={elem => (this.backButton = elem)} />;

 

2. withNavigationFocus

也是一个高阶组件,对内传递给子组件isFocused属性。注意️,由于是属性传递,会导致组件重新渲染,需要shouldComponentUpdate来控制组件渲染次数。

 1 import React from 'react';
 2 import { Text } from 'react-native';
 3 import { withNavigationFocus } from 'react-navigation';
 4 
 5 class FocusStateLabel extends React.Component {
 6   render() {
 7     return <Text>{this.props.isFocused ? 'Focused' : 'Not focused'}</Text>;
 8   }
 9 }
10 
11 export default withNavigationFocus(FocusStateLabel);

 

3. 全局变量

还有一种办法就是将某个 navigator 保存为全局变量,这样不同层级的页面也可以方便地互相导航。

 1 // lib.js
 2 import { NavigationActions } from 'react-navigation';
 3 
 4 let _root;
 5 const setTopLevelNavigator = (navigatorRef) => {
 6     _root = navigatorRef;
 7 }
 8 const getTopLevelNavigator = () => {
 9     return _root
10 }
11 export default {
12   setTopLevelNavigator,
13   getTopLevelNavigator
14 };
15 
16 // app.js
17 const App = () => {
18   return (
19     <RootNavigator ref={navigation.setTopLevelNavigator} />
20   );
21 };
22 
23 // page.js
24 const navigator = navigation.getTopLevelNavigator();
25 navigator.dispatch(NavigationActions.navigate({
26     routeName: 'Drawer',
27     action: DrawerActions.openDrawer()
28 }))

 

四、滴滴打车路由设计 

 React Navigation 学习 随笔 第11张   React Navigation 学习 随笔 第12张   React Navigation 学习 随笔 第13张    React Navigation 学习 随笔 第14张 

  • 首先,我们有一个广告页、登录页、主页的选择的场景,这三个页面是互斥的,只会存在一个,这种场景就适合用 SwitchNavigator。

  • 顺风车、出租车明显是 TopTabNavigator 的交互——注意它们的上方还有一个类似标题栏的东西,这意味着可以在外面再套一层 StackNavigator(订单页也是如此)。

  • 而这一层 StackNavigator 和订单页的 StackNavigator 都是属于 DrawerNavigator 的内容,于是我们就有了下图这样一个路由的结构。

React Navigation 学习 随笔 第15张

 

五、与 React Native 配合

1. Scrollables

使用 react-native 的ScrollView/FlatList/SectionList的时候,有一个非常方便的交互设计:点击手机顶部的时候可以快速滚到顶部初始位置。如果想要点击 TabNavigator 的 Tab 时,也想有这种效果怎么办?可以直接使用 react-navigation 封装过的 ScrollView/FlatList/SectionList。

 

2. SafeAreaView

react-native 的SafeAreaView大家都知道,可以让手机在 ios 的刘海屏/美人尖等异型屏上能正常显示。

react-navigation 提供的 SafeAreaView 则多了一个属性forceInset,可以让我们更加精细地控制四边的padding。它在 top | bottom | left | right | vertical | horizontal 几种方向上有两种值可以设置:'always' 和 'nerver'。

这里要注意的是,如果 SafeAreaView是包裹在页面上的,不包括导航栏的高度,如下图左红色部分。如果 SafeAreaView 是包裹在 RootNavigator 上的,就包括导航栏的高度,如下图右蓝色部分。当然就算我们只放在页面上,导航栏的高度也对异性屏做了兼容,使得我们的页面在ios各种机型上正常显示(react-navigation 4.x)。

React Navigation 学习 随笔 第16张React Navigation 学习 随笔 第17张

那么,Android 异型屏怎么办?借助 react-native-device-info 识别是否有 notch,然后设置 SafeAreaView 的高度

 1 import { Platform } from 'react-native';
 2 import SafeAreaView from 'react-native-safe-area-view';
 3 import DeviceInfo from 'react-native-device-info';
 4 
 5 if (Platform.OS === 'android' && DeviceInfo.hasNotch()) {
 6   SafeAreaView
 7     .setStatusBarHeight
 8     /* Some value for status bar height + notch height */
 9     ();
10 }

 

六、监听路由事件

NavigiationEvents 是 react-navigation 导出的一个组件,它上面有五个属性。在任何组件上都可以放置<NavigiationEvents />,路由事件会自顶向下传导,父组件、子组件的事件处理函数会依次被触发。

  • onWillFocus

  • onDidFocus

  • onWillBlur

  • onDidBlur

  • navigator(默认当前所处上下文)

 1 import React from 'react';
 2 import { View } from 'react-native';
 3 import { NavigationEvents } from 'react-navigation';
 4 
 5 const MyScreen = () => (
 6   <View>
 7     <NavigationEvents
 8       onWillFocus={payload => console.log('will focus', payload)}
 9       onDidFocus={payload => console.log('did focus', payload)}
10       onWillBlur={payload => console.log('will blur', payload)}
11       onDidBlur={payload => console.log('did blur', payload)}
12     />
13     {/*
14       Your view code
15     */}
16   </View>
17 );
18 
19 export default MyScreen;

 

七、其他

TypeScript 支持

https://reactnavigation.org/docs/en/typescript.html

2.14.0 之前的版本使用 react-native-screens 来进行 native 侧的性能优化

https://reactnavigation.org/docs/en/react-native-screens.html

状态保持(实验性)

涉及到的 API: persistNavigationState 、 loadNavigationState 

 1 const AppNavigator = createStackNavigator({ })
 2 const persistNavigationState = async (navState) => {
 3   try {
 4       await AsyncStorage.setItem('myNavigator', JSON.stringigy(navState))
 5   } catch (e) {
 6   }
 7 }
 8 const loadNavigationState = async() => {
 9   const jsonString = await AsynStorage.getItem('myNavigator')
10   return JSON.parse(jsonString)
11 }
12 const App = () => <AppContainer
13     persistNavigationState={persistNavigationState}
14     loadNavigationState={loadNavigationState}
15 />

此功能在开发模式下特别有用。你可以使用以下方法,有选择地启用它:

1 const AppContainer = createStackNavigator({ })
2 function getPersistenceFunctions () {
3     return __DEV__ ? {
4       persistNavigationState,
5     loadNavigationState
6   } : undefined
7 }
8 const App = () => <AppContainer {...getPersistenceFunctions()} />

由于状态是异步加载的,你可以在 AppContainer 中使用属性renderLoadingExperimental渲染一个空页面

自定义Android返回键行为

默认情况下,当用户按下Android 物理返回键时,reat-navigation会返回到上一个页面,如果没有可返回的页面,则退出应用。

自定义行为需要使用 react-native 的BackHandler这个API

 1 import { BackHander } from 'react-native';
 2 constructor() {
 3     this._didFocusSubscription = props.navigation.addListener(
 4     'didFocus',
 5     payload => BackHandler.addEventListener(
 6         'hardeareBackPress',
 7       this.onBackButtonPressAndroid, // 返回 true 则表示我们已经处理了该事件,并且react-navigation 的事件监听器不会被调用,因此不会销毁当前页。 返回false会该方法继续执行 - react-navigation 的事件监听器将销毁当前页面。
 8     )
 9   )
10 }
11 componentDidMount() {
12     this._willBlurSubscription = this.props.navigation.addListener(
13       'willBlur',
14       payload =>
15         BackHandler.removeEventListener(
16           'hardwareBackPress',
17           this.onBackButtonPressAndroid
18         )
19     );
20 }
21   componentWillUnmount() {
22     this._didFocusSubscription && this._didFocusSubscription.remove();
23     this._willBlurSubscription && this._willBlurSubscription.remove();
24   }

  

参考:https://reactnavigation.org/

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄