React Navigation 5.x(一)常用知识点梳理

为了阅读体验,分为上下两篇。算不上教程,主要目的还是摘取使用这个库时的常用知识点和解决方案,便于自己记忆和查阅。

本篇梳理React Navigation 5.x 的一些基础API、嵌套导航注意事项、如何设计合理的嵌套路由等。
第二篇主要讲如何实现我自身App的两个需求:1、在嵌套路由中动态配置顶部标题栏(tab导航器嵌套在根部stack导航器的首屏);2、监听tab点击事件,触发时将对应screen重置到初始状态(数据)。会把自己的代码结构放出来。React Navigation 5.x(二)嵌套路由动态配置标题栏及自定义tabbar点击事件

循序渐进,我们先看一些干货,最后再来实现这两个功能。

一、堆栈导航器Navigator和Screen的具体参数配置及说明

这边只列举比较实用的。如有需要可以去官网查阅API:createStackNavigator

1. 导航器组件Navigator常用参数(Props):
参数名 说明
initialRouteName 导航器初次加载时要渲染的路由的名字,对应Screen(屏幕组件)的name,默认渲染该栈内第一个Sreen
screenOptions 为该栈内所有Sreen配置通用属性,即可把Screen的options里的属性提到这里统一设置。当这些Screen有相同的属性时,没有必要复制多份并在它们的options上重复设置。相同的属性,Screen的options里配置的优先级更高。
keyboardHandlingEnabled 默认值为true,若设置为false,屏幕上的键盘将不会在导航到新屏幕时自动消失。
headerMode 设置该栈标题栏的形式 'float''(ios模式)|'screen'(Android模式)|'none' (不显示头部标题栏)
2. 导航器内的屏幕组件Screen常用参数(统一在options里配置):
参数名 类型 说明
title String Screen标题栏的文字
header Function 返回一个React Element作为自定义标题栏。要使用这个配置,必须先确保设置Navigator的headerMode:'screen',以及定义好标题栏高度,e.g. headerStyle: { height: 80 } 参考示例(1)
headerTitle String | Function 如果是函数,返回一个接收参数的React Element,作为自定义标题栏文字组件。 参考示例(2)
headerShown Boolean 屏幕的标题栏显示与否,在父Navigator没有设置headerMode: 'none'的情况下,默认是true
headerTitleAlign Boolean 屏幕标题栏文字的对齐方式,可选值:leftcenter,未设置时,iOS默认居中,Android则靠左。
headerRight Function 返回一个React Element以自定义标题栏的右侧。
headerLeft Function 返回一个React Element以自定义标题栏的左侧。默认使用HeaderBackButton组件,你可以使用它来覆盖后退按钮,参考示例(3):
headerStyle Object 标题栏样式,如背景颜色等
headerTitleStyle Object 标题栏文字组件(headerTitle)样式
headerTintColor String 标题栏文字颜色

(1) header配置示例

header: ({ scene, previous, navigation }) => {
  const { options } = scene.descriptor;
  const title =
    options.headerTitle !== undefined
      ? options.headerTitle
      : options.title !== undefined
      ? options.title
      : scene.route.name;
  return (
    <MyHeader
      title={title}
      leftButton={
        previous ? <MyBackButton onPress={navigation.goBack} /> : undefined
      }
      style={options.headerStyle}
    />
  );
};

(2) headerTitle配置示例

function LogoTitle(props) {
  return (
    <>
      <Image
        style={{ width: 50, height: 50 }}
        source={require('@expo/snack-static/react-native-logo.png')}
      />
      <Text>{props.props.title}</Text>
    </>
  );
}
function StackScreen() {
  const title = '自定义标题' ;
  // 这边标题一般都是我们通过获取路由参数再经过方法判断确定的,这里写死为了方便演示如何把title额外传给自定义组件。
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="Home"
        component={HomeScreen}
        options={{ headerTitle: props => <LogoTitle props={{...props, title}} /> }}
      />
    </Stack.Navigator>
  );
}

(3) headerLeft示例

import { HeaderBackButton } from '@react-navigation/stack';
<Screen
  name="Home"
  component={HomeScreen}
  options={{
    headerLeft: (props) => (
      <HeaderBackButton
        {...props}
        onPress={() => {
          // Do something
        }}
      />
    ),
  }}
/>;

二、navigation属性及方法(Actions)

App里的每个screen组件都能通过props接收到一个navigation属性,它包含了各种调度导航动作的便利功能/方法。不同的导航器接收到的navigation能执行的方法是有区别的:
这边只展开讲了部分方法,每个蓝色Action都加了API直达链接。

比如我们有个导航屏幕:
<Stack.Screen key="Profile-1" name="Profile" component={component} />

  • 通用方法

    • navigate - 导航到特定路由,参数必须包含name或key属性,可选参数params(合并到目标路由的参数)。通俗理解就是转到这个name/key对应的路由。如果正在当前路由屏幕组件页面,那么是不会有任何反应的。
      格式:navigation.navigate(name: string, [params: object])
    navigation.navigate( 'Profile',  { user: 'jane' })
    
    • goBack - 返回导航堆栈历史记录的上一个屏幕。navigation.goBack()
    • reset - 可以让我们用新的导航状态替换当前的导航状态,即移除现有的已经入栈的屏幕和历史记录,设置新的入栈屏幕们。如果希望在更改状态时保留现有的屏幕,可以使用CommonActions.set结合navigation.dispatch。像这样:
    import { CommonActions } from '@react-navigation/native';
    navigation.dispatch(
      CommonActions.reset({
        index: 1,
        routes: [
          { name: 'Home' },
          {
            name: 'Profile',
            params: { user: 'jane' },
          },
        ],
      })
    );
    
    • setParams - 更新当前特定路由的参数。 作用就像React的setState,传入的参数和旧的params合并对象而非覆盖。
      如果要为特定路由更新参数,则可添加source属性,值为该路由的key。
    import { CommonActions } from '@react-navigation/native';
    
    navigation.dispatch({
      ...CommonActions.setParams({ user: 'Wojtek' }),
      source: route.key,
    });
    
    • setOptions 可以在屏幕组件内部,根据它的props、state、context,来定制我们的屏幕组件选项(screen options),比如title等。
  • 堆栈导航器

    • replace - 用新路由替换导航状态navigation state中当前或指定的路由。以下是替换state中特定路由的示例:
    import { StackActions } from '@react-navigation/native';
    navigation.dispatch({
      ...StackActions.replace('Profile', { // 要新替换上的路由name
        user: 'jane',
      }),
      source: route.key, // 要被替换的路由的key
      target: navigation.dangerouslyGetState().key, // 要新替换上的路由的key
    });  
    
    • push - 添加一条新路由到导航堆栈顶部。格式同navigate
      和调用navigate的区别是,navigate会先尝试查找具有目标name的现有路由并跳转过去,并且仅在堆栈中还没有这个路由时才推送新路由。而push在当目标路由已经存在于导航堆栈时,仍然会推送新路由,因此一个路由可以多次访问(形成多条历史记录)。
    • pop - 默认回到导航栈历史记录的上一步。有一个可选参数(count),允许你指定弹出多少个屏幕。navigation.pop(count: number)
    • popToTop - 返回堆栈中的第一个屏幕,关闭所有其他屏幕。navigation.popToTop()
  • tab选项卡导航器

    • jumpTo - 跳转至tab导航器中的现有路由。
      格式:navigation.jumpTo(name: string, [params: object])
  • drawer抽屉导航器

    • jumpTo - 跳转至drawer导航器中的现有路由。格式同tab导航器的jumpTo
    • openDrawer - 打开drawer导航器面板。格式:navigation.openDrawer()
    • closeDrawer - 关闭drawer面板。格式:navigation.closeDrawer()
    • toggleDrawer - 切换drawer面板开关状态。格式:navigation.toggleDrawer()
  • 高级API参考

    • dispatch - dispatch方法允许我们发送一个导航动作对象(包含用于生成特定基于某类型导航器的操作方法),来确定导航状态如何更新。除非实在无法直接通过navigate,goBack等方法完成我们所需的操作。不然应该避免使用它。尽量都通过navigation.[普通方法]属性来导航。
      dispatch可调度的对象除了CommonActions,还有StackActions 、还有DrawerActions 、还有TabActions 。这仨都扩展于CommonActions。
// 要先获取特定的导航动作创造器
import { CommonActions } from '@react-navigation/native';

navigation.dispatch(
  // 再去触发方法
  CommonActions.navigate({
    name: 'Profile',
    params: {
      user: 'jane',
    },
  })
);

三、导航状态Navigation state

navigation state是React Navigation存储应用程序的路由结构和历史记录的对象。
比如,在主屏幕嵌套了一个标签导航器的堆栈导航器,可能具有如下导航状态:

const state = {
  type: 'stack',
  key: 'stack-1',
  routeNames: ['Home', 'Profile', 'Settings'],
  routes: [
    {
      key: 'home-1',
      name: 'Home',
      state: {
        key: 'tab-1',
        routeNames: ['Feed', 'Library', 'Favorites'],
        routes: [
          { key: 'feed-1', name: 'Feed', params: { sortBy: 'latest' } },
          { key: 'library-1', name: 'Library' },
          { key: 'favorites-1', name: 'Favorites' },
        ],
        index: 0,
      },
    },
    { key: 'settings-1', name: 'Settings' },
  ],
  index: 1,
};

每个导航状态对象中包含的属性:

  • type-这个导航状态归属的导航器的类型,例如stacktabdrawer
  • key -识别导航器的唯一键。
  • routeName-包含所属导航器的每个屏幕name(字符串)的数组。
  • routes-在导航器中呈现的路由对象(屏幕)的列表。它还在堆栈导航器中表示历史记录。此数组中至少应存在一项。
  • index-正获得焦点的路由对象在routes数组中的索引。
  • history-访问过的项目列表。这是一个可选属性,并非在所有导航器中都存在。比如它仅存在于核心的tab和抽屉导航器中。history数组中的项目可以根据导航器而变化。此数组中至少应存在一项。
  • stale-除非显式设置了stale属性,否则值默认是false。也就表示导航状态对象需要“自动补齐”

routes数组中的每个路由对象(route)都可以包含以下属性:

  • key-屏幕的唯一键。会自动创建或在导航到此屏幕时添加。
  • name-屏幕名称。在导航器组件层次结构中定义。
  • params-可选,一个包含参数的对象,有导航动作时定义,例如navigate('Home', { sortBy: 'latest' })
  • state -可选,嵌套在此屏幕内的子导航器的导航状态对象。只会在导航事件发生后才挂到路由对象上。

注:每个screen组件的props.route,就是上面说的routes数组中的路由对象,内容为这个屏幕的路由数据。

四、设计合理的导航结构

嵌套导航器就是在一个Navigator的一个Screen里渲染的Navigator,作为一个组件元素赋给Screen的component属性。
一个应用通常都拥有底部选项卡(tabbar),一般是主页的标准配置。同时应用中的部分页面(比如登录页等)不需要tabbar。要实现这一点,能从导航结构入手就不要去动态设置隐藏/显示tabbar。最简单的方法是将选项卡导航器嵌套在堆栈导航器的第一个屏幕中,将不需要tabbar的Screen放在这个屏幕后面。
像下面的例子:一个tab导航器就被嵌套在stack导航器里。

首页为底部tab栏的典型嵌套结构 (下面五、(3)还会用这个例子举证)
function HomeTabs() { 
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={Home} />
      <Tab.Screen name="Feed" component={Feed} />
    </Tab.Navigator>
  );
}
function App() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeTabs} />
      <Stack.Screen name="Profile" component={Profile} />
      <Stack.Screen name="Settings" component={Settings} />
    </Stack.Navigator>
  );
}

另外一种应用中常见的导航模式,把stack导航器嵌套在drawer导航器的每个屏幕中

function Root() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Profile" component={Profile} />
      <Stack.Screen name="Settings" component={Settings} />
    </Stack.Navigator>
  );
}
function App() {
  return (
    <NavigationContainer>
      <Drawer.Navigator>
        <Drawer.Screen name="Home" component={Home} />
        <Drawer.Screen name="Root" component={Root} />
      </Drawer.Navigator>
    </NavigationContainer>
  );
}

如果我们想从Home导航到Root,这样操作:navigation.navigate('Root'); Root的初始屏幕即Profile就会展示。如果你想展示的是Settings这一屏,就需要这样做:navigation.navigate('Root', { screen: 'Settings' }); 如果你要带参数跳转,现在就得这样做:

navigation.navigate('Root', {
  screen: 'Settings',
  params: { user: 'jane' },
});

五、保持嵌套导航器定义的初始路由不变(initialRouteName的值)

当你指定导航到嵌套导航器的某一屏时(navigation.navigate('Root', { screen: 'Settings' });),导航器定义初始路由就会被替换成这一屏,也就是说,下次直接导航这个嵌套导航器的时候(navigation.navigate('Root'); ),会默认显示这个Screen(Settings)。
如果不想初始路由被改变,我们就要在跳转的时候加一个initial: false,,如下:

navigation.navigate('Root', {
  screen: 'Settings',
  initial: false,
});

六、尽量避免深度嵌套

在能实现需求的基础上,请尽可能地少嵌套导航栈,建议层数最多不要超过两层。因为这会有很多副作用。
比如会引起低端设备的内存和性能问题。
影响代码可读性,过于冗余复杂变得难以维护。
Tab里再放Tab,Drawer里再放Drawer,会带来不好的用户体验。
如果你为了代码逻辑更清晰,想为Navigtor下的Screen分类,可以考虑像这样做:

// Define multiple groups of screens in objects like this
const commonScreens = {
  Help: HelpScreen,
};

const authScreens = {
  SignIn: SignInScreen,
  SignUp: SignUpScreen,
};

const userScreens = {
  Home: HomeScreen,
  Profile: ProfileScreen,
};

// Then use them in your components by looping over the object and creating screen configs
// You could extract this logic to a utility function and reuse it to simplify your code
<Stack.Navigator>
  {Object.entries({
    // Use the screens normally
    ...commonScreens,
    // Use some screens conditionally based on some condition
    ...(isLoggedIn ? userScreens : authScreens),
  }).map(([name, component]) => (
    <Stack.Screen name={name} component={component} />
  ))}
</Stack.Navigator>;

七、使用嵌套导航器,其他要注意并弄清的点

(1) 每个导航器保管它自己的导航历史

比如,当你在一个被嵌套在Screen里的堆栈导航器上点击返回按钮的时候,它会返回到本导航器(就是被嵌套的stack导航器)导航历史中的上一页,而不是返回到上级导航器中。

(2) 每个导航器中的屏幕有它自己的参数

比如,传递给嵌套导航器中的screen的任何参数都在该屏幕的route prop中,且不能被它的父或子导航器中的屏幕访问。
如果要从子屏幕访问父屏幕的参数,可以使用React Context将参数暴露给子屏幕。

(3) 导航action会优先由当前导航器处理,如果当前导航器不能处理则通过冒泡的方式由上一级导航器处理

比如,你在一个被嵌套的导航器的screen中调用navigation.goBack(),那么只有当你在该导航器的首页时你才会返回到父导航器中。其他的action像navigate工作原理相同。也就是说,只有当被嵌套的导航器不能处理这个action时,父导航器才会试图去处理它。
在上面的例子(首页为底部tab栏的典型嵌套结构)中,当你在Feed页调用navigate('Messages'),嵌套的tab导航器会处理这个action,但当你在这里调用navigate('Settings'),就会由它的父导航器来处理了。

(4) 导航器的一些特定方法可以在子导航器中使用

比如,如果一个stack导航器嵌套在drawer导航器中,那么drawer导航器的openDrawercloseDrawertoggleDrawer等方法在被嵌套的stack导航器传递给屏幕的navigation属性中依然是可用的。但是如果stack导航器是drawer的父导航器,那么它里面的screen是不能访问这些方法的,因为它没有被嵌套在drawer导航器里。
同样,如果一个tab导航器被嵌套在stack导航器中,那么tab导航器screen中的navigation属性会新得到pushreplace这两个方法。

如果你想从父导航器中分派动作给嵌套的子导航器,可以使用 navigation.dispatch
具体语句:navigation.dispatch(DrawerActions.toggleDrawer());

(5) 被嵌套的导航器不会响应父级导航器的事件

比如说,你有一个嵌套在tab导航器中的stack导航器,那么stack导航器的screen在用navigation.addListener绑定监听事件时,不能接收到由父tab导航器触发出的事件,比如tabPress。为了能够响应父导航器的事件,你可以用navigation.dangerouslyGetParent().addListener来显式地监听父级导航器事件。

useEffect(() => {
  const unsubscribe = navigation
    .dangerouslyGetParent()
    .addListener('tabPress', (e) => {
      // ...
  });
  return unsubscribe;
}, [navigation]);
(6) 父级导航器的UI先于子导航器被渲染

例如,将stack导航器嵌套在drawer导航器内部时,你会看到drawer显示在stack导航器标题的上方。但是如果将drawer导航器嵌套在stack导航器中,则drawer将出现在stack标题下方。这是在决定如何嵌套导航器时要考虑的一个要点。

在开发应用时,你可能会根据需求来选用下面这些模式:

  • 在根stack导航器的首屏嵌套tab导航器——当你通过push跳转页面的时候,新的页面会覆盖掉标签栏。
  • 在drawer导航器的每个页面嵌套stack导航器----即先渲染抽屉效果再渲染stack导航器的头部
  • tab导航器的每个页面都嵌套stack导航器----tab导航器的标签栏仍然可见。常见的就是点击tab将stack置顶。
    详细官方说明:Nesting navigators
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容