深入学习React Native之路由导航

深入学习React Native之路由导航
路由是前端项目一个重要的组成部分,因为我们项目都是由多个页面组成,即使单页面项目也会有路由,多个页面之间跳转就是通过路由或者导航器来实现的。在RN 0.44之前的版本,我们可以直接使用官方提供的Navigator
组件来实现跳转;从0.44版本开始,Navigator
被官方从RN的核心组件库中剥离出来,主推的一个导航库就是React Navigation
,它性能也很接近原生,我们今天就来学习下它的用法。
路由
React Navigation
库每个版本的改动还是挺大的,比如3.x创建堆栈导航和创建选项卡导航都是直接在react-navigation
库中导出create函数,而4.x中堆栈路由是从react-navigation-stack
这个库导出,5.x版本库名又改成了@react-navigation/stack
,6.x版本又双叒叕改成@react-navigation/native-stack
,因此对新手及其不友好,很容易让人看了头大。

不过好在导航方式主要是三种:堆栈导航(StackNavigator)、选项卡导航(TabNavigator)和抽屉导航(DrawerNavigator),而且导航方式、传参都大差不差,因此本文主要以目前最新的6.x为例。
堆栈导航
堆栈导航是比较常见的导航方式,为应用程序在不同屏幕之间转换提供导航和管理的方式;其有些类似于web浏览器处理导航状态的方式。首先我们安装一些依赖:
# 安装导航的核心库
yarn add @react-navigation/native
# 安装导航的外部依赖库
yarn add react-native-screens react-native-safe-area-context
# 安装堆栈导航的主要库
yarn add @react-navigation/native-stack
要在项目里使用导航,我们首先要在项目的根组件创建一个路由导航容器,将我们的路由都包裹(一般是在App.js中),有点类似于Vue的<router-view />
:
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
export default function App() {
return (
<NavigationContainer>
{/* 导航组件 */}
</NavigationContainer>
);
}
我们导出createNativeStackNavigator
函数,用于配置堆栈路由的管理;它返回了包含两个组件的对象:Screen和Navigator,他们都是配置导航器所需的React组件,其中Screen组件是一个高阶组件,会增强props;在使用的页面中,会携带navigation
对象和route
对象,下面我们会介绍这两个对象的用法。

我们新建一个StackRouter.js
,将所有的堆栈导航配置统一在这个文件配置:
// StackRouter.js
import {createNativeStackNavigator} from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function HomeScreen() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
</View>
);
}
export default function StackRouter() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
/>
</Stack.Navigator>
);
}
在根组件中引入我们的堆栈导航组件即可:
// App.js
import StackRouter from './src/StackRouter';
export default function App() {
return (
<NavigationContainer>
<StackRouter></StackRouter>
</NavigationContainer>
);
}

我们的程序很多时候都不止一个页面,我们可以在堆栈导航中继续加入其他的列表页、详情页等等;initialRouteName
配置初始化的路由,可以设置成非第一个Screen页面:
// StackRouter.js
export default function StackRouter() {
return (
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeScreen}
/>
<Stack.Screen
name="List"
component={ListScreen}
/>
<Stack.Screen
name="Detail"
component={DetailScreen}
/>
</Stack.Navigator>
);
}
我们可以在每个路由上通过options
配置不同参数,比如标题、导航栏颜色等:
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
title: '首页',
headerStyle: {
height: 80,
backgroundColor: '#2196F3',
},
}}
/>
有时候,我们想要给一个页面传入额外的参数,我们可以把页面组件放到上下文中包裹,并传入props:
<Stack.Screen name="Home">
{(props) => <HomeScreen {...props} extraData={someData} />}
</Stack.Screen>
上面的用法在配置同一个页面不同路径时会很有用;比如我们的新建和编辑页面可以做成一个页面,配置不同路由通过传入额外的参数对两个页面进行区分。
路由跳转
在不同页面间,我们需要进行路由跳转;我们上面说过,在所有的页面组件中,都会携带一个navigation
对象,它是react-navigation注入的路由对象,它上面有很多的函数,可以进行不同形式的跳转。
如果我们跳转到未定义的路由,在开发版本中会报错,而在生产环境中不会发生任何事,
我们调用navigation.navigate()
函数来跳转,直接传入Stack.Navigator
中定义路由名name:
<Button
onPress={() => this.props.navigation.navigate('List')}
title="Go List">
</Button>

