七、导航模式

几乎每个应用程序的关键部分都是导航。时至今日,这个话题仍然让许多本地开发人员头疼。让我们看看哪些库可用,哪些库适合您的项目。本章从可用库的分类开始。然后,我们将介绍一个新的项目并使用它。我们一次只关注一个图书馆。一旦我们完成这项工作,我将在您编写导航代码时,向您介绍所使用的模式,以及这些模式意味着什么。请记住在您的机器和手机上试用该代码。

在本章中,您将了解以下内容:

  • 为什么在 React Native 中有许多可供选择的路由库?
  • 导航库面临哪些挑战?
  • 本机导航和 JavaScript 导航之间有什么区别?
  • 如何使用选项卡导航、抽屉导航和堆栈导航。
  • 本机解决方案的基础知识:您将第一次弹出 CreateReact 本机应用程序。

反应本地导航选项

通常情况下,如果你是一名初学者,你尝试在谷歌上搜索React Native navigation,你最终会头疼。可供选择的方案数量很多。这有几个原因:

  • 一些早期的库不再维护,因为维护人员已经退出
  • 一些拥有资源的公司开办了一个图书馆,然后将员工的注意力转移到其他方面
  • 一些解决方案被证明效率低下,或者实施了更好的解决方案
  • 不同的方法有一个体系结构原因,这导致需要维护不同的解决方案

我们将重点讨论最后一点,因为了解哪个库适合您的需要是至关重要的。我们将讨论解决方案,以便在本章末尾,您将知道为您的项目选择哪个库。

设计者导航模式

在我们深入了解库的世界之前,我想向您展示在应用程序中设计导航的不同方法。通常,这是项目设计师的工作;然而,一旦您了解了折衷,在其上添加一个代码模式层就更容易了。

移动应用程序由屏幕和过渡组成。总之,这些可以用下图表示:

This is an example diagram representing the screens of a tasks application

