路由是前端项目一个重要的组成部分,因为我们项目都是由多个页面组成,即使单页面项目也会有路由,多个页面之间跳转就是通过路由或者导航器来实现的。在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
,将所有的堆栈导航配置统一在这个文件配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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> ); }
|
在根组件中引入我们的堆栈导航组件即可:
| import StackRouter from './src/StackRouter'; export default function App() { return ( <NavigationContainer> <StackRouter></StackRouter> </NavigationContainer> ); }
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
我们的程序很多时候都不止一个页面,我们可以在堆栈导航中继续加入其他的列表页、详情页等等;initialRouteName
配置初始化的路由,可以设置成非第一个Screen页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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两个参数,这种方式会很有用,例如我们在设置导航栏的组件时,获取这两个参数进行跳转操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <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支持以下五种事件:
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
集中配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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按钮。我们可以利用上面的堆栈导航,将两种导航方式进行嵌套使用。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| 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
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
元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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,敬请期待。