零、环境篇
在使用 react-navigation 之前,我们需要创建一个 react-native 项目。目前像大象这种App虽然支持 react-native 应用,但是支持的 mrn 1.x 所装的 react-navigation 是 1.x 版本,版本较低,而现在 react-navigation 最新版本是 4.x。所以我们抛弃 mrn,自己创建一个rn项目试试(参考 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
创建这些导航的语法都是类似的:
import { createAppContainer, createSwitchNavigator } from 'react-navigation';
import { createDrawerNavigator } from 'react-navigation-drawer';
import { createStackNavigator } from 'react-navigation-stack';
import { createMaterialTopTabNavigator } from 'react-navigation-tabs';
const navigator = create***Navigator(
// routes
{
Home: { // 如果没有 navigation 等其他选项,也可以简写为:Home: Home
screen: Home // 加载的组件
navigationOptions: {}, // screen 配置
path: 'people/:name', // deep-link 或者 web应用 场景下使用
}
},
// configs
{
initialRouteName: '', // 初始路由
navigationOptions: {}, // navigator 的配置
defaultNavigationOptions: {}, // screens 的配置
paths: {} // deep-link 场景
...
}
)
export default createAppContainer(navigator)
2. 认识 navigationOptions
navigationOptions
可以写在 route 中,可以写在 navigator 中(3.x 开始叫 defaultNavigationOptions
),也可以写在 screen 中。优先级是 route > screen > navigator。
({ navigation, screenProps, navigationOptions }) => ({ // object | function
title: '标题', // ⚠️默认情况下按照平台惯例设置,所以在 iOS 上标题居中,在 Android 上左对齐
headerTitle: <Title />, // 也可以设置一个组件,它可以通过 nativation.getParam、setParams 和页面通信,也可以使用 redux 等
headerRight: <Title />,
headerLeft: <Title />, // 会覆盖返回按钮
headerStyle: {}, // 整个标题栏
headerTintColor: '', // 标题和返回按钮的颜色
headerTitleStyle: {}, // 标题的样式
})
3. 认识 createAppContainer
createAppContainer
将导航配置转变成 React 组件,这时它就可以放在项目的任何地方了。生成的组件可以接受两个属性:onNavigationStateChange
和 uriPrefix
const AppContainer = createAppContainer(navigator);
<AppContainer
onNavigationStateChange={(prevState, newState, action) => {}} // 监听所有的路由状态变化
uriPrefix="/app" // deep-link 场景
/>
二、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
,重置当前 navigatordismiss
,退出当前 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
属性传递出子组件的引用
import React from 'react';
import { Button } from 'react-native';
import { withNavigation } from 'react-navigation';
class MyBackButton extends React.Component {
render() {
return (
<Button
title="Back"
onPress={() => {
this.props.navigation.goBack();
}}
/>
);
}
}
export default withNavigation(MyBackButton);
// 使用
<MyBackButton onRef={elem => (this.backButton = elem)} />;
2. withNavigationFocus
也是一个高阶组件,对内传递给子组件 isFocused
属性。注意⚠️,由于是属性传递,会导致组件重新渲染,需要 shouldComponentUpdate
来控制组件渲染次数。
import React from 'react';
import { Text } from 'react-native';
import { withNavigationFocus } from 'react-navigation';
class FocusStateLabel extends React.Component {
render() {
return <Text>{this.props.isFocused ? 'Focused' : 'Not focused'}</Text>;
}
}
export default withNavigationFocus(FocusStateLabel);
3. 全局变量
还有一种办法就是将某个 navigator 保存为全局变量,这样不同层级的页面也可以方便地互相导航。
import { NavigationActions } from 'react-navigation';
let _root;
const setTopLevelNavigator = (navigatorRef) => {
_root = navigatorRef;
}
const getTopLevelNavigator = () => {
return _root
}
export default {
setTopLevelNavigator,
getTopLevelNavigator
};
const App = () => {
return (
<RootNavigator ref={navigation.setTopLevelNavigator} />
);
};
const navigator = navigation.getTopLevelNavigator();
navigator.dispatch(NavigationActions.navigate({
routeName: 'Drawer',
action: DrawerActions.openDrawer()
}))
四、滴滴打车路由设计
- 首先,我们有一个广告页、登录页、主页的选择的场景,这三个页面是互斥的,只会存在一个,这种场景就适合用 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 的高度
import { Platform } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import DeviceInfo from 'react-native-device-info';
if (Platform.OS === 'android' && DeviceInfo.hasNotch()) {
SafeAreaView
.setStatusBarHeight
/* Some value for status bar height + notch height */
();
}
六、监听路由事件
NavigiationEvents 是 react-navigation 导出的一个组件,它上面有五个属性。在任何组件上都可以放置
- onWillFocus
- onDidFocus
- onWillBlur
- onDidBlur
- navigator(默认当前所处上下文)
import React from 'react';
import { View } from 'react-native';
import { NavigationEvents } from 'react-navigation';
const MyScreen = () => (
<View>
<NavigationEvents
onWillFocus={payload => console.log('will focus', payload)}
onDidFocus={payload => console.log('did focus', payload)}
onWillBlur={payload => console.log('will blur', payload)}
onDidBlur={payload => console.log('did blur', payload)}
/>
{/*
Your view code
*/}
</View>
);
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
自定义Android返回键行为
默认情况下,当用户按下Android 物理返回键时,reat-navigation会返回到上一个页面,如果没有可返回的页面,则退出应用。
自定义行为需要使用 react-native 的 BackHandler 这个API