如果我们在List列表页也调用navigate('List')
,我们发现不会产生任何的效果,因为我们已经在列表页面了。navigate的含义是跳到这个页面,有点类似vue-router的router.replace
。

如果我们确实想要打开多个页面,可以将navigate改成push:
<Button
onPress={() => this.props.navigation.push('List')}
title="Go List Again">
</Button>
每次我们调用push
,都会在历史记录中新增一条记录,这也就是堆栈导航的由来;而调用navigate
,它会尝试在现在的路由堆栈中查找是否有这个路由,没有的话才会新增。
在堆栈导航的顶部有一个返回按钮,点击后可以返回上一个页面,我们也调用navigation.goBack()
来触发返回:
<Button
onPress={() => this.props.navigation.goBack()}
title="Go Back">
</Button>
有时候堆栈导航的层级很深,我们需要穿越好几个页面才能返回到第一个页面;在这种情况下如果我们明确的知道我们想要回到的是Home页,我们可以直接调用navigation.navigate('Home')
,清除所有的路由并且回到Home。
另一种方式是调用navigation.popToTop()
,这样将清除路由堆栈并回到堆栈的第一个页面。
function ListScreen({navigation}) {
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<Text>List Screen</Text>
<Button
onPress={() => navigation.push('List')}
title="Push List"></Button>
<Button
onPress={() => navigation.navigate('Home')}
title="Go Home"></Button>
<Button onPress={() => navigation.popToTop()} title="Popup"></Button>
</View>
);
}
传递参数
我们已经创建了多个页面并且在页面之间进行跳转,我们还需要传递不同的参数;例如从列表页到详情页需要传id,我们将需要传递的数据作为navigate函数的第二个参数。
function ListScreen({navigation}) {
return (
<View>
<Button
title="Go to Details"
onPress={() => {
navigation.navigate('Details', {
id: 86,
otherParam: 'anything you want here',
});
}}
/>
</View>
);
}
route
对象是Screen组件增强的props,它里面包含一个属性params,就是用来接受传递过来的参数:
function DetailsScreen({route, navigation}) {
const {id, otherParam} = route.params;
return (
<View>
<Text>Details Screen</Text>
<Text>id: {id}</Text>
<Text>otherParam: {otherParam}</Text>
</View>
);
}
页面也能更新自己的参数,就像更新state状态一样;通过navigation.setParams
进行更新:
navigation.setParams({
otherParam: 'someText',
});
在传递参数时,虽然我们可以将整个数据传过去,例如下面的方式:
// 避免这样的写法
navigation.navigate('Detail', {
user: {
id: '18',
firstName: 'Jane',
lastName: 'Done',
age: 25,
},
});
我们接收的时候也看似很方便的可以通过route.params.user
就能获取到数据;但是这是一种反模式,例如用户数据等,应该通过接口获取,或者放在全局的用户列表中,然后通过id进行获取。
navigation.navigate('Profile', { userId: '18' });
导航栏配置
我们在上面介绍了配置导航栏的标题,我们继续看下options
还有哪些用法;它除了接收一个对象,还可以接收一个返回对象的函数;函数的方式可以接收navigation和route两个参数,这种方式会很有用,例如我们在设置导航栏的组件时,获取这两个参数进行跳转操作。
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={({navigation, route}) => {
return {
headerTitle: () => (
<View>
<Text>标题:{route.params.title}</Text>
</View>
),
headerRight: () => (
<Button
title="Go List"
onPress={() => navigation.navigate('List')}></Button>
),
};
}}></Stack.Screen>
</Stack.Navigator>
headerTitle
可以覆写标题组件,替换成我们自定义的;headerRight
定义导航栏右侧的组件,可以是一些功能性的,比如设置、帮助等等,可能会涉及路由的跳转,因此我们可以将options设置成函数的形式。
我们可以调整导航栏标题的样式,通过下面三个参数:
- headerStyle:整个标题的样式,可以设置
backgroundColor
背景颜色。 - headerTintColor:返回按钮和标题文字的颜色。
- headerTitleStyle:标题文字的样式,可以设置
fontFamily
和fontWeight
。
我们肯定希望我们的App大部分的导航栏都是相同样式,有个别样式需要定制;我们将Options移动到Stack.Navigator的screenOptions
上:
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: '#f4511e',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
}}>
<Stack.Screen
name="Home"
component={HomeScreen}
></Stack.Screen>
</Stack.Navigator>

