React Navigation 学习

 

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

零、环境篇

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

 

一、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     })

 

 

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 永远都是所有路由的集合。
     

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

 

2. stack 专用导航API

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

  • pop,弹出页面

  • popToTop,弹出到底部路页面

  • replace,替换

  • reset,重置当前 navigator

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

  

 

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

 

四、滴滴打车路由设计 

            

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

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

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

 

五、与 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)。

那么,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;

路由事件会自顶向下传导,父组件、子组件的事件处理函数会依次被触发:parent will focus > child will focus > parent didfocus > child didfocus,blur 也是一样。

 

七、其他

状态保持(实验性)(需要借助 @react-native-community/async-storage 或其他存储工具)

涉及到的 API: persistNavigationState 、 loadNavigationState ,功能是——刷新页面后可维持之前的路由状态,即使进程被杀掉也可以恢复噢。恢复后历史堆栈仍在,可以使用 goBack、replace等操作。

 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渲染一个空页面

 

TypeScript 支持

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

 

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

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

 

自定义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/

posted @ 2020-01-17 20:06  ppJuan  阅读(993)  评论(0编辑  收藏  举报