React-和-TypeScript-学习指南-三-

79 阅读45分钟

React 和 TypeScript 学习指南(三)

原文:zh.annas-archive.org/md5/5da49be498b161721792aaa3c885dee9

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:风格化 React 前端的方法

在本章中,我们将使用四种不同的方法来对我们在前几章中工作的警报组件进行样式设计。首先,我们将使用纯 CSS 并了解这种方法的不利之处。然后,我们将转向使用 CSS 模块,这将解决纯 CSS 的主要问题。接下来,我们将使用一个名为 Emotion 的 CSS-in-JS 库和一个名为 Tailwind CSS 的库,并了解这些库各自的优点。

我们还将学习如何在 React 应用中使用 SVG 并在警报组件的信息和警告图标中使用它们。

我们将涵盖以下主题:

  • 使用纯 CSS

  • 使用 CSS 模块

  • 使用 CSS-in-JS

  • 使用 Tailwind CSS

  • 使用 SVG

技术要求

本章我们将使用以下技术:

本章中使用的所有代码片段都可以在以下网址找到:github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition/tree/main/Chapter5

使用纯 CSS

我们将从这个部分开始,通过设置一个包含警报组件的 React 和 TypeScript 项目来启动,该组件来自 第三章*,设置 React 和 TypeScript*。接下来,我们将添加来自 第三章 的警报组件,并使用纯 CSS 对其进行样式设计。最后,我们将探讨纯 CSS 的一些挑战,并了解我们如何减轻这些问题。

创建项目

我们将使用的是我们在 第三章 结尾时完成的项目。您可以在以下位置找到它:github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition/tree/main/Chapter3/Section2-Creating-a-project-with-Create-React-App/myapp。要本地复制此项目,请执行以下步骤:

  1. 在您选择的文件夹中打开 Visual Studio Code。

  2. 在终端中运行以下命令以克隆本书的 GitHub 仓库:

    git clone https://github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition.git
    
  3. Learn-React-with-TypeScript-2nd-Edition\Chapter3\Section2-Creating-a-project-with-Create-React-App\myapp 子文件夹中重新打开 Visual Studio Code。这包含在 第三章 结尾时的项目状态。

  4. 运行以下命令以安装所有依赖项:

    npm i
    

项目现在已设置好。接下来,我们将花一些时间了解如何在 React 组件中使用纯 CSS。

理解如何引用 CSS

Create React App 已经在项目中启用了纯 CSS 的使用。实际上,如果你查看 App.tsx 文件,它已经使用了纯 CSS:

...
import './App.css';
...
function App() {
  return (
    <div className="App">
      ...
    </div>
  );
}
...

App.css 文件中导入 CSS 样式,并在外部的 div 元素上引用 App CSS 类。

React 使用 className 属性而不是 class,因为 class 是 JavaScript 中的一个保留字。className 属性在转译过程中被转换为 class 属性。

CSS 导入语句是 webpack 的一个特性。当 webpack 处理所有文件时,它将包含所有导入的 CSS 到包中。

执行以下步骤以探索项目生成的 CSS 包:

  1. 首先,打开并查看 App.css 文件。正如我们之前所看到的,App.cssApp.tsx 文件中被使用。然而,它包含了一些不再使用的 CSS 类,例如 App-headerApp-logo。在我们添加警报组件时,这些类在 App 组件中被引用,然后我们移除了它们。保留这些多余的 CSS 类。

  2. 打开 index.tsx 文件,你会注意到导入了 index.css。然而,在这个文件中没有引用任何 CSS 类。如果你打开 index.css,你会注意到它只包含针对元素名称的 CSS 规则,而没有 CSS 类。

  3. 在终端中运行以下命令以生成生产构建:

    npm run build
    

几秒钟后,构建工件将出现在项目根目录下的 build 文件夹中。

  1. build 文件夹中打开 index.html 并注意所有空白都被移除了,因为它已经针对生产进行了优化。接下来,找到引用 CSS 文件的 link 元素,并记下路径 – 它将类似于 /static/css/main.073c9b0a.css

图 5.1 – index.html 中的链接元素

图 5.1 – index.html 中的链接元素

  1. 打开引用的 CSS 文件。所有空白都被移除了,因为它已经针对生产进行了优化。注意它包含来自 index.cssApp.css 的所有 CSS,包括多余的 App-headerApp-logo CSS 类。

图 5.2 – 包含多余的 App-header CSS 类的打包 CSS 文件

图 5.2 – 包含多余的 App-header CSS 类的打包 CSS 文件

这里关键点是 webpack 不会移除任何多余的 CSS – 它将包含所有已导入的 CSS 文件中的所有内容。

接下来,我们将使用纯 CSS 来样式化警报组件。

在警报组件中使用纯 CSS

现在我们已经了解了如何在 React 中使用纯 CSS,让我们来样式化警报组件。执行以下步骤:

  1. src 文件夹中添加一个名为 Alert.css 的 CSS 文件。这个文件可以在 GitHub 上找到:github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition/blob/main/Chapter5/Section1-Using-plain-CSS/app/src/Alert.css 以便复制。

  2. 我们将逐步添加 CSS 类,并理解每个类中的样式。首先,在 Alert.css 中添加一个 container 类:

    .container {
    
      display: inline-flex;
    
      flex-direction: column;
    
      text-align: left;
    
      padding: 10px 15px;
    
      border-radius: 4px;
    
      border: 1px solid transparent;
    
    }
    

这将在外部的 div 元素上使用。样式使用内联 flexbox,项目垂直流动并左对齐。我们还添加了一个漂亮的圆角边框以及在边框和子元素之间的少量填充。

  1. container 中添加以下额外的类,这些类可以在其中使用:

    .container.warning {
    
      color: #e7650f;
    
      background-color: #f3e8da;
    
    }
    
    .container.information {
    
      color: #118da0;
    
      background-color: #dcf1f3;
    
    }
    

我们将使用这些类为不同类型的 alert 添加适当的颜色。

  1. 为头部容器元素添加以下类:

    .header {
    
      display: flex;
    
      align-items: center;
    
      margin-bottom: 5px;
    
    }
    

这将应用于包含图标、标题和关闭按钮的元素。它使用一个水平流动的 flexbox,子元素垂直居中。它还在 alert 消息之前添加了一个小的间隙。

  1. 现在为图标添加以下类,使其宽度为 30 像素:

    .header-icon {
    
      width: 30px;
    
    }
    
  2. 接下来,添加以下类以应用于标题,使其加粗:

    .header-text {
    
      font-weight: bold;
    
    }
    
  3. 添加以下类以应用于关闭按钮:

    .close-button {
    
      border: none;
    
      background: transparent;
    
      margin-left: auto;
    
      cursor: pointer;
    
    }
    

这移除了边框和背景。它还将按钮对齐到标题的右侧,并给它一个指针鼠标光标。

  1. 为内容元素添加以下类:

    .content {
    
      margin-left: 30px;
    
      color: #000;
    
    }
    

这添加了一个左外边距,使消息水平与标题对齐,并将文本颜色设置为黑色。

这完成了所有的 CSS 类定义。

  1. 打开 Alert.tsx 并为刚刚创建的 CSS 文件添加一个导入语句:

    import './Alert.css';
    
  2. 现在我们将在 alert 组件的元素中引用我们刚刚创建的 CSS 类。在 alert JSX 中添加以下高亮的 CSS 类名引用来完成此操作:

    <div className={`container ${type}`}>
    
      <div className="header">
    
        <span
    
          ...
    
          className="header-icon"
    
        >
    
          {type === "warning" ? "⚠" : "ℹ"}
    
        </span>
    
        <span className="header-text">{heading}</span>
    
      </div>
    
      {closable && (
    
        <button
    
          ...
    
          className="close-button"
    
        >
    
          ...
    
        </button>
    
      )}
    
      <div className="content">{children}</div>
    
    </div>
    

现在 alert 组件中的元素正在通过导入的 CSS 文件中的 CSS 类进行样式化。

  1. 将关闭按钮移动到位于头部容器内部,在 header 元素下方:

    <div className={`container ${type}`}>
    
      <div className="header">
    
        ...
    
        <span className="header-text">{heading}</span>
    
        {closable && (
    
          <button
    
            aria-label="Close"
    
            onClick={handleCloseClick}
    
            className="close-button"
    
          >
    
            <span role="img" aria-label="Close">
    
            </span>
    
          </button>
    
        )}
    
      </div>
    
      <div className="content">{children}</div>
    
    </div>;
    
  2. 通过在终端中运行 npm start 启动应用在开发模式下。

几秒钟后,改进的 alert 组件将在浏览器中显示:

图 5.3 – 使用纯 CSS 样式的 alert 组件

图 5.3 – 使用纯 CSS 样式的 alert 组件

这完成了 alert 组件的样式,但让我们继续,以便我们可以观察到纯 CSS 的一个缺点。

经历 CSS 冲突

我们现在将看到一个 CSS 与不同组件冲突的例子。保持应用在开发模式下运行,然后按照以下步骤操作:

  1. 打开 App.tsx 并将 div 元素上引用的 CSS 类从 App 更改为 container

    <div className="container">
    
      <Alert ...>
    
        ...
    
      </Alert>
    
    </div>
    
  2. 打开 App.css 并将 App CSS 类重命名为 container,并为其添加 20px 的填充:

    .container {
    
      text-align: center;
    
      padding: 20px;
    
    }
    
  3. 现在,查看正在运行的应用程序,并注意警报不再在页面上水平居中。使用浏览器 DevTools 检查元素。如果你检查 App 组件中的 div 元素,你会看到来自警报组件中 container CSS 类的样式也应用到了它上面,以及我们刚刚添加的 container CSS 类。因此,text-align CSS 属性是 left 而不是 center

图 5.4 – CSS 类冲突

图 5.4 – CSS 类冲突

  1. 在继续之前,通过按 Ctrl + C 停止运行的应用程序。

这里关键点是,纯 CSS 类的作用域是整个应用程序,而不仅仅是导入的文件。这意味着如果 CSS 类有相同的名称,它们可能会发生冲突,正如我们刚才所经历的。

解决 CSS 冲突的一个方法是仔细命名,在 App 组件中使用 container 可以命名为 App__container,而在 Alert 组件中的 container 可以命名为 Alert__container。然而,这需要开发团队所有成员的自律。

注意

BEM 代表 Block(块)、Element(元素)、Modifier(修饰符),是 CSS 类名的一个流行命名约定。更多信息可以在以下链接中找到:css-tricks.com/bem-101/

这里是对本节内容的快速回顾:

  • Create React App 配置 webpack 以处理 CSS,以便 CSS 文件可以导入到 React 组件文件中

  • 导入的 CSS 文件中的所有样式都应用于应用程序 – 没有作用域或删除冗余样式

接下来,我们将学习一种不会在组件间出现 CSS 冲突的样式方法。

使用 CSS 模块

在本节中,我们将学习一种称为 CSS 模块 的 React 应用程序样式方法。我们将首先了解 CSS 模块,然后我们将使用它们在我们的警报组件中。

理解 CSS 模块

CSS modules 是一个开源库,可在 GitHub 上找到 github.com/css-modules/css-modules,它可以添加到 webpack 处理中,以方便 CSS 类名的自动作用域。

CSS 模块是一个 CSS 文件,就像在上一节中一样;然而,文件名有一个 .module.css 扩展名而不是 .css。这个特殊的扩展名允许 webpack 区分 CSS 模块文件和纯 CSS 文件,以便可以对其进行不同的处理。

CSS 模块文件可以按照以下方式导入到 React 组件文件中:

import styles from './styles.module.css';

这与导入纯 CSS 的语法类似,但定义了一个变量来保存 CSS 类名映射信息。在前面的代码片段中,CSS 类名信息被导入到一个名为 styles 的变量中,但变量名可以是任何我们选择的。