这样,所有的导航栏的配置都是相同的了;有时候我们还需要和导航栏互动,修改导航栏的配置,我们可以调用navigation.setOptions
来重新设置options。
function HomeScreen({ navigation }){
return (
<View>
<Button title="Update" onPress={()=> navigation.setOptions({
title: 'New Home',
headerRight: () => <Button title="Setting"></Button>,
})}></Button>
</View>)
}

导航的生命周期
在上一小节中,我们在不同的页面中进行导航跳转,当我们从a页面去b页面时,我们怎么才能知道即将要离开a页面?从b页面返回时,如果我们需要更新a页面中的数据,那我们在a页面如何监听呢?
很多同学会理所当然的认为离开a页面时,我们直接在componentWillUnmount
处理可以了;但是实际上,a页面只是暂时的隐藏到后台了,它并没有被销毁,始终保持了挂载状态,因此它的componentWillUnmount并不会被调用;而b页面则是会进入时创建,返回时被销毁。

选项卡导航在操作时也会观察到类似的情况,由于它有多个tab页,我们可以将它想象成多个堆栈导航,它的每个tab切换时也只是将页面隐藏,并不会销毁。那我们怎么回到刚开始的问题,如何发现用户在进入它和离开它呢?
我们通过navigation
导航来订阅相关的事件,通过监听对应的事件来了解页面何时进入以及离开。
export default class Screen extends Component {
constructor(props) {
super(props);
}
componentDidMount(){
this.props.navigation.addListener('focus', () => {
console.log('页面进入');
});
this.props.navigation.addListener('blur', () => {
console.log('页面离开');
});
}
}
navigation支持以下五种事件:
- focus:当屏幕聚焦时。
- blur:当屏幕失去焦点时。
- state:当导航器的状态改变时,通过event接收新的状态(event.data.state)。
- beforeRemove:当用户要离开页面时,可以阻止用户离开。
- tabPress:点击tab页面切换
navigation.addListener
返回一个函数,可以在组件销毁时调用来取消订阅的事件:
export default class Screen extends Component {
constructor(props) {
super(props);
}
componentDidMount(){
this._focus = this.props.navigation.addListener('focus', () => {
// ....
});
}
componentWillUnmount(){
this._focus()
}
}
选项卡导航
我们再来看下另一种常见的导航方式:选项卡导航;我们在常用的App中都能看到这种导航方式,如微信、知乎、某东、某猫底部导航,在屏幕底部显示三到五个App的主要板块,能够很方便的让用户在目标板块之间进行切换操作,避免路由层次过深。

React Natigation中主要使用的选项卡导航就是@react-navigation/bottom-tabs
,其他的还有下面这种material风格的:

