深入学习React Native之路由导航

深入学习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对象,下面我们会介绍这两个对象的用法。

深入浅出React Native(异步图书出品)
京东
¥46.90
去购买​

我们新建一个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>

 

动图封面
 
navigate跳转

 

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

React Native开发指南
京东
¥28.50
去购买​

如果我们确实想要打开多个页面,可以将navigate改成push:

<Button 
  onPress={() => this.props.navigation.push('List')} 
  title="Go List Again">
</Button>

 

动图

push跳转

 

每次我们调用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:标题文字的样式,可以设置fontFamilyfontWeight

我们肯定希望我们的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>

 

动图封面
 
screenOptions

 

这样,所有的导航栏的配置都是相同的了;有时候我们还需要和导航栏互动,修改导航栏的配置,我们可以调用navigation.setOptions来重新设置options。

function HomeScreen({ navigation }){
  return (
  <View>
    <Button title="Update" onPress={()=> navigation.setOptions({
      title: 'New Home',
      headerRight: () => <Button title="Setting"></Button>,
    })}></Button>
  </View>)
}

 

动图封面
 
setOptions

 

导航的生命周期

在上一小节中,我们在不同的页面中进行导航跳转,当我们从a页面去b页面时,我们怎么才能知道即将要离开a页面?从b页面返回时,如果我们需要更新a页面中的数据,那我们在a页面如何监听呢?

很多同学会理所当然的认为离开a页面时,我们直接在componentWillUnmount处理可以了;但是实际上,a页面只是暂时的隐藏到后台了,它并没有被销毁,始终保持了挂载状态,因此它的componentWillUnmount并不会被调用;而b页面则是会进入时创建,返回时被销毁。

React Native:用JavaScript开发移动应用
京东
¥32.94
去购买​

选项卡导航在操作时也会观察到类似的情况,由于它有多个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风格选项卡导航

 

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>

 

动图封面
 
Tab导航

 

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

 

Badge

 

路由跳转和嵌套

上面堆栈导航我们介绍过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>
  );
}

 

SafeAreaView

 

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

 

导航栏和底部自动处理了

 

抽屉式导航

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

React Native移动开发实战
京东
¥36.90
去购买​

因此抽屉式导航也逐渐衰落了,我们在大多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,敬请期待。

 

如果觉得写得还不错,请关注我的专栏,定期前端好文分享

更多文章,以及更好的阅读体验敬请访问博客地址:

 

 

编辑于 2022-09-15 21:20

posted on 2022-12-02 17:33  漫思  阅读(303)  评论(0编辑  收藏  举报

导航