CSS 类名映射信息变量是一个包含与 CSS 类名对应的属性名的对象。每个类名属性包含一个用于 React 组件的作用域类名值。以下是将导入到名为 MyComponent 的组件中的映射对象的一个示例:

{
  container: "MyComponent_container__M7tzC",
  error: "MyComponent_error__vj8Oj"
}

作用域 CSS 类名以组件文件名开头,然后是原始 CSS 类名,接着是一个随机字符串。这种命名结构防止类名冲突。

CSS 模块中的样式在组件的 className 属性中如下引用:

<span className={styles.error}>A bad error</span>

元素上的 CSS 类名将解析为作用域类名。在上面的代码片段中,styles.error 将解析为 MyComponent_error__ vj8Oj。因此,运行中的应用中的样式将是作用域样式名称,而不是原始类名。

使用 Create React App 创建的项目已经安装并配置了 CSS 模块和 webpack。这意味着我们不需要安装 CSS 模块就可以在我们的项目中开始使用它们。

接下来,我们将在我们工作的警报组件中使用 CSS 模块。

在警报组件中使用 CSS 模块

现在我们已经理解了 CSS 模块,让我们在警报组件中使用它们。执行以下步骤:

  1. 首先将 Alert.css 重命名为 Alert.module.css;现在这个文件可以作为 CSS 模块使用。

  2. 打开 Alert.module.css 并将 CSS 类名更改为驼峰式而不是中划线式。这将使我们能够更容易地在组件中引用作用域 CSS 类名 – 例如,styles.headerText 而不是 styles["header-text"]。更改如下:

    .headerIcon {
    
      ...
    
    }
    
    .headerText {
    
      ...
    
    }
    
    .closeButton {
    
      ...
    
    }
    
  3. 现在,打开 Alert.tsx 并将 CSS 导入语句更改为如下导入 CSS 模块:

    import styles from './Alert.module.css';
    
  4. 在 JSX 中,更改类名引用以使用 CSS 模块的 scoped 名称:

    <div className={`${styles.container} ${styles[type]}`}>
    
      <div className={styles.header}>
    
        <span
    
          ...
    
          className={styles.headerIcon}
    
        >
    
          {type === "warning" ? "⚠" : "ℹ"}
    
        </span>
    
        {heading && (
    
          <span className={styles.headerText}>{heading}</        span>
    
        )}
    
        {closable && (
    
          <button
    
            ...
    
            className={styles.closeButton}
    
          >
    
            ...
    
          </button>
    
        )}
    
      </div>
    
      <div className={styles.content}>{children}</div>
    
    </div>
    
  5. 通过在终端中运行 npm start 来启动应用。

几秒钟后,样式化的警报将出现。这次警报将水平居中,这是样式不再冲突的标志。

  1. 使用浏览器的 DevTools 检查 DOM 中的元素。你会看到警报组件现在正在使用作用域 CSS 类名。这意味着警报容器样式不再与应用容器样式冲突。

图 5.5 – CSS 模块作用域的类名

图 5.5 – CSS 模块作用域的类名

  1. 在继续之前,通过按 Ctrl + C 停止运行中的应用。

  2. 为了完善我们对 CSS 模块的理解,让我们看看生产构建中的 CSS 会发生什么。然而,在我们这样做之前,让我们在 Alert.module.css 的底部添加一个冗余的 CSS 类:

    ...
    
    .content {
    
      margin-left: 30px;
    
      color: #000;
    
    }
    
    .redundant {
    
      color: red;
    
    }
    
  3. 现在,通过在终端中执行 npm run build 来创建生产构建。

几秒钟后,在 build 文件夹中创建构建工件。

  1. 打开捆绑的 CSS 文件,你会注意到以下要点:

    • 它包含来自 index.cssApp.css 和我们刚刚创建的 CSS 模块的所有 CSS。

    • CSS 模块中的类名具有作用域。这将确保生产环境中的样式不会冲突,就像开发模式中那样。

    • 它包含来自 CSS 模块的冗余 CSS 类名。

图 5.6 – 包含在 CSS 包中的冗余 CSS 类

图 5.6 – 包含在 CSS 包中的冗余 CSS 类

这样就完成了将警报组件重构为使用 CSS 模块的过程。

注意

更多关于 CSS 模块的信息,请访问 GitHub 仓库 github.com/css-modules/css-modules

这里是对我们关于 CSS 模块所学内容的回顾:

  • CSS 模块允许 CSS 类名自动作用域到 React 组件上。这防止了不同 React 组件的样式冲突。

  • CSS 模块不是浏览器标准功能;相反,它是一个开源库,可以添加到 webpack 流程中。

  • 在使用 Create React App 创建的项目中,CSS 模块是预安装和预配置的。

  • 与纯 CSS 类似,冗余的 CSS 类不会从生产 CSS 包中删除。

接下来,我们将学习另一种为 React 应用程序添加样式的途径。

使用 CSS-in-JS

在本节中,我们首先理解 CSS-in-JS 及其优点。然后,我们将重构我们使用的警报组件以实现 CSS-in-JS,并观察它与 CSS 模块的不同之处。

理解 CSS-in-JS

CSS-in-JS 不是浏览器功能,甚至不是一个特定的库 – 而是一种库类型。CSS-in-JS 库的流行例子有 styled-componentsEmotion。styled-components 和 Emotion 之间没有显著差异 – 它们都非常流行,并且具有相似的 API。我们将在本章中使用 Emotion。

情感生成的是具有作用域的样式,类似于 CSS 模块。然而,你是在 JavaScript 中而不是在 CSS 文件中编写 CSS,因此得名 CSS-in-JS。实际上,你可以直接在 JSX 元素上编写 CSS,如下所示:

<span
  css={css`
    font-weight: 700;
    font-size: 14;
  `}
>
  {text}
</span>

每个 CSS-in-JS 库的语法略有不同 – 以下示例是来自 Emotion 样式的代码片段。

将样式直接放在组件上允许开发者完全理解组件,而无需访问另一个文件。这显然会增加文件大小,可能会使代码更难阅读。然而,可以通过将子组件识别并从文件中提取出来来减轻大文件大小。或者,可以将样式从组件文件中提取到一个导入的 JavaScript 函数中。

CSS-in-JS 的一个巨大好处是你可以将逻辑混合到样式之中,这对于高度交互的应用程序非常有用。以下示例包含一个依赖于 important 属性的 font-weight 条件和依赖于 mobile 属性的 font-size 条件:

<span
  css={css`
    font-weight: ${important ? 700 : 400};
    font-size: ${mobile ? 15 : 14};
  `}
>
  {text}
</span>

使用 JavaScript 字符串插值来定义条件语句。

相当于纯 CSS 的示例可能类似于以下示例,为不同的条件创建单独的 CSS 类:

<span
  className={`${important ? "text-important" : ""} ${
    mobile ? "text-important" : ""
  }`}
>
  {text}
</span>

如果一个元素的样式高度条件化,那么 CSS-in-JS 可能更容易阅读,当然也更容易编写。

接下来,我们将在我们工作的警报组件中使用 Emotion。

在警报组件中使用情感

现在我们已经了解了 CSS-in-JS,让我们在警报组件中使用 Emotion。为此,执行以下步骤。所有使用的代码片段都可以在 github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition/blob/main/Chapter5/Section3-Using-CSS-in-JS/app/src/Alert.tsx 找到:

  1. Create React App 没有安装和设置 Emotion,因此我们首先需要安装 Emotion。在终端中运行以下命令:

    npm i @emotion/react
    

这将需要几秒钟的时间来安装。

  1. 打开 Alert.tsx 并删除 CSS 模块导入。

  2. 在文件顶部添加对 Emotion 的 css 属性的导入,并添加一个特殊注释:

    /** @jsxImportSource @emotion/react */
    
    import { css } from '@emotion/react';
    
    import { useState } from 'react';
    

这个特殊注释将 JSX 元素转换为使用 Emotion 的 jsx 函数进行转换,而不是使用 React 的 createElement 函数。Emotion 的 jsx 函数为包含 Emotion 的 css 属性的元素添加样式。

  1. 在 JSX 中,我们需要将所有的 className 属性替换为等效的 Emotion css 属性。样式基本上与我们之前创建的 CSS 文件中定义的相同,所以解释不会重复。

我们将一次样式化一个元素,从外部的 div 元素开始:

<div
  css={css`
    display: inline-flex;
    flex-direction: column;
    text-align: left;
    padding: 10px 15px;
    border-radius: 4px;
    border: 1px solid transparent;
    color: ${type === "warning" ? "#e7650f" : "#118da0"};
    background-color: ${type === "warning"
      ? "#f3e8da"
      : "#dcf1f3"};
  `}
>
  ...
</div>

在这个代码片段中有几个重要的点需要解释:

  • css 属性通常不在 JSX 元素上有效。文件顶部的特殊注释 (/** @jsxImportSource @emotion/react */) 允许这样做。

  • 在这种情况下,css 属性被设置为 css。有关标签模板字面量的更多信息,请参阅 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals

  • 标签模板字面量在运行时将样式转换为 CSS 类。我们将在 步骤 14 中验证这一点。

  • 使用字符串插值来实现颜色的条件样式。记住,我们不得不使用纯 CSS 或 CSS 模块定义三个 CSS 类。这个 CSS-in-JS 版本可能更易于阅读,当然也更简洁。

  1. 接下来,设置头部容器的样式:

    <div
    
      css={css`
    
        display: flex;
    
        align-items: center;
    
        margin-bottom: 5px;
    
      `}
    
    >
    
      <span role="img" ... > ... </span>
    
      <span ...>{heading}</span>
    
      {closable && ...}
    
    </div>
    
  2. 接下来,按照以下方式设置图标样式:

    <span
    
      role="img"
    
      aria-label={type === "warning" ? "Warning" :     "Information"}
    
      css={css`
    
        width: 30px;
    
      `}
    
    >
    
      {type === "warning" ? "⚠" : "ℹ"}
    
    </span>
    
  3. 然后,按照以下方式设置标题样式:

    <span
    
      css={css`
    
        font-weight: bold;
    
      `}
    
    >
    
      {heading}
    
    </span>
    
  4. 现在,按照以下方式设置关闭按钮样式:

    {closable && (
    
      <button
    
        ...
    
        css={css`
    
          border: none;
    
          background: transparent;
    
          margin-left: auto;
    
          cursor: pointer;
    
        `}
    
      >
    
        ...
    
      </button>
    
    )}
    
  5. 最后,按照以下方式设置消息容器样式:

    <div
    
      css={css`
    
        margin-left: 30px;
    
        color: #000;
    
      `}
    
    >
    
      {children}
    
    </div>
    
  6. 在终端中运行 npm start 来启动应用程序。警报组件将像之前一样出现。

  7. 使用浏览器的开发者工具检查 DOM 中的元素。警报组件使用范围 CSS 类名,类似于 CSS 模块:

图 5.7 – 情感的范围类名

图 5.7 – Emotion 的作用域类名

  1. 在继续之前,通过按Ctrl + C停止运行中的应用程序。

  2. 为了完善我们对 Emotion 的理解,让我们看看生产构建中的 CSS 会发生什么。首先,通过在终端中执行npm run build来创建生产构建。

几秒钟后,构建工件将在build文件夹中创建。

  1. 打开build/static/css文件夹中的捆绑 CSS 文件。注意,Emotion 样式不在其中。这是因为 Emotion 通过 JavaScript 在运行时生成样式,而不是在构建时。如果你这么想,样式不能在构建时生成,因为它们可能依赖于仅在运行时才知道值的 JavaScript 变量。

这完成了对警报组件的重构,以使用 CSS-in-JS。

注意

更多关于 emotion 的信息,请访问他们的网站emotion.sh/docs/introduction