material风格主要是@react-navigation/material-bottom-tabs
和@react-navigation/material-top-tabs
,使用方法都大同小异,只是风格不同,需要和App整体的风格协调。我们回到bottom-tabs,首先需要进行安装:
npm install @react-navigation/bottom-tabs
# 或者
yarn add @react-navigation/bottom-tabs
我们看下bottom-tabs
的简单使用案例:
const Tab = createBottomTabNavigator();
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
class TabRouter extends Component {
render() {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="List" component={ListScreen} />
</Tab.Navigator>
)
}
}
这样我们看到底部多了两个tab按钮,但是没有icon,比较简陋;这里引入react-native-vector-icons
这个库,包含很多icon图标。遵循安装教程安装好后,我们在它的主页上,找到我们需要的icon,这里包含了AntDesign、FontAwesome、Ionicons、MaterialIcons等一众丰富的图标。
我们可以给每个tab设置单独的options,但是为了方便统一,我们在Tab.Navigator上的screenOptions
集中配置:
import Ionicons from 'react-native-vector-icons/Ionicons';
<Tab.Navigator
screenOptions={({route}) => ({
tabBarActiveTintColor: 'tomato',
tabBarInactiveTintColor: 'gray',
tabBarIcon: ({focused, color, size}) => {
let iconName;
if (route.name === 'Home') {
iconName = focused ? 'home': 'home-outline';
}
if (route.name === 'List') {
iconName = focused ? 'list-circle': 'list-circle-outline';
}
return <Ionicons name={iconName} size={size} color={color} />;
},
})}>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="List" component={ListScreen} />
</Tab.Navigator>

tabBarActiveTintColor和tabBarInactiveTintColor从名字我们就能看出,是设置激活状态和非激活的icon和label的颜色(注意,是两者的颜色);tabBarIcon
用来设置icon图标,接收一个函数,函数传入三个参数:focused(boolean)、color(string)和size(number)。
focused用来表示tab激活或者非激活,很好理解,但是这里的color和size就很奇怪了,我们打印color的值,发现和上面的active color和inactive color相同;这个色值是为了和icon下面的label色值保持统一而传入的,我们可以用它的色值,也可以和label不同;size则是导航预期icon的大小。
如果我们只需要展示图标,而不要label,将tabBarShowLabel
选项置为false即可。
<Tab.Navigator
screenOptions={({route}) => ({
tabBarShowLabel: false,
})}>
....
</Tab.Navigator>
徽章
有时候,我们想要在tab按钮上加一个徽章(Badge),来标识未读信息,可以使用tabBarBadge
选项:
<Tab.Screen name="Home" component={HomeScreen} options={{ tabBarBadge: 3 }} />

路由跳转和嵌套
上面堆栈导航我们介绍过navigate和push的用法,而选项卡导航就比较简单了,由于两个tab是同一级关系,直接调用navigate就能实现路由跳转:
function HomeScreen({ navigation }) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Home!</Text>
<Button
title="去列表页"
onPress={() => navigation.navigate('List')}
/>
</View>
);
}
选项卡导航中不能调用push函数。
想象一下,在选项卡导航中我们经常用到不止一个路由,在列表页后面我们需要跳转到详情页;但如果直接把详情页面放到Tab.Screen
中肯定是不行的,这样只会增加一个tab按钮。我们可以利用上面的堆栈导航,将两种导航方式进行嵌套使用。
const Tab = createBottomTabNavigator();
const ListStack = createNativeStackNavigator();
function ListStackScreen() {
return (
<ListStack.Navigator>
<ListStack.Screen
name="List"
component={ListScreen}>
</ListStack.Screen>
<ListStack.Screen
name="Detail"
component={DetailScreen}>
</ListStack.Screen>
</ListStack.Navigator>
);
}
function TabStackRouter({ navigation }) {
return (
<Tab.Navigator>
<Tab.Screen
name="Home"
component={HomeScreen} />
<Tab.Screen
name="ListStack"
component={ListStackScreen} />
</Tab.Navigator>
)
}
我们创建了一个ListStack
堆栈导航,相当于在tab页面中嵌套了一层堆栈导航;我们很明显发现List页面会有两个头部的导航栏,我们将ListStack的headerShown
置为false即可将它的导航栏隐藏:
function TabStackRouter({ navigation }) {
<Tab.Navigator>
<Tab.Screen
name="ListStack"
component={ListStackScreen}
options={{
headerShown: false,
}} />
</Tab.Navigator>
}