上图的主要要点如下:

  • 每个应用程序由顶级屏幕组成(主页项目搜索
  • 从顶层屏幕中,您可以向前和向下导航树(项目=>项目任务列表
  • 有时,您会向后过渡(任务=>项目任务列表

考虑到这一点,让我们研究一下有助于我们实现这些转换的组件。

导航到顶级屏幕

通常使用以下三种备选方案中的一种或多种导航到顶级屏幕:

  • 经典的底部导航,就像我们已经实现的那样。这通常使用图标或图标与文本的组合。根据所做的选择,这允许我们放置两到五个链接。这通常在平板电脑设计中避免:

An example of classic bottom navigation

  • 导航抽屉,从屏幕侧面打开。这包含一个链接列表,可能超过五个。这可能是复杂的,可以包括在顶部的用户配置文件。这往往是通过放置在其中一个上角的汉堡图标打开的:

An example of drawer navigation

  • 选项卡,位于屏幕顶部,至少成对显示。选项卡的数量可以超过四个,在这种情况下,选项卡可以水平滚动。这不仅用于顶级导航,还用于相同深度屏幕之间的任何导航。

在图形的不同级别之间导航

一旦我们达到一定的水平,有时我们想进一步探索这个特定的领域。在 Tasks 应用程序中,这意味着选择一个项目或在项目本身中选择一个特定的任务。

通常,要在图表中向下导航,我们使用以下命令:

  • 容器,包括列表、卡片、图像列表和图像卡
  • 简单按钮、文本链接或图标

但是,为了返回图表,我们通常使用以下内容:

  • 背面图标,如箭头,通常位于左上角或左下角
  • 按钮或链接,带有后退|取消|重新开始等文本
  • 位于编辑/创建屏幕相关部分的十字图标

对你们中的一些人来说,这种知识是自然产生的;然而,我遇到了一些提案或早期的设计草案,它们显然混淆了这些概念,最终严重影响了用户体验。实验是好的,但只能在使用标准和众所周知的模式的受控环境中进行,这对大多数用户来说都是自然的。

For experimenting with design, you should implement A/B tests. These require the ability to run different versions of the app in production for different subsets of users. Thanks to analytics, you can later assess whether A or B was a better choice. Finally, all of the users can be migrated to the winning scenario.

在图形的同一级别上导航

在更复杂的应用程序中,除了顶层导航之外,您还需要在相同深度的不同屏幕之间进行水平转换。

要在同一级别的屏幕之间切换,可以使用以下命令:

  • 选项卡,类似于顶级导航部分中讨论的选项卡
  • 屏幕滑动(按字面意思在屏幕之间滑动)
  • 在容器中滑动(例如,查看任务描述、连接的任务或任务、注释)可以通过选项卡连接
  • 左箭头或右箭头,或指示您在标高内位置的点

类似地,您也可以将其用于数据收集。然而,数据集合提供了更大的自由度,可以使用列表或限制较少的容器,这些容器也可以利用顶部/底部滑动。

考虑到设计者是如何解决导航问题的,现在让我们讨论如何使其性能更好,以及如何维护导航图。

开发人员的导航模式

老实说,归根结底,这是一个足够好的 JavaScript 实现吗?如果是这样的话,让我们利用它来实现我们的利益(即,在 JavaScript 中跟踪、控制、日志等等)。随着时间的推移,React 本地社区似乎成功创建了一个稳定的东西,称为 React 导航:

"React Navigation is entirely made up of React components and the state is managed in JavaScript on the same thread as the rest of your app. This is what makes React Navigation great in many ways but it also means that your app logic contends for CPU time with React Navigation — there's only so much JavaScript execution time available per frame." - React Navigation official documentation, available at: https://reactnavigation.org/docs/en/limitations.html.

但是,正如前面引用中所讨论的,这与您的应用程序在 CPU 周期方面存在竞争。这意味着它正在消耗资源,并在一定程度上降低了应用程序的速度。

JavaScript 导航的优点如下:

  • 您可以使用 JavaScript 代码调整和扩展解决方案。
  • 当前的实现对于中小型应用程序来说性能足够好。
  • 状态是用 JavaScript 管理的,可以轻松地与状态管理库(如 Redux)集成。
  • 该 API 与本机 API 解耦。这意味着,如果 React Native 最终超越 Android 和 iOS,API 将保持不变,一旦库维护人员实施,这将使您能够在另一个平台上使用相同的解决方案。
  • 易学。
  • 适合初学者。

JavaScript 导航的缺点如下:

  • 它很难以性能的方式实现。
  • 对于大型应用程序来说,它可能仍然太慢。
  • 有些动画与本机动画略有不同。
  • 某些手势或动画可能与本机手势或动画完全不同(例如,如果本机系统更改默认值,或者由于历史更改而存在不一致)。
  • 很难与本机代码集成。
  • 根据当前文档,路由应该是静态的。
  • 某些解决方案可能不可用(例如,与本机生命周期的连接),如果您曾经创建过本机导航,则可能会出现这些解决方案。
  • 有限的国际支持(例如,截至 2018 年 7 月,一些 JavaScript 导航库(包括 React 导航)不支持从右到左的模式)。

另一方面,让我们看看本机导航。

本机导航的优点如下:

  • 本机导航可以通过系统库进行优化,例如,可以将导航堆栈容器化
  • 本机导航优于 JavaScript 导航
  • 它利用了每个系统的独特功能
  • 利用本机生命周期并通过动画与之挂钩的能力
  • 大多数实现都与状态管理库集成

本机导航的缺点如下:

  • 有时,它违背了 React Native 的目的——它将导航分散到各个系统,而不是统一导航。
  • 很难跨平台提供一致的 API,甚至根本不一致。
  • 单一真相来源不再真实——我们的状态泄漏到特定平台内部管理状态的本机代码。这会消磨时间。
  • 有问题的状态同步–所选库要么根本不承诺立即进行状态同步,要么实现不同的锁,从而将应用程序的速度降低到通常无法达到目的的程度。 一些专家认为 NavigatorIOS 库的开发人员(截至 2018 年 7 月,官方 React 原生文档中仍提到)在这方面做了大量工作,但其未来仍不确定。
  • 它需要使用本机系统的工具和配置。
  • 它面向有经验的开发人员。

您需要考虑所有这些因素,并在选择任何一种之前做出正确的权衡。但在我们深入研究代码之前,请关注下一节。

重新构造应用程序

没有人喜欢所有特性交织在一起的巨大单片代码库。随着应用程序的增长,我们可以做些什么来防止这种情况?确保明智地定位代码文件,并采用标准化的方法。

下面是一个单片代码库的示例,它一旦超过 10000 行就会让您头疼:

An example of a directory structure that is not good enough for large projects

想象一下,一个目录有 1200 个还原器可以滚动浏览。您可能会改用搜索。相信我,对于 1200 个减速器,这也变得很困难。

相反,按功能对代码进行分组要好得多。因此,在调查应用程序的某个孤立部分时,我们将有一个清晰的文件范围:

An example of a directory structure that may be good for medium to large projects

要查看此新结构的运行情况,请查看第 7 章导航模式src文件夹中的Example 1代码文件。

If you have ever worked with microservices, think of it as if you wanted your features to be simple micro services within your frontend code base. A screen may ask them to operate by sending data, and expects a certain output. In some architectures, every such entity also creates its own Flux store. This is a good separation of concerns for large projects.

反应导航

浏览器有一个内置的导航解决方案,React Native 需要一个自己的导航解决方案,这背后有一个原因:

"In a web browser, you can link to different pages using an anchor () tag. When the user clicks on a link, the URL is pushed to the browser history stack. When the user presses the back button, the browser pops the item from the top of the history stack, so the active page is now the previously visited page. React Native doesn't have a built-in idea of a global history stack like a web browser does -- this is where React Navigation enters the story." - React Navigation official documentation, available at: https://reactnavigation.org/docs/en/hello-react-navigation.html.

综上所述,我们的移动导航不仅可以像在浏览器中看到的那样处理,还可以以我们喜欢的任何自定义方式处理。这是由于历史原因造成的,因为某些屏幕更改通常与特定操作系统的用户能够识别的特定动画有关。因此,明智的做法是尽可能密切地关注它们,使其与当地人的感觉相似。

使用 React 导航

让我们使用以下命令安装库,开始 React 导航之旅:

yarn add react-navigation

一旦安装了库,让我们尝试最简单的路径,并使用类似于浏览器中看到的类型的堆栈导航系统。

For those of you who do not know, or have forgotten what a stack is, the name stack comes from a real-life analogy to a set of items stacked on top of each other. Item can be pushed to the stack (placed at the top), or popped from the stack (taken from the top). A special structure, pushing this idea further, resembles a horizontal stack with access from both the bottom and top. Such a structure is called a queue; however, we will not use queues in this book.

在上一节中,我对文件结构进行了重构。作为重构的一部分,我创建了一个名为TaskListScreen的新文件,该文件由我们的代码库中的功能组成:

// src / Chapter 7 / Example 2 / src / screens / TaskListScreen.js
export const TaskListScreen = () => (
    <View>
 <AddTaskContainer />    // Please note slight refactor
 <TaskListContainer />   // to two separate containers
    </View>
);

export default withGeneralLayout(TaskListScreen);

withGeneralLayoutHOC 也是重构的一部分,它所做的只是用标题和底部栏包装屏幕。这种包裹的TaskList组件可以称为Screen并直接提供给 React 导航设置:

// src / Chapter 7 / Example 2 / src / screens / index.js

export default createStackNavigator({
    TaskList: {
        screen: TaskListScrn,
        path: 'project/task/list', // later on: 'project/:projectId/task/list'
        navigationOptions: { header: null }
    },
    ProjectList: {
        screen: () => <View><Text>Under construction.</Text></View>,
        path: 'project/:projectId'
    },
    // ...
}, {
    initialRouteName: 'TaskList',
    initialRouteParams: {}
});

这里,我们使用一个createStackNavigator函数,它需要两个对象:

  • 表示此StackNavigator应处理的所有屏幕的对象。每个屏幕都应指定表示此屏幕和路径的组件。您也可以使用navigationOptions定制您的屏幕。在我们的例子中,我们不想要默认的标题栏。
  • 表示导航器自身设置的对象。您可能需要定义初始路由名称及其参数。

完成这项工作后,我们完成了 hello world 的导航–我们有一个屏幕在工作。

具有反应导航功能的多屏幕

是时候给我们的StackNavigator添加一个任务屏幕了。使用新学到的语法,为任务详细信息创建占位符屏幕。以下是我的实施:

// src / Chapter 7 / Example 3 / src / screens / index.js
// ...
Task: {
    screen: () => <View><Text>Under construction.</Text></View>,
    path: 'project/task/:taskId',
    navigationOptions: ({ navigation }) => ({
        title: `Task ${navigation.state.params.taskId} details`
    })
},
// ...

这一次,我还通过了navigationOptions,因为我想使用带有特定标题的默认导航器顶栏:

An example of how the new Task screen could look

要导航到任务详细信息,我们需要一个单独的链接或按钮将我们带到那里。让我们在目录结构的顶部创建一个可重用的目录,如下所示:

// src / Chapter 7 / Example 3 / src / components / NavigateButton.js
// ...
export const NavigateButton = ({
    navigation, to, data, text
}) => (
    <Button
        onPress={() => navigation.navigate(to, data)}
        title={text}
    />
);
// ...
export default withNavigation(NavigateButton);

前面代码段的最后一行使用了withNavigationHOC,这是 React 导航的一部分。此 HOC 为NavigateButton提供导航道具。Todatatext需要手动传递给组件:

// src / Chapter 7 / Example 3 / src / features / tasks / views / TaskList.js
// ...
<View style={styles.taskText}>
    <Text style={styles.taskName}>
        {task.name}
    </Text>
    <Text>{task.description}</Text>
</View>
<View style={styles.taskActions}>
 <NavigateButton
        data={{ taskId: task.id }}
 to="Task"
        text="Details" />
</View>
// ...

就这样!让我们看看下面的结果。使用第三章造型图案中的技巧,如果您觉得设计需要稍加润色:

Each Task row is now displaying a Details link

现在,您可以点击“详细信息”按钮导航到“任务详细信息”屏幕。

选项卡导航

因为我们已经有了底部图标控件,所以让它们工作起来非常简单。这是选项卡导航的经典示例:

// src / Chapter 7 / Example 4 / src / screens / index.js
export default createBottomTabNavigator(
    {
        Home: createStackNavigator({
            TaskList: {
                // ...
            },
            // ...
        }, {
            // ...
        }),
        Search: () => (
            <View>
                <Text>Search placeholder. Under construction.</Text>
            </View>
        ),
        Notifications: () => (
            <View>
                <Text>Notifications placeholder. Under construction.</Text>
            </View>
        )
    },
    {
        initialRouteName: 'Home',
        initialRouteParams: {}
    }
);

请注意使用速记创建屏幕。我不使用对象,而是直接传递组件:

By default, React Navigation will create a bottom bar for us

要禁用该栏,我们需要传递相应的道具,如下所示:

// src / Chapter 7 / Example 4 / src / screens / index.js
// ...
{
    initialRouteName: 'Home',
    initialRouteParams: {},
    navigationOptions: () => ({
 tabBarVisible: false
    })
}
// ...

现在,我们需要让图标响应用户的触摸。首先,创建一个可以在应用程序中重用的NavigateIcon组件。检查存储库中的完整代码示例,但此处提供了一个示例:

// src / Chapter 7 / Example 4 / src / components / NavigateIcon.js
export const NavigateIcon = ({
    navigation, to, data, ...iconProps
}) => (
    <Ionicons
        {...iconProps}
        onPress={() => navigation.navigate(to, data)}
    />
);
// ...
export default withNavigation(NavigateIcon);

NavigateIcon替换现有图标相当简单,如下所示:

// src / Chapter 7 / Example 4 / src / layout / views / GeneralAppView.js
import NavIonicons from '../../components/NavigateIcon';
<View style={styles.footer}>
    <NavIonicons
        to="Home"
        // ...
    />
    <NavIonicons
        to="Search"
        // ...
    />
    <NavIonicons
        to="Notifications"
        // ...
    />
</View>

最后要注意的是总平面布置。SearchNotifications屏幕应显示我们的自定义底部导航。由于我们了解到了 HOC 模式,这一点非常简单:

// src / Chapter 7 / Example 4 / src / screens / index.js
// ...
Search: withGeneralLayout(() => (
    <View>
        <Text>Search placeholder. Under construction.</Text>
    </View>
)),
Notifications: withGeneralLayout(() => (
    <View>
        <Text>Notifications placeholder. Under construction.</Text>
    </View>
)) // ...

结果显示在以下屏幕截图中:

The Search screen with its placeholder.

请通过向withGeneralLayoutHOC 添加配置对象来修复标头名称。

抽屉导航

现在是实施抽屉导航的时候了,以允许用户访问不太常用的屏幕,如下所示:

// src / Chapter 7 / Example 5 / src / screens / index.js
// ...
export default createDrawerNavigator({
    Home: TabNavigation,
    Profile: withGeneralLayout(() => (
        <View>
            <Text>Profile placeholder. Under construction.</Text>
        </View>
    )),
    Settings: withGeneralLayout(() => (
        <View>
            <Text>Settings placeholder. Under construction.</Text>
        </View>
    ))
});

由于我们已经准备好了默认的抽屉,让我们添加一个图标来显示它。汉堡包图标是最受欢迎的图标,通常位于标题角之一:

// src / Chapter 7 / Example 5 / src / layout / views / MenuView.js
const Hamburger = props => (<Ionicons
    onPress={() => props.navigation.toggleDrawer()}
    name="md-menu"
    size={32}
    color="black"
/>);
// ...

const MenuView = withNavigation(Hamburger);

现在,只需将其放置在GeneralAppView组件的标题部分,并适当设置其样式:

// src / Chapter 7 / Example 5 / src / layout / views / GeneralAppView.js
<View style={styles.header}>
    // ...
    <View style={styles.headerMenuIcon}>
       <MenuView />
    </View>
</View>

就这样,我们的抽屉功能齐全。您的抽屉可能看起来像这样:

Opened drawer menu on the iPhone X simulator.

你可以点击右上角的汉堡图标打开抽屉。

重复数据的问题

任务列表组件获取成功装载时显示列表所需的数据。但是,没有实施任何机制来防止数据重复。这本书并不是要提供解决常见问题的方法。但是,让我们考虑一些您可以实施的解决方案:

  • 更改 API 并依赖唯一的任务标识符(如 ID、UUID 或 GUID)。请确保筛选只允许唯一的。
  • 清除每个请求的数据。这是好的;然而,在我们的例子中,我们将丢失未保存的(与 API 相关的)任务。
  • 保持状态,仅请求一次。这只适用于我们的简单用例。在更复杂的应用程序中,您需要更频繁地更新数据。

好的,记住这一点,让我们最后深入了解基于本机导航解决方案的库。

反应本机导航

在本节中,我们将使用本机导航解决方案。React Native Navigation 是 Android 和 iOS 本机导航的包装器。

我们的目标是通过 React 导航重新创建我们在上一节中实现的功能。

关于设置的几句话

在本节中,您可能面临的最大挑战之一是设置库。请遵循最新的安装说明。慢慢来如果你不熟悉这些工具和生态系统,可能需要 8 个多小时。

请按照以下链接的安装说明进行操作:https://github.com/wix/react-native-navigation

This book uses the API from version 2 of React Native Navigation. To use the same code examples, you will need to install version 2 too.

您可能还需要弹出 Create React Native 应用程序,或使用react-native init引导另一个项目并复制其中的关键文件。如果您在该过程中遇到困难,请尝试使用src/Chapter 7/Example 6/(仅 React Native)或src/Chapter 7/Example 7/(整个 React Native 导航设置)中的代码。我用了react-native init并复制了所有重要的东西。

在工作设置的路径上肯定会有错误。不要生气;使用 React-Native 和 React-Native 导航搜索有关 StackOverflow 或 GitHub 问题的任何错误。

本机导航的基础知识

第一个大的变化是缺少AppRegistryregisterComponent调用。相反,我们将使用Navigation.setRoot(...),它将完成这项工作。只有当我们确定应用程序已成功启动时,才应调用setRoot函数,如下所示:

// src / Chapter 7 / Example 7 / src / screens / index.js
import { Navigation } from 'react-native-navigation';
// ...
export default () => Navigation.events().registerAppLaunchedListener(() => {
    Navigation.setRoot({
        // ...
    });
});

我们的根/条目文件将只调用 React 本机导航功能:

import start from './src/screens/index';

export default start();

可以更有趣的是我们在setRoot函数中加入了什么。基本上,我们在这里有一个选择:堆栈导航或选项卡导航。在我们之前的应用程序之后,顶级应用程序将是选项卡导航(抽屉导航在 React Native navigation 中是解耦的)。

At the time of writing this book, using the default built-in bottom bar is the only option to retain previous capabilities. Once library authors release version 2 of RNN and fix Navigation.mergeOptions(...), you will be able to implement custom bottom bars. 

首先,让我们删除默认的顶部栏并自定义底部栏:

// src / Chapter 7 / Example 7 / src / screens / index.js
// ...
Navigation.setRoot({
    root: {
        bottomTabs: {
            children: [
            ],
            options: {
                topBar: {
                    visible: false,
                    drawBehind: true,
                    animate: false
                },
                bottomTabs: {
                    animate: true
                }
            }
        }
    }
});

完成后,我们就可以定义选项卡了。React 本机导航中要做的第一件事是注册屏幕:

// src / Chapter 7 / Example 7 / src / screens / index.js
// ...
Navigation.registerComponent(
    'HDPRN.TabNavigation.TaskList',
    () => TaskStackNavigator, store, Provider
);
Navigation.registerComponent(
    'HDPRN.TabNavigation.SearchScreen',
    () => SearchScreen, store, Provider
);
Navigation.registerComponent(
    'HDPRN.TabNavigation.NotificationsScreen',
    () => NotificationsScreen, store, Provider
);

当我们注册了所有三个基本屏幕后,我们可以继续进行选项卡定义,如下所示:

// src / Chapter 7 / Example 7 / src / screens / index.js
// ...
children: [
    {
        stack: {
            id: 'HDPRN.TabNavigation.TaskListStack',
            // TODO: Check below, let's handle this separately
        }
    },
    {
        component: {
            id: 'HDPRN.TabNavigation.SearchScreen',
            name: 'SearchScreen',
            options: {
                bottomTab: {
                    text: 'Search',
                    // Check sources if you want to know
                    // how to get this icon variable
                    icon: search 
                }
            }
        }
    },
    // Notifications config object omitted: similar as for Search
]

我们定义了三个选项卡中的每个选项卡—TasksSearchNotifications。关于Tasks,这是另一个导航器。Stack导航器可配置如下:

stack: {
    id: 'HDPRN.TabNavigation.TaskListStack',
    children: [{
        component: {
            id: 'HDPRN.TabNavigation.TaskList',
            name: 'HDPRN.TabNavigation.TaskList',
        }
    }],
    options: {
        bottomTab: {
            text: 'Tasks',
            icon: home
        }
    }
}

在前面的代码片段中,bottomTab选项设置底部栏中的文本和图标:

The Tasks tab with React Native Navigation

进一步调查

我将把如何实现导航元素(如抽屉或任务详细信息屏幕)的研究留给那些足够勇敢的人。在撰写本文时,React Native Navigation v2 非常不稳定,我选择不再发布该库中的任何代码片段。对于大多数读者来说,这应该足以获得总体感觉。

总结

在本章中,我们最终用比以前多得多的视图扩展了我们的应用程序。您已经了解了移动应用程序中导航的不同方法。在 React 原生世界中,它要么是原生导航,要么是 JavaScript 导航,要么是两者的混合。除了学习导航本身,我们还使用了包括StackNavigationTabNavigationDrawerNavigation在内的组件。

我们还首次从本机导航库中弹出了 createreact 本机应用程序并安装了本机代码。我们开始真正深入地研究这个问题。现在是时候退一步,更新我们的 JavaScript 知识了。我们将学习不仅在 React Native 中有益,而且在 JavaScript 总体上有益的模式。

进一步阅读

  • 反应导航常见错误–来自官方文档,可从以下网址获得:

https://reactnavigation.org/docs/en/common-mistakes.html

  • 在 React Native 中导航的千种方式,由 Charles Mangwa 编写:

https://www.youtube.com/watch?v=d11dGHVVahk.

  • 用于导航的导航操场:

https://expo.io/@反应导航/导航游乐场

  • 世博会航海文献:

https://docs.expo.io/versions/v29.0.0/guides/routing-and-navigation

  • 选项卡上的材质设计:

https://material.io/design/components/tabs.html#placement

  • 关于本机存储库内导航的部分:

https://github.com/jondot/awesome-react-native#navigation