这是关于 Emotion 和 CSS-in-JS 我们所学的总结:

  • CSS-in-JS 库的样式是在 JavaScript 中定义的,而不是在 CSS 文件中。

  • Emotion 的样式可以直接在 JSX 元素上使用css属性定义。

  • 一个巨大的好处是可以在样式上直接添加条件逻辑,这有助于我们更快地设置交互式组件的样式。

  • 由于 Emotion 的样式依赖于 JavaScript 变量,它们在运行时而不是在构建时应用,这允许优雅地定义条件样式逻辑,但也意味着会有轻微的性能损失,因为样式是在运行时创建和应用的。

接下来,我们将了解另一种不同的方法来设置 React 前端。

使用 Tailwind CSS

在本节中,我们将首先理解 Tailwind CSS 及其优势。然后,我们将重构我们一直在使用的警报组件,以使用 Tailwind,并观察它与我们所尝试的其他方法有何不同。

理解 Tailwind CSS

Tailwind 是一组预构建的 CSS 类,可用于设置应用程序的样式。它被称为实用优先 CSS 框架,因为预构建的类可以被视为灵活的实用工具。

以下是一个 CSS 类的示例:bg-white,它将元素的背景设置为白色 – bgbackground的缩写。另一个例子是bg-orange-500,它将背景颜色设置为橙色的 500 号色调。Tailwind 包含一个很棒的颜色调色板,可以进行自定义。

可以组合使用实用类来设置元素的样式。以下示例展示了如何在 JSX 中设置按钮元素的样式:

<button className="border-none rounded-md bg-emerald-700 text-white cursor-pointer">
  ...
</button>

下面是对前面示例中使用的类的解释:

  • border-none移除元素的边框。

  • rounded-md使元素的边框圆角。md代表medium。也可以使用lg(大型)或甚至full,以获得更圆的边框。

  • bg-emerald-700将元素的背景颜色设置为翡翠色的 700 号色调。

  • text-white将元素的文本颜色设置为白色。

  • cursor-pointer将元素的指针设置为指针。

实用类是低级别的,专注于样式化非常具体的东西。这使得类具有灵活性,允许它们高度可重用。

Tailwind 可以通过在类名前加上 hover: 来指定当元素处于悬停状态时应应用该类。以下示例在悬停时将按钮背景设置为更深的翡翠色:

<button className="md border-none rounded-md bg-emerald-700 text-white cursor-pointer hover:bg-emerald-800">
  ...
</button>

因此,Tailwind 的一个关键点是,我们不会为每个想要样式的元素编写新的 CSS 类 - 相反,我们使用大量经过深思熟虑的现有类。这种方法的优点是它有助于使应用程序看起来既美观又一致。

注意

关于 Tailwind 的更多信息,请参阅以下链接的网站:tailwindcss.com/。Tailwind 网站是搜索和理解所有可用实用类的一个关键资源。

接下来,我们将安装和配置 Tailwind,用于包含我们一直在工作的警报组件的项目。

安装和配置 Tailwind CSS

现在我们已经了解了 Tailwind,让我们在警报组件项目中安装和配置它。为此,执行以下步骤:

  1. 在 Visual Studio 项目中,首先通过在终端运行以下命令来安装 Tailwind:

    npm i -D tailwindcss
    

Tailwind 库作为开发依赖项安装,因为它在运行时不是必需的。

  1. Tailwind 通过使用名为 PostCSS 的库集成到 Create React App 项目中。PostCSS 是一个使用 JavaScript 转换 CSS 的工具,Tailwind 作为插件在其中运行。通过在终端运行以下命令来安装 PostCSS:

    npm i -D postcss
    
  2. Tailwind 还推荐另一个名为 Autoprefixer 的 PostCSS,它为 CSS 添加供应商前缀。通过在终端运行以下命令来安装它:

    npm i -D autoprefixer
    
  3. 接下来,在终端运行以下命令以生成 Tailwind 和 PostCSS 的配置文件:

    npx tailwindcss init -p
    

几秒钟后,将创建两个配置文件。Tailwind 配置文件名为 tailwind.config.js,PostCSS 配置文件名为 postcss.config.js

  1. 打开 tailwind.config.js 并指定 React 组件的路径如下:

    module.exports = {
    
      content: [
    
        './src/**/*.{js,jsx,ts,tsx}'
    
      ],
    
      theme: {
    
        extend: {},
    
      },
    
      plugins: [],
    
    }
    
  2. 现在,打开 src 文件夹中的 index.css 并在文件顶部添加以下三行:

    @tailwind base;
    
    @tailwind components;
    
    @tailwind utilities;
    

这些被称为 指令,将在构建过程中生成 Tailwind 所需的 CSS。

Tailwind 已安装并准备好使用。

接下来,我们将使用 Tailwind 为我们一直在工作的警报组件进行样式设计。

使用 Tailwind CSS

现在,让我们使用 Tailwind 为警报组件进行样式设计。我们将删除 emotion 的 css JSX 属性,并在 JSX 的 className 属性中使用 Tailwind 实用类名。为此,执行以下步骤:

  1. 打开 Alert.tsx 并从删除文件顶部的特殊 emotion 注释和 css 导入语句开始。

  2. 将最外层 div 元素的 css 属性替换为 className 属性,如下所示:

    <div
    
      className={`inline-flex flex-col text-left px-4 py-3     rounded-md border-1 border-transparent`}
    
    >
    
      ...
    
    </div>
    

这里是刚刚使用的工具类的解释:

  • inline-flexflex-col 创建一个垂直流动的内联弹性盒子

  • text-left 将项目对齐到左侧

  • px-4 添加了 4 个间距单位的左右填充

  • py-3 添加了顶部和底部 3 个间距单位的填充

  • 我们之前遇到过 rounded-md —— 这会使 div 元素的角落变得圆滑

  • border-1border-transparent 添加了一个透明的 1 像素边框

注意

间距单位在 Tailwind 中定义,是一个比例刻度。一个间距单位等于 0.25rem,大约等于 4px

  1. 仍然在最外层的 div 元素上,使用字符串插值添加以下条件样式:

    <div
    
      className={`inline-flex flex-col text-left px-4 py-3 rounded-md border-1 border-transparent ${
    
        type === 'warning' ? 'text-amber-900' : 'text-      teal-900'
    
      } ${type === 'warning' ? 'bg-amber-50' : 'bg-teal-    50'}`}
    
    >
    
      ...
    
    </div>
    

文本颜色设置为 900 琥珀色调用于警告警报,900 蓝绿色调用于信息警报。背景颜色设置为 50 琥珀色调用于警告警报,50 蓝绿色调用于信息警报。

  1. 接下来,将标题容器的 css 属性替换为 className 属性,如下所示:

    <div className="flex items-center mb-1">
    
      <span role="img" ... > ... </span>
    
      <span ... >{heading}</span>
    
      {closable && ...}
    
    </div>
    

这里是刚刚使用的工具类的解释:

  • flexitems-center 创建了一个水平流动的弹性盒子,其中项目垂直居中

  • mb-1 在元素的底部添加了 1 个间距单位边距

  1. 将图标上的 css 属性替换为 className 属性,如下所示:

    <span role="img" ... className="w-7">
    
      {type === 'warning' ? '⚠' : 'ℹ'}
    
    </span>
    

w-7 将元素宽度设置为 7 个间距单位。

  1. 将标题上的 css 属性替换为 className 属性,如下所示:

    <span className="font-bold">{heading}</span>
    

font-bold 将元素的字体重量设置为粗体。

  1. 将关闭按钮上的 css 属性替换为 className 属性,如下所示:

    {closable && (
    
      <button
    
        ...
    
        className="border-none bg-transparent ml-auto cursor-      pointer"
    
      >
    
        ...
    
      </button>
    
    )}
    

在这里,border-none 移除了元素边框,bg-transparent 使元素背景透明。ml-auto 将左边距设置为自动,使元素右对齐。cursor-pointer 将鼠标光标设置为指针。

  1. 最后,将消息容器的 css 属性替换为 className 属性,如下所示:

    <div className="ml-7 text-black">
    
      {children}
    
    </div>
    

ml-7 将元素左边缘设置为 7 个间距单位,text-black 将文本颜色设置为黑色。

  1. 通过在终端中运行 npm start 来运行应用程序。几秒钟后,应用程序将在浏览器中显示。

注意,由于 Tailwind 的默认颜色方案和一致的间距,警报组件看起来更美观。

  1. 使用浏览器的 DevTools 检查 DOM 中的元素。注意使用的 Tailwind 工具类,并注意间距单位使用 CSS rem 单位。

一个需要注意的关键点是,没有发生 CSS 类名作用域。不需要任何作用域,因为类是通用的和可重用的,而不是特定于任何元素。

图 5.8 – 使用 Tailwind 定制的警报

图 5.8 – 使用 Tailwind 定制的警报

  1. 在继续之前,通过按 Ctrl + C 停止运行应用程序。

  2. 为了结束我们对 Tailwind 的理解,让我们看看生产构建中的 CSS 会发生什么。首先,通过在终端中执行 npm run build 来创建一个生产构建。

几秒钟后,构建工件将在 build 文件夹中创建。

  1. build/static/css 文件夹打开打包的 CSS 文件。注意文件开头的基 Tailwind 样式。你还会看到我们使用的所有 Tailwind 类都包含在这个文件中。

图 5.9 – 打包的 CSS 文件中的 Tailwind CSS 类

图 5.9 – 打包的 CSS 文件中的 Tailwind CSS 类

注意

一个重要的观点是 Tailwind 不会添加所有它的 CSS 类——那样会产生一个巨大的 CSS 文件!相反,它只添加在应用中使用的 CSS 类。

这就完成了将警报组件重构为使用 Tailwind 的过程。

这里是对我们关于 Tailwind 学习的回顾:

  • Tailwind 是一组经过深思熟虑的可重用 CSS 类集合,可以应用于 React 元素。

  • Tailwind 有一个不错的默认调色板和 4 像素的间距刻度,这两者都可以自定义。

  • Tailwind 是一个 PostCSS 插件,在构建时执行。

  • 与 Emotion 不同,Tailwind 不会产生运行时性能惩罚,因为样式不是在运行时创建和应用的。

  • 只有在 React 元素上使用的类才包含在 CSS 构建包中。

接下来,我们将使警报组件中的图标看起来更美观。

使用 SVG

在本节中,我们将学习如何在 React 中使用 SVG 文件以及如何将它们用于警报组件的图标。

理解如何在 React 中使用 SVG

SVG 代表 可缩放矢量图形,它由基于数学公式的点、线、曲线和形状组成,而不是特定的像素。这使得它们在缩放时不会扭曲。图标的品质对于正确呈现非常重要——如果它们被扭曲,会使整个应用感觉不专业。在现代网络开发中,使用 SVG 为图标是很常见的。

Create React App 在创建项目时配置 webpack 使用 SVG 文件。实际上,logo.svg 在模板 App 组件中被引用,如下所示:

import logo from './logo.svg';
...
function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        ...
      </header>
    </div>
  );
}
export default App;

在前面的示例中,logo 被导入为 SVG 文件的路径,然后用于 img 元素的 src 属性以显示 SVG。

另一种引用 SVG 的方法是将其作为组件引用,如下所示:

import { ReactComponent as Logo } from './logo.svg';
function SomeComponent() {
  return (
    <div>
      <Logo />
    </div>
  );
}

SVG React 组件在名为 ReactComponent 的命名导入中可用。在前面的示例中,SVG 组件被别名 Logo,然后在 JSX 中使用。

接下来,我们将学习如何在警报组件中使用 SVG。

将 SVG 添加到警报组件

执行以下步骤以将警报组件中的表情符号图标替换为 SVG:

  1. 首先,在 src 文件夹中创建三个名为 cross.svginfo.svgwarning.svg 的文件。然后,从 GitHub 仓库 github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition/tree/main/Chapter5/Section5-Using-SVGs/app/src 复制并粘贴这些文件的内容。

  2. 打开 Alert.tsx 并添加以下导入语句以将 SVG 作为 React 组件导入:

    import { ReactComponent as CrossIcon } from './cross.svg';
    
    import { ReactComponent as InfoIcon } from './info.svg';
    
    import { ReactComponent as WarningIcon } from './warning.svg';
    

我们已经为 SVG 组件赋予了适当的别名。

  1. 更新包含表情符号图标的 span 元素,以使用以下 SVG 图标组件:

    <span
    
      role="img"
    
      aria-label={type === 'warning' ? 'Warning' :     'Information'}
    
      className="inline-block w-7"
    
    >
    
      {type === 'warning ' ? (
    
        <WarningIcon className="fill-amber-900 w-5 h-5" />
    
      ) : (
    
        <InfoIcon className="fill-teal-900 w-5 h-5" />
    
      )}
    
    </span>;
    

我们已经使用 Tailwind 适当地调整了图标的大小和颜色。

  1. 接下来,更新表情符号关闭图标为以下 SVG 关闭图标:

    <button
    
      aria-label="Close"
    
      onClick={handleCloseClick}
    
      className="border-none bg-transparent ml-auto cursor-    pointer"
    
    >
    
      <CrossIcon />
    
    </button>
    
  2. 通过在终端中运行 npm start 来运行应用程序。几秒钟后,应用程序将以包含改进的警报组件的浏览器形式出现:

图 5.10 – 带有 SVG 图标的警报

图 5.10 – 带有 SVG 图标的警报

这就完成了警报组件——它现在看起来好多了。

下面是关于在 React 应用中使用 SVG 的快速回顾:

  • Webpack 需要配置以打包 SVG 文件,Create React App 为我们做了这个配置。

  • SVG 文件的默认导入是 SVG 的路径,然后可以在 img 元素中使用。

  • 可以使用名为 ReactComponent 的命名导入来在 JSX 中将 SVG 引用为 React 组件。

接下来,我们将总结本章所学的内容。

摘要

在本章中,我们学习了四种样式化方法。

首先,我们了解到纯 CSS 可以用来样式化 React 应用,但所有导入的 CSS 文件中的样式都会被打包,无论是否使用了某个样式。此外,样式并不是作用域到特定的组件——我们观察到 container CSS 类名与 AppAlert 组件冲突。

接下来,我们学习了关于 CSS 模块的内容,它允许我们以作用域到组件的方式导入纯 CSS 文件。我们了解到 CSS 模块是一个开源库,在用 Create React App 创建的项目中预先安装和配置。我们看到这解决了 CSS 冲突问题,但没有移除冗余样式。

然后,我们讨论了 CSS-in-JS 库,它允许在 React 组件上直接定义样式。我们使用 emotion 的 css 属性来样式化警报组件,而不需要外部 CSS 文件。这种方法的优点是,条件样式逻辑可以更快地实现。我们了解到 emotion 的样式作用域类似于 CSS 模块,但作用域是在运行时而不是在构建时发生的。我们还了解到,这种方法的微小性能成本是因为样式是在运行时创建的。

我们探讨的第四种样式方法是使用 Tailwind CSS。我们了解到 Tailwind 提供了一组可重用的 CSS 类,可以应用于 React 元素,包括一个漂亮的默认调色板和 4 px 的间距刻度,这两者都可以自定义。我们还了解到,只有使用的 Tailwind 类会被包含在生产构建中。

最后,我们了解到 Create React App 配置了 webpack 以启用 SVG 文件的使用。SVG 可以作为 img 元素中的路径引用,或者作为名为 import 的 React 组件使用。

在下一章中,我们将探讨使用名为 React Router 的流行库在 React 应用中实现多页面的方法。

问题

回答以下问题以检查您对 React 样式的了解:

  1. 为什么以下使用纯 CSS 可能会有问题?

    <div className="wrapper"></div>
    
  2. 我们有一个使用 CSS 模块样式化的组件,如下所示:

    import styles from './styles1.module.css';
    
    function ComponentOne() {
    
      return <div className={styles.wrapper}></div>;
    
    }
    

我们还有一个使用 CSS 模块样式化的组件,如下所示:

import styles from './styles2.module.css';
function ComponentTwo() {
  return <div className={styles.wrapper}></div>;
}

由于它们都使用wrapper类名,这些div元素的样式是否会冲突?

  1. 我们有一个使用 CSS 模块样式化的组件,如下所示:

    import styles from './styles3.module.css';
    
    function ComponentThree() {
    
      return <div className={styles.wrapper}>
    
    </div>
    
    }
    

styles3.module.css中的样式如下:

.wrap {
  display: flex;
  align-items: center;
  background: #e7650f;
}

当应用运行时,样式没有被应用。问题是什么?

  1. 我们正在定义一个具有kind属性的可用按钮组件,该属性可以是"square""rounded"。圆形按钮应该有 4px 的边框半径,而方形按钮应该没有边框半径。我们如何使用 Emotion 的css属性定义这种条件样式?

  2. 我们正在使用 Tailwind 对按钮元素进行样式化。它目前被样式化为以下这样:

    <button className="bg-blue-500 text-white font-bold py-2 px-4 rounded">
    
      Button
    
    </button>
    

我们如何通过将按钮背景设置为用户悬停时的 700 度蓝色来增强样式?

  1. 如下引用一个 logo SVG:

    import Logo from './logo.svg';
    
    function LogoComponent() {
    
      return <Logo />;
    
    }
    

然而,logo 没有被渲染。问题是什么?

  1. 我们正在使用 Tailwind 对具有color属性以确定其颜色的按钮元素进行样式化,该属性如下所示:

    <button className={`bg-${color}-500 text-white font-bold py-2 px-4 rounded`}>
    
      Button
    
    </button>
    

然而,按钮颜色不起作用。问题是什么?

答案

  1. 包装器 CSS 类可能会与其他类冲突。为了降低这种风险,可以将类名手动范围限制到组件中:

    <div className="card-wrapper"></div>
    
  2. CSS 不会冲突,因为 CSS 模块会将类名范围限制在每个组件中。

  3. 组件中引用了错误的类名 - 它应该是wrap而不是wrapper

    import styles from './styles3.module.css';
    
    function ComponentThree() {
    
      return <div className={styles.wrap}>
    
    </div>
    
    }
    
  4. 按钮上的css属性可以是以下这样:

    <button
    
      css={css`
    
        border-radius: ${kind === "rounded" ? "4px" : "0px"};
    
      `}
    
    >
    
      ...
    
    </button>
    
  5. 样式可以调整如下以包括悬停样式:

    <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
    
      ...
    
    </button
    
  6. Logo将持有 SVG 的路径而不是组件。导入语句可以调整如下以导入一个 logo 组件:

    import { ReactComponent as Logo } from './logo.svg';
    
    function LogoComponent() {
    
      return <Logo />;
    
    }
    
  7. bg-${color}-500类名有问题,因为这只能在运行时解决,因为存在color变量。使用的 Tailwind 类在构建时确定并添加到包中,这意味着相关的背景颜色类不会打包。这意味着背景颜色样式不会应用到按钮上。

第六章:使用 React Router 进行路由

在本章中,我们将构建一个简单的应用程序,实现以下页面:

  • 欢迎用户的首页

  • 列出所有产品的产品列表页面

  • 一个提供特定产品详细信息的产品页面

  • 专为特权用户设计的管理员页面

这一切都将使用名为 React Router 的库来管理。

通过这种方式,我们将学习如何从产品列表到产品页面实现静态链接,并在产品页面上实现产品 ID 的路由参数。我们还将了解在应用程序的搜索功能中关于表单导航和查询参数的内容。

最后,本章将介绍如何懒加载页面代码以提高性能。

因此,在本章中,我们将涵盖以下主题:

  • 介绍 React Router

  • 声明路由

  • 创建导航

  • 使用嵌套路由

  • 使用路由参数

  • 创建错误页面

  • 使用索引路由

  • 使用搜索参数

  • 编程导航

  • 使用表单导航

  • 实现懒加载

技术要求

在本章中,我们将使用以下技术:

本章中所有的代码片段都可以在网上找到,地址为 github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition/tree/main/Chapter6

介绍 React Router

在本节中,我们在了解 React Router 是什么以及如何安装它之前,首先创建一个新的 React 项目用于应用程序。

创建项目

我们将使用 Visual Studio Code 在本地开发应用程序,这需要一个基于 Create React App 的新项目设置。我们已经多次介绍过这一点,所以在本章中我们将不介绍步骤——相反,请参阅 第三章设置 React 和 TypeScript。创建一个具有您选择的名称的应用程序项目。

我们将使用 Tailwind CSS 来设计应用程序。我们已经在 第五章前端设计方法 中介绍了如何安装和配置 Tailwind,因此您创建了 React 和 TypeScript 项目后,请安装并配置 Tailwind。

理解 React Router

如其名所示,React Router 是 React 应用程序的路由库。路由器负责选择在应用程序中显示的内容。例如,当请求 /products/6 路径时,React Router 负责确定要渲染哪些组件。对于包含多个页面的任何应用程序,路由器都是必不可少的,并且 React Router 已经是许多年 React 的流行路由库。

安装 React Router

React Router 包含在一个名为 react-router-dom 的包中。使用以下终端命令在项目中安装它:

npm i react-router-dom

TypeScript 类型包含在 react-router-dom 中,因此不需要单独安装。

接下来,我们将在应用中创建一个页面并声明一个显示该页面的路由。

声明路由

我们将从这个部分开始创建一个列出应用产品的页面组件。然后我们将学习如何使用 React Router 的 createBrowserRouter 函数创建路由器并声明路由。

创建产品列表页面

产品列表页面组件将包含应用中所有 React 工具的列表。按照以下步骤创建:

  1. 我们将首先创建页面的数据源。首先,在 src 文件夹中创建一个名为 data 的文件夹,然后在 data 文件夹中创建一个名为 products.ts 的文件。

  2. 将以下内容添加到 products.ts 中(您可以从 GitHub 仓库 github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition/blob/main/Chapter6/src/data/products.ts 复制并粘贴):

    export type Product = {
    
      id: number,
    
      name: string,
    
      description: string,
    
      price: number,
    
    };
    
    export const products: Product[] = [
    
      {
    
        description:
    
          'A collection of navigational components that         compose declaratively with your app',
    
        id: 1,
    
        name: 'React Router',
    
        price: 8,
    
      },
    
      {
    
        description: 'A library that helps manage state       across your app',
    
        id: 2,
    
        name: 'React Redux',
    
        price: 12,
    
      },
    
      {
    
        description: 'A library that helps you implement       robust forms',
    
        id: 3,
    
        name: 'React Hook Form',
    
        price: 9,
    
      },
    
      {
    
        description: 'A library that helps you interact with       a REST API',
    
        id: 4,
    
        name: 'React Apollo',
    
        price: 10,
    
      },
    
      {
    
        description: 'A library that provides utility CSS       classes',
    
        id: 5,
    
        name: 'Tailwind CSS',
    
        price: 7,
    
      },
    
    ];
    

这是一个包含应用中所有 React 工具的 JavaScript 数组列表。

注意

通常,这类数据位于某个服务器上,但这超出了本章的范围。我们将在 第九章与 RESTful API 交互 中详细介绍如何与服务器数据交互,包括如何使用 React Router 高效地完成此操作。

  1. 现在我们将创建产品列表页面组件。首先,在 src 文件夹中创建一个名为 pages 的文件夹,用于存放所有页面组件。接下来,在 pages 文件夹中创建一个名为 ProductsPage.tsx 的文件,用于产品列表页面组件。

  2. 将以下 import 语句添加到 ProductsPage.tsx 中以导入我们刚刚创建的产品:

    import { products } from '../data/products';
    
  3. 接下来,开始创建 ProductsPage 组件,输出页面的标题:

    export function ProductsPage() {
    
      return (
    
        <div className="text-center p-5">
    
          <h2 className="text-xl font-bold text-slate-600">
    
            Here are some great tools for React
    
          </h2>
    
        </div>
    
      );
    
    }
    

这使用 Tailwind 类使标题变大、加粗、灰色并水平居中。

  1. 接下来,在 JSX 中添加产品列表:

    <div className="text-center p-5 text-xl">
    
      <h2 className="text-base text-slate-600">
    
        Here are some great tools for React
    
      </h2>
    
      <ul className="list-none m-0 p-0">
    
        {products.map((product) => (
    
          <li key={product.id} className="p-1 text-base text-        slate-800">
    
            {product.name}
    
          </li>
    
        ))}
    
      </ul>
    
    </div>
    

Tailwind 类从无序列表元素中移除了项目符号、边距和填充,并将列表项设置为灰色。

注意,我们使用产品数组 map 函数遍历每个产品并返回一个 li 元素。使用 Array.map 是 JSX 循环逻辑的常见做法。

注意列表项元素上的 key 属性。React 需要在循环中的元素上使用此属性以有效地更新相应的 DOM 元素。key 属性的值必须在数组中是唯一的且稳定的,所以我们使用了产品 ID。

目前这完成了产品页面的创建。这个页面在应用中还没有显示,因为它不是其组件树的一部分——我们需要使用 React Router 声明它,我们将在下一步中这样做。

理解 React Router 的路由器

React Router 中的路由是一个跟踪浏览器 URL 并执行导航的组件。React Router 中有几个路由器可用,推荐用于 Web 应用程序的是名为 createBrowserRouter 的函数,它创建一个浏览器路由器。

createBrowserRouter 需要一个包含应用程序中所有 路由 的参数。一个路由包含一个路径和当应用程序的浏览器地址匹配该路径时要渲染的组件。以下代码片段创建了一个具有两个路由的路由器:

const router = createBrowserRouter([
  {
    path: 'some-page',
    element: <SomePage />,
  },
  {
    path: 'another-page',
    element: <AnotherPage />,
  }
]);

当路径是 /some-page 时,将渲染 SomePage 组件。当路径是 /another-page 时,将渲染 AnotherPage 组件。

createBrowserRouter 返回的路由器被传递给一个 RouterProvider 组件,并放置在 React 组件树的较高位置,如下所示:

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

现在我们开始理解 React Router 的路由器,我们将在我们的项目中使用它。

声明产品路由

我们将在应用中使用 createBrowserRouterRouterProvider 声明产品列表页面。执行以下步骤:

  1. 我们将创建自己的组件来包含所有的路由定义。在 src 文件夹中创建一个名为 Routes.tsx 的文件,包含以下 import 语句:

    import {
    
      createBrowserRouter,
    
      RouterProvider,
    
    } from 'react-router-dom';
    
    import { ProductsPage } from './pages/ProductsPage';
    

我们已从 React Router 中导入了 createBrowserRouterRouterProvider。我们还导入了 ProductsPage,我们将在下一个 products 路由中渲染它。

  1. import 语句下方添加以下组件,以定义具有 products 路由的路由器:

    const router = createBrowserRouter([
    
      {
    
        path: 'products',
    
        element: <ProductsPage />,
    
      },
    
    ]);
    

因此,当路径是 /products 时,将渲染 ProductsPage 组件。

  1. 仍然在 Routes.tsx 中,在路由器下创建一个名为 Routes 的组件,如下所示:

    export function Routes() {
    
      return <RouterProvider router={router} />;
    
    }
    

此组件将 RouterProvider 包装起来,并将路由传递给它。

  1. 打开 index.tsx 文件,在其他的 import 语句下方添加我们刚刚创建的 Routes 组件的 import 语句:

    import { Routes } from './Routes';
    
  2. Routes 而不是 App 作为顶级组件渲染,如下所示:

    root.render(
    
      <React.StrictMode>
    
        <Routes />
    
      </React.StrictMode>
    
    );
    

这使得我们定义的 products 路由成为组件树的一部分。这意味着当路径是 /products 时,产品列表页面将在应用中渲染。

  1. 删除对 App 组件的 import 语句,因为目前不需要它。

  2. 使用 npm start 运行应用。

出现一个错误屏幕,解释说当前路由未找到:

图 6.1 – React Router 的标准错误页面

图 6.1 – React Router 的标准错误页面

错误页面来自 React Router。正如错误消息所建议的,我们可以提供自己的错误屏幕,我们将在本章的后面做到这一点。

  1. 将浏览器 URL 更改为 http://localhost:3000/products

你将看到产品列表页面组件按以下方式渲染:

图 6.2 – 产品列表页面

图 6.2 – 产品列表页面

这确认了 products 路由运行良好。在我们回顾并进入下一节之前,保持应用运行。

在本节中,我们回顾一下我们学到了什么:

  • 在 Web 应用程序中,React Router 使用 createBrowserRouter 定义路由。

  • 每个路由都有一个路径和一个组件,当浏览器的 URL 与该路径匹配时,将渲染该组件。

  • createBrowserRouter 返回的路由器被传递给一个 RouterProvider 组件,该组件应放置在组件树的高层。

关于 createBrowserRouter 的更多信息,请参阅 React Router 文档中的以下链接:reactrouter.com/en/main/routers/create-browser-router。关于 RouterProvider 的更多信息,请参阅 React Router 文档中的以下链接:reactrouter.com/en/main/routers/router-provider

接下来,我们将学习关于可以执行导航的 React Router 组件。

创建导航

React Router 附带名为 LinkNavLink 的组件,它们提供导航。在本节中,我们将在应用程序顶部创建一个包含 React Router 的 Link 组件的导航栏。然后我们将用 NavLink 组件替换 Link,并了解两个组件之间的区别。

使用 Link 组件

执行以下步骤以创建包含 React Router 的 Link 组件的应用程序头部:

  1. 首先,在 src 文件夹中创建一个名为 Header.tsx 的应用程序头部文件,包含以下 import 语句:

    import { Link } from 'react-router-dom';
    
    import logo from './logo.svg';
    

我们已从 React Router 中导入了 Link 组件。

我们还导入了 React 标志,因为我们将在应用程序头部包含导航选项。

  1. 创建 Header 组件如下所示:

    export function Header() {
    
      return (
    
        <header className="text-center text-slate-50       bg-slate-900 h-40 p-5">
    
          <img
    
            src={logo}
    
            alt="Logo"
    
            className="inline-block h-20"
    
          />
    
          <h1 className="text-2xl">React Tools</h1>
    
          <nav></nav>
    
        </header>
    
      );
    
    }
    

该组件包含一个包含 React 标志、应用程序标题和一个空 nav 元素的 header 元素。我们使用了 Tailwind 类来使头部灰色,并将标志和标题水平居中。

  1. 现在,在 nav 元素内部创建一个链接:

    <nav>
    
      <Link
    
        to="products"
    
        className="text-white no-underline p-1"
    
      >
    
        Products
    
      </Link>
    
    </nav>
    

Link 组件有一个 to 属性,它定义了要导航到的路径。要显示的文本可以在 Link 内容中指定。

  1. 打开 Routes.tsx 并为刚刚创建的 Header 组件添加一个导入语句:

    import { Header } from './Header';
    
  2. router 定义中,添加一个渲染 Header 组件的根路径,如下所示:

    const router = createBrowserRouter([
    
      {
    
        path: '/',
    
        element: <Header />,
    
      },
    
      {
    
        path: 'products',
    
        element: <ProductsPage />,
    
      },
    
    ]);
    

我们刚刚所做的不太理想,因为 Header 组件需要在所有路由上显示,而不仅仅是根路由。然而,它将允许我们探索 React Router 的 Link 组件。我们将在 使用嵌套 路由 部分中整理这个问题。

  1. 在运行中的应用程序中,将浏览器地址更改为应用程序的根目录。新的应用程序头部出现,包含 产品 链接:

图 6.3 – 应用程序头部

图 6.3 – 应用程序头部

  1. 现在,使用浏览器的 DevTools 检查应用程序头部元素:

图 6.4 – 头部组件检查

图 6.4 – 头部组件检查

我们可以看到 Link 组件被渲染为一个 HTML 锚元素。

  1. 在 DevTools 中选择 网络 选项卡并清除任何显示的现有请求。点击应用头部中的 产品 链接。浏览器将导航到产品列表页面。

注意,没有为产品列表页面发起网络请求。这是因为 React Router 使用客户端导航覆盖了锚元素的默认行为:

图 6.5 – 客户端导航

图 6.5 – 客户端导航

最后,请注意,在产品列表页面上应用头部消失了,这不是我们想要的效果。我们将在 使用嵌套 路由 部分解决这个问题。

在我们进入下一个部分之前,保持应用运行。

导航工作得很好,但如果在产品列表页面活动时,产品链接有不同的样式会更好。我们将在下一个改进中实现这一点。

使用 NavLink 组件

React Router 的 NavLink 类似于 Link 元素,但允许它在活动时以不同的方式样式化。这对于导航栏来说非常方便。

执行以下步骤以在应用头部将 Link 替换为 NavLink

  1. 打开 Header.tsx 并将 Link 引用更改为 NavLink

    import { NavLink } from 'react-router-dom';
    
    ...
    
    export function Header() {
    
      return (
    
        <header ...>
    
          ...
    
          <nav>
    
            <NavLink
    
              to="products"
    
              className="..."
    
            >
    
              Products
    
            </NavLink>
    
          </nav>
    
        </header>
    
      );
    
    }
    

目前,应用头部看起来和表现完全相同。

  1. NavLink 组件上的 className 属性接受一个函数,可以根据页面是否处于活动状态有条件地样式化它。将 className 属性更新为以下内容:

    <NavLink
    
      to="products"
    
      className={({ isActive }) =>
    
        `text-white no-underline p-1 pb-0.5 border-solid        border-b-2 ${
    
          isActive ? "border-white" : "border-transparent"
    
        }`
    
      }
    
    >
    
      Products
    
    </NavLink>
    

该函数接受一个参数 isActive,用于定义链接的页面是否处于活动状态。如果链接处于活动状态,我们已为其添加了底部边框。

我们目前还看不到这个更改的影响,因为 产品 链接还没有出现在产品列表页面上。我们将在下一个部分解决这个问题。

这样就完成了应用头部以及我们对 NavLink 组件的探索。

总结一下,NavLink 在我们想要突出显示活动链接时非常适合用于主应用导航,而 Link 则非常适合我们应用中的其他所有链接。

有关 Link 组件的更多信息,请参阅以下链接:reactrouter.com/en/main/components/link。有关 NavLink 组件的更多信息,请参阅以下链接:reactrouter.com/en/main/components/nav-link

接下来,我们将学习嵌套路由。

使用嵌套路由

在本节中,我们将介绍 嵌套路由 以及它们有用的场景,然后在我们的应用中使用嵌套路由。嵌套路由还将解决我们在前几节中遇到的应用头部消失的问题。

理解嵌套路由

嵌套路由允许路由的一部分渲染组件。例如,以下模拟通常使用嵌套路由实现:

图 6.6 – 嵌套路由的使用案例

图 6.6 – 嵌套路由的使用案例

模拟显示有关客户的信息。路径确定活动标签页 – 在模拟中,/customers/1234/history

一个Customer组件可以渲染这个屏幕的壳体,包括客户的姓名、图片和标签页标题。渲染标签页内容的组件可以与Customer组件解耦,并与路径耦合。

这个特性被称为嵌套路由,因为Route组件嵌套在彼此内部。以下是模拟路由的示例:

const router = createBrowserRouter([
  {
    path: 'customer/:id',
    element: <Customer />,
    children: [
      {
        path: 'profile',
        element: <CustomerProfile />,
      },
      {
        path: 'history',
        element: <CustomerHistory />,
      },
      {
        path: 'tasks',
        element: <CustomerTasks />,
      },
    ],
  },
]);

这种定义路由的嵌套方法使得它们易于阅读和理解,正如您在前面的代码片段中所看到的。

嵌套路由的一个关键部分是子组件在父组件中的渲染位置。在前面的代码片段中,CustomerProfile组件将在Customer组件中渲染在哪里?解决方案是 React Router 的Outlet组件。以下是从模拟中Customer组件的Outlet组件示例:

export function Customer() {
  ...
  return (
    <div>
      <Name ... />
      <Picture ... />
      <nav>
        <NavLink to="profile" ... >Profile</NavLink>
        <NavLink to="history" ... >History</NavLink>
        <NavLink to="tasks" ... >Tasks</NavLink>
      </nav>
      <Outlet />
    </div>
  );
}

因此,在这个例子中,CustomerProfile组件将在Customer组件的导航选项之后渲染。请注意,Customer组件与嵌套内容解耦。这意味着可以在不更改Customer组件的情况下向客户页面添加新标签页。这是嵌套路由的另一个好处。

接下来,我们将在我们的应用中使用嵌套路由。

在应用中使用嵌套路由

在我们的应用中,我们将使用App组件作为应用的壳体,它渲染根路径。然后我们将产品列表页面嵌套在这个组件中:

  1. 打开App.tsx,将所有现有内容替换为以下内容:

    import { Outlet } from 'react-router-dom';
    
    import { Header } from './Header';
    
    export default function App() {
    
      return (
    
        <>
    
          <Header />
    
          <Outlet />
    
        </>
    
      );
    
    }
    

该组件渲染应用头部,并在其下方渲染嵌套内容。

注意

空的 JSX 元素<></>React 片段。React 片段不会添加到 DOM 中,并且作为 React 组件只能返回单个元素的解决方案,因此它们是在 React 组件中返回多个元素的一种方式,同时保持 React 的愉悦。

  1. 打开Routes.tsx,导入我们刚刚修改的App组件,并移除对Header组件的import

    import {
    
      createBrowserRouter,
    
      RouterProvider,
    
    } from 'react-router-dom';
    
    import { ProductsPage } from './pages/ProductsPage5';
    
    import App from './App';
    
  2. 更新router定义如下:

    const router = createBrowserRouter([
    
      {
    
        path: '/',
    
        element: <App />,
    
        children: [
    
          {
    
            path: 'products',
    
            element: <ProductsPage />,
    
          }
    
        ]
    
      }
    
    ]);
    

产品列表页面现在嵌套在App组件内部。

  1. 如果您回到运行中的应用,您将看到应用头部现在出现在产品列表页面上。您还会看到下划线的产品链接,因为它是一个活动链接:

图 6.7 – 产品列表页面的应用头部

图 6.7 – 产品列表页面的应用头部

总结一下,嵌套路由允许为不同的路径段渲染组件。Outlet组件用于在父组件内渲染嵌套内容。

更多关于Outlet组件的信息,请参阅以下链接:reactrouter.com/en/main/components/outlet

接下来,我们将学习路由参数。

使用路由参数

在本节中,我们将了解路由参数及其在应用中使用路由参数之前如何有用。

理解路由参数

路由参数是路径中的一个可变段。变量段的值对组件可用,以便它们可以条件性地渲染某些内容。

在以下路径中,1234 是一个客户的 ID:/customers/1234/

这可以如下定义为一个路由参数:

{ path: '/customer/:id', element: <Customer /> }

一个冒号 (:) 后跟一个名称定义了一个路由参数。选择一个有意义的参数名称取决于我们,所以路径中的 :id 段是前面路由中的路由参数定义。

可以在路径中使用多个路由参数,如下所示:

{
  path: '/customer/:customerId/tasks/:taskId',
  element: <CustomerTask />,
}

路由参数名称显然必须在路径中是唯一的。

路由参数通过 React Router 的 useParams 钩子对组件可用。以下代码片段是一个示例,说明了如何获取 customerIdtaskId 路由参数的值:

const params = useParams<Params>();
console.log('Customer id', params.customerId);
console.log('Task id', params.taskId);

从代码片段中我们可以看到,useParams 有一个泛型参数,它定义了参数的类型。前面代码片段的 type 定义如下:

type Params = {
  customerId: string;
  taskId: string;
};

需要注意的是,路由参数的值始终是字符串,因为它们是从路径中提取的,而路径是字符串。

现在我们已经了解了路由参数,我们将在我们的应用中使用一个路由参数。

在应用中使用路由参数

我们将在我们的应用中添加一个产品页面来显示每个产品的描述和价格。页面的路径将包含一个用于产品 ID 的路由参数。执行以下步骤以实现产品页面:

  1. 我们将首先创建产品页面。在 src/pages 文件夹中,创建一个名为 ProductPage.tsx 的文件,并包含以下 import 语句:

    import { useParams } from 'react-router-dom';
    
    import { products } from '../data/products';
    

我们已从 React Router 中导入了 useParams 钩子,这将允许我们获取 id 路由参数的值——即产品的 ID。我们还导入了 products 数组。

  1. 按照以下方式开始创建 ProductPage 组件:

    type Params = {
    
      id: string;
    
    };
    
    export function ProductPage() {
    
      const params = useParams<Params>();
    
      const id =
    
        params.id === undefined ? undefined :       parseInt(params.id);
    
    }
    

我们使用 useParams 钩子获取 id 路由参数,如果它有值,则将其转换为整数。

  1. 现在,添加一个变量,将其分配给具有路由参数中 ID 的产品:

    export function ProductPage() {
    
      const params = useParams<Params>();
    
      const id =
    
        params.id === undefined ? undefined :       parseInt(params.id);
    
      const product = products.find(
    
        (product) => product.id === id
    
      );
    
    }
    
  2. 在 JSX 中从 product 变量返回产品信息:

    export function ProductPage() {
    
      ...
    
      return (
    
        <div className="text-center p-5 text-xl">
    
          {product === undefined ? (
    
            <h1 className="text-xl text-slate-900">
    
              Unknown product
    
            </h1>
    
          ) : (
    
            <>
    
              <h1 className="text-xl text-slate-900">
    
                {product.name}
    
              </h1>
    
              <p className="text-base text-slate-800">
    
                {product.description}
    
              </p>
    
              <p className="text-base text-slate-800">
    
                {new Intl.NumberFormat('en-US', {
    
                  currency: 'USD',
    
                  style: 'currency',
    
                }).format(product.price)}
    
              </p>
    
            </>
    
          )}
    
        </div>
    
      );
    
    }
    

如果找不到产品,将返回 Unknown product。如果找到产品,将返回其名称、描述和价格。我们使用 JavaScript 的 Intl.NumberFormat 函数来格式化价格。

这样就完成了产品页面的创建。

  1. 下一个任务是添加产品页面的路由。打开 Routes.tsx 并为产品页面添加一个 import 语句:

    import { ProductPage } from './pages/ProductPage';
    
  2. 为产品页面添加以下突出显示的路由:

    const router = createBrowserRouter([
    
      {
    
        path: '/',
    
        element: <App />,
    
        children: [
    
          {
    
            path: 'products',
    
            element: <ProductsPage />,
    
          },
    
          {
    
            path: 'products/:id',
    
            element: <ProductPage />,
    
          }
    
        ]
    
      }
    
    ]);
    

因此,/products/2 路径应该返回一个 React Redux 的产品页面。

  1. 在运行的应用中,将浏览器 URL 更改为 localhost:3000/products/2。React Redux 产品应该显示出来:

图 6.8 – 产品页面

图 6.8 – 产品页面

  1. 本节的最后一个任务是将在产品列表页面上的产品列表转换为打开相关产品页面的链接。打开 ProductsPage.tsx 并从 React Router 中导入 Link 组件:

    import { Link } from 'react-router-dom';
    
  2. 在 JSX 中的产品名称周围添加一个 Link 组件:

    <ul className="list-none m-0 p-0">
    
      {products.map((product) => (
    
        <li key={product.id}>
    
          <Link
    
            to={`${product.id}`}
    
            className="p-1 text-base text-slate-800           hover:underline"
    
          >
    
            {product.name}
    
          </Link>
    
        </li>
    
      ))}
    
    </ul>
    

链接路径相对于组件的路径。鉴于组件路径是 /products,我们将链接路径设置为产品 ID,它应该与 product 路由匹配。

  1. 返回正在运行的应用程序并转到产品列表页面。将鼠标悬停在产品上,你现在会看到它们是链接:

图 6.9 – 产品列表链接

图 6.9 – 产品列表链接

  1. 点击其中一个产品,将显示相关产品页面。

这完成了关于路由参数的这一部分。以下是一个快速回顾:

  • 路由参数是在路径中定义的可变段,使用冒号后跟参数名称表示

  • 可以使用 React Router 的 useParams 钩子访问路由参数

关于 useParams 钩子的更多信息,请参阅 React Router 文档中的以下链接:reactrouter.com/en/main/hooks/use-params

记得我们在声明路由部分遇到的 React Router 的错误页面吗?接下来,我们将学习如何自定义该错误页面。

创建错误页面

在本节中,我们将了解 React Router 中的错误页面是如何工作的,然后再在我们的应用程序中实现一个。

理解错误页面

目前,当发生错误时,会显示一个 React Router 内置的错误页面。我们可以通过在运行的应用程序中输入一个无效路径来检查这一点:

图 6.10 – 标准的 React Router 错误页面

图 6.10 – 标准的 React Router 错误页面

由于在路由器中找不到匹配的路由,因此引发了一个错误。错误页面上的 404 未找到 消息证实了这一点。

这个标准的错误页面并不理想,因为信息是针对开发者而不是真实用户。此外,应用头部没有显示,因此用户无法轻松导航到确实存在的页面。

正如错误消息所暗示的,可以在路由上使用 errorElement 属性来覆盖标准错误页面。以下是一个为客户的路由定义的自定义错误页面的示例;如果此路由上发生任何错误,将渲染 CustomersErrorPage 组件:

const router = createBrowserRouter([
  ...,
  {
    path: 'customers',
    element: <CustomersPage />,
    errorElement: <CustomersErrorPage />
  },
  ...
]);

现在我们已经开始了解 React Router 中的错误页面,我们将在我们的应用程序中实现一个。

添加错误页面

执行以下步骤以在应用程序中创建一个错误页面:

  1. 首先,在 src/pages 文件夹中创建一个名为 ErrorPage.tsx 的新页面,内容如下:

    import { Header } from '../Header';
    
    export function ErrorPage() {
    
      return (
    
        <>
    
          <Header />
    
          <div className="text-center p-5 text-xl">
    
            <h1 className="text-xl text-slate-900">
    
              Sorry, an error has occurred
    
            </h1>
    
          </div>
    
        </>
    
      );
    
    }
    

该组件简单地返回应用头部,并在下面显示一个 抱歉,发生了错误 的消息。

  1. 打开 Routes.tsx 并为错误页面添加一个 import 语句:

    import { ErrorPage } from './pages/ErrorPage';
    
  2. 按如下方式在根路由上指定错误页面:

    const router = createBrowserRouter([
    
      {
    
        path: '/',
    
        element: <App />,
    
        errorElement: <ErrorPage />,
    
        children: ...
    
      },
    
    ]);
    

在根路由上指定错误页面意味着如果有任何路由有错误,它将会显示。

  1. 切换回运行中的应用,并将浏览器 URL 更改为 localhost:3000/invalid。将显示错误页面:

图 6.11 – 错误页面

图 6.11 – 错误页面

  1. 这是一个好的开始,但我们可以通过提供用户更多从 React Router 的 useRouteError 钩子中获取的信息来改进它。再次打开 ErrorPage.tsx 并添加 useRouteErrorimport 语句:

    import { useRouteError } from 'react-router-dom';
    
  2. 在组件的 return 语句之前使用 useRouteError 将错误分配给 error 变量:

    export function ErrorPage() {
    
      const error = useRouteError();
    
      return ...
    
    }
    
  3. error 变量是 unknown 类型 – 你可以通过悬停在其上验证这一点。我们可以使用类型谓词函数来允许 TypeScript 将其缩小到我们可以处理的内容。在组件下方添加以下类型谓词函数:

    function isError(error: any): error is { statusText: string } {
    
      return "statusText" in error;
    
    }
    

该函数检查错误对象是否有 statusText 属性,如果有,则给它赋予具有此属性的类型。

  1. 我们现在可以使用这个函数来渲染 statusText 属性中的信息:

    return (
    
      <>
    
        <Header />
    
        <div className="text-center p-5 text-xl">
    
          <h1 className="text-xl text-slate-900">
    
            Sorry, an error has occurred
    
          </h1>
    
          {isError(error) && (
    
            <p className="text-base text-slate-700">
    
              {error.statusText}
    
            </p>
    
          )}
    
        </div>
    
      </>
    
    );
    
  2. 在运行中的应用中,错误信息以无效路径的形式显示在错误页面上:

图 6.12 – 包含错误信息的错误页面

图 6.12 – 包含错误信息的错误页面

这就完成了关于错误页面的本节内容。关键点是使用路由上的 errorElement 属性来捕获和显示错误。可以通过 useRouteError 钩子获取特定的错误信息。

更多关于 errorElement 的信息,请参阅以下链接:reactrouter.com/en/main/route/error-element。更多关于 useRouteError 钩子的信息,请参阅以下链接:reactrouter.com/en/main/hooks/use-route-error

接下来,我们将学习关于索引路由的内容。

使用索引路由

目前,应用的根路径除了标题外不显示任何内容。在本节中,我们将学习索引路由,以便在根路径上显示一个友好的欢迎信息。

理解索引路由

一个 index 布尔属性,如下例所示:

{
  path: "/",
  element: <App />,
  children: [
    {
      index: true,
      element: <HomePage />,
    },
    ...,
  ]
}

接下来,我们将使用索引路由在我们的应用中添加一个首页。

在应用中使用索引路由

执行以下步骤,在我们的应用中使用索引路由添加一个首页:

  1. src/pages 文件夹中创建一个名为 HomePage.tsx 的新文件,内容如下:

    export function HomePage() {
    
      return (
    
        <div className="text-center p-5 text-xl">
    
          <h1 className="text-xl text-slate-900">Welcome to         React Tools!</h1>
    
        </div>
    
      );
    
    }
    

页面显示一个欢迎信息。

  1. 打开 Routes.tsx 并导入我们刚刚创建的首页:

    import { HomePage } from './pages/HomePage';
    
  2. 按如下方式将首页作为根路径的索引页面添加:

    const router = createBrowserRouter([
    
      {
    
        path: '/',
    
        element: <App />,
    
        errorElement: <ErrorPage />,
    
        children: [
    
          {
    
            index: true,
    
            element: <HomePage />,
    
          },
    
          ...
    
        ]
    
      }
    
    ]);
    
  3. 我们将在标题中添加到标志和应用程序标题的链接,以便跳转到首页。打开 Header.tsx 并从 React Router 导入 Link 组件:

    import { NavLink, Link } from 'react-router-dom';
    
  4. 按如下方式将根页面的链接包裹在标志和标题周围:

    <header ...>
    
      <Link to="">
    
        <img src={logo} ... />
    
      </Link>
    
      <Link to="">
    
        <h1 ...>React Tools</h1>
    
      </Link>
    
      <nav>
    
        ...
    
      </nav>
    
    </header>
    
  5. 在运行的应用程序中,点击应用程序标题将转到根页面,您将看到显示的欢迎信息:

图 6.13 – 欢迎页面

图 6.13 – 欢迎页面

这完成了关于索引路由本节的介绍。

回顾一下,索引路由是一个默认子路由,它使用一个 index 布尔属性定义。

更多关于索引路由的信息,请参阅以下链接:reactrouter.com/en/main/route/route#index

接下来,我们将学习搜索参数。

使用搜索参数

在本节中,我们将学习 React Router 中的搜索参数,并使用它们在应用程序中实现搜索功能。

理解搜索参数

? 字符和 & 字符分隔。搜索参数有时被称为 typewhen,它们是搜索参数:https://somewhere.com/?type=sometype&when=recent

React Router 有一个钩子,它返回用于获取和设置搜索参数的函数,称为 useSearchParams

const [searchParams, setSearchParams] = useSearchParams();

searchParams 是一个 JavaScript 的 URLSearchParams 对象。在 URLSearchParams 上有一个 get 方法,可以用来获取搜索参数的值。以下示例获取了一个名为 type 的搜索参数的值:

const type = searchParams.get('type');

setSearchParams 是一个用于设置搜索参数值的函数。函数参数是一个对象,如下例所示:

setSearchParams({ type: 'sometype', when: 'recent' });

接下来,我们将向我们的应用程序添加搜索功能。

向应用程序添加搜索功能

我们将在应用程序的页眉中添加一个搜索框。提交搜索将用户带到产品列表页面,并列出符合搜索条件的产品集合。执行以下步骤:

  1. 打开 Header.tsx 文件,并将 useSearchParams 添加到 React Router 的导入中。同时,添加一个从 React 导入 FormEvent 类型的 import 语句:

    import { FormEvent } from 'react';
    
    import {
    
      NavLink,
    
      Link,
    
      useSearchParams
    
    } from 'react-router-dom';
    
  2. 使用 useSearchParams 钩子解构函数以在 return 语句之前获取和设置搜索参数:

    export function Header() {
    
      const [searchParams, setSearchParams] = useSearchParams();
    
      return ...
    
    }
    
  3. 在标志上方添加以下搜索表单:

    <header ...>
    
      <form
    
        className="relative text-right"
    
        onSubmit={handleSearchSubmit}
    
      >
    
        <input
    
          type="search"
    
          name="search"
    
          placeholder="Search"
    
          defaultValue={searchParams.get('search') ?? ''}
    
          className="absolute right-0 top-0 rounded py-2 px-3         text-gray-700"
    
        />
    
      </form>
    
      <Link to="">
    
        <img src={logo} ... />
    
      </Link>
    
      ...
    
    </header>
    

表单包含一个搜索框,其默认值是 search 参数的值。searchParams.get 如果参数不存在,则返回 null,因此在这种情况下使用 ?? 将搜索框的默认值设置为空字符串。

注意

?? 运算符如果左操作数是 nullundefined,则返回右操作数;否则,返回左操作数。更多信息,请参阅以下链接:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing

表单提交会调用一个 handleSearchSubmit 函数,我们将在下一步实现它。

  1. return 语句上方添加一个 handleSearchSubmit 函数,如下所示:

    export function Header() {
    
      const [searchParams, setSearchParams] =     useSearchParams();
    
      function handleSearchSubmit(e:     FormEvent<HTMLFormElement>) {
    
        e.preventDefault();
    
        const formData = new FormData(e.currentTarget);
    
        const search = formData.get('search') as string;
    
        setSearchParams({ search });
    
      }
    
      return ...
    
    }
    

提交处理程序参数使用 FormEvent 类型进行类型化。FormEvent 是一个泛型类型,它接受元素的类型,对于表单提交处理程序,这个类型是 HTMLFormElement

我们在提交处理器的参数上使用 preventDefault 方法来防止表单被提交到服务器,因为我们在这个函数中处理所有逻辑。

我们使用 JavaScript FormData 接口获取搜索字段的值。然后,我们使用类型断言将搜索字段值的类型设置为字符串。

提交处理器的最后一行代码设置了搜索参数的值。这将更新浏览器的 URL 以包含此搜索参数。

注意

我们将在 第七章 与表单一起工作 中学习更多关于 React 中表单的知识。

  1. 现在,我们需要根据搜索参数的值过滤产品列表。打开 ProductsPage.tsx 并将 useSearchParams 添加到 import 语句中:

    import { Link, useSearchParams } from 'react-router-dom';
    
  2. ProductsPage 组件的顶部,按照以下方式从 useSearchParams 中解构 searchParams

    export function ProductsPage() {
    
      const [searchParams] = useSearchParams();
    
      return ...
    
    }
    
  3. return 语句之前添加以下函数以通过搜索值过滤产品列表:

    const [searchParams] = useSearchParams();
    
    function getFilteredProducts() {
    
      const search = searchParams.get('search');
    
      if (search === null || search === "") {
    
        return products;
    
      } else {
    
        return products.filter(
    
          (product) =>
    
            product.name
    
              .toLowerCase()
    
              .indexOf(search.toLowerCase()) > -1
    
        );
    
      }
    
    }
    
    return ...
    

函数首先获取 search 参数的值。如果没有搜索参数或值为空字符串,则返回完整的产品列表。否则,使用数组的 filter 函数过滤产品列表,检查搜索值是否包含在产品名称中,不考虑大小写。

  1. 在 JSX 中使用我们刚刚创建的函数来输出过滤后的产品。将 products 的引用替换为对 getFilteredProducts 的调用,如下所示:

    <ul className="list-none m-0 p-0">
    
      {getFilteredProducts().map((product) => (
    
        <li
    
          key={product.id}
    
          className="p-1 text-base text-slate-800"
    
        >
    
          <Link
    
            to={`${product.id}`}
    
            className="p-1 text-base text-slate-800           hover:underline"
    
          >
    
            {product.name}
    
          </Link>
    
        </li>
    
      ))}
    
    </ul>
    
  2. 在运行中的应用程序中,当在主页上时,在搜索框中输入一些搜索条件,然后按 Enter 键提交搜索。

搜索参数已添加到浏览器中的 URL。然而,它并没有导航到产品列表页面。不用担心这个问题,因为我们在下一节中会解决这个问题:

图 6.14 – 添加到 URL 中的搜索参数

图 6.14 – 添加到 URL 中的搜索参数

本节的关键点是,React Router 的 useSearchParams 钩子允许你设置和获取 URL 搜索参数。这些参数也以 JavaScript URLSearchParams 对象的结构进行组织。

关于 useSearchParams 钩子的更多信息,请参阅 React Router 文档中的以下链接:reactrouter.com/en/main/hooks/use-search-params。有关 URLSearchParams 的更多信息,请参阅 developer.mozilla.org/en-US/docs/Web/API/URLSearchParams

接下来,我们将探索另一个允许程序化导航的 React Router 钩子。

程序化导航

React Router 的 LinkNavLink 组件允许声明式导航。然而,有时我们必须强制导航 – 实际上,这对于我们应用程序中的搜索功能导航到产品列表页面非常有用。在本节中,我们将学习如何使用 React Router 进行编程式导航,并使用它来完成应用程序的搜索功能。执行以下步骤:

  1. 打开 Header.tsx 并从 React Router 中导入 useNavigate 钩子:

    import {
    
      NavLink,
    
      Link,
    
      useSearchParams,
    
      useNavigate
    
    } from 'react-router-dom';
    

useNavigate 钩子返回一个我们可以用来执行编程式导航的函数。

  1. 在调用 useSearchParams 钩子之后调用 useNavigate。将结果分配给名为 navigate 的变量:

    export function Header() {
    
      const [searchParams, setSearchParams] =     useSearchParams();
    
      const navigate = useNavigate();
    
      ...
    
    }
    

navigate 变量是一个可以用于导航的函数。它接受一个参数,用于指定要导航到的路径。

  1. handleSearchSubmit 中,将 setSearchParams 调用替换为 navigate 调用,以便使用相关搜索参数跳转到产品列表页面:

    function handleSearchSubmit(e: FormEvent<HTMLFormElement>) {
    
      e.preventDefault();
    
      const formData = new FormData(e.currentTarget);
    
      const search = formData.get('search') as string;
    
      navigate(`/products/?search=${search}`);
    
    }
    
  2. 我们不再需要 setSearchParams,因为搜索参数的设置已包含在导航路径中,因此从 useSearchParams 调用中删除此部分:

    const [searchParams] = useSearchParams();
    
  3. 在运行的应用程序中,在搜索框中输入一些搜索条件,然后按 Enter 键提交搜索。

搜索参数用于跳转到产品列表页面。当产品列表页面出现时,将显示正确筛选的产品:

图 6.15 – 带筛选产品的产品列表页面

图 6.15 – 带筛选产品的产品列表页面

因此,编程式导航是通过使用 useNavigate 钩子实现的。这返回一个函数,可以导航到传递给它的路径。

关于 useNavigate 钩子的更多信息,请参阅 React Router 文档中的以下链接:reactrouter.com/en/main/hooks/use-navigate

接下来,我们将重构搜索表单的导航,以使用 React Router 的 Form 组件。

使用表单导航

在本节中,我们将使用 React Router 的 Form 组件在提交搜索条件时导航到产品列表页面。Form 是 HTML form 元素的包装器,它处理客户端的表单提交。这将取代 useNavigate 的使用并简化代码。执行以下步骤:

  1. Header.tsx 中,首先从 React Router 的 import 中删除 useNavigate,并用 Form 组件替换它:

    import {
    
      NavLink,
    
      Link,
    
      useSearchParams,
    
      Form
    
    } from 'react-router-dom';
    
  2. 在 JSX 中,将 form 元素替换为 React Router 的 Form 组件:

    <Form
    
      className="relative text-right"
    
      onSubmit={handleSearchSubmit}
    
    >
    
      <input ... />
    
    </Form>
    
  3. 在 JSX 中的 Form 元素中,删除 onSubmit 处理程序。用以下 action 属性替换,以便将表单发送到 products 路由:

    <Form
    
      className="relative text-right"
    
      action="/products"
    
    >
    
      ...
    
    </Form>
    

React Router 的表单提交模仿了原生 form 元素提交到服务器路径的方式。然而,React Router 将表单提交到客户端路由。此外,Form 默认模仿 HTTP GET 请求,因此 URL 将自动添加 search 参数。

  1. 剩余的任务是删除以下代码:

    • 删除 React 导入语句,因为FormEvent现在是多余的

    • 删除对useNavigate的调用,因为现在不再需要它了

    • 删除handleSearchSubmit函数,因为现在不再需要它了

  2. 在运行中的应用中,在搜索框中输入一些搜索条件,然后按Enter键提交搜索。这将像之前一样工作。

这大大简化了代码!

我们将在第七章第九章中学习更多关于 React Router 的Form组件。本节的关键要点是Form包装了 HTML 的form元素,并在客户端处理表单提交。

关于Form的更多信息,请参阅 React Router 文档中的以下链接:reactrouter.com/en/main/components/form

接下来,我们将学习一种可以应用于应用中大型页面的性能优化类型。

实现懒加载

目前,我们应用中的所有 JavaScript 代码都是在应用首次加载时一起加载的。这在大型应用中可能会出现问题。在本节中,我们将学习如何在组件的路由变为活动状态时才加载其 JavaScript 代码。这种模式通常被称为懒加载。在我们的应用中,我们将创建一个懒加载的管理员页面。

理解 React 懒加载

默认情况下,所有 React 组件都会被打包在一起,并在应用首次加载时一起加载。这对大型应用来说效率不高——尤其是当用户不使用很多组件时。懒加载 React 组件解决了这个问题,因为懒加载组件不包括在初始加载的包中;相反,它们的 JavaScript 代码在渲染时才会被获取和加载。

懒加载 React 组件主要有两个步骤。首先,组件必须按照以下方式动态导入:

const LazyPage = lazy(() => import('./LazyPage'));

在代码块中,lazy是 React 中的一个函数,它使得导入的组件可以懒加载。请注意,懒加载的页面必须是默认导出——懒加载不适用于命名导出。

Webpack 可以将LazyPage的 JavaScript 代码分割成单独的包。请注意,这个单独的包将包括LazyPage的任何子组件。

第二步是在 React 的Suspense组件内部按照以下方式渲染懒加载组件:

<Route
  path="lazy"
  element={
    <Suspense fallback={<div>Loading…</div>}>
      <LazyPage />
    </Suspense>
  }
/>

Suspense组件的fallback属性可以被设置为在懒加载页面正在获取时渲染的元素。

接下来,我们将在我们的应用中创建一个懒加载管理员页面。

将懒加载管理员页面添加到应用中

执行以下步骤以将懒加载管理员页面添加到我们的应用中:

  1. src/pages文件夹中创建一个名为AdminPage.tsx的文件,并包含以下内容:

    export default function AdminPage() {
    
      return (
    
        <div classNa"e="text-center p-5 text"xl">
    
          <h1 classNa"e="text-xl text-slate-"00">Admin         Panel</h1>
    
          <p classNa"e="text-base text-slate-"00">
    
            You shou'dn't come here often becaus' I'm lazy
    
          </p>
    
        </div>
    
      );
    
    }
    

该页面非常小,因此它不是懒加载的一个很好的用例。然而,它的简单性将使我们能够专注于如何实现懒加载。

  1. 打开Routes.tsx并从 React 中导入lazySuspense

    import { lazy, Suspense } fr'm 're'ct';
    
  2. 按如下方式导入管理员页面(这一点很重要,即它必须出现在所有其他import语句之后):

    const AdminPage = lazy(() => impo't('./pages/AdminP'ge'));
    
  3. 按如下方式添加admin路由:

    const router = createBrowserRouter([
    
      {
    
        pat':''/',
    
        element: <App />,
    
        errorElement: <ErrorPage />,
    
        children: [
    
          ...,
    
          {
    
            pat': 'ad'in',
    
            element: (
    
              <Suspense
    
                fallback={
    
                  <div classNa"e="text-center p-5 text-xl                 text-slate-"00">
    
                    Loading...
    
                  </div>
    
                }
    
              >
    
                <AdminPage />
    
              </Suspense>
    
            )
    
          }
    
        ]
    
      }
    
    ]);
    

管理员页面的路径是/admin。当管理员页面的 JavaScript 被获取时,将渲染一个加载指示器。

  1. 打开Header.tsx并在Products链接之后添加管理员页面的链接,如下所示:

    <nav>
    
      <NavLink ... >
    
        Products
    
      </NavLink>
    
      <NavLink
    
        "o="ad"in"
    
        className={({ isActive }) =>
    
          `text-white no-underline p-1 pb-0.5 border-solid          border-b-2 ${
    
            isActive"? "border-wh"te"": "border-transpar"nt"
    
          }`
    
        }
    
      >
    
        Admin
    
      </NavLink>
    
    </nav>
    
  2. 在跑步应用中,打开浏览器开发者工具并转到网络标签,清除任何现有请求。通过从 限制菜单中选择慢 3G来降低连接速度:

图 6.16 – 设置慢速连接

图 6.16 – 设置慢速连接

  1. 现在,点击页眉中的管理员链接。加载指示器出现,因为管理员页面的 JavaScript 正在下载:

图 6.17 – 加载指示器

图 6.17 – 加载指示器

在管理员页面下载完成后,它将在浏览器中渲染。如果你查看 DevTools 中的网络标签,你会看到管理员页面包正在懒加载的确认信息:

图 6.18 – 管理员页面下载

图 6.18 – 管理员页面下载

这完成了关于懒加载 React 组件的部分。总之,通过动态导入组件文件并在Suspense组件内渲染组件来实现 React 组件的懒加载。

关于懒加载 React 组件的更多信息,请参阅 React 文档中的以下链接:reactjs.org/docs/code-splitting.html

这也完成了本章的内容。接下来,我们将回顾我们关于 React Router 所学的知识。

摘要

React Router 为我们提供了一套全面的组件和钩子,用于管理我们应用中页面之间的导航。我们使用createBrowserRouter来定义我们所有 Web 应用的路线。一个路由包含一个路径和一个组件,当路径与浏览器 URL 匹配时,将渲染该组件。我们使用errorElement属性为路由渲染一个自定义错误页面。

我们使用嵌套路由允许App组件渲染应用外壳和其中的页面组件。我们在App组件内部使用 React Router 的Outlet组件来渲染页面内容。我们还使用根路由上的索引路由来渲染欢迎信息。

我们使用 React Router 的NavLink组件来渲染导航链接,当它们的路由处于活动状态时会被突出显示。Link组件非常适合具有静态样式要求的其他链接 – 我们将其用于产品列表上的产品链接。我们使用 React Router 的Form组件在提交搜索表单时导航到产品列表页面。

路由参数和查询参数允许将参数传递到组件中,以便它们可以渲染动态内容。useParams提供了访问路由参数的权限,而useSearchParams提供了访问查询参数的权限。

React 组件可以懒加载以提高启动性能。这是通过动态导入组件文件并在 Suspense 组件内部渲染组件来实现的。

在下一章中,我们将学习所有关于 React 中的表单知识。

问题

让我们通过以下问题来测试我们对 React Router 的知识:

  1. 我们在应用中声明了以下路由:

    const router = createBrowserRouter([
    
      {
    
        path: "/",
    
        element: <App />,
    
        errorElement: <ErrorPage />,
    
        children: [
    
          { path: "customers", element: <CustomersPage /> }
    
        ]
    
      }
    
    ]);
    

当路径是 /customers 时,哪个组件将会渲染?

当路径是 /products 时,哪个组件将会渲染?

  1. 在一个可以处理 /customers/37 路径的路由中,路径会是什么?37 是一个客户 ID,可能会变化。

  2. 一个 settings 页面的路由如下定义:

    {
    
      path: "/settings",
    
      element: <SettingsPage />,
    
      children: [
    
        { path: "general", element: <GeneralSettingsTab /> },
    
        { path: "dangerous", element: <DangerousSettingsTab       /> }
    
      ]
    
    }
    

设置页面有 /settings/general/settings/dangerous,分别。然而,当请求这些路径时,设置页面没有显示任何标签内容——那么,我们在 SettingsPage 组件中可能遗漏了什么?

  1. 我们正在实现一个应用中的导航栏。当点击导航项时,应用应该导航到相关页面。我们应该使用哪个 React Router 组件来渲染导航项?Link 还是 NavLink

  2. 路由如下定义:

    { path: "/user/:userId", element: <UserPage /> }
    

UserPage 组件内部,以下代码用于从浏览器 URL 获取用户 id 信息:

const params = useParams<{id: string}>();
const id = params.id;

然而,id 总是 undefined。问题是什么?

  1. 以下 URL 包含一个在 customers 页面上的搜索参数示例:

/``customers/?search=cool company

然而,以下实现中出现了错误:

function getFilteredCustomers() {
  const criteria = useSearchParams.get('search');
  if (criteria === null || criteria === "") {
    return customers;
  } else {
    return customers.filter(
      (customer) =>
        customer.name.toLowerCase().indexOf(criteria.          toLowerCase()) > -1
    );
  }
}

问题是什么?

  1. 一个 React 组件如下所示进行懒加载:

    const SpecialPage = lazy(() => import('./pages/SpecialPage'));
    
    const router = createBrowserRouter([
    
      ...,
    
      {
    
        path: '/special',
    
        element: <SpecialPage />,
    
      },
    
      ...
    
    ]);
    

然而,React 抛出了一个错误。问题是什么?

答案

  1. 当路径是 /customers 时,CustomersPage 将会渲染。

当路径是 /products 时,ErrorPage 将会渲染。

  1. 路径可以是 path="customers/:customerId"

  2. 很可能是因为 Outlet 组件没有被添加到 SettingsPage 中。

  3. 这两个都可以工作,但 NavLink 更好,因为它允许在活动状态下对项目进行样式化。

  4. 引用的路由参数应该是 userId

    const params = useParams<{userId: string}>();
    
    const id = params.userId;
    
  5. 钩子必须在函数组件的最顶层调用。此外,useSearchParams 钩子没有直接的 get 方法。以下是修正后的代码:

    const [searchParams] = useSearchParams();
    
    function getFilteredCustomers() {
    
      const criteria = searchParams.get('search');
    
      ...
    
    }
    
  6. 懒加载的组件必须嵌套在 Suspense 组件内部,如下所示:

    {
    
      path: '/special',
    
      element: (
    
        <Suspense fallback={<Loading />}>
    
          <SpecialPage />
    
        </Suspense>
    
      )
    
    }