安全区域
在iPhone X、iPhone XR等设备上,顶部的刘海设计和底部的小黑条都可能会遮住我们的App内容,因此需要进行适配;尽管RN提供了SafeAreaView
,但它有一些问题,React Navigation提供了更好用的react-native-safe-area-context
:

首先我们yarn安装,在iOS平台多一步pod安装:
yarn add react-native-safe-area-context
# iOS Platform
npx pod-install
首先在根组件使用SafeAreaProvider
,这是一个提供者,本身不会对布局产生影响,只有在该组件包裹下的子组件才能使用react-native-safe-area-context提供的功能,因此我们通常把它包裹在App组件:
import { SafeAreaProvider } from 'react-native-safe-area-context';
function App() {
return (<SafeAreaProvider initialMetrics={null}>
<NavigationContainer>{/*(...) */}</NavigationContainer>
</SafeAreaProvider>);
}
在我们要适配的页面引入SafeAreaView
自动处理:
import { SafeAreaView } from 'react-native-safe-area-context';
function HomeScreen() {
return (
<SafeAreaView
style={{ flex: 1, justifyContent: 'space-between', alignItems: 'center' }}
>
<Text>This is Top Text.</Text>
<Text>This is Bottom Text.</Text>
</SafeAreaView>
);
}

React Navigation内置了react-native-safe-area-context,一般如果使用了导航栏和底部的tab则无需处理。

抽屉式导航
抽屉式导航是从侧边栏划出抽屉一样的效果,抽屉式导航的核心是「隐藏」
,突出核心功能,隐藏一些不必要的操作,比如一些简报、新闻、栏目等等;在小屏幕时代,内容篇幅展示有限,会把一些辅助的功能放到侧边栏里;但是随着屏幕尺寸越来越大,通过新的交互方式,我们的App已经能够容纳足够多的内容,抽屉式导航的缺点也逐渐暴露:交互效率低下,处于操作盲区,单手操作不便。

因此抽屉式导航也逐渐衰落了,我们在大多App中也很难看到这种导航方式,仅有少数还保留,我们看下在虎嗅App上使用抽屉式导航的效果:

要在我们的App中使用这种导航,我们首先安装几个依赖:
yarn add @react-navigation/drawer
yarn add react-native-gesture-handler react-native-reanimated
和其他两种导航方式相同,我们需要把创建函数从依赖中导出:
import { createDrawerNavigator } from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function DrawerRouter() {
return (
<Drawer.Navigator>
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="List" component={ListScreen} />
</Drawer.Navigator>
);
}
我们可以点击左上角的按钮展开抽屉,或者在页面调用如下API进行手动打开或关闭:
navigation.openDrawer();
navigation.closeDrawer();
navigation.toggleDrawer();
还可以定义drawerPosition
,设置打开抽屉的位置,支持left(默认)和right:
<Drawer.Navigator
screenOptions={{
drawerPosition: 'right',
}}>
...
</Drawer.Navigator>
我们可以设置打开抽屉的内容,通过drawContentView
很容易的覆写内容。默认情况下在ScrollView下渲染了一个DrawerItemList
元素,在这基础上我们可以进行自定义,也可以使用DrawerItem
元素:
import {
DrawerContentScrollView,
DrawerItemList,
DrawerItem,
} from '@react-navigation/drawer';
function drawContentView(props) {
return (
<DrawerContentScrollView {...props}>
<DrawerItemList {...props} />
{/* 这里加入想要新增的一些元素 */}
<DrawerItem label="Help" onPress={...} />
</DrawerContentScrollView>
);
}
function DrawerRouter() {
return (
<Drawer.Navigator
drawerContent={props => drawContentView(props)}>
...
</Drawer.Navigator>
);
}

彩蛋
经过前面几篇对RN的学习,我们基本已经掌握了RN的相关开发技巧,下面我们就开始进行实战环节,从零开始开发一款属于我们自己的App,敬请期待。
如果觉得写得还不错,请关注我的专栏,定期前端好文分享
更多文章,以及更好的阅读体验敬请访问博客地址: