美文网首页
React Navigation 5.x(一)常用知识点梳理

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

作者: AizawaSayo | 来源:发表于2021-01-09 04:09 被阅读0次

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

    本篇梳理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

    相关文章

      网友评论

          本文标题:React Navigation 5.x(一)常用知识点梳理

          本文链接:https://www.haomeiwen.com/subject/vaylgktx.html