React-企业级实践指南-全-

React 企业级实践指南(全)

原文:Practical Enterprise React

协议:CC BY-NC-SA 4.0

一、在 React 中领先

本章将简要概述一些关键的 React 概念,包括将 React 作为前端开发工具的优势和潜在好处。

第一 React

React 是一个开源 JavaScript 库,用于创建交互式用户界面(ui)和前端应用。它在世界各地不断增长的用户数量和强大的社区可能证明了这样一个事实,即 React 正在实现其存在的理由,开发用户界面,特别是更快的和交互式的用户界面。

您“声明”您希望您的 UI 看起来如何以及它应该如何表现,React 将按照您的指示并按照您的描述在您的浏览器中呈现它。

脸书于 2011 年创建了 React,并于 2013 年开源。从技术上讲,React 是一个库,但是由于它的行为和功能,许多用户称它为框架。也许,描述或比较 React 行为的一种方式是将其视为流行的架构模式模型-视图-控制器(MVC)中的视图。

基于组件的体系结构

React 应用的核心是由组件组成的。可重用组件更准确地说。它是用户界面的一部分,每个元素都有其特定的逻辑。

React 的模块化特性允许独立开发应用的功能,并在项目内外重用。例如,在图 1-1 中,我们可以将网页分解成各种组件,如导航条、英雄部分、页脚等。

img/506956_1_En_1_Fig1_HTML.jpg

图 1-1

典型网站截图。来源: www.reactjs.org

一个导航栏组件,如图 1-2 所示,包含页面标题和导航元素。它通常位于屏幕的顶部。

img/506956_1_En_1_Fig2_HTML.jpg

图 1-2

导航栏组件的一个示例

如图 1-3 所示,hero section 组件包含图像,通常是大图像,旨在从页面的视野中脱颖而出并吸引注意力。

img/506956_1_En_1_Fig3_HTML.jpg

图 1-3

英雄部分组件的示例

因此,如果你仔细观察,React 组件是独立的、自包含的可重用代码块,我们可以将它们放在一起创建复杂的用户界面。

是的,React 应用由许多可重用的组件组成。在任何编程语言中,组件都可以被认为是简单的函数。每个 React 应用的根都是一个被恰当地称为根组件的元素,它将整个应用(包括它的所有子组件)结合在一起。

文档对象模型(DOM)

学习 React 时另一个值得理解的概念是它的虚拟文档对象模型,简称为虚拟 DOM。

简单地说,虚拟 DOM 仅仅是真实 DOM 的一种表示。

数据或状态的改变首先在虚拟 DOM 上完成。在计算出变化之后,真正的 DOM 就被更新了。结果呢?整体性能更快,用户体验更好。React 仅在状态元素发生变化时重新渲染组件及其子组件。

实质上,虚拟 DOM 的工作方式如下:

  • 如果数据发生变化,虚拟 DOM 中的整个 UI 都会重新呈现。

  • 重新渲染会创建一个具有相应更改的新虚拟 DOM。

  • 接下来,对新旧虚拟 DOM 之间的差异进行比较或计算。

  • 然后,真正的 DOM 只更新已经改变的元素或状态,而不是整个 DOM 树。

所以,是的,虚拟 DOM 的巧妙创建和使用是 React 快的原因之一。

下面是一些展示 React 的虚拟 DOM 如何工作的可视化表示。

img/506956_1_En_1_Fig4_HTML.jpg

图 1-4

仅当子组件的状态依赖于父组件时,子组件才会重新渲染

在图 1-4 中,如果子组件依赖于父组件的更新状态,改变父状态将重新呈现子组件。

img/506956_1_En_1_Fig5_HTML.jpg

图 1-5

如果子组件不依赖于父组件的更新状态,它们不会在真实的 DOM 中重新呈现

在图 1-5 中,如果子组件不依赖于父组件的更新状态,改变父状态不会重新呈现子组件。

就性能而言,访问或创建虚拟 DOM 比在真实 DOM 中构建或重新呈现要便宜。

图 1-6 提供了真实 DOM 和虚拟 DOM 之间的概要比较。

img/506956_1_En_1_Fig6_HTML.jpg

图 1-6

真实 DOM 和虚拟 DOM 的对照表

客户端渲染和服务器端渲染

您可以选择通过客户端呈现(CSR)或服务器端呈现(SSR)来实现 React 应用。开发人员可以在客户端和服务器端构建独立和自包含的应用组件。

客户端呈现(CSR)是一种相对较新的呈现网站的方式,它使用 JavaScript 在 UI 中显示内容。在客户端运行代码有一定的性能优势,包括在每次修改代码时使界面更具交互性。当数据改变时,React 将有效地更新和重新呈现我们的组件。

CSR 上的初始页面加载应该比较慢,但是页面重新加载可能会变得非常快,因为整个 UI 不是在服务器上调用的。

React 服务器端呈现(SSR)意味着组件在服务器上呈现,输出是 HTML 内容。做 SSR 的一个论据是,它有更好的应用性能,特别是对于内容密集型应用;另一个原因是,与做 CSR 相比,HTML 输出对 SEO 更友好。

单向流/单向数据绑定

React 更多地用于单向流或单向数据绑定。向下的数据流是 React 中允许更快和更有效(更不用说容易测试的代码)开发时间的事情之一。

单向数据绑定允许您更好地控制您的应用开发,因为组件应该是不可变的,并且其中的数据不能被更改。要直接编辑任何元素,必须使用回调函数。

为什么要 React?

与其他框架、库或编程语言相比,React 不仅学习速度快;它也是后端不可知的,允许用户使用它,无论他们的栈是什么。特别是 JavaScript 开发人员,可以很快精通 React 开发。

一个很大的加分点是在其官方网站上有一个写得很好的文档,易于遵循和理解。您可以将 React 视为 CMS(内容管理系统)世界的 WordPress,因为您的大多数问题都可以通过安装开源库来解决。

React 也被认为是 SEO 友好的,这意味着 React 组件对于 Google 索引来说更加简单。这对企业来说是件大事。

img/506956_1_En_1_Fig7_HTML.jpg

图 1-7

学习和使用 React 的理由

图 1-7 说明了 React 为何成为开发者和许多知名商业品牌的最爱。

学习和使用 React 的一些原因,以及为什么它将在许多年内保持相关性:

  • React 中虚拟 DOM 的威力。它只更新和呈现同样在 DOM 中更新和生成的元素。

  • 快速渲染和可重用组件。创建可在项目内外重用的封装的独立组件。

  • React 可以使用 Next.js 在服务器上呈现。

  • 要开发移动应用,可以使用 React Native,这是一个使用 React 的移动应用开发框架。此外,您可以在 React 本地移动应用中重用 React web 应用的业务逻辑部分。

  • React 相对于其他 JavaScript 框架,甚至是 Angular 或 Ember.js 等前端框架来说,还是比较容易学的。

  • 大量的工具可供选择,包括 React DevTools、Redux DevTools 和 MobX DevTools。

  • React 是开源的,拥有一个由世界各地活跃的社区或团体组成的强大生态系统。

最后一个,巨大的 React 社区和支持,是一个重要的因素,不管你信不信。这个活跃的社区也转化为开发人员创建许多工具和第三方库来帮助您的开发体验。

基于个人经验,我们已经求助于许多 React 团体,如 Slack、Gitter、脸书团体、Discord、Spectrum 和 Twitter。或者,每当我们遇到困难或需要澄清时,我们会联系某个特定的人。

几乎总是有人给我们答复,根据我们的经验,通常不到一个小时,可能是因为活跃的成员来自全球不同的时区。

React 中的职业机会

对 React 的兴趣高于 Vue.js、Angular 等其他流行的前端框架和库(图 1-8 )。如果你快速搜索一个前端开发人员的工作,你会看到许多公司在寻找精通 React 的开发人员或有 React 经验的人。我们相信这种趋势将会持续数年。

img/506956_1_En_1_Fig8_HTML.jpg

图 1-8

对 React、Angular 和 Vue 的兴趣不断增加。来源: https://trends.google.com/trends/explore?date=2019-09-01

如果你想扩大你作为前端开发人员和全栈开发人员的招聘需求,掌握 React 是一条必由之路。

React 开发人员目前有很高的就业市场需求,在可预见的未来。是的,你可能有自己喜欢的语言、库或框架,这是可以理解的。尽管如此,坦率地说,学习一门新语言或一种新编程工具的标准之一应该是你有多容易被雇佣,无论是作为一名自由职业者还是作为一家公司的一部分。

摘要

本章概述了关键的 React 概念,包括学习 React 的优势和好处。我们了解到所有 React 应用的核心是其可重用组件,以及虚拟 DOM 如何实现整体快速性能和更好的用户体验。

React 变得如此流行的另一个原因是,它让开发人员有可能在客户端和服务器端构建和呈现独立和自包含的组件。最后,我们表明,随着兴趣和需求的持续增长,React 技能是一个很好的职业发展方向。

在下一章,我们将开始下载和安装构建 React 应用所需的软件包。

二、Node 包管理器入门

在我们启动和运行 React 之前,我们需要一个安装程序来下载和管理我们的 JavaScript 软件包。最流行的包管理器之一是 Node 包管理器(NPM)。另一个是纱,一个较新的包装经理,据说从 NPM 汲取了很多灵感。对于我们的项目,我们选择使用 NPM,而不是纱线。如果你喜欢,你可以用纱线。

我们的项目将使用 NPM,所以本章将介绍一些最相关的 NPM 命令和快捷方式,包括语义版本和 NPM 脚本。我们将简单讨论一下 Yarn,并浏览一些常用的 Yarn 命令。

现在不要担心遵循这里的命令,因为一旦我们开始构建我们的项目,我们将在后续章节中一步一步地做它。

Node 包管理器概述

在开始之前,让我们回顾一下什么是 NPM,以及我们如何在我们的应用中使用它。NPM 类似于其他的包管理器,比如 Ruby on Rails 中的 RubyGems 或者 Python 中的 PIP。

NPM 于 2010 年发布,是 JavaScript 的包库管理器,通常预装 Node.js,这是一个用于构建服务器端应用的环境。如果你还没有安装 Node.js,去他们的网站 www.nodejs.org (图 2-1 )。

img/506956_1_En_2_Fig1_HTML.jpg

图 2-1

Node 的网站

确保安装长期支持(LTS)版本,因为它比当前版本更稳定。如果您已经安装了 Node,您可以检查版本:

$ node –version

说到 Node 版本管理,让我向您介绍一下——如果您还不熟悉的话——Node 版本管理器,简称为nvm

Node 版本管理器

Node 版本管理器(NVM)是一个用于管理 Node.js 运行时的不同版本的工具。

您可以轻松地降级或升级 Node.js 版本,如果您面对的是只与特定范围的 Node.js 版本兼容的遗留应用或 JavaScript 库,这将非常方便。

用一个命令安装nvm:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash

#or

wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.0/install.sh | bash

要在 Mac 和 Linux 上完整安装 NVM,请点击这里 https://github.com/nvm-sh/nvm

不幸的是,nvm不支持 Windows,但是根据 WSL 版本的不同,它可以在 Windows Subsystem for Linux (WSL)中实现。

要在 Windows 中下载并安装 NVM,请点击此处 https://github.com/coreybutler/nvm-windows

安装 NVM 后,您可以安装特定版本的 Node:

$ nvm install 14
$ nvm use 14

前面的命令将安装并使用 Node v14 的最新次要版本。现在,如果有一个 JavaScript 项目或库只在 Node v12 中运行,您可以很容易地切换到那个版本:

$ nvm install 12
$ nvm use 12

前面的命令将安装并使用 Node v12 的最新次要版本。

对于任何 JavaScript 开发人员来说,另一个不可或缺的资源是 npmjs 网站,在那里我们可以搜索需要为我们的项目安装的库或框架。

安装软件包

你可以使用 NPM 安装 NPM 注册表中任何可用的库或框架。你可以前往 www.npmjs.com 查看更多。如果您已经有了旧版本,您可以按如下方式进行更新。

对于 Mac 和 Linux 用户:

$ npm install -g npm@latest

Node 封装模块

软件包以本地或全局模式安装。全局模式包可从命令行界面(CLI)获得,而本地模式包安装在父工作文件的node_modules文件夹中。

设置新的或现有的 NPM 软件包:

$ npm init

$ npm init –-y

填写空白处或只按Enter接受默认值或添加标志–-y来快速生成 package.json 文件。

如果您想要克隆其他人的代码,请在项目根目录下运行以下命令:

$ npm install or npm i

这将自动获取运行应用所需的所有已声明的包。声明的包位于 package.json 文件中。

Package.json 和包锁. json

Package.json 是使用 NPM 和 Node 应用时的重要清单文件。它包含您的所有应用信息,尤其是您的应用正确运行所需的依赖项或模块。这个清单文件也是许多开发人员在运行项目的本地版本时首先要查看的。

Package-lock.json 是你的 package.json 文件的副本,版本依赖树。

清单 2-1 中所示的 package.json 文件是我们在终端中执行 npm install 命令时自动创建的。这只是一个 package.json 文件的示例对象结构,包括依赖项和 devDependencies。我们将在第四章开始为我们的应用安装软件包和库。

{ "name": "npmproject",
   "version": "1.0.0",
   "private"
   "description": "NPM commands",
   "main": "index.js",
   "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test",
        "eject": "react-scripts eject",
   "backend": "json-server --watch db.json --port 5000 -- delay=500",
   "start:fullstack": "concurrently \"npm run backend\" \"npm run start\""
 },
    "author": "Devlin Duldulao",
    "license": "MIT",
    "dependencies": {
    "react": "¹⁶.8.6",
               "react-dom": "¹⁶.8.6",
},
"devDependencies": {
    "husky": "⁴.3.0",
    "json-server": "⁰.16.2",
    "lint-staged": "¹⁰.4.0",
    "mobx-react-devtools": "⁶.1.1",
    "prettier": "².1.2"
  },
 }

Listing 2-1A Sample of a package.json File

而 package.json 用于项目属性、作者、版本、描述、脚本、许可证等依赖项。,package-lock.json 同样会自动创建,以将依赖项锁定到特定的版本号。

如清单 2-1 所示,脚本对象是我在已有项目中首先读到的,因为脚本告诉我们运行项目或构建项目需要运行什么命令。脚本对象还通过它的键和值对帮助我们运行更短的命令。你可以在脚本中做的另一件很酷的事情是定制它。

假设您想要同时使用脚本'npm run backend'和'npm run start'。然后你可以在你的脚本中添加一个快捷方式,比如“npm start:fullstack”,然后运行它:

"start:fullstack": "concurrently \"npm run backend\" \"npm run start\""

如果您想亲自尝试使用start:fullstack命令,您可以并行安装 npm。Concurrently 是一个非常方便的工具,允许我们同时运行多个命令:

$ npm i concurrently

在我们的例子中,依赖项最初只包含 react 和 react-dom 等最少的元素,它们是我们的应用在运行时需要的库。但是也请注意devDependencies对象。devDependencies对象包含 JavaScript 库,您希望只在本地开发和测试期间添加这些库,因为您在生产中不需要它们。一些很好的例子包括 Gulp、Prettier 和 ESLint。

语义版本化或 Semver

有几种类型的版本控制,但是语义版本控制或 semver 是最流行的一种。

在所谓的语义版本化中,有三个版本号需要考虑。在其官网上, www.semver.org/ ,我们可以看到如下的总结:

给出了一个版本号专业。MINOR.PATCH,增量:

当您进行不兼容的 API 更改时,增量为主要版本,

当您以向后兼容的方式添加功能时,增量为次要版本,

当您进行向后兼容的错误修复时,增量为修补版本。

预发布和构建元数据的附加标签可作为主版本的扩展。小调。补丁格式。??

语义版本控制的一个例子是“³.2.1".

第一个数字(3)是主要版本,(2)是次要版本,(1)是修补版本。

脱字符^告诉 NPM,我们将接受该方案的主要版本;次要版本和修补程序版本可能会有所不同。另一种写法是,例如,2.x,2 是主要版本。

在某些情况下,您会看到波浪号(即“3.2.1”)。这种特殊的版本化可以理解为“我们将主版本设为 3,次版本设为 2,但是补丁版本可以是任何数字。”你也可以把它写成 3.2.x。

如果您需要所有三个版本的特定数字,只需删除插入符号或波浪号字符。

故事

Yarn 是一个新的 JavaScript 包管理器。 Yarn 由脸书于 2016 年发布,旨在解决困扰 NPM 的一些性能和安全问题(当时!).如前所述,我们不会使用纱线,但你可以用它来代替 NPM。在下文中,我们强调了两者在语法上的一些相似之处和不同之处。

表格 2-1 和 2-2 强调了这两个包管理器之间的异同。

表 2-2

NPM 和纱线之间的不同命令

|

命令

|

新公共管理理论

|

故事

|
| --- | --- | --- |
| To install dependencies | npm install | yarn install |
| To install packages | npm install[package-name] | yarn add [package-name] |
| To uninstall packages | npm uninstall [package-name] | yarn remove [package-name] |
| To install packages globally | npm install –global [package-name] | yarn global add [package-name] |
| To uninstall packages globally | npm uninstall –global [package-name] | yarn global remove [package-name] |
| To update packages * for updating minor and patch releases only | npm update [package-name] | yarn upgrade [package-name] |
| To install only regular dependencies | npm install --production | yarn --production |
| To show only the top-level dependencies | npm list -g --depth 0 | yarn list --depth=0 |
| To install and save packages in devDependencies | npm install --save-dev [package-name | yarn add [package-name] -D |

表 2-1

NPM 和纱线之间的常用命令

|

命令

|

新公共管理理论

|

故事

|
| --- | --- | --- |
| To initialize a project | npm init | yarn init |
| To set up the defaults | npm init –y | yarn init -y |
| To check if any package is outdated | npm outdated | yarn outdated |
| To clear local cache | npm cache clean | yarn cache clean |
| To run a script | npm run build | yarn run |
| To see a list of installed dependencies | npm list | yarn list |

使用 NPM 的技巧:

  1. 要快速生成 package.json 文件,可以使用npm init –-y

  2. private: true添加到 package.json 中,以防止意外发布任何私有回购。

  3. 在 devDependencies 中添加用于开发目的的包(例如,传输代码或运行测试)。

  4. 不要删除 package.json,但是可以在提交之前删除 package-lock.json。

  5. 如果您遇到 yarn.lock 并希望使用 npm,只需删除 yarn.lock 并进行 NPM 安装,以在您的应用中自动创建 package-lock.json。

  6. 从 Git 存储库克隆项目后,您需要运行 npm install。

  7. 不建议将 node_modules 推送到您的源代码控制 repo(如 Git)中。

摘要

在这一章中,我们讨论了如何开始使用 NPM,包括您需要理解的各种关键命令。我们还研究了如何使用nvm轻松地从一个 Node 版本切换到另一个 Node 版本。在下一章,我们将讨论各种 React 组件,以及如何在我们的应用中使用它们。

三、React 函数组件和 TypeScript 入门

在前一章中,我们学习了如何使用 Node 包管理器或 NPM,它是一个命令行工具,允许我们安装和更新应用包。

本章将研究 React 函数组件,这已经成为编写最新 React 应用的现状,以及使用 TypeScript 定义函数组件所用属性的语法。简单地说,React 函数组件本质上是返回 JSX 或 JavaScript XML 的 JavaScript 函数。

我们将使用 TypeScript 设置一个基本的 create-react-app (CRA)应用,并使用 CRA TypeScript 模板检查一些关键特性。TypeScript 是 JavaScript 的强类型超集,它带来了直观的特性,以强大的方式扩展了 JS,例如以多种方式定义对象的结构。

我们将讨论在 TypeScript 中使用类型化的明显好处,尤其是在构建大型企业级应用时。此外,我们将编写 React 函数组件,或 React。简称 FC,并触碰两个常用的 React 挂钩:useStateuseEffect

创建一个创建-React-应用应用

是开始构建单页面应用(SPA)的最流行也是最简单的方法。它也是脸书官方的样板文件生成器,用于创建一个简单的或入门的 React 应用。

CRA 使用预配置的 webpack 版本进行开发,使我们能够立即深入我们的代码并构建我们的 React 应用,而无需手动设置和配置我们的本地开发环境和生产版本。

最低要求是 Node> = 8.10,NPM >= 5.6。

首先,在您的终端中,检查您是否已经拥有 NPX:

$ npx -v

或者您可以通过运行以下命令单独安装它

$ npm install -g npx

NPX(Node 包执行)是 NPM 自带的。NPX 对于一次性软件包很有用,它从 NPM 注册表执行软件包,而不在本地安装它们。

Note

如果您之前已经通过 npm install -g create-react-app 全局安装了 create-react-app,则需要使用

npm uninstall -g create-react-app

或者

yarn global remove create-react-app

这是为了确保 NPX 始终使用最新版本。不再支持 CRA 的全局安装。

现在,让我们用以下命令创建我们的第一个create-react-app应用:

$npx create-react-app <name-of-your-app> --template
typescript

cd <name-of-your-app>

npm start

命名约定是使用所有小写字母,并用破折号分隔单词。- template TypeScript 标志告诉 CRA 使用 TS 作为默认语法并添加所需的配置。要启动开发服务器,运行

您可以在 package.json 中的“脚本”下找到完整的命令脚本。

运行该命令后,在您的浏览器中检查您的应用—http://localhost:3000——并看到如图 3-1 所示的 CRA 初始加载页面。

img/506956_1_En_3_Fig1_HTML.jpg

图 3-1

带打字稿的 CRA 初始加载页

当您打开 IDE 或编辑器时,您应该能够看到如图 3-2 所示的目录。我们可以在下面看到 CRA 自动生成的项目或默认文件夹结构。

img/506956_1_En_3_Fig2_HTML.jpg

图 3-2

最初使用 create-react-app 创建的文件(CRA)

让我们回顾一下用 create-react-app 生成的一些文件。首先,让我们检查将要构建的项目的两个关键文件:

public/index.html是我们应用的主页面 HTML 模板,包括 React 代码和 React 渲染上下文。在这里,您可以找到让 JavaScript 运行应用的“根”。它也是 React 应用的挂载点:

 <!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width,         initial-scale=1" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <title>React App</title>
  </head>
  <body>
    <section id="root"></section>
  </body>
</html>

持有来自 React DOM 的主渲染调用。它导入我们的 App.tsx 组件,该组件告诉 React 在哪里呈现它:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

以下是 CRA 附带的一些文件和文件夹的简短定义和用法:

它包含了 JavaScript 库和一些我们已经安装的库的依赖项。我们通常不把这个文件夹包含在我们的 Git 库中,因为它很大,大约 100mb 到 500mb,这取决于我们在应用中安装了多少个包。

这是 CRA 附带的默认文件。当基于当前的 React 应用构建渐进式 Web 应用(PWA)时,需要配置文件。

对 PWA 的一个简单解释是,它允许您脱机或在没有互联网连接的情况下运行应用。它的作用是缓存你的应用的数据或内容,这样即使应用离线,你仍然可以看到你网站的内容。仅供参考,如果没有互联网连接,你将再也看不到霸王龙了。我相信我们都看过(你甚至可以播放它!)那只恐龙是吧?

public/robots.txt:又称机器人排除协议或标准。你可以在这里声明你想在谷歌搜索结果中隐藏的页面或 HTML。示例页面包括管理页面,因为您的用户不需要知道您的应用的管理页面。同样,你可以在这里指定你希望或者不希望哪个搜索引擎来索引你的站点。

src:包含 app UI 代码,包括组件和 CSS 样式。这是 React app 的核心。我们还可以看到App.tsx,我们应用的入口点,以及index.tsx,它引导我们的应用。

gitignore:位于根文件的文本文件;这是您放置希望 Git 忽略的文件的地方,比如您的node_modules和。env files。Git 已经跟踪的文件不受影响。使用命令git rm –cached停止索引一个已经被跟踪的文件。

如果你已经在你的机器上安装了 Yarn,默认情况下,CRA 会搜索 Yarn package manager,但是如果它不可用,它会返回到 NPM。如果您使用 Yarn,那么您将会看到yarn-lock.json文件。

package.json:我们已经在前一章中讨论过package.json,但是简单回顾一下,它管理我们项目应用的依赖、脚本、版本和其他元数据。

tsconfig.json:根目录下的这个文件表示该目录是 TypeScript 项目的根目录。

tsconfig.json内部是compilerOptions对象,具有以下一些配置:

  • "target" : "es5":现代浏览器支持所有 ES6 特性,但是您希望您的代码向后兼容 ECMAScript 2009 或更老的环境。

  • "lib":类型检查过程中包含的标准类型定义。

  • "strict" : true:这是 TypeScript 中严格的编译器选项;这意味着您选择了默认严格的类型安全模式,而不必单独启用每个编译器选项。

  • "module" : "esnext":指定代码生成。

  • "noEmit" : true:不要发布 JavaScript 源代码、源代码图或声明,而是使用 Babel。

  • 是否支持 JSX。tsx 文件。

  • "include":打字稿要检查的文件和文件夹。

使用 React 的 TypeScript 的声明文件

声明文件也称为。d .ts文件以@types.为前缀

使用声明文件——也称为定义文件——我们可以避免错误处理库,并在我们的编辑器中获得非常有用和强大的“智能感知”或自动完成功能。

让我们快速浏览一下我们的package.json文件,看看我们的 CRA 打字稿模板中包含的. d.ts 文件:

    @testing-library/jest-dom":
    "@testing-library/react":
    "@testing-library/user-event":
    "@types/jest":
    "@types/node":
    "@types/react":
    "@types/react-dom":

为了确定我们最近安装的库是否需要声明文件(@types),我们转到名为 DefinitelyTyped 的 GitHub 存储库,声明文件就存放在这里。另一种方法是直接进入 www.npmjs.com ,在搜索框中给库名加上前缀@types/。幸运的是,许多人已经为最常见和最流行的 JavaScript 库编写了声明文件。

如果你使用一个需要类型声明文件的外部库,而你没有安装它,你会得到错误或者你的 IDE 会抱怨。

打字稿中的打字

打字稿中的打字本身就是文件。是的,一开始可能看起来有点额外的工作,但从长远来看,这是值得的。许多最初看不到它的价值的 React 开发人员说,他们现在无法想象不使用它。

考虑下面的例子。我们明确定义了每个变量的类型——名称是字符串,年龄是数字,等等:

const name: string = "Jane Doe",
      age: number = "18",
      isStudent: boolean = true;

您会注意到,当您自己编写类型脚本时,代码的描述是代码本身的一部分。

如果你熟悉 JavaScript,你会知道它是无类型的。JavaScript 可能是唯一一种既爱又恨的编程语言,因为它的灵活性——有时可能有点过分——在于它的“规则”是按照我们想要的或我们想要的方式传递数据和对象。

现在,想象一下,在一个团队中有一群人,用成千上万的代码构建一个应用。代码会很快变得混乱和复杂。您可能会遇到在不存在的对象中调用的方法,或者从未声明的变量。

但是在 TypeScript 中,我们需要定义我们的对象和数据格式。这样,编译器就可以智能地搜索或检测我们的代码,并捕捉潜在的错误,比如用错误的参数调用函数,或者引用当前范围内不可用的变量。

使用 TypeScript 的显著好处包括拥有增强的工具选项,如自动完成、重构和在整个应用中查找用法。你可以在这里阅读更多关于打字稿的内容: www.typescriptlang.org/docs/handbook/basic-types.html

了解 React 功能组件与 Props

React 功能组件(React。FC)是一个 JavaScript/ES6 函数,它接受参数作为组件的属性,并返回有效的 JavaScript XML (JSX)。如果需要,它还会将 props 作为参数。

下面是一个带有属性的简单功能组件:

function Message(props) {
  return <div>{props.message}</div>
}

现在让我们创建一个 React 函数组件。在src文件夹中,创建一个名为components,的目录,在components,文件夹中创建一个名为Customer.tsx的文件。

在我们继续之前,让我们首先使用 TypeScript 的类型别名来定义我们的 props。另一种方法是使用interface,,但是我们更喜欢使用类型别名来确定我们的属性:

import React from "react";

type Person = {
  firstName: string;
  lastName: string;
  age: number;
  isLegal: boolean;
};

请注意,我们定义了一个名为Person的类型,它指定了组件将采用的属性。firstNamelastName是必需的strings,,而age是必需的numberisLegal是必需的boolean

通过创建一个type,,我们现在可以在项目中的任何地方使用它,就好像它是一个字符串、数字或者任何原语或引用类型。

在我们新创建的组件中,使用 Person 类型作为属性的模型或形状。请参见以下内容:

const Customer = (props: Person) => {
  const { firstName, lastName, age, isLegal } = props;

  return (
    <div>
      <h1> Hello React</h1>
    </div>
  );
};

export default Customer;

我们可以看到,Customer是一个无状态的函数组件,它接受一个props对象,并析构< Person >所有需要的属性。

现在我们已经有了带有属性的函数组件,让我们进入 App.tsx 来渲染它。

首先,我们需要像这样导入组件:

import Customer from "./components/Customer";

在我们的回访中:

function App() {
  return (
    <div className="App">
      <Customer
        firstName="Jane"
         lastName="Doe"
         age={21}
         isLegal={true} />
    </div>
  );
}

export default App;

当我们在代码编辑器中使用代码智能感知功能时,创建类型定义也很有用。

代码编辑器中添加的智能感知

为我们的属性创建类型并声明组件的参数的另一个好处是我们在编辑器中得到的所谓的智能感知或代码建议。VS 代码中支持智能感知。

img/506956_1_En_3_Fig3_HTML.jpg

图 3-3

编辑器中的代码建议/智能感知

你猜怎么着?如果编写 Customer 组件时没有传递所有必需的值,就会出现 TypeScript 错误。甚至在你运行它之前,你已经可以在中看到红色的曲线来指示一个错误或者一个严重的警告。

img/506956_1_En_3_Fig4_HTML.jpg

图 3-4

编译器中的类型脚本错误

但是等等。如果不需要申报或者不用全部属性呢?你可以在属性类型中使用?来选择一些属性:

type Person = {
  firstName: string;
  lastName: string;
  age: number;
  address?: string;     // the address is optional
  isLegal: boolean;
};

关键是您可以选择不在组件中定义所有的类型,以实现自动完成的优秀工具和类型脚本的类型安全特性。

是的,在开始时,可能需要一点努力来设置和习惯在变量和函数中使用类型化。尽管如此,从长远来看,它会给我们一个更好的开发者体验和一个可维护的应用。

在我们继续之前,让我们再多谈谈 React 钩子和两个流行的钩子,即useStateuseEffect

React 钩

React 钩子本质上是 JavaScript 函数,是“React 16.8 中的新增功能”。它们让你不用写类就能使用状态和其他 React 特性。”

还要记住,钩子在类内部是不起作用的。钩子是一个很大的话题,但是我们会在开发应用的过程中学习基本的概念。但是现在,这里有三个钩子的重要规则需要记住:

  1. 只在 React 函数的顶层调用钩子。

  2. 仅从 React 函数组件调用挂钩,而不是从常规 JavaScript 函数调用挂钩。

  3. 仅从自定义挂钩调用挂钩。

React 钩子:使用状态

useState是一个钩子,它将允许函数组件中的状态变量。useState也是我们最有可能经常使用的一个。

假设我们想更新 JSX 的一个州。我们如何在我们的函数组件中做到这一点呢?嗯,我们用useState来表示!

在组件内部创建一个本地状态,并传递一个默认值。注意useState是 React 的一个命名导出:

import React, { useState } from "react";
import "./App.css";

function App() {
  const [title, setTitle] = useState("Practical React Enterprise");

方括号中的状态变量是一种叫做数组析构的 JavaScript 语法。第一个变量[title],也就是 getter,被定位为useState返回的第一个值,[setTitle]是 setter 或 set 函数,让我们更新它。

如何在我们的 JSX (HTML 元素)中使用状态变量或本地状态?在 React 中,由于我们的useState是在 JavaScript 中,我们需要用花括号{}将 getter 括起来,以便在 JSX 或 HTML div 中读取它的值:

...
      <div className="App">
      <h1
        style={{
          color: "blue",
          marginBottom: "5rem",
        }}
      >
        {title}
      </h1>

img/506956_1_En_3_Fig5_HTML.jpg

图 3-5

使用本地状态呈现 UI 的屏幕截图

现在我们有了本地值,但是如何更新值[title]?是 setter 函数[setTitle]的工作。顺便说一下,如果你问的是 div 中的className,我们在 React 中使用className而不是class来防止名称冲突,因为后者是现代 JavaScript 和 TypeScript 中的保留关键字。

在我们继续之前,我们需要简单讨论一下什么是 JSX 或 JavaScript XML。在 React JSX 中,我们可以将 HTML 和 JavaScript 结合在一起编写。我们可以运行 JS 代码(逻辑、函数、变量等。)中,就像我们声明{title}{setTitle()}时一样。

接下来,我们需要一个事件来触发 JSX 中的 setter 函数setTitle。那么我们如何找到一个事件呢?如果您在基本代码 <h1> (中使用前缀为“on”的,见图 3-6 ,将出现一个窗口,显示所有可用事件:

img/506956_1_En_3_Fig6_HTML.jpg

图 3-6

IDE 中的代码建议

return (
  <div className="App">
      <h1 onClick={() =>
   setTitle("Become an Effective React Developer from Day 1")
        }}</h1>
    </div>
  );
}

export default App;

img/506956_1_En_3_Fig7_HTML.jpg

图 3-7

使用本地状态更新用户界面

进入你的浏览器,点击标题“实际 React 企业”来更新本地状态标题。

例如,我们也可以将事件放在<button><div>.上。让我们创建一个handleAlert函数,并将它放在<按钮>上,以弹出一条警告消息:

const handleAlert = () => {
  alert("I'm a button!");
};

return (
  <div className="App">
    <button
      style={{
        color: "#ffff",
        height: "5rem",
        width: "10rem",
        backgroundColor: "tomato",
        borderRadius: "5px",
        fontSize: "18px",
      }}
      onClick={handleAlert}
    >
      Click me
    </button>

  </div>
);

img/506956_1_En_3_Fig8_HTML.jpg

图 3-8

弹出警告消息

React 挂接:useEffect

useEffect用于所有副作用,本质上取代了我们之前使用的生命周期方法(componentDidUpdate, componentDidMount, componentWillUnmount)。这个全面的钩子接受两个参数——第一个是必需的函数,第二个是特定状态的可选参数。

现在,假设我们想在用户登陆我们的应用时触发一个事件,或者在 UI 中的所有内容呈现之前触发一个事件。

是的,为此我们有useEffect。我们将做一个简单的 handleAlert 消息,当用户登陆浏览器时弹出,不需要在我们的 JSX 中调用它:

import React, {useEffect, useState} from "react";
import "./App.css";

function App() {
  const [title, setTitle] = useState("Practical React Enterprise");

  const handleAlert = () => {
    alert("I'm a button!");
  };

  useEffect(() => {
    alert("Welcome to the Practical React Enterprise!");
  });

  return (
...

img/506956_1_En_3_Fig9_HTML.jpg

图 3-9

具有使用效果的警告消息

useEffect的一个用例是,当您想要向 web 服务发送一个请求,以获取您的用户数据或一系列值,如客户姓名、地址等。

也可以把 setter 放在useEffect里面,比如:

  useEffect(() => {
    // handleAlert();
    setTitle("Updating the React Enterprise using useEffect");
  }, []);

注意到useEffect钩子中的方括号了吗?

  1. 数组括号将防止useEffect在无限循环中连续运行。

  2. 您还可以在数组中添加一个状态。并且状态值(即title)的任何变化将触发useEffect重新呈现:

  useEffect(() => {
    //another state here
  }, [title]);

技巧

  1. 不要使用"eject": "react-scripts eject"命令,除非你需要暴露 CRA 在应用开始时包含的管道,比如 Babel RC 和 Webpack。只有当您需要在 React 项目中进行一些定制时,才这样做。请记住,一旦弹出,您将无法回到 create-react-app 的原始样板结构。

  2. 在运行时代码实现之前声明类型。当前模块中使用的类型必须在顶部可见。

  3. 在命名约定中,组件应该用 PascalCase,而方法应该用 camelCase。

摘要

在本章中,我们使用了一个简单的 create-react-app 样板文件和 TypeScript 来展示构建 react 应用的最简单的方法。我们通过 TypeScript 学习了 CRA 的一些基本元素,以及它们如何一起正确运行我们的应用。

我们还看到了在 React 应用中使用 TypeScript 的一些好处,包括

  • 代码建议和选项使我们更容易阅读他人的代码。这可以提高团队沟通的效率和代码库的一致性。

  • IntelliSense 和错误捕捉在运行代码的早期和之前

不知何故,我们设法用 TypeScript 解构了 React 函数组件。

但是我们在这里触及的只是冰山一角,特别是在理解使用 React 的 TypeScript 的全部功能以及 TS 可以给 JavaScript 代码带来什么方面。

由于这本书是关于构建一个实用的企业级应用,我们将在将它应用到我们的代码中时更深入地研究它。在下一章中,我将向您展示如何以一种有用而有效的方式建立我们项目应用的基础,这样您将从第一天起就成为一名高效的 React 开发人员。

四、设置企业级应用

无论您是编程领域的新手还是专家,您一定想知道其他开发人员是如何设置和配置他们的项目应用的,对吗?

因此,这一章探讨了一种有效和智能的方法来建立一个生产就绪的样板文件。

我们将研究如何为我们的项目选择正确的样板,使用工具、框架和模块,允许我们管理性能、样式、异步和构建实际应用所需的其他基本内容。

是的,我们不仅仅是从无处不在的 ToDo list 应用开始学习新的框架或语言。这是一个你可以确信会在很多企业级 React 应用中看到的项目——尤其是配置甚至文件夹排列。更重要的是,它将具有良好的架构和高度的可伸缩性。

首先,我们将看看我用来为特定应用找到正确样板的标准。然后,让我们看看使用样板文件的利弊。

如何选择一个好的样板

像我认识的许多经验丰富的“聪明”的开发人员一样,我坚信应该使用样板文件,或者至少对此持开放态度,对吗?唯一的技巧——或者实际上可能是最难的部分——是为你的项目找到并选择正确的样板文件。

**在决定了样板文件之后,您需要做的第一件事就是了解您在 package.json 中看到的每个 NPM 包

Tip

在决定样板文件是否适合您之前,请检查样板文件中包含的库和工具。

获得一个包或依赖项的简单定义的一个简单方法是进入 www.npmjs.com 并在那里搜索它。

我有几个标准来决定什么是特定项目的正确样板,但以下是三个主要标准:

  1. 它包含了我需要的大部分(如果不是全部)核心功能。这是最关键的过程。寻找一个样板文件,让实际上检查你需要的所有主要特性,这样你就不必做大量的定制工作,这会耗费你大量的时间。总是阅读样板的特征和描述。

  2. 有一定数量的投稿人或作者。这是样板文件好的另一个关键部分。许多有经验的、活跃的开发人员正在为样板文件做贡献,并为更新和其他东西维护它。

  3. 它包含频繁的更新或提交。在 GitHub 上检查最新的补丁或提交。一个维护良好的模板或样板文件会有来自贡献者的定期更新或提交。

请记住,每个项目甚至项目团队都可能意味着许多不同的样板应用。大多数时候,你决定的样板文件取决于项目本身,你的合作伙伴,甚至是客户。

但关键是,你会希望从一个标准的代码基础开始,快速启动并运行。

使用样板文件的好处

作为一名开发人员,我确信您已经经历了似乎永无止境的设置所有库、主题和一开始就需要的依赖项的折磨。光是将它们全部配置好而不出错就要花费数小时。

相信我。如果我们要自己实现全套的 React、TypeScript、Redux、Redux 工具包、测试和所有其他必要的东西,这将花费我们比我们愿意花费的时间长得多。这是假设当我们一个接一个地安装它们时,一切都顺利进行。没有错误,所有的库版本都很好地集成在一起。

以下是我为什么说使用正确的样板文件作为你应用的起点可以让你从第一天起就成为一个有能力并且高效的开发者的原因:

  1. 您可以减少设置和开发时间。作为开发人员,我们都知道重用代码或模板非常有意义。使用样板文件可以减少使用相同代码模式的设置和开发时间,并避免从头开始重写。

  2. 你可以派生出一个样板文件,并根据你的需要进行定制。当我还是初级开发人员时,快速学习一个新的库或框架的一个方法是查看两个或更多流行的应用。我会研究和比较它们,包括所有使用的依赖项。之后,我会根据自己的需求构建一些东西。

  3. 你可以研究这款应用的架构良好的结构。样板文件立即向您介绍了一种设计 React 项目的优秀方法。这就像是首席架构师将构建应用的蓝图交给你。

  4. 你可以受益于该领域专家的贡献。有一个样板就像有一个高级开发人员或前端架构师已经给了你开始项目的指导方针。这是无价的,尤其是如果你是团队或特定工具的新手。在确定对你有用的东西之前,你不必做研究和测试所有的东西。

使用样板文件的缺点

像任何其他好东西一样,总会有反对的声音。当然有,对吧?但是诀窍是认识和理解缺点,并根据清单中的优点来权衡它们。也就是说,让我们来看看反对在应用中使用样板文件而不是从头开始构建项目的一些原因。

让我们来看看使用样板文件的一些缺点:

  1. 你可能会发现自己陷入了一个时间黑洞。你实际上仍然需要检查和了解事情是如何运行的。当然,在选择特定的样板文件之前,您需要了解其中包含的核心特性。有时,您可能会发现自己要么添加额外的基本功能,要么去掉多余的不需要的模块。

  2. 你可能会陷入不必要的复杂之中。您可能会得到比您需要的更多的特性和比必要的更高的复杂性。

在考虑了前面所有的要点之后,现在是时候设置我们选择的样板文件了。

克隆样板文件

首先,如果你想随书一起编码,去 my GitHub 并克隆项目的起点。确保从第四章开始。

我分叉这个样板文件只是为了防止你在一年或两年后阅读这本书,所以你仍然有相同的代码库,以防你想跟随我。我建议您使用我的分叉样板文件,以确保获得相同的开发体验。

你会在图 4-1 中找到我的 GitHub 的链接,如果你想和我一起编码,你应该从那里开始。

img/506956_1_En_4_Fig1_HTML.jpg

图 4-1

实际企业 React 从源头: github。com/webmasterdevlin/practical-enterprise-react/tree/master/chapter-4/starter-boilerplate

然而,一旦你读完了这本书,并且很好地掌握了构建企业级 React 应用的过程,我强烈建议你在准备制作应用时使用图 4-2 中的原始样板文件。

您可以在图 4-2 中找到 React 样板 CRA 模板的主副本。

img/506956_1_En_4_Fig2_HTML.jpg

图 4-2

React 样板 CRA 模板来源: github。react-boilerplate/react-boilerplate-CRA-template

Note

不要忘记比较 package.json 中库的版本,以避免任何错误。

老实说,您甚至可以搜索另一个更适合您的项目规范的样板文件。

我的目标是为您提供构建生产就绪型应用的一步一步的过程,并通过武装您使用正确的工具,或至少如何选择它们,使您成为更自信的企业级应用开发人员。

React 样板遇到 CRA

我们为这个项目选择的模板是 create-react-app (CRA)和 react 样板的完美结合,后者是最受欢迎和喜爱的入门模板,包括所有行业标准工具:Redux、Redux 工具包、TypeScript 等等。

以下是我为我们的项目选择这个特殊样板的原因:

  • create-react-app :内置 react 样板,可以弹出进行定制配置。

  • Redux :管理 React 应用全局状态的独立库。Redux 存储是应用的核心。Redux 是类似流量的单向数据流的更好的实现。

  • TypeScript :通过在点击刷新之前捕捉错误,改善开发人员的体验。TS 还通过加强类型安全来防止代价高昂的错误。最后,由于参数和模型的类型化,TypeScript 是自文档化的。

  • Jest :在 React 生态系统中相当受欢迎的一个测试跑者。

  • Reselect :用于切分 Redux 状态,并向 React 组件提供必要的子树。

  • react-router :用于该样板文件中的路由。

以下是需要安装在样板文件上的附加库。npm install我们将使用以下软件包:

  • Material-UI是 Google 在 2014 年开发的,它提供了一个可选的 CssBaseline 组件,用于更快的 web 开发。它使用基于网格的布局、动画、过渡、填充等等。

  • clsx是一个用于有条件地构造类名字符串的小工具。

  • Formik是一个非常流行的 React 和 React Native 开源表单库。

  • Yup是一个用于值解析和验证的 JavaScript 模式构建器。

  • axios是一个允许我们向外部资源发出 HTTP 请求的库。

对于依赖关系:

$ npm i [package name]

 @material-ui/core
 @material-ui/icons
 @material-ui/lab
 @material-ui/pickers
 @material-ui/styles
 clsx
 formik
 @types/yup
 yup
 axios

对于 devDependencies:

$ npm i -D [package name]

cypress
concurrently
json-server
json-server-auth

在我们继续之前,有必要了解汇聚在一起形成单一工作应用的不同工具和技术。

强壮的

在我看来,Husky 是添加到我们应用中的一个很好的工具。它本质上防止您提交或推送错误的提交或代码;它与 TypeScript 的打字功能一起增加了保护:

"husky(remove-everything-in-these-parentheses.See-the-issue-#29)": {
    "hooks": {
      "pre-commit": "npm run checkTs && lint-staged"
    }
  },

属国

在 IDE 中打开项目并打开 package.json 文件,检查是否安装了所有的依赖库:

"dependencies": {
  "@material-ui/core": "4.11.1",
  "@material-ui/icons": "4.9.1",
  "@material-ui/lab": "4.0.0-alpha.56",
  "@material-ui/pickers": "3.2.10",
  "@material-ui/styles": "4.11.1",
  "@reduxjs/toolkit": "1.4.0",
  "@testing-library/jest-dom": "5.11.6",
  "@testing-library/react": "10.0.1",
  "@types/fontfaceobserver": "0.0.6",
  "@types/jest": "25.1.4",
  "@types/node": "13.9.3",
  "@types/react": "16.9.25",
  "@types/react-dom": "16.9.3",
  "@types/react-helmet": "5.0.15",
  "@types/react-redux": "7.1.11",
  "@types/react-router-dom": "5.1.6",
  "@types/react-test-renderer": "16.9.2",
  "@types/styled-components": "5.1.4",
  "@types/testing-library__jest-dom": "5.9.5",
  "@types/webpack-env": "1.15.3",
  "@types/yup": "0.29.9",
  "axios": "0.21.0",
  "clsx": "1.1.1",
  "cross-env": "7.0.2",
  "eslint-config-prettier": "6.15.0",
  "eslint-plugin-prettier": "3.1.4",
  "fontfaceobserver": "2.1.0",
  "formik": "2.2.5",
  "husky": "4.3.0",
  "i18next": "19.8.4",
  "i18next-browser-languagedetector": "4.0.2",
  "jest-styled-components": "7.0.3",
  "lint-staged": "10.5.2",
  "node-plop": "0.26.2",
  "plop": "2.7.4",
  "prettier": "2.2.0",
  "react": "16.13.0",
  "react-app-polyfill": "1.0.6",
  "react-dom": "16.13.0",
  "react-helmet-async": "1.0.7",
  "react-i18next": "11.7.3",
  "react-redux": "7.2.2",
  "react-router-dom": "5.2.0",
  "react-scripts": "4.0.1",
  "react-test-renderer": "16.13.0",
  "redux-injectors": "1.3.0",
  "redux-saga": "1.1.3",
  "reselect": "4.0.0",
  "sanitize.css": "11.0.0",
  "serve": "11.3.2",
  "shelljs": "0.8.4",
  "styled-components": "5.2.1",
  "stylelint": "13.8.0",
  "stylelint-config-recommended": "3.0.0",
  "stylelint-config-styled-components": "0.1.1",
  "stylelint-processor-styled-components": "1.10.0",
  "ts-node": "8.8.2",
  "typescript": "3.9.7",
  "yup": "0.31.0"
},

仔细看看一些依赖关系

"cross-env":运行在各种平台上设置和使用环境变量的脚本

"eslint-config-prettier":关闭所有可能与更漂亮相冲突的规则

"eslint-plugin-prettier":运行更漂亮,ESLint 规则和日志不同

"fontfaceobserver":监控网页字体何时加载

"husky":防止错误的提交或推送

"i18next":适用于任何 JavaScript 环境或浏览器的国际化框架

"i18next-browser-language detector":检测浏览器中的用户语言

"jest-styled-components":改善测试体验

"lint-staged":通过强制执行设定的代码风格,帮助确保没有错误进入存储库

"node-plop":帮助自动生成代码,无需使用命令行

"plop":便于在整个团队中统一创建文件

增强风格和代码的一致性

"react-app-polyfill":多种浏览器填充,包括常用语言功能

"react-dom":使用 DOM 的 React 包

"react-helmet-async":React 头盔的叉子

"react-i18next":React 的国际化

"react-redux":Redux 官方 React 绑定

"react-router-dom":React 路由的 DOM 绑定

CRA 使用的配置和脚本

"react-test-renderer" : React 包进行快照测试

"redux-injectors":仅在必要时装载 Redux 减速器和 redux-saga,而不是一次全部装载

"redux-saga":用于 Redux 处理副作用的 Saga 中间件

"sanitize.css":确保 HTML 元素和其他默认样式的风格一致

"serve":给出一个清晰的界面来列出目录的内容

"shelljs":基于 Node.js API 的 Unix shell 命令的可移植(Windows/Linux/OS X)实现

帮助防止错误并确保风格的一致性

"ts-node":node . js 的类型脚本执行环境和 REPL,支持源映射

"Cypress":用于端到端测试

在脚本里面插入: "cypress:open": "cypress open"

      $ npm run cypress:open

类型依赖关系

这些是类型脚本定义:

    "@material-ui/core": "4.11.1",
    "@material-ui/icons": "4.9.1",
    "@material-ui/lab": "4.0.0-alpha.56",
    "@material-ui/pickers": "3.2.10",
    "@material-ui/styles": "4.11.1",
    "@reduxjs/toolkit": "1.3.2",
    "@testing-library/jest-dom": "5.1.1",
    "@testing-library/react": "10.0.1",
    "@types/fontfaceobserver": "0.0.6",
    "@types/jest": "25.1.4",
    "@types/node": "13.9.3",
    "@types/react": "16.9.25",
    "@types/react-dom": "16.9.3",
    "@types/react-helmet": "5.0.15",
    "@types/react-redux": "7.1.7",
    "@types/react-router-dom": "5.1.3",
    "@types/react-test-renderer": "16.9.2",
    "@types/styled-components": "5.0.1",
    "@types/testing-library__jest-dom": "5.0.2",
    "@types/webpack-env": "1.15.1",
    "@types/yup": "0.29.9",

查看这里的“脚本”。这非常令人惊讶,因为它们包含了许多脚本,所以您不必亲自动手:

"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "test:generators": "ts-node --project=./internals/ts-node.tsconfig.json ./internals/testing/test-generators.ts",
    "cypress:open": "cypress open",
    "start:prod": "npm run build && serve -s build",
    "checkTs": "tsc --noEmit",
    "eslint": "eslint --ext js,ts,tsx",
    "lint": "npm run eslint -- src",
    "lint:fix": "npm run eslint -- --fix src",
    "lint:css": "stylelint src/**/*.css",
    "generate": "cross-env TS_NODE_PROJECT='./internals/ts-node.tsconfig.json' plop --plopfile internals/generators/plopfile.ts",
    "prettify": "prettier --write"
  },

"engines": {
    "npm": ">=6.4.1",
    "node": ">=10.13.0"
  },
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": [
      "npm run eslint -- --fix"
    ],
    "*.{md,json}": [
      "prettier --write"
    ]
  },

  "husky": {
    "hooks": {
      "pre-commit": "npm run checkTs && lint-staged"
    }
  },

"jest": {
    "collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}",
      "!src/**/*/*.d.ts",
      "!src/**/*/Loadable.{js,jsx,ts,tsx}",
      "!src/**/*/types.ts",
      "!src/index.tsx",
      "!s rc/serviceWorker.ts"
    ],

  "devDependencies": {
    "concurrently": "5.3.0",
    "cypress": "6.0.0",
    "json-server": "0.16.3",
    "json-server-auth": "2.0.2"
  }
}

前面的包不是我们的应用中需要的唯一的包。我们将安装一些第三方 JS 库。

Tip

你可以去我在 https://github.com/webmasterdevlin/practical-enterprise-react/ 的 GitHub 查看我们是否在相同的安装包版本上工作。

现在让我们来设置您的 VS 代码,但是如果您愿意的话,也可以使用您喜欢的 IDE。

设置 Visual Studio 代码(混合文本编辑器)

我们需要熟悉我们的文本编辑器 VS 代码,以帮助我们获得最佳的开发者体验。

安装 VS 代码扩展市场中可用的扩展,根据您的喜好定制您的 IDE:

  • vscode-icons :灯光主题。

  • 材质图标主题:为深色主题。

  • 代码拼写检查器:帮助你发现常见的拼写错误。错误的主要原因之一是打字错误。

  • 导入成本:这让你对你的库的文件大小有一个概念。

  • Version lens :给你一个简单的方法来更新你的软件包,并检查是否有可用的更新。在对主要版本进行更新时要小心,以免破坏您的代码。次要版本或补丁可能是安全的:

    • 单击向上箭头进行更新。进行更改后,再次运行 npm 安装。
  • 路径自动完成:为 VS 代码提供路径自动完成。

  • ES7 React/Redux/graph QL/React-Native snippets:为您的语法提供信心,因为它在您编码时提供了 snippers。

  • Live Share :允许您与其他人实时协作编辑和调试,无论使用或开发的是什么编程语言或工具。

  • ESLint :一个优秀的 linter,它在你运行代码之前检测并警告你语法错误。

  • Chrome 调试器(Debugger for Chrome):VS 代码扩展,用于调试 Google Chrome 浏览器或任何支持 Chrome DevTools 协议的目标中的 JS 代码。

摘要

让我们总结一下到目前为止我们在本章中学到的东西。

我们为 VS 代码安装了各种扩展,以帮助我们在构建应用时获得更好的开发体验。

我们讨论了如何选择正确的样板,在开始构建您的应用时使用样板的优缺点,以及为什么我们选择在我们的应用中使用 React 样板 CRA 模板。

最后但同样重要的是,我们列出了所有已安装的软件包库及其相应的版本号,以确保您在随书一起编码时会使用相同的版本号。

在下一章,我们将继续 React Router,React 的标准路由库,并创建 SPAs,或具有动态路由功能的单页应用。**

五、React 路由的导航

本章使用 React Router 处理导航,React Router 是一个用于在 web 应用中处理和管理路线的特定库,我们将按如下方式处理:

  1. 在根目录下创建一个 JavaScript 文件 router.js。对于 TS 文件,我们将使用 router.tsx。

  2. 使用 react-router-dom 中的路由来定义路径和与路径相关联的组件,例如:

  3. 从 react-router-dom 包装我们在交换机内部创建的所有路由,例如:

<Route path={"/hello"} component={HelloComponent}/>

  1. 使用 react-router-dom 中的链接成功导航到任何页面,例如:
<Switch><Router .../> and more...</Switch>

<Link to="/hello">Hello</Link>

在这个过程中,我们将学习如何在我们正在构建的应用中使用我们首选的设计库,即 Material-UI。

我们还将讨论组件的延迟加载或代码分割,以加快初始加载时间并改善用户体验。在许多相对较小的 React SPAs(单页应用)中,代码分割的概念可能是不必要的,或者可能对性能没有太大影响。然而,如果我们有一个很大的应用,比如说,有一个管理仪表板和一个客户门户,那么在初始加载时尝试加载整个应用并不是一个好主意。

为什么选择 React 路由?

React 路由是 React 的动态路由库。它由以下包组成:react-router、react-router-dom 和 react-router-native。核心是react-router,,而另外两个是特定于环境的。React-router-dom是针对 web 应用,r eact-router-native是针对使用 React Native 的移动应用。

每当我们需要通过我们的 React 应用在多个视图中导航时,路由都会管理 URL。React Router 使我们的应用 UI 和 URL 彼此保持同步。这些好处包括:

  • 它是可堆肥的。

  • 向网页添加链接很容易。

  • 它可以将路由添加到 spa 上的几个不同页面。

  • 它根据来自 URL 的路由有条件地呈现组件。

为什么是 Material-UI?

使用 UI 库的一个基本原因是它节省了我们设计应用的时间。我们不需要重新发明轮子,我们可以使用现成的东西,更重要的是,它已经经过了社区的考验。

当然,选择 UI 设计取决于您的项目规范。不过,通常情况下,我会先看看其他开发人员最推荐和最流行的,比如 Material-UI。同样,我会查看 GitHub 上的活跃贡献者,帮助维护项目的贡献者。在 npmjs.com 中,Material-UI 的周下载量约为 140 万次。

您在应用中可能遇到的任何问题也很有可能已经得到解决,并记录在 Stack Overflow 或其他网站上。

其他值得一试的顶级 UI 设计包括 Ant Design 和 React-Bootstrap。我们将在后面的章节中更多地讨论 React 组件的样式。

入门指南

让我们开始为我们的应用构建 React 路由导航。首先,让我们对一些默认文件进行一些编辑,然后一步一步地重新构建它。

在你的src/app/ folder中,删除以下两个目录:containers/HomePagecomponents/NotFoundPage.见图 5-1 。

img/506956_1_En_5_Fig1_HTML.jpg

图 5-1

删除文件夹:容器和组件

它应该会破坏应用中的某些东西。因此,打开文件src/app/index.tsx,用下面的代码行更新文件:

export function App() {
  return (
    <BrowserRouter>
      <Helmet
        titleTemplate="%s - React Boilerplate"
        defaultTitle="React Boilerplate"
      >
        <meta name="description" content="A React Boilerplate application" />
      </Helmet>

      <Switch>
        <Route exact path= '/' component={} />

      </Switch>
      <GlobalStyle />
    </BrowserRouter>
  );
}

Listing 5-1Updating index.tsx

接下来,在 app 目录下,创建另一个名为views的文件夹。在views下,创建另一个名为pages的文件夹。最后,在pages目录中,创建一个新文件,并将其命名为Home.tsx。文件夹结构见图 5-2 。

img/506956_1_En_5_Fig2_HTML.jpg

图 5-2

新文件夹结构

如果使用 VS 代码,打开文件Home.tsx并输入代码片段rafce(React 箭头函数导出组件的缩写)。在 WebStorm 中,片段是rsc(React 无状态组件的简称,没有 prop 类型和 ES6 模块系统)。

图 5-3 展示了如何在我们的代码编辑器中使用代码片段。确保您已经从 VS 代码市场安装了扩展 ES7 React/Redux/graph QL/React-Native snippets。

img/506956_1_En_5_Fig3_HTML.jpg

图 5-3

键入 rafce 片段,react arrow 函数导出组件的缩写

如清单 5-2 所示,在返回声明<h1>Home Page</h1>中添加标题。

import React from 'react';

const Main = () => {
  return (
    <div>
      <h1>Home Page</h1>
    </div>
  );
};

export default Main;

Listing 5-2Adding h1 Heading to the Home Page

在我们的app/index.tsx文件中,我们将 Home 组件添加到我们的 Route 路径中,如清单 5-3 所示。

export function App() {
  return (
    <BrowserRouter>
      <Helmet
        titleTemplate="%s - React Boilerplate"
        defaultTitle="React Boilerplate"
      >
        <meta name="description" content="A React Boilerplate application" />
      </Helmet>

      <Switch>
        <Route exact path="/" component={Home} />
      </Switch>
      <GlobalStyle />
    </BrowserRouter>
  );
}

Listing 5-3Adding the Home Component to routes.tsx

当您运行应用并转到默认的localhost:3000时,您应该会看到呈现的页面。

图 5-4 是在 UI 中呈现的主页。

img/506956_1_En_5_Fig4_HTML.jpg

图 5-4

将主页呈现给用户界面

我们在这里做了什么?我们已经展示了使用 React Router 导航应用的简单方法。

接下来,让我们创建另一个文件,并将其命名为material-buttons.tsx,,路径如下:app/components/material-buttons.tsx.

这里我们将在 UI 中呈现一些按钮。我们将使用来自我们之前导入的 Material-UI 的设计。去 Material-UI 的网站,具体是按钮组件。

图 5-5 显示了到 Material-UI 网站的链接。

img/506956_1_En_5_Fig5_HTML.jpg

图 5-5

使用材质 UI 按钮组件

选择包含的按钮设计。确保选择 TypeScript 选项,复制完整的源代码并粘贴到我们的material-buttons.tsx文件中。

Important Note

将文件从ContainedButtons重命名为MaterialButtons

之后,转到Home.tsx使用新创建的材质 UI 按钮组件,如清单 5-4 所示。

import React from "react";
import MaterialButtons from 'app/components/material-buttons’;

const Home = () => {
  return (
    <div>
          <h1>Main Page</h1>
          <MaterialButtons/>
    </div>
  );
};

export default Home;

Listing 5-4Copying the Material-UI Button Component

在您的本地主机中,您应该会看到以下变化,如图 5-6 所示。

img/506956_1_En_5_Fig6_HTML.jpg

图 5-6

呈现材质-UI 按钮组件

既然我们已经有了可以在设计中使用 Material-UI 库的概念证明,我们就可以开始配置路由和导航了。

但是首先,让我们快速回顾一下 React 路由的安装。

基础概述

通过安装作为样板文件一部分的react-router-dom,我们可以访问index.tsx中的以下三个组件:<BrowserRouter>, <Route>,<Switch>.

打开您的index.tsx文件,让我们逐一讨论:

<BrowserRouter>是基本配置。它包装了其他组件,并使它们与 URL 保持同步。

<Switch>是 web 应用的动态部分。顾名思义,它会根据 URL 进行更改或动态切换。Switch 组件确保将呈现匹配 URL 位置的第一个路由子组件。

<Route>需要一个“路径属性”,当它获得匹配的或精确的 URL 路径时被呈现。

现在让我们看看如何在应用中导航 URL 路径,从一个组件到达下一个组件。

React-router-dom允许我们改变路径的几种方式,包括最常见的一种叫做<Link>的标签,我们会在我们的应用中经常用到。为了更清晰的代码和关注点的分离,我们将在the app directory and name it routes.tsx.创建一个新文件

创建 routes.tsx

我们的路线路径可能会变得更长,因此我们将创建一个新文件,并将其命名为routes.tsx .,而不是在我们的app/index.tsx文件中构建所有路线

以下是文件路径:

app ➤ routes.tsx

接下来,我们将把 index.tsx 中的<Switch>组件移动到我们新创建的routes.tsx中,如清单 5-5 所示。

import React, { lazy, Suspense } from 'react';
import { Switch, Route } from 'react-router-dom';
import Home from './views/pages/Home';

const Routes = () => {
  return (
      <Switch>
        <Route exact path="/" component={Home} />
       </Switch>
);
}
export default Routes;

Listing 5-5Moving Switch and Route Components to routes.tsx

现在我们需要使用我们在index.tsx,新创建的<Routes />,如清单 5-6 所示。

export function App() {
  return (
    <BrowserRouter>
      <Helmet
        titleTemplate="%s - React Boilerplate"
        defaultTitle="React Boilerplate"
      >
        <meta name="description" content="A React Boilerplate application" />
      </Helmet>
      <Routes/>
      <GlobalStyle/>
    </BrowserRouter>
  );
}

Listing 5-6Using the Routes in index.tsx

让我们创建另一个视图页面,并将其命名为路径 views/pages 下的AboutPage.tsx。添加一个

这是关于页面

清单 5-7 是关于页面组件。

import React from 'react';

const AboutPage = () => {

return (
  <div>
   <h1>This is the About Page</h1>
  </div>
);
 };
export default AboutPage;

Listing 5-7Creating AboutPage and Adding h1 Heading

不要忘记在 routes.tsx 中添加新组件,并在顶部导入 About Page 组件:

<Route exact path="/about" component={AboutPage} />

检查你的localhost:3000/about看看它是否还在工作。

Note

当构建一个应用时,我通常在编写任何代码之前收集需求。我总是检查我是否需要为应用创建一个仪表板或一个管理仪表板。我们尽可能首先制定路由,并确定是否有嵌套路由。

现在,我们可以在应用中导航 URL 路径。第一项任务是构建一个仪表板。

构建仪表板

进入 Material-UI 的网站,搜索一个导航栏或应用栏。他们是一样的。抓取简单 App Bar 的 TS 源代码。见图 5-7 中简单 App 栏截图。

img/506956_1_En_5_Fig7_HTML.jpg

图 5-7

Material-UI 网站上的简单应用栏截图

导航栏

navbar组件是我们放在应用顶部的东西,它允许我们在不同的页面之间切换,比如Home, About, Login,等。

在文件夹 components 下创建一个新文件,并将其命名为navigation-bar.tsx.

粘贴从 Material-UI 简单应用栏抓取的源代码(见图 5-8 )。不要忘记将默认文件名从ButtonAppBar更改为NavigationBar.

此时,让我们删除文件material-buttons.tsx,因为我们不再需要它了。我们创建它只是为了快速展示如何在组件页面中使用材质 UI 设计。

之后,再次打开app/index.tsx,渲染NavigationBar。确保将它放在<Routes/>组件的顶部或之前,以将导航栏永久定位在 web 应用的顶部区域,如清单 5-8 所示。

export function App() {
  return (
    <BrowserRouter>
      <Helmet
        titleTemplate="%s - React Boilerplate"
        defaultTitle="React Boilerplate"
      >
        <meta name="description" content="A React Boilerplate application" />
      </Helmet>
      <NavigationBar />
      <Routes/>
      <GlobalStyle/>
    </BrowserRouter>
  );
}

Listing 5-8Using the NavigationBar in index.tsx

检查您的localhost:3000以查看 UI 中的导航栏。

img/506956_1_En_5_Fig8_HTML.jpg

图 5-8

在浏览器中显示导航栏

我们将在接下来的几章中详细讨论各种 React 样式方法。但同时,我只想指出,Material-UI 的样式解决方案来自许多其他样式库,如 styled-components。

Material-UI 的核心是使用在运行时和服务器端工作的 CSS-in-JS。我们将在后面的章节中更多地讨论 CSS-in-JS。

看看 Material-UI 中的导航栏样式组件。我们使用 makeStyles,它基本上允许我们为每个样式表创建多个样式规则。然后,我们将在 NavigationBar 组件内部调用返回的函数 useStyles:

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    root: {
      flexGrow: 1,
    },
    menuButton: {
      marginRight: theme.spacing(2),
    },
    title: {
      flexGrow: 1,
    },
  }),
);

在我看来,Material-UI 是最好的样式库之一,尤其是对于 React 应用。这些组件是可重用的、快速的和声明性的。在其 GitHub 上,它也有超过 63K 颗恒星,这意味着我并不孤单,在我看来。

添加导航链接

让我们在导航栏中添加一些按钮和导航链接,并将它们命名如下——<Home> <About> <Dashboard> – as shown in列表 5-9 。

不要忘记从“react-router-dom”导入命名组件:

import { Link } from 'react-router-dom';

Note

我们将在本章后面详细讨论 React 应用可以使用的不同导航路径,但是现在,让我们先构建我们的组件。

import React from 'react';
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Button from '@material-ui/core/Button';
import { Link } from 'react-router-dom';
import { colors } from '@material-ui/core';

export default function NavigationBar() {
  const classes = useStyles();

  return (
    <div className={classes.root}>
      <AppBar position="static">
        <Toolbar>
          <Link className={`${classes.link} ${classes.title}`} to={'/'}>
            LOGO
          </Link>
          <Button color="inherit">
            <Link to={'/'}>
              Home
            </Link>
          </Button>
          <Button color="inherit">
              About
          </Button>
          <Button color="inherit">
              Dashboard
          </Button>
          <Button color="inherit">
              Login
          </Button>
        </Toolbar>
      </AppBar>
    </div>
  );
}

Listing 5-9Adding Navigation Links to the navbar

同时,用 logo 替换单词“News ”,因为那是我们稍后要放置图标 LOGO 的地方。

我们还添加了链接样式对象。参见清单 5-10 中的编辑。

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    root: {
      flexGrow: 1,
    },
    menuButton: {
      marginRight: theme.spacing(2),
    },
    link: {
     color: colors.lightBlue[50],
     textDecoration: 'none',
     },
    title: {
      flexGrow: 1,
    },
  }),
);

Listing 5-10Adding a link Style Object

正如你在清单 5-11 中看到的,我们现在可以使用保留的 React 单词<className>.在我们的按钮导航链接中使用它

首先,我们通过调用useStyles钩子并将其存储在一个变量中来使用它。出于可读性的考虑,我们将变量命名为classes:

  const classes = useStyles();

然后让我们将导航链接添加到其余的按钮上。

export default function NavigationBar() {
  const classes = useStyles();

  return (
    <div className={classes.root}>
      <AppBar position="static">
        <Toolbar>
          <Link className={`${classes.link} ${classes.title}`} to={'/'}>
            LOGO

          </Link>
          <Button color="inherit">
            <Link className={classes.link} to={'/'}>
              Home
            </Link>
          </Button>
          <Button color="inherit">
            <Link className={classes.link} to={'/about'}>
              About
            </Link>
          </Button>
          <Button color="inherit">
            <Link className={classes.link} to={'/dashboard'}>
              Dashboard
            </Link>
          </Button>
          <Button color="inherit">
            <Link className={classes.link} to={'/login'}>
              Login
            </Link>
          </Button>
        </Toolbar>
      </AppBar>
    </div>
  );
}

Listing 5-11Adding the Navigation Links and CSS Class Objects in the NavigationBar

在我们的 LOGO 组件中,注意我们需要使用反勾号``来使用按钮组件中的两个 CSS 类对象。

Link className={`${classes.link} ${classes.title}`} to={'/'}> LOGO </Link>

Listing 5-12Using Backticks for Two or More CSS Class Objects

请务必清理任何未使用或变灰的导入库。

接下来,让我们在 views/pages 下添加另一个组件,并将其命名为NotFoundPage.tsx.

再次键入代码片段“rafce”,以便为我们创建无状态箭头组件。并且还添加了标题

404 页面未找到

import React from 'react';

const NotFoundPage = () => {
  return (
      <div>
         <h1>404 Page Not Found</h1>
      </div>
      )
 }
export default NotFoudPage;

Listing 5-13Creating a 404 Not Found Page

别忘了定义NotFoundPage after importing it in routes.tsx的路径:

import NotFoundPage from './views/pages/NotFoundPage';
<Route exact path="/not-found"component={NotFoundPage} />

导航 React 路由:

在我们的routes.tsx中,让我们添加<Redirect />,,幸运的是它已经内置在 react-router-dom 中。

有许多将用户重定向到特定页面的用例。一般来说,如果我们需要改变 URL 路径而不需要用户点击该路径的链接,我们使用<Redirect />

这里有几个条件:

  • 当用户访问受限页面时。

  • 当用户正在访问找不到或不再存在的页面时。

  • 当用户在地址栏中键入错误的路径时(输入错误!).

  • 登录成功,用户现在被定向到主页或仪表板。

现在,让我们为所有没有在 404 页面上定义的路径创建一个<Redirect />:

<Route path={'/not-found'} component={NotFoundPage} exact />
        <Redirect from={'*'} to={'/not-found'} exact />

在我们结束这一部分之前,让我们在页面上做一些样式设计,尤其是填充和边距。

添加容器样式的类组件

转到app/index.tsx,,我们将使用 Material-UI 将<Routes/>包装在一个容器中。从 Material-UI 导入命名组件,直接使用包装 Routes 组件:

...
import { Container } from '@material-ui/core';

<Container>
    <Routes />
</Container>

检查浏览器,你会注意到,如图 5-9 所示,标题< h1 >不再被推到左上角。该样式适用于在<Container> <Routes/> </Container>中包装的所有页面。

img/506956_1_En_5_Fig9_HTML.jpg

图 5-9

使用材质中的容器-用户界面

但是不要太担心这些页面的特定样式。我们只是向您展示使用 Material-UI 来设计我们的应用的简单方便。

请记住,本书的主要目标是指导您构建企业级应用,并让您了解开发人员如何类似地开发他们的应用。最后,你还是要根据你的具体需求来决定如何制作你的应用。

那么仪表盘呢?让我们现在就开始吧。

创建仪表板布局

app文件夹中,创建一个名为layouts,的文件夹,然后创建一个子文件夹,命名为dashboard-layout

在仪表板布局文件夹中,创建文件dashboard-sidebar-navigation.tsx

路径是 app布局仪表盘-布局仪表盘-工具条-导航. tsx

让我们用下面的代码填充新创建的文件。首先,导入以下库。

import React, { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { createStyles, makeStyles } from '@material-ui/core/styles';
import Drawer from '@material-ui/core/Drawer';
import Toolbar from '@material-ui/core/Toolbar';
import { useRouteMatch } from 'react-router';

Listing 5-14Importing Libraries to dashboard-sidebar-navigation.tsx

好了,在我们继续之前,让我们简要解释一下我们在这里引入的几个不熟悉的库。我们将从react-router-dom中姗姗来迟的<Link>和应用中导航路径的其他方式开始。

导航路径:

来自react-router-dom<Link>允许用户导航应用,并在不刷新的情况下重新呈现页面。它为我们的应用提供了一个简单明了的导航路径。

我们可以使用它的一些常见方法:

  • to: string:通过连接位置的路径名、搜索和哈希属性创建。比如:<Link to="/products/?sort=name" />

  • to: object:这可以是以下任何属性:路径名、搜索、散列和状态。

  • replace: bool:如果为 true,单击链接将替换历史栈中的当前条目,而不是添加新条目。比如:<Link to="/products" replace />

  • to: function:当前位置作为参数传递,位置表示应该作为对象或字符串返回。例如:

<Link to={location => ({ ...location, pathname: "/products" })} />
<Link to={location => `${location.pathname}?sort=name`} />

React 钩子:使用者游戏

useRouteMatch是一个钩子,我们在使用<Route>.时通常需要它,useRouteMatch 允许我们访问一个<match>对象,并在组件内部使用它;所以不用渲染一个<Route>,,直接用useRouteMatch.

好了,现在我们已经讨论了我们在这里导入的一些库,现在让我们创建我们的DashboardSidebarNavigation组件。

const DashboardSidebarNavigation = () => {
  const classes = useStyles();
  const { url } = useRouteMatch();

  useEffect(() => {}, []);

  return (

      <div className={classes.root}>
        <Drawer
          className={classes.drawer}
          variant="permanent"
          classes={{
            paper: classes.drawerPaper,
          }}
          anchor="left"
        >
          <Toolbar
            style={{ width: '6rem', height: 'auto' }}
            className={classes.toolbar}
          >
            <Link to={`${url}`} className={classes.logoWithLink}>
              Logo
            </Link>
          </Toolbar>

        </Drawer>
      </div>

  );
};

export default DashboardSidebarNavigation;

Listing 5-15Creating the DashboardSidebarNavigation Component

我们使用 Material-UI 抽屉和工具栏的样式,并使用组件作为我们的导航路径。

样式组件来自 Material-UI。

const drawerWidth = 240;

const useStyles = makeStyles(theme =>
  createStyles({
    root: {
      display: 'flex',
    },
    drawer: {
      width: drawerWidth,
      flexShrink: 0,
    },
    drawerPaper: {
      width: drawerWidth,
    },
    drawerContainer: {
      overflow: 'auto',
    },
    toolbar: theme.mixins.toolbar,
    content: {
      flexGrow: 1,
      padding: theme.spacing(3),
    },
    link: { textDecoration: 'none', color: 'inherit' },
    logoWithLink: {
      display: 'flex',
      alignItems: 'center',
      textDecoration: 'none',
      color: 'inherit',
    },
  }),
);

Listing 5-16Styling the dashboard-sidebar-navigation Using Material-UI

完成dashboard-sidebar-navigation.tsx,后,在dashboard-layout文件夹下创建一个index.tsx并复制下面的代码。

import React from 'react';
import { Grid } from '@material-ui/core';

import DashboardSidebarNavigation from './dashboard-sidebar-navigation';

type Props = {
  children: React.ReactNode;
};

const Dashboard = ({ children }: Props) => {
  return (
    <Grid
      container
      direction="row"
      justify="flex-start"
      alignItems="flex-start"
    >
      <DashboardSidebarNavigation /> {children}
    </Grid>
  );
};

export default Dashboard;

Listing 5-17Creating the Dashboard Component

仪表板组件

这是怎么回事?我们有主仪表板组件及其子属性,它们是主要的内容页面。子属性为用户提供了从一个视图或页面成功导航到另一个视图或页面的路由。

请记住,仪表板中的 children 参数是 React 组件的入口点。例如,如果我们在仪表板中传递一个按钮组件,它将在 DashboardSidebarNavigation 组件旁边呈现。

我们现在有了一个包含 DashboardSidebarNavigation 的可重用仪表板布局。

至于<Grid>,,它用于布局的定位。基于 12 列网格布局的网格响应布局非常强大,因为它可以适应任何屏幕大小和方向。

现在我们已经有了布局,下一步是创建一个仪表板默认组件,这是用户在仪表板中看到的第一个东西。

在视图中,创建一个新的仪表板文件夹,并在其中创建两个新文件:dashboard-default-content.tsxsettings-and-privacy.tsx

路径看起来像这样:

 Views ➤ dashboard ➤ dashboard-default-content.tsx
 Views ➤ dashboard ➤ settings-and-privacy.tsx

键入代码片段“rafce”创建一个无状态箭头组件,并返回一个标题与组件名称相同的

。请参见下面的示例。

此外,请确保在组件名称中重命名并使用 Pascal 命名约定。

import React from 'react';

const DashboardDefaultContent = () => {
  return (
    <div>
      <h1>Dashboard Default Content</h1>
    </div>
  );
};

export default DashboardDefaultContent;

Listing 5-18Creating the DashboardDefaultContent Component

settings-and-privacy.tsx文件做同样的事情。

import React from 'react';

const SettingsAndPrivacy = () => {
  return (
    <div>
      <h1>Settings and Privacy</h1>
    </div>
  );
};

export default SettingsAndPrivacy;

Listing 5-19Creating the SettingsAndPrivacy Component

目前,我们只是设置了没有样式的仪表板和导航,因为我们只是想展示一个概念证明,我们能够成功地从一个页面导航到下一个页面。

现在,让我们在routes.tsx中定义路径。但在这种情况下,我们将使用渲染属性。

渲染属性

www.reactjs.org 网站上,术语渲染 属性指的是“使用一个值为函数的属性在 React 组件之间共享代码的技术。”

好吧,简单地说,这意味着我们的路由组件有一个渲染属性,它将一个函数作为一个值。然后,路由组件使用该函数来呈现该函数提供的任何内容——在我们的例子中,就是仪表板。

实际上,我们只是以一种有效的方式重用代码。

<Route path={'/dashboard'}
                render={({match: {path}}) => (
                    <Dashboard>
                        <Switch>
                            <Route exact path={path + '/'}
                                component={DashboardDefaultContent}/>

   <Route exact path={path + '/settings-and-privacy'}
                component={SettingsAndPrivacy}/>

                        </Switch>
                    </Dashboard>
                   )}>
 </Route>

Listing 5-20Using the Render Props in routes.tsx

在 React 路由库中,<Route />具有导航到不同视图或页面的行为。当路径匹配时,它呈现组件。

接下来,让我们更新dashboard-sidebar-navigation.tsx来添加设置和隐私按钮以及注销按钮。

为了有一点风格,首先,让我们导入组件,如清单 5-21 所示,从 Material-UI 到我们的dashboard-sidebar-navigation.tsx

import { List, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
import SettingsIcon from '@material-ui/icons/Settings';

Listing 5-21Importing the Material-UI Components to dashboard-sidebar-navigation.tsx

仪表板侧边栏导航的附加样式组件如清单 5-22 所示。

紧接在之后且仍在内,添加以下代码:

<return (

      <div className={classes.root}>
        <Drawer
          className={classes.drawer}
          variant="permanent"
          classes={{
            paper: classes.drawerPaper,
          }}
          anchor="left"
        >
          <Toolbar
            style={{ width: '6rem', height: 'auto' }}
            className={classes.toolbar}
          >
            <Link to={`${url}`} className={classes.logoWithLink}>
              Logo
            </Link>
          </Toolbar>
          <div className={classes.drawerContainer}>
            <List>
              <Link className={classes.link} to={`${url}/settings-and-privacy`}>
                <ListItem button>
                  <ListItemIcon>
                    <SettingsIcon />
                  </ListItemIcon>
                  <ListItemText primary={'settings and privacy'} />
                </ListItem>
              </Link>
              <a className={classes.link} href={'/'}>
                <ListItem button>
                  <ListItemIcon>
                    <ExitToAppIcon />
                  </ListItemIcon>
                  <ListItemText primary={'logout'} />
                </ListItem>
              </a>
            </List>
          </div>
        </Drawer>
      </div>

  );
};

Listing 5-22Additional Components and Styling for dashboard-sidebar-navigation.tsx

如果您在我们的 UI 中单击“Dashboard”按钮,您应该能够看到以下附加组件:

设置和隐私

注销

img/506956_1_En_5_Fig10_HTML.jpg

图 5-10

仪表板默认内容用户界面

如果您单击设置和隐私,您应该可以成功导航到该页面。当你点击主页、关于或登录时也是一样——最后一个页面目前只是一个 404 页面,因为我们还没有构建它。此外,注销,现在,只是基本上刷新页面。

进入 React.lazy()

但是等等。我们还没完呢。我们仍然可以改进我们的路线导航。我们可以在惰性加载的帮助下做到这一点,惰性加载是加速我们的包的加载时间的最有效的方法之一。

那么什么是懒加载呢?

惰性加载的概念很容易理解。它仅仅是首先呈现用户界面中最重要的部分,同时根据需要悄悄地加载不重要的部分或项目。

React 中的延迟加载是 16.6.0 版本中发布的一个相对较新的特性。随着应用变得越来越复杂、强大和庞大,导致加载时间增加,并对用户体验产生负面影响。

作为最佳实践,我们应该始终考虑用户,包括那些使用移动数据和慢速互联网连接的用户。

Note

React.lazy(),现在已经完全集成到核心的 React 库中,已经取代了一个第三方库 react-loadable。

但是使用 react-loadable 进行服务器端代码拆分,因为 React.lazy()和悬念还不能用于服务器端渲染。

React lazy loading 允许我们使用代码分割缓慢或逐渐加载组件,而无需安装额外的库。这种方法使我们能够在初始加载时呈现必要的或关键的界面项,同时根据需要懒散地、安静地展开其他部分。

根据 React 官方文档,在 React 应用中进行代码拆分的最佳方式是通过动态导入()语法。急切加载是我们的 web 应用的默认行为,这意味着如果你下载所有的页面资源,例如,主页,那么一切都将被一口气下载完。

这样做的潜在问题是,如果应用很大,它会产生一个瓶颈——因此,我们需要进行代码拆分。

让我们展示一些代码拆分前后的示例代码。

import MyComponent from './MyComponent';

function OneComponent() {
     return (
          <div>
               <MyComponent />
          </div>
     );
}

Listing 5-23Showing Eager Loading vs. Lazy Loading

const MyComponent = React.lazy(() => import('./MyComponent'));

function OneComponent() {
     return (
           <div>
                <MyComponent />
           </div>
     );
}

注意,使用React.lazy()创建的组件需要包装在 React 中。悬念,所以我们现在来回顾一下。

React 悬念

当惰性组件被加载时,悬念就像占位符内容。请注意,我们可以用一个暂记组件在不同的层次级别包装几个惰性组件。

一个小小的警告:在 React 官方文档中,悬念仍然是一个实验性的 API,在 React 团队表示它已经完全可以投入生产之前,它仍然可能会发生一些变化。

然而,这个已经发布了几年的通知并没有阻止 React 开发者在大型应用或生产级应用中使用 React.lazy()和悬念。

提示:为了获得最佳的用户体验,如果你的懒惰组件加载失败,就将它封装在一个错误边界<ErrorBoundary />中。

让我们看看 DevTools,展示一下主页和 About Page 的急切加载。一切一气呵成,如图 5-11 所示。

img/506956_1_En_5_Fig11_HTML.jpg

图 5-11

DevTools 中急切加载主页和 About 页面的截图

你会看到所有的组件甚至在应用初始加载时就已经加载了。这意味着,即使您从主页导航到“关于”页面,也不会添加任何新内容。

现在,让我们来看看一个急切加载的组件和一个延迟加载的组件之间的区别。

急切加载与缓慢加载

现在,让我们比较一个包装在延迟加载中的组件和一个渴望加载的组件。

在我们的routes.tsx中,导入命名的组件:

import React, {lazy, Suspense}from 'react';

但是在我们比较这两个组件之前,我们需要用 React 悬念来包装我们的懒惰组件。所以让我们现在就开始吧。

接下来,为了便于比较,我们将延迟加载以下组件: About 和 Dashboard

const Routes = () => {
  return (
    <Suspense>
    <Switch>
      {/*eager loading */}
      <Route exact path="/" component={Home} />

      {/*lazy loading */}
      <Route exact path="/about" component={AboutPage} />

      {/*lazy loadings*/}
        <Route
         exact path={'/about'}
          component={lazy(() => import('./views/pages/AboutPage'))}
           />

        <Route
           exact path={'/dashboard'}
          render={({ match: { path } }) => (
            <Dashboard>
              <Switch>
                <Route
                  path={path + '/'}
                  component={lazy(
                    () => import('./views/dashboard/dashboard-default-content'), )}
                />

              <Route
                exact
                path={path + '/settings-and-privacy'}
                component={SettingsAndPrivacy}
              />
            </Switch>
          </Dashboard>
        )}
      ></Route>

      <Route exact path="/not-found" component={NotFoundPage} />
      <Redirect from={'*'} to={'/not-found'} exact />
    </Switch>

    </Suspense>
  );
};

Listing 5-24Lazy Loading the Components About and Dashboard for Comparison

但是等等。如果您查看浏览器,您会看到以下错误。

img/506956_1_En_5_Fig12_HTML.jpg

图 5-12

未能编译 React 悬念

这是因为悬念需要属性“回退”

React 查找树,遇到第一个<Suspense>组件,并呈现其回退。

使用所需的暂记属性 fallback,并且不要忘记导入命名的组件。线性进度条来自 Material-UI。

...
import { LinearProgress } from '@material-ui/core';

export const Routes = () => {
  return (
    <Suspense fallback={<LinearProgress style={{ margin: '10rem' }} />}>

Listing 5-25Using the Suspense Prop fallback

好了,这应该让我们的悬念部分工作了。你也可以延迟加载设置和隐私,但是一定要让主页保持即时加载,这样我们就可以看到比较了。

刷新浏览器。刷新主页的同时打开 DevTools,如图 5-13 所示。

img/506956_1_En_5_Fig13_HTML.jpg

图 5-13

在初始加载时急切加载主页

但是,当您单击延迟加载的 About 页面时,您可以看到突出显示的部分只是添加的部分。突出显示的部分最初没有呈现在主页上,因为还不需要它们。参见图 5-14 。

img/506956_1_En_5_Fig14_HTML.jpg

图 5-14

延迟加载关于页面

当您单击延迟加载的仪表板时,您会注意到同样的情况。参见图 5-15 。

img/506956_1_En_5_Fig15_HTML.jpg

图 5-15

延迟加载仪表板

当我们从 About 页面导航到 Dashboard 页面时,突出显示的部分是唯一加载的内容。

你可能认为文件和加载时间可能无关紧要或很少,但是想象一下如果你的应用已经有了大量的代码,你有 50 页或更多。你的用户,尤其是使用移动应用或网速较慢的用户,会感觉到这一点。

摘要

我希望到目前为止,您已经对客户端路由的一般工作原理以及如何使用 React 路由库在 React 中实现路由有了一个大致的了解。我们还谈到了如何在我们的应用中使用延迟加载来加速我们的初始加载时间,最终目标是改善用户体验。

在下一章,我们将处理写本地状态、发送 HTTP 请求和使用 ApexCharts。

六、编写本地状态、发送 HTTP 请求和 ApexCharts

在前一章中,我们用react-router解决了导航问题。现在,我们将编写本地状态,使用 Axios 发送 HTTP 请求,并安装 ApexCharts 来帮助我们在应用中创建交互式图表。

HTTP 请求是任何与后端服务器通信的 web 应用的基本部分;与此同时,顾名思义,React 中的本地状态是在本地处理的,或者是在一个组件中独立处理的。必须设置状态中的任何更改,否则将使用 setState 函数重新呈现状态。

Axios 和 Fetch API 是我们如何使用 REST APIs 的两种最流行的方法。Axios 是基于 promise 的 HTTP 客户端,而 Fetch API 是 JavaScript 原生的,是现代浏览器内置的 HTTP 客户端。

在这一章中,我们将尽最大努力使用 Axios 进行 API 请求,以保持条理和干爽。编程中的 DRY 概念是抽象的过程,以避免代码的重复。由于从技术上来说,我们没有后端服务器,我们还将使用 JSON 服务器创建一个假的 Rest API。

ApexCharts 是一个开源图表库,我们将使用它来为我们的网页创建现代外观和交互式可视化。

但是在我们开始编写本地状态和发送 HTTP 请求之前,我们将在 TypeScript 的编译器选项中进行一些重构。

这更多是为了我们在应用中使用 TypeScript 时的个人喜好。是的,我们喜欢 TypeScript 给我们的类型安全特性,而 JavaScript 不能;然而,我们也不想失去 JavaScript 在开发应用时提供的灵活性。简而言之,我们希望两全其美。

您可以决定是否退出 TypeScript 中默认的严格类型检查选项。有两种方法可以解决这件事。

TypeScript 中的严格类型检查选项

如前所述,在我们的应用中,有两种方式可以让我们个人使用或设置 TypeScript 中的类型检查选项。

第一个是将值设置或更改为“false”为此,在您的项目应用中,打开 tsconfig.json 文件并在“compilerOptions”下查看。将严格值标志设置为 false。

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "noFallthroughCasesInSwitch": true,
    "jsx": "react",
    "baseUrl": "./src"
  },
  "include": ["src"]
}

Listing 6-1tsconfig.json File

默认情况下,tsconfig.json 中 compilerOptions 的 strict 模式设置为true。这意味着应用中的所有代码或文件都将使用严格模式进行验证或类型检查。

更多 tsconfig 参考可以查看 www.typescriptlang.org/tsconfig

但是图 6-1 是类型检查标志的简要描述。

img/506956_1_En_6_Fig1_HTML.jpg

图 6-1

类型检查选项标志

第二种方法是有时选择退出 TypeScript 中的类型检查选项,并允许默认值 strict,这是正确的,但是要在compilerOptions :中添加以下内容

"noImplicitAny": false,
"strictNullChecks": false,
    ...
    "strict": true

TypeScript Tips

img/506956_1_En_6_Figa_HTML.jpg

使用 json-server 添加虚假数据

使用我们自己的假 REST API 的主要好处是前端和后端开发工作的解耦。如果不同的团队在前端和后端工作,那么团队只需要讨论并同意 JSON 结构,然后就可以独立工作了。

在前端,使用假数据可以加快开发进程;不需要等待后端完成构建真正的 API。

设置呢?它允许我们通过本地服务器从文件系统中提供 JSON。

让我们转到我们的package.json文件来检查我们的json-server作为一个 devDependency:

"devDependencies":{
"concurrently": "5.3.0",
"cypress": "6.0.0",
"json-server": "0.16.3",
"json-server-auth": "2.0.2" } }

Json-server是一个假的 Node.js 服务器,仅仅通过运行 npmjs.com , json-server中的json-server.就被搭建了起来,被描述为“在不到 30 秒的时间内获得零编码的完整假 REST API”是的,根据经验,它做得很好。

让我们运行它来测试。将以下代码添加到您的脚本中:

"backend": "json-server --watch db.json --port 5000 --delay=1000"

在终端中,运行$ npm run backend。

您将看到以下资源端点。

img/506956_1_En_6_Fig2_HTML.jpg

图 6-2

默认端点

让我们检查一下http://localhost:5000/posts。这是因为安装了 JSONView Chrome 扩展。它在浏览器中重新格式化 JSON 响应,而不是一长串字符串,就像我们编辑器中更漂亮的一样。Firefox 浏览器中也有这个扩展。

img/506956_1_En_6_Fig3_HTML.jpg

图 6-3

渲染本地主机:5000/post

现在,让我们回到我们的项目;您会注意到在根目录中已经为我们自动生成了一个名为db.json的新文件。打开它,您将看到示例端点。

现在我们已经看到它正在工作,让我们用我们自己的假数据替换自动生成的端点。复制以下数据。

{
  "sales": [
    {
      "id": "sgacus86fov",
      "name": "This week",
      "data": [30, 40, 25, 50, 49, 21, 70, 51]
    },
    {
      "id": "saftyaf56",
      "name": "Last week",
      "data": [23, 12, 54, 61, 32, 56, 81, 19]
    }
  ]
}

Listing 6-2db.json Server Fake Data

清单 6-2 有端点“销售”,它有两个对象,分别是"This week""Last week"。它有唯一的数据让我们从数组中获取特定的对象。让我们试试 localhost:5000/sales 是否可行。在我们的本地浏览器中,您应该会看到如图 6-4 所示的响应。

img/506956_1_En_6_Fig4_HTML.jpg

图 6-4

渲染数据

使用 Axios 发送 HTTP 请求

在构建复杂或大型应用时,创建 axios 实例非常有用,尤其是当我们需要集成多个 API 时。实现默认的 Axios 实例有时会很麻烦,或者意味着在整个应用中重复代码。

您何时可以实现实例的一些示例:

  • 从路径构建完整的 URL

  • 分配默认标题

  • 贮藏

  • 网络错误的全局处理

  • 自动设置请求的授权头

  • 规范化和管理错误响应

  • 转换请求和响应正文

  • 刷新访问令牌并重试请求

  • 设置自定义实例

现在我们已经有了,让我们创建我们的 axios 配置。创建一个名为axios.ts的新文件。文件路径为src/api/axios.ts

import axios from 'axios';

/*create an instance of axios with a default base URI when sending HTTP requests*/
/*JSON Server has CORS Policy by default*/

const api = axios.create({ baseURL: 'http://localhost:5000/', });

 export default api;

 export const EndPoints = { sales: 'sales', };

Listing 6-3axios.ts

我们从 axios 导入 axios,并且我们正在使用 axios 的 create 函数,该函数将创建 axios 的一个实例,并放入名为 api 的变量。

在实例 api 中,有一个选项可以传递我们配置的对象。

目前,我们的默认配置是localhost:5000``/.``localhost:5000,因为baseURL是 axios 中任何 HTTP 请求的默认 URL。基本上,它是网址的一致或永久的部分。

在一个位置创建一个baseURL让我们能够根据需要轻松地编辑它,并在我们的应用开发中保持干燥。我们不需要在每次创建 HTTP 请求时重复输入 API 的baseURL

我们只需要通过 api 的端点(即销售)。见图 6-5 中的样品。这也将有助于我们避免在输入端点时出现输入错误。

img/506956_1_En_6_Fig5_HTML.jpg

图 6-5

如何使用 api 端点

在我们的 axios 对象中有很多选项可以使用。我们获得了这种内置的智能感知,因为 axios 是使用 TypeScript 构建的。

img/506956_1_En_6_Fig6_HTML.jpg

图 6-6

axios 对象的选项配置

好了,我们暂时结束了。随着我们创建更多端点,我们将不断更新 axios 文件。

塑造物体

在 TypeScript 中,我们通过对象类型传递数据,而在 JavaScript 中,我们通过对象分组和表示数据。简单来说,类型让我们知道一个条目是什么样子:字符串、数字、布尔、数组、函数等。

回想一下在 TypeScript 中塑造我们的对象的两种方法:接口类型别名。他们或多或少是一样的;你选择什么只是个人喜好的问题。

因为我们使用 TypeScript,所以我们需要定义对象的形状或类型。首先,创建一个新文件,并将其命名为sale-type.ts.

src/models/sale-type.ts

Listing 6-4Creating the Shape or Model of Our Object

我们销售对象的形状或模型具有字符串类型和数字数组类型的数据:

//type alias

type SaleType = {
  name: string;
  data: number[];
}

使用 Axios 发出请求

在我们继续之前,让我们回顾一下 axios 是什么。Axios 是一个基于 promise 的 HTTP 客户端,用于浏览器和 Node.js。它允许我们拦截和取消请求,并提供一个内置的功能,称为客户端保护,防止跨站点请求伪造。

让我们创建一个新文件,并将其命名为 saleService.ts:

src/services/saleService.ts

我们将导入我们最近创建的文件 api/axios、来自 TypeScript 配置的端点以及模型 sale-type。

import api, { EndPoints } from 'api/axios';
import { SaleType } from 'models/sale-type';

export async function getSalesAxios() {
  return await api.get<SaleType[]>(EndPoints.sales);
}

/* Other commonly-used api methods:
  api.post
  api.put
  api.delete
  api.patch
*/

/* The < > bracket here is a Generic type that Typescript adapted from OOP.
   It means that the return value of the getSalesAxios is an array of SaleType.
   This would likewise help us with the TypeScript intell-sense when using it.
*/

Listing 6-5Sending Http Requests

async-await 仅仅意味着 axios 函数是基于承诺的。

现在,让我们测试我们的api.get请求。

转到src/app/views/dashboard/dashboard-default-content.tsx

我们将在这里使用额外的魔法,为此,我们将使用来自React的生命周期钩子useEffect

从 React 导入命名组件。

同样,useEffect是一个生命周期挂钩,它在 React 组件呈现后运行。在这个钩子中,我们将调用getSalesAxios().

确保从services/saleService.导入组件

import React, {useEffect} from 'react';
import { getSalesAxios } from 'services/saleService';

const DashboardDefaultContent = () => {

  useEffect(() => {
  //code to run after render goes here

    getSalesAxios();
  }, []);  //ß empty array means to ‘run once’

  return (
    <div>
      <h1>Dashboard Default Content</h1>
    </div>
  );
};

export default DashboardDefaultContent;

Listing 6-6Calling the getSalesAxios in DashboardDefaultContent

Tips

在 useEffect 中传递一个空数组[ ]作为第二个参数,以限制其运行或在第一次渲染后仅运行一次 useEffect。

接下来,打开两个终端并执行以下操作:

$ npm run start
$ npm run backend

localhost:3000/刷新我们的浏览器,打开 Chrome DevTools,确保你在网络选项卡和 XHR。我们正在做所有这些所谓的我如何构建一个应用的一小步一小步的过程。

尝试先执行低层次的悬挂果实,也许写很多概念证明,或者在 DevTools 上检查是否一切都按预期方式运行。

无论如何,在你的浏览器中,点击菜单选项卡➤仪表板。

观察你的 Chrome DevTools 您应该看到响应方法:Get 和状态代码:200 OK.

点击响应选项卡,查看来自json-server.的销售对象数据

所以现在我们确信 UI 正在接收来自json-server的数据。我们发送了 HTTP 请求,并收到了来自 json-server 的 JSON 响应。

但是由于我们可能需要将 getSalesAxios 包装在一个 async-await 中,我们将在useEffect.之外调用它

我们一会儿会处理useEffect()的。

img/506956_1_En_6_Fig7_HTML.jpg

图 6-7

在 EffectCallback 中使用 async 时显示错误

因此,让我们在 useEffect 之外创建一个可用的函数,并将其命名为fetchSales.

const fetchSales = async () => {

    const data = await getSalesAxios();
    console.log(data);
  }

Listing 6-7Creating the fetchSales Function

我们解构了响应,只得到数据属性,并将其命名为“数据”

接下来,确保在useEffect.中调用新创建的函数,这就是我说我们稍后将使用useEffect()的意思。

const DashboardDefaultContent = () => {
  useEffect(() => {
    fetchSales();
  }, []);

Listing 6-8Calling fetchSales in useEffect

刷新浏览器并确保再次打开 Chrome DevTools,您应该会成功地从 json-server 获得响应。

既然我们知道可以从 json-server 中检索数据,接下来要做的就是使用 DashboardDefaultContent 中的useState创建一个本地状态。

添加 React 中的组件useState

不要忘记从文件夹services/saleService.中导入getSalesAxios

import { SaleType } from 'models/sale-type';
import React, { useEffect, useState } from 'react';
import { getSalesAxios } from 'services/saleService';

const DashboardDefaultContent = () => {

const [sales, setSales] = useState<SaleType[]>([]);

Listing 6-9Creating a Local State

我们如何更新setSales的值或数据?

在我们的fetchSales组件中,让我们调用setSales并用我们的useState.中相同类型的对象传递数据

const fetchSales = async () => {
    const { data } = await getSalesAxios();
    console.log(data);  // ← to check in the console if we are successfully getting the data
    setSales(data);
  };

Listing 6-10Updating the Data of setSales

好的,就像我之前说的,在我继续之前,我会经常做这个小小的概念证明。

让我们检查一下我们是否真的将数据传递给了 HTML 中的setSales.,键入 h2 头并呈现销售数组的长度。

return (
    <div>
      <h1>Dashboard Default Content</h1>
      <h2>{sales.length}</h2>
    </div>
  );
};

Listing 6-11Rendering the setSales

当您检查浏览器时,您应该看到 2 ,因为我们的销售数组包含两个对象。去Chrome DevToolsNetwork看实物。

现在我们知道它正在工作,下一个计划是改进仪表板的默认内容。

在 components 文件夹中,让我们为网页创建一个模板。模板页面将包含应用于我们应用中每个页面的填充、间距和其他样式。

文件路径为 src ➤ app ➤组件➤页面. tsx

导入以下组件:

import React,{ forwardRef, HTMLProps, ReactNode }from 'react'
import { Helmet } from 'react-helmet-async';

让我们在这里回顾一些组件。

转发引用:这是添加一个对 HTML 的引用。在 reactjs.org 中,说的是通过一个组件自动传递一个 ref 给它的一个子组件的技术,比如转发 refs 给 DOM 组件或者转发 refs 给高阶组件(hoc)。

头盔:添加一些标签,并允许我们建立一个 SEO 友好的应用。

接下来,我们将在清单 6-12 中定义我们的类型或类型定义。

type Props = {
  children?: ReactNode;
  title?: string;
} & HTMLProps<HTMLDivElement>;

Listing 6-12Defining the Type Alias

我们正在创建 ReactNode 类型的 prop children 和 string 类型的 prop title,两者都可以通过追加?来空化。我们还使用了HTMLDivElement.类型的HTMLProps

这是我们的页面组件,它将成为我们所有网页的可重用模板。

const Page = forwardRef<HTMLDivElement, Props>(
  ({ children, title = '', ...rest }, ref) => {
    return (
      <div ref={ref as any} {...rest}>
        <Helmet>
          <title>{title}</title>
        </Helmet>
        {children}
      </div>
    );
  },
);
export default Page;

Listing 6-13Creating a Reusable Page Component

暂时就这样了。

让我们回到我们的DashboardDefaultContent组件,给仪表板添加更多样式。

将以下样式组件添加到DashboardDefaultContent.

样式包括图表样式主题、背景颜色、数据表、图例、描边工具提示、x 轴和 y 轴等。你可以在 Material-UI 网站上了解更多相关信息。这有点长,所以请耐心听我说:

const useStyles = makeStyles(() => ({
  root: {
    minHeight: '100%',
  },
}));

const getChartStyling = (theme: Theme) => ({
  chart: {
    background: theme.palette.background.paper,
    toolbar: {
      show: false,
    },
  },
  colors: ['#13affe', '#fbab49'],
  dataLabels: {
    enabled: false,
  },
  grid: {
    borderColor: theme.palette.divider,
    yaxis: {
      lines: {
        show: false,
      },
    },
  },
  legend: {
    show: true,
    labels: {
      colors: theme.palette.text.secondary,
    },
  },
  plotOptions: {
    bar: {

      columnWidth: '40%',
    },
  },
  stroke: {
    show: true,
    width: 2,
    colors: ['transparent'],
  },
  theme: {
    mode: theme.palette.type,
  },
  tooltip: {
    theme: theme.palette.type,
  },
  xaxis: {
    axisBorder: {
      show: true,
      color: theme.palette.divider,
    },
    axisTicks: {
      show: true,
      color: theme.palette.divider,
    },
    categories: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    labels: {
      style: {
        colors: theme.palette.text.secondary,
      },
    },
  },
  yaxis: {
    axisBorder: {
      show: true,
      color: theme.palette.divider,
    },
    axisTicks: {
      show: true,
      color: theme.palette.divider,
    },
    labels: {
      style: {
        colors: theme.palette.text.secondary,
      },
    },
  },
});

为了在 React 组件中使用它,我们需要从 Material-UI 导入主题和makeStyles:

import { Theme } from '@material-ui/core/styles';
import { makeStyles } from '@material-ui/styles';

然后,让我们在 DashboardDefaultContent 中创建两个变量——类和主题——并分别分配useStylesuseTheme样式。

const DashboardDefaultContent = () => {
  const classes = useStyles();
  const theme = useTheme();

  const [sales, setSales] = useState<SaleType[]>([]);

  useEffect(() => {
    fetchSales();
  }, []);

  const fetchSales = async () => {
    const { data } = await getSalesAxios();
    setSales(data);
  };

Listing 6-14Adding useStyles and useTheme

设置好所有这些之后,我们需要做的下一件事是安装 ApexCharts。

安装 ApexCharts

ApexCharts 是一个开源的现代 JS 图表库,用于使用 API 构建交互式图表和可视化。各种现代浏览器都支持它。让我们安装:

npm install react-apexcharts apexcharts

回到 DashboardDefaultContent,从 Material-UI 核心库中导入 ApexCharts 和其他一些样式组件。

import chart from 'react-apexcharts';
import {
  Box,
  Card,
  CardContent,
  Container,
  Grid,
  Typography,
  useTheme,
} from '@material-ui/core';

Listing 6-15Importing Additional Material-UI Components

至此,我们将添加之前创建的页面组件:

import Page from 'app/components/page';

使用页面组件和我们创建的图表。用下面的代码替换页面组件中的返回标题。

<Page className={classes.root} title="Dashboard">
      <Container maxWidth={'sm'}>
        <Typography variant="h4" color="textPrimary">
          Dashboard
        </Typography>
        <Box my={5}>
          <Grid container spacing={3}>
            <Grid item xs={12}>
              <Card>
                <CardContent>
        <Typography variant="h5"  color="textPrimary">
                    Sales
                  </Typography>
                  <Chart
                    options={getChartStyling(theme)}
                    series={sales}
                    type="bar"
                    height={'100%'}
                  />
                </CardContent>
              </Card>
            </Grid>
          </Grid>
        </Box>
      </Container>

Listing 6-16Updating the Page Component

所以我们重用了页面组件,并将其命名为“仪表板”容器和其他样式组件一起设置了DashboardDefaultContent页面的外观。

运行应用,您应该会在浏览器中看到这些变化。

img/506956_1_En_6_Fig8_HTML.jpg

图 6-8

呈现更新的页面组件

虽然还有很长的路要走,但我们正在努力完成我们的仪表板页面。

在此之前,转到srcindex.ts,并删除<React.StrictMode>,这样我们就不会在开发应用时收到不必要的警告和错误:

const ConnectedApp = ({ Component }: Props) => (
  <Provider store={store}>
    <HelmetProvider>
        <Component />
    </HelmetProvider>
  </Provider>
);

接下来,打开文件src/app/index.tsx并删除<NavigationBar/>,因为我们将用一个新的布局组件替换它:

export function App() {
  return (
    <BrowserRouter>
      <Helmet
        titleTemplate="%s - React Boilerplate"
        defaultTitle="React Boilerplate"
      >
        <meta name="description" content="A React Boilerplate application" />
      </Helmet>
      <NavigationBar />
      <Container>
        <Routes />
      </Container>
      <GlobalStyle />
    </BrowserRouter>
  );
}

创建主布局

在 layouts 目录中,让我们在 main-layout 目录下创建一个新文件夹并命名为main-layout.,创建它的 index.tsx 文件。以下是文件路径。

src ➤ app ➤ layouts ➤ main-layout ➤ index.tsx

Listing 6-17Creating a New Main-Layout Component Page

将下面的代码复制到index.tsx :

import React, { ReactNode } from 'react';
import { makeStyles } from '@material-ui/core';

import NavigationBar from './navigation-bar';

type Props = {

  children?: ReactNode;
};

const MainLayout = ({ children }: Props) => {
  const classes = useStyles();

  return (

      <NavigationBar />
      <div className={classes.root}>
        <div className={classes.wrapper}>
          <div className={classes.contentContainer}>
            <div className={classes.content}>{children}</div>
          </div>
        </div>
      </div>

  );
};

const useStyles = makeStyles(theme => ({
  root: {
    backgroundColor: theme.palette.background.default,
    display: 'flex',
    height: '100%',
    overflow: 'hidden',
    width: '100%',
  },
  wrapper: {
    display: 'flex',
    flex: '1 1 auto',
    overflow: 'hidden',
    paddingTop: 64,
  },
  contentContainer: {
    display: 'flex',
    flex: '1 1 auto',
    overflow: 'hidden',
  },

  content: {
    flex: '1 1 auto',
    height: '100%',
    overflow: 'auto',
  },
}));

export default MainLayout;

在我们再次运行或检查浏览器是否一切正常之前,我们将做一个小的文件夹重组。

将组件文件夹中的导航栏. tsx 放在新创建的 main-layout 文件夹下。

img/506956_1_En_6_Fig9_HTML.jpg

图 6-9

将 navigation-bar.tsx 移动到主布局文件夹中

完成后,转到应用根文件夹的 index.tsx:

Src ➤ app ➤ index.tsx

我们将用刚刚创建的<MainLayout>替换<Container>。不要忘记导入命名的组件。

...
import MainLayout from './layouts/main-layout';
...

      <MainLayout>
        <Routes />
      </MainLayout>

Listing 6-18Using the MainLayout in the index.tsx of app

http://localhost:3000/dashboard,刷新你的浏览器,你会注意到样式和间距的一些变化。

img/506956_1_En_6_Fig10_HTML.jpg

图 6-10

呈现更新的主布局

接下来,让我们回到仪表板布局文件夹的index.tsx文件。我们将添加一些样式,包括间距和布局。让我们从 Material-UI 核心样式中导入makeStyles:

import {makeStyles} from '@material-ui/core/styles';

然后,为样式添加以下代码。

const useStyles = makeStyles(theme => ({
  root: {
    display: 'flex',
    height: '100%',
    overflow: 'hidden',
    width: '100%',
  },
  wrapper: {
    display: 'flex',
    flex: '1 1 auto',
    overflow: 'hidden',
    paddingTop: 64,
    [theme.breakpoints.up('lg')]: {
      paddingLeft: 256,
    },
  },
  contentContainer: {
    display: 'flex',
    flex: '1 1 auto',
    overflow: 'hidden',
  },

  content: {
    flex: '1 1 auto',
    height: '100%',
    overflow: 'auto',
  },
}));

Listing 6-19Adding Styling to the index.tsx of dashboard-layout

接下来,添加useStyles并将{children}包裹在<div>.

const classes = useStyles()

...

<DashboardSidebarNavigation />{' '}
      <div className={classes.wrapper}>
        <div className={classes.contentContainer}>
          <div className={classes.content}>{children}</div>
        </div>
      </div>

Listing 6-20Adding the useStyles Component to the index.tsx of the dashboard-layout

检查仪表板,您应该会看到一个更好的、响应更快的布局。

使用 React 羽化图标

最后但同样重要的是,我们将在侧边栏仪表板中添加一个菜单。我们将使用 React 羽毛图标。

img/506956_1_En_6_Figb_HTML.jpg

安装 React 羽毛库:

npm i react-feather

安装完成后,打开dashboard-sidebar-navigation.tsx,从 react-feather 导入 PieChart 组件。我们只是将它重命名为PieChartIcon.另一个我们需要导入的东西是来自 Material-UI 核心的DividerListSubheader:

import { PieChart as PieChartIcon } from 'react-feather';
import { Divider, ListSubheader } from '@material-ui/core';

Logo之后和settings-and-privacy之前,添加以下代码。

<Link to={`${url}`} className={classes.logoWithLink}>
              Logo
            </Link>
          </Toolbar>
          <div className={classes.drawerContainer}>
            <List>

...

<ListSubheader>Reports</ListSubheader>
              <Link className={classes.link} to={`${url}`}>
                <ListItem button>
                  <ListItemIcon>
                    <PieChartIcon />
                  </ListItemIcon>
                  <ListItemText primary={'Dashboard'} />
                </ListItem>
              </Link>

...
<Link className={classes.link} to={`${url}/settings-and-privacy`}>

Listing 6-21Updating the dashboard-sidebar-navigation.tsx

刷新浏览器以查看更改,应如图 6-11 所示。点击按钮,查看一切是否正常。

img/506956_1_En_6_Fig11_HTML.jpg

图 6-11

更新仪表板后的用户界面-侧栏-导航

摘要

我们学习了如何使用本地状态,在考虑 DRY(不要重复自己)原则的同时发送 HTTP 请求,以及使用名为 ApexCharts 的开源图表库为我们的应用创建交互式可视化。

在下一章中,我们将更深入地研究我们的应用,因为我们开始在 Material-UI 的数据表上使用 Formik 和 Yup 验证来编写输入表单。希望我们能把所有这些组件放在一起,并在浏览器上呈现出来,看看一切是如何工作的。

七、编写数据表、表单和验证

在前一章中,我们学习了如何写本地状态和发送 HTTP 请求。我们还安装了 ApexCharts 来创建我们的可视化图表。

我们现在将继续构建我们的应用,添加新的组件为我们的数据表打下基础,并开始使用 Formik 编写表单,使用 Yup 进行输入验证。这一章是两部分系列的第一部分,因为它是一个相当长的主题。第一部分是创建数据表和其他样式组件,这将是第二部分(下一章)的基础,重点是编写表单和输入验证。

本章的最终存储库在这里:

img/506956_1_En_7_Figa_HTML.jpg

来源: https://github.com/webmasterdevlin/practical-enterprise-react/tree/master/chapter-7

组件概述

在我们继续我们的项目应用之前,让我们回顾一下我们将在本章中使用的一些库或组件。

表单处理

表单允许应用用户通过我们的组件直接输入和提交数据,从个人资料页面或登录屏幕到购物结账页面等。这也是为什么它是任何 web 应用的关键部分的主要原因。

根据我自己的经验和许多其他 React 开发人员的经验,在 React 应用中创建表单可能会非常乏味。更重要的是,我们从头开始创建的表单可能容易出错,因为我们需要自己处理所有的 React。

这就是我选择使用 Formik 构建表单的原因。您可以使用许多其他优秀的表单库,包括 Redux Form、Formsy 和 React Forms。

很好

这为我们提供了表单、字段和错误消息组件来创建表单、添加表单字段和显示错误消息。福米克给了我们三个支柱:

  • initialValue:表格字段的初始值。

  • validate:用于表单字段中的验证规则。

  • 当我们点击提交的时候,这个函数就会起作用。

当我们开始构建表单时会有更多的介绍。我觉得还是用代码展示比较好。

是的

Yup 是 JavaScript 中的对象模式验证器(清单 7-1 )。有了 Yep,我们可以

  • 定义对象模式及其验证。

  • 使用所需的模式和验证创建验证器对象。

  • 使用 Yup 实用函数验证对象是否有效(满足模式和验证)。如果它不满足验证,将返回一条错误消息。

//define the object schema and its validation 

const book = {
        published: 1951,
  author: "JD Salinger",
  title: "The Catcher in the Rye",
  pages: 234
};

//create a validator object with the required schema and validation

const yup = require("yup");

const yupObject = yup.object().shape({
  published: yup.number.required(),
  author: yup.string.required(),
  title: yup.string.required(),
  pages: yup.number()

});

Listing 7-1An Example of Creating the Validations with Yup

为了演示我们如何将所有这些结合在一起使用,我们将构建一个产品仪表板,列出所有产品并将新产品添加到我们的应用中。

首先,我们将使用 Material-UI 中的数据表组件来显示数据集。

数据表

产品创建视图

转到views ➤ dashboard,新建一个文件夹,命名为product。在产品文件夹下,创建另一个文件夹,命名为ProductCreateView.

ProductCreateView文件夹中,创建一个新文件并命名为Header.tsx.

以下是文件路径:

views ➤ dashboard ➤ product ➤ ProductCreateView ➤ Header.tsx

打开Header.tsx,,输入 VS 代码的片段rafce或 WebStorm 的片段rsc后,暂时添加头<h1>Header - CreativeView Works!</h1>

参见清单 7-2 关于创建 ProductCreateView 的 Header 组件。

import React from 'react'.

const Header = () => {
  return (
    <div>
      <h1>Header - CreativeView Works!</h1>
    </div>
  )
}

export default Header;

Listing 7-2Creating the Header Component of ProductCreateView

仍然在ProductCreateView文件夹中,我们将添加另一个文件,并将其命名为ProductCreateForm.tsx.

产品创建表单

以下是文件路径:

views ➤ dashboard ➤ product ➤ ProductCreateView ➤ ProductCreateForm.tsx

ProductCreateForm.tsx添加一个

标签。参见清单 7-3 关于创建 ProductCreateForm.tsx.

import React from 'react'

const ProductCreateForm = () => {
  return (
    <div>
      <h1>ProductCreateForm Works! h1>
    </div>
  )
}

export default ProductCreateForm;

Listing 7-3Creating the ProductCreateForm.tsx

接下来,在ProductCreateView目录下,添加一个index.tsx文件,该文件将导入我们刚刚创建的两个组件:Header.tsxProductCreateForm.tsx.

清单 7-4 创建index.tsx

import React from 'react';
import { Container, makeStyles } from '@material-ui/core';

import Header from './Header';
import ProductCreateForm from './ProductCreateForm';

const ProductCreateView = () => {
  const classes = useStyles();

  return (

      <Container>
        <Header />
        <ProductCreateForm />
      </Container>

  );
};

const useStyles = makeStyles(theme => ({}));

export default ProductCreateView;

Listing 7-4Creating the index.tsx of ProductCreateView

所以我们现在已经完成了。我们稍后将回头讨论这些组件。我们要做的下一件事是创建产品列表视图。

产品列表视图

我们将在产品中创建另一个文件夹,并将其命名为ProductListView,并在该文件夹下添加两个新文件,分别命名为Header.tsxResults.tsx,:

views ➤ dashboard ➤ product ➤ ProductListView ➤ Header.tsx
Views ➤ dashboard ➤ product ➤ ProductListView ➤ Results.tsx

打开Header.tsx并复制如下代码。

import React from 'react';
import { makeStyles } from '@material-ui/core';

const Header = () => {
  const classes = useStyles();
  return (
    <div>
      <h1>Header - ListView - Works!</h1>
    </div>
  );
};
const useStyles = makeStyles(theme => ({
        root: {},
        action: {
          marginBottom: theme.spacing(1),
          '& + &': {
             marginLeft: theme.spacing(1),
           },
        },
     }));

export default Header;

Listing 7-5Creating the Header.tsx of ProductListView

您可以在Results.tsx.上自己做同样的事情,但是,将

标题改为

"Results - Works!"

完成Results.tsx,之后,我们将为ProductListView添加index.tsx

import React from 'react';
import {Container, makeStyles} from '@material-ui/core';
import Header from './Header';
import Results from './Results';

const ProductListView = () => {
  const classes = useStyles();

  return (
    <Container>
      <Header/>
      <Results/>
    </Container>
  );
};

const useStyles = makeStyles(theme =>
  createStyles({
    backdrop: {
      zIndex: theme.zIndex.drawer + 1,
      color: '#fff',
    },
    root: {
      minHeight: '100%',
      paddingTop: theme.spacing(3),
      paddingBottom: 100,
    },
  }),
);

export default ProductListView;

Listing 7-6Creating the index.tsx of ProductListView

当我们需要做一些更改时,我们将在后面讨论所有这些组件。

更新路线

现在,我们需要更新我们的routes——为每个新创建的组件更新一个路由路径:ProductCreateViewProductListView.

我们将在路径中注册这两个索引。打开文件

src/app/routes.tsx

routes.tsx文件中,定位Dashboardsettings and privacy. W e 将在它们之间添加新的路由路径,如清单 7-7 所示。

export const Routes = () => {
  return (
    <Suspense fallback={<LinearProgress style={{ margin: '10rem' }} />}>
      <Switch>
        {/*eager loading*/}
        <Route path={'/'} component={HomePage} exact />
        {/*lazy loadings*/}
        <Route
          path={'/about'}
     component={lazy(() => import('./views/pages/AboutPage'))}
          exact
        />

        <Route
          path={'/dashboard'}
          render={({ match: { path } }) => (
            <Dashboard>
              <Switch>
                <Route
                  path={path + '/'}
                  component={lazy(
                    () => import('./views/dashboard/dashboard-default-content'),
                  )}
                  exact
                />
                <Route
                  path={path + '/list-products'}
                  component={lazy(
                    () =>       import('./views/dashboard/product/ProductListView'),
                  )}
                  exact
                />

                 <Route
                  path={path + '/create-product'}
                  component={lazy(
                    () => import('./views/dashboard/product/ProductCreateView'),
                  )}
                  exact
                />
              </Switch>
            </Dashboard>

Listing 7-7Registering the Route Paths of ProductCreateView and ProductListView

注册路线后,我们将更新侧栏仪表板。

更新侧栏仪表板

我们将在侧栏仪表板中创建两个新菜单,即列出产品创建产品

转到dashboard-sidebar-navigation.tsx:

app ➤ layouts ➤ dashboard-layout ➤ dashboard-sidebar-navigation.tsx

我们将在上述文件中从 Feather 导入一些图标。

import {PieChart as PieChartIcon,
        ShoppingCart as ShoppingCartIcon,
        ChevronUp as ChevronUpIcon,
        ChevronDown as ChevronDownIcon,
        List as ListIcon,
        FilePlus as FilePlusIcon,
        LogOut as LogOutIcon,} from 'react-feather';

Listing 7-8Updating the Named Imports for the dashboard-sidebar-navigation

请注意,我们已经对导入的图标进行了重命名,以便它们更具可读性,或者团队中的其他开发人员一眼就能理解它们的用途。

接下来,我们将添加一个本地状态(useState)并创建一个事件处理程序handleClick来更新本地状态。但是首先,不要忘记从React.导入useState组件

import React, { useEffect, useState } from 'react';
...
const [open, setOpen] = useState(false)

  useEffect(() => {}, []);

  const handleClick =() => {
    setOpen(!open)
  };

Listing 7-9Adding useState and an Event Handler to dashboard-sidebar-navigation

之后,我们将在浏览器中呈现一个可折叠的菜单。

创建可折叠的侧边栏菜单

让我们在仪表板和设置和隐私之间添加一个可折叠的菜单。

首先,让我们从 Material-UI Core 导入组件塌陷:

import { Collapse, Divider, ListSubheader } from '@material-ui/core';

然后,让我们将下面的代码添加到可折叠菜单中。我们将使用本地状态open和事件处理器handleClick以及 Material-UI 核心的样式图标组件。

<List>
              <ListSubheader>Reports</ListSubheader>
              <Link className={classes.link} to={`${url}`}>
                <ListItem button>
                  <ListItemIcon>
                    <PieChartIcon />
                  </ListItemIcon>
                  <ListItemText primary={'Dashboard'} />
                </ListItem>
              </Link>

              <ListSubheader>Management</ListSubheader>
              <ListItem button onClick={handleClick}>
                <ListItemIcon>
                  <ShoppingCartIcon />
                </ListItemIcon>
                <ListItemText primary="Products" />
                {open ? <ChevronUpIcon /> : <ChevronDownIcon />}
              </ListItem>
              <Collapse in={open} timeout="auto" unmountOnExit>
                <List component="div" disablePadding>
        <Link className={classes.link} to={`${url}/list-products`}>
         <ListItem button className={classes.nested}>
                      <ListItemIcon>
                        <ListIcon />
                      </ListItemIcon>
                      <ListItemText primary="List Products" />
                    </ListItem>
                  </Link>
        <Link className={classes.link} to={`${url}/create-product`}>
                    <ListItem button className={classes.nested}>
                      <ListItemIcon>
                        <FilePlusIcon />
                      </ListItemIcon>
                      <ListItemText primary="Create Product" />
                    </ListItem>
                  </Link>
                </List>
              </Collapse>

              <a className={classes.link} href={'/'}>
                <ListItem button>
                  <ListItemIcon>
                    <LogOutIcon />
                  </ListItemIcon>
                  <ListItemText primary={'logout'} />
                </ListItem>
              </a>
            </List>
          </div>
        </Drawer>
      </div>

Listing 7-10Creating a Collapsible Menu (Material-UI) for dashboard-sidebar-navigation

那么我们的可折叠菜单是怎么回事呢? 我们添加了管理作为列表子标题,在它下面,我们使用可折叠产品菜单的<ShoppingCartIcon />来显示菜单List ProductsCreate Product.

当用户点击时,<ChevronUpIcon /><ChevronDownIcon />将打开和折叠菜单。

在您的编辑器中,您可能会也可能不会注意到{classes.nested}.上的一条红色曲线

无论如何,我们需要在这里做更多的事情。这是因为我们需要将它添加到我们的useStyle组件中。加在最下面就行了。

nested: {
      paddingLeft: theme.spacing(4),
    },

Listing 7-11Updating the useStyle Component of dashboard-sidebar-navigation

现在运行应用,检查一切是否仍然正常。您应该会看到更新后的侧边栏导航,如下所示。

img/506956_1_En_7_Fig1_HTML.jpg

图 7-1

更新了仪表板的用户界面-侧栏-导航

单击“列出产品”和“创建产品”来检查您是否可以在页面之间成功导航。您应该能够看到我们编写的 h1 头:

(Shown when clicking the List Products tab)

Header - ListView Works!
Results - Works!

(Showing when clicking the Create Product tab)

Header - CreativeView Works!
ProductCreateForm Works!

既然我们已经完成了可以导航到新页面的概念验证,我认为是时候做一些清理并删除设置和隐私选项卡了。我们不再需要它了;稍后我们会添加更多的菜单。

清理一下…

routes.tsx中删除settings-and-privacy

删除文件settings-and-privacy.tsx

接下来,进行dashboard-sidebar-navigation.tsx

我们将在这里进行两处编辑:

1.删除设置和隐私。

2.然后用我们自己的<LogoutIcon />替换默认的<ExitToAppIcon />

           <a className={classes.link} href={'/'}>
                <ListItem button>
                  <ListItemIcon>

<LogOutIcon/>
                  </ListItemIcon>
                  <ListItemText primary={'logout'} />
                </ListItem>
              </a>

Listing 7-12Logout Icon in dashboard-sidebar-navigation

我可能忘记使用 Material-UI 中的<Divider />了,所以我们现在就把它放上去。把它放在</Toolbar>.之后

<<Toolbar
            style={{ width: '6rem', height: 'auto' }}
            className={classes.toolbar}
          >
            <Link to={`${url}`} className={classes.logoWithLink}>
              Logo
            </Link>
          </Toolbar>
          <Divider />

Listing 7-13Adding the Divider Component in dashboard-sidebar-navigation

现在运行或刷新浏览器,如果设置和隐私已被删除,看看是否一切仍然工作。

定义产品类型的类型别名

之后,我们将继续在数据表中实现产品。由于我们使用 TypeScript,我们将首先开始构建我们的模型类型或接口。在这种情况下,我更喜欢使用类型。

在 models 目录中,创建一个新文件,并将其命名为product-type.ts.。我们的ProductType对象的形状如下所示。

这就像一个枚举字符串。这里的管道|基本上是一个联合,允许我们选择三个选项中的任何一个。*/

export type InventoryType = 'in_stock' | 'limited' | 'out_of_stock';

export type ProductType = {
  id: string;
  attributes: string[];
  category: string;
  //union means can be string or number
  createdAt: string | number;
  currency: string;
  // the ? means nullable
  image?: string;
  inventoryType: InventoryType;
  isAvailable: boolean;
  isShippable: boolean;
  name: string;
  price: number;
  quantity: number;
  updatedAt: string | number;
  variants: number;
  description: string;
  images: string[];
  includesTaxes: boolean;
  isTaxable: boolean;
  productCode: string;
  productSku: string;
  salePrice: string;
};

Listing 7-14Creating the Shape of the ProductType Object

形状或类型在这里是不言自明的。为了代码的可维护性和在编辑器中获得智能感知,我们现在需要付出额外的努力。从长远来看,现在这样做可以省去我们很多痛苦。

创建产品端点

在我们进入服务之前,让我们更新 axios 配置中的端点。打开axios.ts并添加产品端点。

export const EndPoints = {
  sales: 'sales',
  products: 'products'
};

Listing 7-15Adding the Products Endpoint in axios.ts

现在我们已经为我们的销售和产品设置了端点,是时候设置他们的 HTTP 服务了。

创建产品服务

我们将在名为productService.ts的新文件中使用该端点,我们将在 services 目录下创建该文件:

services ➤ productService.ts

打开新文件并添加函数来创建产品服务,如清单 7-16 所示。

import api, {EndPoints} from '../api/axios';
import {ProductType} from '../models/product-type';

export async function getProductAxios() {
  return await api.get<ProductType[]>(EndPoints.products);
}

export async function postProductAxios(product: ProductType) {
  return await api.post<ProductType>(EndPoints.products, product);
}

Listing 7-16Creating productService.ts

在清单 7-16 中,我们创建了两个函数:

getProductAxiospostProductAxios

两者都使用 Axios 向 JSON 服务器发送请求,返回类型分别是 ProductType: <ProductType[ ]><ProductType>,的数组。

两个函数都是异步等待类型。

在这之后,让我们用一个产品样本或四个对象的数组来更新我们的db.json

更新 db.json 数据

转到 db.json 文件并添加以下数据,如清单 7-17 所示。

"products": [
    {
      "id": "5ece2c077e39da27658aa8a9",
      "attributes": ["Cotton"],
      "category": "dress",
      "currency": "$",
      "createdAt": "2021-01-01T12:00:27.87+00:20",
      "image": null,
      "inventoryType": "in_stock",
      "isAvailable": true,
      "isShippable": false,
      "name": "Charlie Tulip Dress",
      "price": 23.99,
      "quantity": 85,
      "updatedAt": "2021-01-01T12:00:27.87+00:20",
      "variants": 2
    },
    {
      "id": "5ece2c0d16f70bff2cf86cd8",
      "attributes": ["Cotton"],
      "category": "dress",
      "currency": "$",
      "createdAt": "2021-01-01T12:00:27.87+00:20",
      "image": null,
      "inventoryType": "out_of_stock",
      "isAvailable": false,
      "isShippable": true,
      "name": "Kate Leopard Dress",
      "price": 95,
      "quantity": 0,
      "updatedAt": "2021-01-01T12:00:27.87+00:20",
      "variants": 1
    },
    {
      "id": "5ece2c123fad30cbbff8d060",
      "attributes": ["Variety of styles"],
      "category": "jewelry",
      "currency": "$",
      "createdAt": 345354345,
      "image": null,
      "inventoryType": "in_stock",
      "isAvailable": true,
      "isShippable": false,
      "name": "Layering Bracelets Collection",
      "price": 155,
      "quantity": 48,
      "updatedAt": "2021-01-01T12:00:27.87+00:20",
      "variants": 5
    },
    {
      "id": "5ece2c1be7996d1549d94e34",
      "attributes": ["Polyester and Spandex"],
      "category": "blouse",
      "currency": "$",
      "createdAt": "2021-01-01T12:00:27.87+00:20",
      "image": null,
      "inventoryType": "limited",
      "isAvailable": false,
      "isShippable": true,
      "name": "Flared Sleeve Floral Blouse",
      "price": 17.99,
      "quantity": 5,
      "updatedAt": "2021-01-01T12:00:27.87+00:20",
      "variants": 1
    }
  ]

Listing 7-17Adding the db.json Data with Product Objects

您会注意到我们已经创建了四个产品对象。这里为了简单起见,对象的名称如下:

"name": "Charlie Tulip Dress",
 "name": "Kate Leopard Dress",
 "name": "Layering Bracelets Collection",
 "name": "Flared Sleeve Floral Blouse",

现在我们已经在 axios 中添加了productService并更新了 db.json,让我们通过发送一个 HTTP 请求来测试它。

发送 HTTP 请求

前往ProductListViewindex.tsx文件。

我们需要 React 的useEffect。在useEffect,里面我们称之为从services/productService?? 进口的getProductAxios,

...
import { getProductsAxios } from 'services/productService';

const ProductListView = () => {
  const classes = useStyles();

  useEffect(() => {
    getProductAxios();
  }, []);

Listing 7-18Using the getProductAxios in ProductListView.tsx

进入 Chrome DevTools,点击网络选项卡,选择 XHR。确保您的 JSON 服务器在localhost:5000/products.运行

单击浏览器中的 List Products,在标题中,您应该会看到 Status Code: 200 OK 来表示来自 JSON 服务器的一个成功的 get 响应。

接下来,单击 Response 选项卡检查 JSON 对象。您应该能够看到我们在db.json.中添加的一系列产品

重构产品列表视图

好了,现在我们知道它正在工作,我们将在 ProductListView 中进行一些代码重构,以反映最佳实践。

转到ProductListViewindex.tsx并执行以下操作:

  • 创建一个本地状态(useState)来更新产品数据的数组。

  • 添加一个名为fetchProducts的 async-await 函数,在这里我们可以调用getProductAxios().

  • 最佳实践是将fetchProducts()放在一个 try-catch 块中。

  • 从 Material-UI 中添加一个背景组件,其工作方式很像加载器微调器。

/局部状态使用泛型类型的类型数组,所以我们一眼就能知道它的形状。将鼠标悬停在上,您将看到它的型号。如果你去掉这里的泛型,你将失去在悬停时看到物体模型形状的能力。你会得到类型‘any’/

  const [products, setProducts] = useState<ProductType[]>([])

/*不需要在这里声明类型 boolean,因为我们已经可以看到它的类型。

通常是原语——不需要显式声明类型。TS 可以推断出来。*/

  const [open, setOpen] = useState(false);

  useEffect(() => {
  fetchProduct()
  }, []);

  const fetchProduct = async () => {
    handleToggle();
    try {
      const { data } = await getProductAxios();
      setProducts(data);
    } catch (e) {
      alert('Something is wrong.');
    }
    handleClose();
  };

  const handleClose = () => {
    setOpen(false);
  };
  const handleToggle = () => {
    setOpen(!open);
  }

Listing 7-19Updating the index.tsx of ProductListView.tsx

我们将使用当地的州作为背景。我们会用一点,但我们需要首先创建一些额外的 UI 样式。

创建附加的用户界面样式

首先,让我们在 UI 中呈现表格,为此,我们将在components文件夹下创建一个新文件,并将其命名为label.tsx

列表 7-20 为桌子的美学设计或造型创建label.tsx

import React, { ReactNode } from 'react';
import clsx from 'clsx';
import { fade, makeStyles } from '@material-ui/core';

//defining the shape or type of our label model

type Props = {
  className?: string;
  color?: 'primary' | 'secondary' | 'error' | 'warning' | 'success';
  children?: ReactNode;
  style?: {};
};

const Label = ({
                 className = '',
                 color = 'secondary',
                 children,
                 style,
                 ...rest
               }: Props) => {
  const classes = useStyles();

  return (
    <span
      className={clsx(
        classes.root,
        {
          [classes[color]]: color,
        },
        className,
      )}
      {...rest}
    >
      {children}
    </span>
  );
};

const useStyles = makeStyles(theme => ({
  root: {
    fontFamily: theme.typography.fontFamily,
    alignItems: 'center',
    borderRadius: 2,
    display: 'inline-flex',
    flexGrow: 0,
    whiteSpace: 'nowrap',
    cursor: 'default',
    flexShrink: 0,
    fontSize: theme.typography.pxToRem(12),
    fontWeight: theme.typography.fontWeightMedium,
    height: 20,
    justifyContent: 'center',
    letterSpacing: 0.5,
    minWidth: 20,
    padding: theme.spacing(0.5, 1),
    textTransform: 'uppercase',
  },
  primary: {
    color: theme.palette.primary.main,
    backgroundColor: fade(theme.palette.primary.main, 0.08),
  },
  secondary: {
    color: theme.palette.secondary.main,
    backgroundColor: fade(theme.palette.secondary.main, 0.08),
  },
  error: {
    color: theme.palette.error.main,
    backgroundColor: fade(theme.palette.error.main, 0.08),
  },
  success: {
    color: theme.palette.success.main,
    backgroundColor: fade(theme.palette.success.main, 0.08),
  },
  warning: {
    color: theme.palette.warning.main,
    backgroundColor: fade(theme.palette.warning.main, 0.08),
  },
}));

export default Label;

Listing 7-20 Creating the label.tsx

接下来,我们需要另一个组件来帮助我们呈现数据表。转到文件夹ProductListView,创建一个新文件,命名为TableResultsHelpers.tsx

让我们导入命名组件并定义对象的类型别名,如清单 7-21 所示。

import React from 'react';
import { InventoryType, ProductType } from 'models/product-type';
import Label from 'app/components/label';

export type TableResultsHelpers = {
  availability?: 'available' | 'unavailable';
  category?: string;
  inStock?: boolean;
  isShippable?: boolean;
};

Listing 7-21Importing the component and adding the type alias for TableResultsHelpers

接下来,让我们将产品渲染的过滤条件应用于用户;参见清单 7-22 。

export const applyFilters = (
  products: ProductType[],
  query: string,
  filters: TableResultsHelpers,
): ProductType[] => {
  return products.filter(product => {
    let matches = true;

    /* the product here comes from the parent component.  */

if (query && !product.name.toLowerCase().includes(query.toLowerCase())) {
      matches = false;
    }
if (filters.category && product.category !== filters.category) {
      matches = false;
    }
if (filters.availability) {
      if (filters.availability === 'available' && !product.isAvailable) {
        matches = false;
      }
      if (filters.availability === 'unavailable' && product.isAvailable) {
        matches = false;
      }}
    if (
      filters.inStock &&
      !['in_stock', 'limited'].includes(product.inventoryType)
    ) {
      matches = false;
    }
    if (filters.isShippable && !product.isShippable) {
      matches = false;
    }
    return matches;
  });
};

/* to limit the products or the number of search results shown*/

export const applyPagination = (
  products: ProductType[],
  page: number,
  limit: number,
): ProductType[] => {
  return products.slice(page * limit, page * limit + limit);
};

export const getInventoryLabel = (
  inventoryType: InventoryType,
): JSX.Element => {
  const map = {
    in_stock: {
      text: 'In Stock',
      color: 'success',
    },
    limited: {
      text: 'Limited',
      color: 'warning',
    },
    out_of_stock: {
      text: 'Out of Stock',
      color: 'error',
    },
  };

  const { text, color }: any = map[inventoryType];
  return <Label color={color}>{text}</Label>;
};

Listing 7-22Creating the TableResultsHelpers

TableResultsHelpers正在使用我们刚刚创建的标签组件。

我们还从models/product-type进口InventoryTypeProductType

表助手是用于 UI 的,所以我们可以在过滤器框中查询或输入,并查看结果列表。

之后,在src下新建一个文件夹,命名为helpers。在 helpers 文件夹下,添加一个新文件,命名为inputProductOptions.ts.这个文件只是用来标记表格的,最好把它放在一个单独的文件里,而不是和组件本身堆在一起。

export const categoryOptions = [
  {
    id: 'all',
    name: 'All',
  },
  {
    id: 'dress',
    name: 'Dress',
  },
  {
    id: 'jewelry',
    name: 'Jewelry',
  },
  {
    id: 'blouse',
    name: 'Blouse',
  },
  {
    id: 'beauty',
    name: 'Beauty',
  },
];

export const availabilityOptions = [
  {
    id: 'all',
    name: 'All',
  },
  {
    id: 'available',
    name: 'Available',
  },
  {
    id: 'unavailable',
    name: 'Unavailable',
  },
];

export const sortOptions = [
  {
    value: 'updatedAt|desc',
    label: 'Last update (newest first)',
  },
  {
    value: 'updatedAt|asc',
    label: 'Last update (oldest first)',
  },
  {
    value: 'createdAt|desc',
    label: 'Creation date (newest first)',
  },
  {
    value: 'createdAt|asc',
    label: 'Creation date (oldest first)',
  },
];

Listing 7-23Creating the Helpers for inputProductOptions

暂时就这样了。现在,我们将安装三个 NPM 库:

  1. numeral.js:一个用于格式化和操作数字的 JavaScript 库。

  2. @types/numeral:numeric . js 是用 JavaScript 构建的,所以我们需要为这个库添加类型。

  3. 这让我们可以很容易地为数据表制作滚动条。

$ npm i numeral
$ npm i @types/numeral
$ npm i react-perfect-scrollbar

成功安装库后,打开文件results.tsx进行一些编辑。我前面提到过,我们将回到这个文件来构建它。

让我们添加以下命名的导入组件,如清单 7-24 所示。除了我们将要安装的来自 Material-UI 核心的几个样式组件之外,我们正在从inputProductOptionsTableResultsHelpersmodels文件夹中的product-type导入组件。

import React, { useState, ChangeEvent } from 'react';
import clsx from 'clsx';
import numeral from 'numeral';
import PerfectScrollbar from 'react-perfect-scrollbar';
import {
  Image as ImageIcon,
  Edit as EditIcon,
  ArrowRight as ArrowRightIcon,
  Search as SearchIcon,
} from 'react-feather';
import {
  Box,
  Button,
  Card,
  Checkbox,
  InputAdornment,
  FormControlLabel,
  IconButton,
  SvgIcon,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TablePagination,
  TableRow,
  TextField,
  makeStyles,
} from '@material-ui/core';

import {
  availabilityOptions,
  categoryOptions,
  sortOptions,
} from 'helpers/inputProductOptions';
import {
  applyFilters,
  applyPagination,
  TableResultsHelpers,
  getInventoryLabel,
} from './tableResultsHelpers';
import { ProductType } from 'models/product-type';

Listing 7-24Adding the Named Import Components to results.tsx

接下来,我们将定义清单 7-25 中对象的类型或形状。

type Props = {
  className?: string;
  products?: ProductType[];
};

Listing 7-25Creating the Shape or Type of the Object in results.tsx

根据类型的定义,我们将创建一些本地状态,如清单 7-26 所示。

const Results = ({ className, products, ...rest }: Props) => {
  const classes = useStyles();

  //Explicitly stating that selectedProducts is an array of type string

const [selectedProducts, setSelectedProducts] = useState<string[]>([]);

  const [page, setPage] = useState(0);
  const [limit, setLimit] = useState(10);
  const [query, setQuery] = useState('');

  /* Explicitly stating that sort is an array of type string so we'll know on mouser hover that value is of type string. */

  const [sort, setSort] = useState<string>(sortOptions[0].value);
  const [filters, setFilters] = useState<TableResultsHelpers | any>({
    category: null,
    availability: null,
    inStock: null,
    isShippable: null,
  });

Listing 7-26Creating the results.tsx Component

接下来,我们将创建以下事件处理程序,如清单 7-27 所示。

/*Updates the query every time the user types on the keyboard  */

const handleQueryChange = (event: ChangeEvent<HTMLInputElement>): void => {
    event.persist();
    setQuery(event.target.value);
  };

  const handleCategoryChange = (event: ChangeEvent<HTMLInputElement>): void => {
    event.persist();

    let value: any = null;

    if (event.target.value !== 'all') {
      value = event.target.value;
    }

    setFilters(prevFilters => ({
      ...prevFilters,
      category: value,
    }));
  };

  const handleAvailabilityChange = (
    event: ChangeEvent<HTMLInputElement>,
  ): void => {
    event.persist();

    let value: any = null;

    if (event.target.value !== 'all') {
      value = event.target.value;
    }

    setFilters(prevFilters => ({
      ...prevFilters,
      availability: value,
    }));
  };

  const handleStockChange = (event: ChangeEvent<HTMLInputElement>): void => {
    event.persist();

    let value: any = null;

    if (event.target.checked) {
      value = true;
    }

    setFilters(prevFilters => ({
      ...prevFilters,
      inStock: value,
    }));
  };

  const handleShippableChange = (
    event: ChangeEvent<HTMLInputElement>,
  ): void => {
    event.persist();

    let value: any = null;

    if (event.target.checked) {
      value = true;
    }

    setFilters(prevFilters => ({
      ...prevFilters,
      isShippable: value,
    }));
  };

  const handleSortChange = (event: ChangeEvent<HTMLInputElement>): void => {
    event.persist();
    setSort(event.target.value);
  };

 /*Updating all selected products */

  const handleSelectAllProducts = (
    event: ChangeEvent<HTMLInputElement>,
  ): void => {
    setSelectedProducts(
      event.target.checked ? products.map(product => product.id) : [],
    );
  };

 /*Updating one selected product */

  const handleSelectOneProduct = (
    event: ChangeEvent<HTMLInputElement>,
    productId: string,
  ): void => {
    if (!selectedProducts.includes(productId)) {
      setSelectedProducts(prevSelected => [...prevSelected, productId]);
    } else {
      setSelectedProducts(prevSelected =>
        prevSelected.filter(id => id !== productId),
      );
    }
  };

 /*This is for the pagination*/

  const handlePageChange = (event: any, newPage: number): void => {
    setPage(newPage);
  };

  const handleLimitChange = (event: ChangeEvent<HTMLInputElement>): void => {
    setLimit(parseInt(event.target.value));
  };

  /* Usually query is done on the backend with indexing solutions, but we're doing it  here just to simulate it */

  const filteredProducts = applyFilters(products, query, filters);
  const paginatedProducts = applyPagination(filteredProducts, page, limit);
  const enableBulkOperations = selectedProducts.length > 0;
  const selectedSomeProducts =
    selectedProducts.length > 0 && selectedProducts.length < products.length;
  const selectedAllProducts = selectedProducts.length === products.length;

Listing 7-27Creating Event Handlers in results.tsx

继续 HTML,我们把 Material-UI 核心中的所有东西都包装在卡片中。我们还添加了Box, TextField, Checkbox和各种表格样式,如清单 7-28 所示。

请记住,所有这些风格都不是你需要从头开始创造的。比方说,你只需进入 Material-UI 网站,搜索“表格”,你就可以根据应用的要求使用那里的任何东西。我们在这里使用的所有 API 都可以在 Material-UI 中找到。

我只是再次向您展示了使用编写良好且受支持的库来使您的编码开发变得更加容易的可能性。当然,正如我之前提到的,有很多 UI 组件库可以使用,Material-UI 只是其中之一。

如果您正在编写代码,从 Material UI 复制粘贴卡片组件,如清单 7-28 所示。必要的时候我们会重构或者做一些改变。

return (
    <Card className={clsx(classes.root, className)} {...rest}>
      <Box p={2}>
        <Box display="flex" alignItems="center">
          <TextField
            className={classes.queryField}
            InputProps={{
              startAdornment: (
                <InputAdornment position="start">
                  <SvgIcon fontSize="small" color="action">
                    <SearchIcon />
                  </SvgIcon>
                </InputAdornment>
              ),
            }}
            onChange={handleQueryChange}
            placeholder="Search products"
            value={query}
            variant="outlined"
          />
          <Box flexGrow={1} />
          <TextField
            label="Sort By"
            name="sort"
            onChange={handleSortChange}
            select
            SelectProps={{ native: true }}
            value={sort}
            variant="outlined"
          >
            {sortOptions.map(option => (
              <option key={option.value} value={option.value}>
                {option.label}
              </option>
            ))}
          </TextField>
        </Box>

        <Box mt={3} display="flex" alignItems="center">
          <TextField
            className={classes.categoryField}
            label="Category"
            name="category"
            onChange={handleCategoryChange}
            select
            SelectProps={{ native: true }}
            value={filters.category || 'all'}
            variant="outlined"
          >
            {categoryOptions.map(categoryOption => (
              <option key={categoryOption.id} value={categoryOption.id}>
                {categoryOption.name}
              </option>
            ))}
          </TextField>
          <TextField
            className={classes.availabilityField}
            label="Availability"
            name="availability"
            onChange={handleAvailabilityChange}
            select
            SelectProps={{ native: true }}
            value={filters.availability || 'all'}
            variant="outlined"
          >
            {availabilityOptions.map(avalabilityOption => (
              <option key={avalabilityOption.id} value={avalabilityOption.id}>
                {avalabilityOption.name}
              </option>
            ))}
          </TextField>

          <FormControlLabel
            className={classes.stockField}
            control={
              <Checkbox
                checked={!!filters.inStock}
                onChange={handleStockChange}
                name="inStock"
              />
            }
            label="In Stock"
          />
          <FormControlLabel
            className={classes.shippableField}
            control={
              <Checkbox
                checked={!!filters.isShippable}
                onChange={handleShippableChange}
                name="Shippable"
              />
            }
            label="Shippable"
          />
        </Box>
      </Box>

      {enableBulkOperations && (
        <div className={classes.bulkOperations}>
          <div className={classes.bulkActions}>
            <Checkbox
              checked={selectedAllProducts}
              indeterminate={selectedSomeProducts}
              onChange={handleSelectAllProducts}
            />
            <Button variant="outlined" className={classes.bulkAction}>
              Delete
            </Button>
            <Button variant="outlined" className={classes.bulkAction}>
              Edit
            </Button>
          </div>
        </div>
      )}
      <PerfectScrollbar>
        <Box minWidth={1200}>
          <Table>
            <TableHead>
              <TableRow>
                <TableCell padding="checkbox">
                  <Checkbox
                    checked={selectedAllProducts}
                    indeterminate={selectedSomeProducts}
                    onChange={handleSelectAllProducts}
                  />
                </TableCell>
                <TableCell />
                <TableCell>Name</TableCell>
                <TableCell>Inventory</TableCell>
                <TableCell>Details</TableCell>
                <TableCell>Attributes</TableCell>
                <TableCell>Price</TableCell>
                <TableCell align="right">Actions</TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {paginatedProducts.map(product => {
                const isProductSelected = selectedProducts.includes(product.id);

                return (
                  <TableRow hover key={product.id} selected={isProductSelected}>
                    <TableCell padding="checkbox">
                      <Checkbox
                        checked={isProductSelected}
                        onChange={event =>
                          handleSelectOneProduct(event, product.id)
                        }
                        value={isProductSelected}
                      />
                    </TableCell>
                    <TableCell className={classes.imageCell}>
                      {product.image ? (
                        <img
                          alt="Product"
                          src={product.image}
                          className={classes.image}
                        />
                      ) : (
                        <Box p={2} bgcolor="background.dark">
                          <SvgIcon>
                            <ImageIcon />
                          </SvgIcon>
                        </Box>
                      )}
                    </TableCell>
                    <TableCell>{product.name}</TableCell>
                    <TableCell>
                      {getInventoryLabel(product.inventoryType)}
                    </TableCell>

                    <TableCell>
                      {product.quantity} in stock
                      {product.variants > 1 &&
                      ` in ${product.variants} variants`}
                    </TableCell>
                    <TableCell>
                      {product.attributes.map(attr => attr)}
                    </TableCell>
                    <TableCell>
                      {numeral(product.price).format(
                        `${product.currency}0,0.00`,
                      )}
                    </TableCell>
                    <TableCell align="right">
                      <IconButton>
                        <SvgIcon fontSize="small">
                          <EditIcon />
                        </SvgIcon>
                      </IconButton>
                      <IconButton>
                        <SvgIcon fontSize="small">
                          <ArrowRightIcon />
                        </SvgIcon>
                      </IconButton>
                    </TableCell>
                  </TableRow>
                );
              })}
            </TableBody>
          </Table>
          <TablePagination
            component="div"
            count={filteredProducts.length}
            onChangePage={handlePageChange}
            onChangeRowsPerPage={handleLimitChange}
            page={page}
            rowsPerPage={limit}
            rowsPerPageOptions={[5, 10, 25]}
          />
        </Box>
      </PerfectScrollbar>
    </Card>
  );
};

Listing 7-28Creating Event Handlers in results.tsx

之后,我们只需要将makeStyles中的useStyles放到results.tsx中。

const useStyles = makeStyles(theme => ({
  availabilityField: {
    marginLeft: theme.spacing(2),
    flexBasis: 200,
  },
  bulkOperations: {
    position: 'relative',
  },
  bulkActions: {
    paddingLeft: 4,
    paddingRight: 4,
    marginTop: 6,
    position: 'absolute',
    width: '100%',
    zIndex: 2,
    backgroundColor: theme.palette.background.default,
  },
  bulkAction: {
    marginLeft: theme.spacing(2),
  },
  categoryField: {
    flexBasis: 200,
  },
  imageCell: {
    fontSize: 0,
    width: 68,
    flexBasis: 68,
    flexGrow: 0,
    flexShrink: 0,
  },

  image: {
    height: 68,
    width: 68,
  },
  root: {},
  queryField: {
    width: 500,
  },
  stockField: {
    marginLeft: theme.spacing(2),
  },
  shippableField: {
    marginLeft: theme.spacing(2),
  },
}));

export default Results;

Listing 7-29Adding the useStyles to results.tsx

我们现在完成了 results.tsx。让我们对ProductListView.index.tsx做一些更新

我们将从 Material-UI 核心导入一些组件,包括页面模板组件,如清单 7-30 所示。

import {
  Backdrop,
  Box,
  CircularProgress,
  Container,
  makeStyles,
} from '@material-ui/core';

import Page from 'app/components/page';

Listing 7-30Adding Named Components to the index.tsx of ProductListView

然后让我们从makeStyles组件中添加useStyles,如清单 7-31 所示。

import { createStyles } from '@material-ui/core/styles';

...

const useStyles = makeStyles(theme =>
  createStyles({
    backdrop: {
      zIndex: theme.zIndex.drawer + 1,
      color: '#fff',
    },
    root: {
      minHeight: '100%',
      paddingTop: theme.spacing(3),
      paddingBottom: 100,
    },
  }),
);

Listing 7-31Adding useStyles to the index.tsx of ProductListView

好了,现在我们已经在 ProductListView 上设置了这些,我们将在 JSX 中使用页面模板、容器和背景,如清单 7-32 所示。

return (
    <Page className={classes.root} title="Product List">
      <Container maxWidth={false}>
        <Header />
        {products && (
          <Box mt={3}>
            <Results products={products} />
          </Box>
        )}
        <Backdrop
          className={classes.backdrop}
          open={open}
          onClick={handleClose}
        >
          <CircularProgress color="inherit" />
        </Backdrop>
      </Container>
    </Page>
  );
};

Listing 7-32Adding Material-UI Components to the index.tsx of ProductListView

确保您的 JSON 服务器在localhost:5000/product s 运行,然后通过单击侧边栏仪表板中的 List Products 来刷新您的 UI。

img/506956_1_En_7_Fig2_HTML.jpg

图 7-2

呈现列表产品的用户界面

摆弄搜索框(搜索产品)、类别、可用性,检查是否可以成功搜索,并根据您键入的关键字获得正确的结果。单击刷新按钮也可以检查带有微调器的背景是否工作。

摘要

我们看到产品菜单到目前为止是有效的,至少有一半——列出产品——但是我们还有很长的路要走,以完成产品侧边栏菜单。你可以说,在我们能够触及事物的本质之前,我们已经奠定了框架基础。

在下一章的第二部分,我们将对 ProductListView 做一些收尾工作,然后直接跳到使用 Formik 和 Yup 验证表单。

八、编写数据表、Formik 表单和 Yup 验证:第二部分

在这个由两部分组成的章节系列的第一部分中,我们开始设置产品菜单,包括使用数据表和其他样式组件的ProductListView,。第二部分将继续使用 Formik 和 Yup 输入验证构建产品菜单。

现在我们有了可以在 UI 中呈现列表产品的概念证明,如清单 8-1 所示,我们现在可以更新ProductListView.Header组件

首先,导入以下命名组件。

import { Link as RouterLink } from 'react-router-dom';
import clsx from 'clsx';
import NavigateNextIcon from '@material-ui/icons/NavigateNext';
import {
  Box,
  Breadcrumbs,
  Button,
  Grid,
  Link,
  SvgIcon,
  Typography,
  makeStyles,
} from '@material-ui/core';
import {
  PlusCircle as PlusCircleIcon,
  Download as DownloadIcon,
  Upload as UploadIcon,
} from 'react-feather';

Listing 8-1Adding Import Components to Header.tsx of ProductListView

接下来,我们将在 Header 组件本身上创建类型定义和其他更改。

我们把所有东西都包装在一个<Grid/>,里面,我们还做了三个按钮:进口出口、新产品。复制粘贴 ProductListView 的 Header 组件;参见清单 8-2 。

/*types definition */

type Props = {
  className?: string;
};

const Header = ({ className, ...rest }: Props) => {
  const classes = useStyles();

  return (
    <Grid
      container
      spacing={3}
      justify="space-between"
      className={clsx(classes.root, className)}
      {...rest}
    >
      <Grid item>
        <Breadcrumbs
          separator={<NavigateNextIcon fontSize="small" />}
          aria-label="breadcrumb"
        >
          <Link
            variant="body1"
            color="inherit"
            to="/dashboard"
            component={RouterLink}
          >
            Dashboard
          </Link>

          <Box>
            <Typography variant="body1" color="inherit">
              List Products
            </Typography>
          </Box>
        </Breadcrumbs>
        <Typography variant="h4" color="textPrimary">
          All Products
        </Typography>
        <Box mt={2}>
          <Button

            className={classes.action}
            startIcon={
              <SvgIcon fontSize="small">
                <UploadIcon />
              </SvgIcon>
            }
          >
            Import
          </Button>
          <Button
            className={classes.action}
            startIcon={
              <SvgIcon fontSize="small">
                <DownloadIcon />
              </SvgIcon>
            }
          >
            Export
          </Button>
        </Box>
      </Grid>

      <Grid item>

         <Button
          color="primary"
          variant="contained"
          className={classes.action}
          component={RouterLink}
          to="/dashboard/create-product"
          startIcon={
            <SvgIcon fontSize="small">
              <PlusCircleIcon />
            </SvgIcon>
          }
        >

          New Product
        </Button>
      </Grid>
    </Grid>
  );
};

Listing 8-2Updating the Header Component of ProductListView

最后,将样式边距添加到ProductListView.的标题组件中

const useStyles = makeStyles(theme => ({
  root: {},
  action: {
    marginBottom: theme.spacing(1),
    '& + &': {
      marginLeft: theme.spacing(1),
    },
  },
}));

export default Header;

Listing 8-3Adding the Styling Component to the Header Component

我们现在已经完成了 ProductListView 让我们开始构建 ProductCreateView。

更新 ProductCreateView

ProductCreateView 是我们为应用添加输入表单的地方。

打开 ProductCreateView 的 Header.tsx:

ProductCreateView ➤ Header.tsx.

我们将从命名的导入组件开始,如清单 8-4 所示。

import { Link as RouterLink } from 'react-router-dom';
import clsx from 'clsx';
import NavigateNextIcon from '@material-ui/icons/NavigateNext';
import {
  Breadcrumbs,
  Button,
  Grid,
  Link,
  Typography,
  makeStyles,
  Box,
} from '@material-ui/core';

Listing 8-4Adding Named Components to the Header.tsx of ProductCreateView

还要创建类型定义,这只是一个 string 类型的可空类名:

type Props = {
  className?: string;
};

然后将下面的更新复制到我们的ProductCreateView的 Header 组件中,如清单 8-5 所示。

const Header = ({ className, ...rest }: Props) => {
  const classes = useStyles();

  return (
    <Grid
      className={clsx(classes.root, className)}
      container
      justify="space-between"
      spacing={3}
      {...rest}
    >
      <Grid item>
        <Breadcrumbs
          separator={<NavigateNextIcon fontSize="small" />}
          aria-label="breadcrumb"
        >
          <Link
            variant="body1"
            color="inherit"
            to="/dashboard"
            component={RouterLink}
          >
            Dashboard
          </Link>
          <Box mb={3}>
            <Typography variant="body1" color="inherit">
              Create Product
            </Typography>
          </Box>
        </Breadcrumbs>
        <Typography variant="h4" color="textPrimary">
          Create a new product
        </Typography>
      </Grid>
      <Grid item>
        <Button component={RouterLink} to="/dashboard/list-products">
          Cancel
        </Button>
      </Grid>
    </Grid>
  );
};

Listing 8-5Updating the Header.tsx of ProductCreateView

最后,让我们添加useStyle组件。

const useStyles = makeStyles(() => ({
  root: {},
}));

Listing 8-6Adding the Style Component to the Header.tsx of ProductCreateView

我们现在可以在ProductCreateViewindex.tsx中使用这个头组件。

import React from 'react';
import { Container, makeStyles } from '@material-ui/core';

import Header from './Header';
import ProductCreateForm from './ProductCreateForm';
import Page from 'app/components/page';

const ProductCreateView = () => {
  const classes = useStyles();

  return (
    <Page className={classes.root} title="Product Create">
      <Container>
        <Header />
        <ProductCreateForm />
      </Container>
    </Page>
  );
};

const useStyles = makeStyles(theme => ({
  root: {
    minHeight: '100%',
    paddingTop: theme.spacing(3),
    paddingBottom: 100,
  },
}));

export default ProductCreateView;

Listing 8-7Updating the index.tsx of ProductCreateView

所以目前来说这很好。我们出发去更新ProductCreateForm

更新 ProductCreateForm

首先,我们需要添加一些额外的 TypeScript 文件。

在文件夹ProductCreateView下,创建一个名为schema的文件夹。在schema,下,我们将为我们的 Yup 产品验证添加一个新文件。

文件路径是

ProductCreateView ➤ schema ➤ yupProductValidation.ts

清单 8-8 显示了 Yup 产品验证模式。

// the * means all

import * as Yup from 'yup';

export const yupProductValidation = Yup.object().shape({
  category: Yup.string().max(255),
  description: Yup.string().max(5000),
  images: Yup.array(),
  includesTaxes: Yup.bool().required(),
  isTaxable: Yup.bool().required(),
  name: Yup.string().max(255).required(),
  price: Yup.number().min(0).required(),
  productCode: Yup.string().max(255),
  productSku: Yup.string().max(255),
  salePrice: Yup.number().min(0),
});

Listing 8-8Creating the yupProductValidation Schema

之后,我们需要为 Formik 创建产品默认值或初始值。最好在一个单独的文件中定义,这样更干净一点。

schema下创建一个新文件,命名为productDefaultValue.ts,,如清单 8-9 所示。

文件路径是

ProductCreateView ➤ schema ➤ productDefaultValue.ts

注意,我们从 models 文件夹中导入了ProductType组件。

import { ProductType } from 'models/product-type';

export const productDefaultValue: ProductType = {
  attributes: [],
  category: '',
  createdAt: '',
  currency: '',
  id: '',
  image: '',
  inventoryType: 'in_stock',
  isAvailable: false,
  isShippable: false,
  name: '',
  quantity: 0,
  updatedAt: '',
  variants: 0,
  description: '',
  images: [],
  includesTaxes: false,
  isTaxable: false,
  productCode: '',
  productSku: '',
  salePrice: '',
  price: 0,
};

Listing 8-9Creating Initial Values in the productDefaultValue.tsx

好了,我们完成了ProductDefaultValue.tsx.现在让我们安装一个名为 Quill 的富文本编辑器库。

安装 React 管

让我们安装名为react-quill.的 React 版本。回想一下,Quill 是现代网络的开源 WYSIWYG 编辑器。React-quill 是包装 Quill.js 的 React 组件:

npm install react-quill

安装完成后,我们需要将组件导入到src目录的index.tsx中。

import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';
import 'react-quill/dist/quill.snow.css';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import * as serviceWorker from 'serviceWorker';
import 'sanitize.css/sanitize.css';

Listing 8-10Adding react-quill to the index.tsx of the src Directory

在 components 文件夹下,创建一个单独的新文件,并将其命名为quill-editor.tsx . The file path is as follows:

app ➤ components ➤ quill-editor.tsx

打开新创建的文件,导入所有必需的命名组件。

import React from 'react';
import clsx from 'clsx';
import ReactQuill from 'react-quill';
import { makeStyles } from '@material-ui/core';

Listing 8-11Importing Named Components to quill-editor.tsx

然后,我们需要类型定义和QuillEditor组件。

type Props = {
  className?: string;
  [key: string]: any;
};

const QuillEditor = ({ className, ...rest }: Props) => {
  const classes = useStyles();

  return <ReactQuill className={clsx(classes.root, className)} {...rest} />;
};

Listing 8-12Adding the QuillEditor Component

在清单 8-12 中,我们还返回了 Quill 并使用了 rest/spread 操作符,这意味着我们将QuillEditor拥有的任何东西传递给ReactQuill

接下来,我们将添加来自 Material-UI Core 的makeStyles,如清单 8-13 所示。

const useStyles = makeStyles(theme => ({
  root: {
    '& .ql-toolbar': {
      borderLeft: 'none',
      borderTop: 'none',
      borderRight: 'none',
      borderBottom: `1px solid ${theme.palette.divider}`,
      '& .ql-picker-label:hover': {
        color: theme.palette.secondary.main,
      },
      '& .ql-picker-label.ql-active': {
        color: theme.palette.secondary.main,
      },
      '& .ql-picker-item:hover': {
        color: theme.palette.secondary.main,
      },
      '& .ql-picker-item.ql-selected': {
        color: theme.palette.secondary.main,
      },
      '& button:hover': {
        color: theme.palette.secondary.main,
        '& .ql-stroke': {
          stroke: theme.palette.secondary.main,
        },
      },

      '& button:focus': {
        color: theme.palette.secondary.main,
        '& .ql-stroke': {
          stroke: theme.palette.secondary.main,
        },
      },
      '& button.ql-active': {
        '& .ql-stroke': {
          stroke: theme.palette.secondary.main,
        },

      },
      '& .ql-stroke': {
        stroke: theme.palette.text.primary,
      },
      '& .ql-picker': {
        color: theme.palette.text.primary,
      },
      '& .ql-picker-options': {
        padding: theme.spacing(2),
        backgroundColor: theme.palette.background.default,
        border: 'none',
        boxShadow: theme.shadows[10],
        borderRadius: theme.shape.borderRadius,
      },
    },
    '& .ql-container': {
      border: 'none',
      '& .ql-editor': {
        fontFamily: theme.typography.fontFamily,
        fontSize: 16,
        color: theme.palette.text.primary,
        '&.ql-blank::before': {
          color: theme.palette.text.secondary,
        },
      },
    },
  },
}));

export default QuillEditor;

Listing 8-13Adding the Styling Components to the QuillEditor Component

这就是羽毛笔编辑器。我们需要创建一个组件,将字节的值转换成人类可读的字符串。

在 utils 文件夹下,创建一个新文件,命名为bytes-to-size.ts; see列表 8-14 。

const bytesToSize = (bytes: number, decimals: number = 2) => {
  if (bytes === 0) return '0 Bytes';

  const k = 1024;
  const dm = decimals < 0 ? 0 : decimals;
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));

  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
};

export default bytesToSize;

Listing 8-14Creating the bytes-to-size.tsx

bytes-to-size 组件检查字节值,并将其转换为用户在上传文件时易于理解的字符串(即 KB、MB、GB、TB)。

下一个任务是创建一个名为 images 的文件夹。

在文件夹public,下创建子文件夹imagesproducts;并在products文件夹下添加一个。名为add_file.svg.的 svg 文件

文件路径如下:

public ➤ images ➤ products ➤ add_file.svg

你可以在下面我的 GitHub 链接中找到第七章相同文件路径下的图片(见图 8-1 )。下载图像并将其复制或拖动到新创建的。svg 文件。

img/506956_1_En_8_Fig1_HTML.jpg

图 8-1

add_file.svg 的屏幕截图

转到github.com/webmasterdevlin/practical-enterprise-react/blob/master/chapter-7/publimg/add_file.svg

现在,让我们为拖放功能导入另一个库。

安装 React Dropzone

npm i react-dropzone

然后,让我们在文件夹组件中创建另一个文件,并将其命名为files-dropzone.tsx.

文件路径如下:

app ➤ components ➤ files-dropzone.tsx

让我们首先添加命名的导入组件,如清单 8-15 所示。

import React, { useState, useCallback } from 'react';
import clsx from 'clsx';
import { useDropzone } from 'react-dropzone';
import PerfectScrollbar from 'react-perfect-scrollbar';
import FileCopyIcon from '@material-ui/icons/FileCopy';
import MoreIcon from '@material-ui/icons/MoreVert';
import {
  Box,
  Button,
  IconButton,

  Link,
  List,
  ListItem,
  ListItemIcon,
  ListItemText,
  Tooltip,
  Typography,
  makeStyles,
} from '@material-ui/core';

import bytesToSize from 'utils/bytes-to-size';

Listing 8-15Adding the Named Import Components to FilesDropzone

清单 8-15 从React Dropzone导入了useDropzone,从React Perfect Scrollbar库中导入了PerfectScrollbar

我们还包括了来自材质 UI 图标的附加图标。最后,我们导入了bytesToSize文件。

接下来,让我们定义组件的类型并创建一些本地状态。

type Props = {
  className?: string;
};

const FilesDropzone = ({ className, ...rest }: Props) => {
  const classes = useStyles();
  const [files, setFiles] = useState<any[]>([]);

  //this will be triggered when we drop a file in our component

  const handleDrop = useCallback(acceptedFiles => {
    setFiles(prevFiles => [...prevFiles].concat(acceptedFiles));
  }, []);

  const handleRemoveAll = () => {
    setFiles([]);
  };

  //useDropzone - we're deconstructing it to get the properties of the object it returns
 //we're assigning handleDrop on onDrop

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop: handleDrop,
  });

  return (
    <div className={clsx(classes.root, className)} {...rest}>
      <div

        className={clsx({
          [classes.dropZone]: true,
          [classes.dragActive]: isDragActive,
        })}
        {...getRootProps()}
      >
        <input {...getInputProps()} />
        <div>
          <img
            alt="Select file"
            className={classes.image}
            srcimg/add_file.svg"        ---> here we added the svg file
          />
        </div>
        <div>
          <Typography gutterBottom variant="h5">
            Select files
          </Typography>
          <Box mt={2}>
            <Typography color="textPrimary" variant="body1">
              Drop files here or click <Link underline="always">browse</Link>{' '}
              thorough your machine
            </Typography>
          </Box>
        </div>
      </div>

      {files.length > 0 && (

          <PerfectScrollbar options={{ suppressScrollX: true }}>
            <List className={classes.list}>
              {files.map((file, i) => (
                <ListItem divider={i < files.length - 1} key={i}>
                  <ListItemIcon>
                    <FileCopyIcon />
                  </ListItemIcon>
                  <ListItemText
                    primary={file.name}
                    primaryTypographyProps={{ variant: 'h5' }}
                    secondary={bytesToSize(file.size)}
                  />

                  <Tooltip title="More options">
                    <IconButton edge="end">
                      <MoreIcon />
                    </IconButton>
                  </Tooltip>
                </ListItem>
              ))}
            </List>
          </PerfectScrollbar>
          <div className={classes.actions}>
            <Button onClick={handleRemoveAll} size="small">
              Remove all
            </Button>
            <Button color="secondary" size="small" variant="contained">
              Upload files
            </Button>
          </div>

      )}
    </div>
  );
};

Listing 8-16Creating the FilesDropzone Component

接下来,我们为FilesDropzone添加样式组件,如清单 8-17 所示。

const useStyles = makeStyles(theme => ({
  root: {},
  dropZone: {
    border: `1px dashed ${theme.palette.divider}`,
    padding: theme.spacing(6),
    outline: 'none',
    display: 'flex',
    justifyContent: 'center',
    flexWrap: 'wrap',
    alignItems: 'center',
    '&:hover': {
      backgroundColor: theme.palette.action.hover,
      opacity: 0.5,
      cursor: 'pointer',
    },
  },
  dragActive: {
    backgroundColor: theme.palette.action.active,
    opacity: 0.5,
  },
  image: {
    width: 130,
  },
  info: {
    marginTop: theme.spacing(1),
  },
  list: {
    maxHeight: 320,
  },
  actions: {
    marginTop: theme.spacing(2),
    display: 'flex',
    justifyContent: 'flex-end',
    '& > * + *': {
      marginLeft: theme.spacing(2),
    },
  },

}));

export default FilesDropzone;

Listing 8-17Adding the Styling Components for the FilesDropzone

重新运行浏览器,导航到“创建产品”菜单,向下滚动以上传图像。您应该会看到与图 8-2 所示相同的内容。

img/506956_1_En_8_Fig2_HTML.jpg

图 8-2

上传图片的应用用户界面

接下来,我们将导入一个通知库来显示通知消息。

安装通知库

Notistack是一个 React 通知库,可以很容易地显示通知。它还允许用户将snackbarstoasts堆叠在一起:

npm i notistack

我们需要在 app 文件夹或根组件的index.tsx中有一个notistack提供者。

添加 snackbar 提供程序并导入命名组件,如清单 8-18 所示。

import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { BrowserRouter } from 'react-router-dom';
import { SnackbarProvider } from 'notistack';
import { GlobalStyle } from 'styles/global-styles';
import MainLayout from './layouts/main-layout';
import { Routes } from './routes';

export function App() {
  return (
    <BrowserRouter>
      <SnackbarProvider dense maxSnack={3}>
        <Helmet
          titleTemplate="%s - React Boilerplate"
          defaultTitle="React Boilerplate"
        >
          <meta name="description" content="A React Boilerplate application" />
        </Helmet>
        <MainLayout>
          <Routes />
        </MainLayout>
        <GlobalStyle />
      </SnackbarProvider>
    </BrowserRouter>
  );
}

Listing 8-18Wrapping the App Component with SnackbarProvider

完成之后,让我们构建 ProductCreateForm。

更新产品创建表单

让我们通过添加命名的导入组件来开始更新 ProductCreateForm,如清单 8-19 所示。

import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import clsx from 'clsx';
import { Formik } from 'formik';
import { useSnackbar } from 'notistack';
import {
  Box,
  Button,
  Card,
  CardContent,
  CardHeader,
  Checkbox,
  Divider,
  FormControlLabel,
  FormHelperText,
  Grid,
  Paper,
  TextField,
  Typography,
  makeStyles,
} from '@material-ui/core';

import FilesDropzone from 'app/components/files-dropzone';
import QuillEditor from 'app/components/quill-editor';
import { postProductAxios } from 'services/productService';
import { yupProductValidation } from'./schema/yupProductValidation';
import { productDefaultValue } from './schema/productDefaultValue';

Listing 8-19Adding the Named Import Components to ProductCreateForm

你会注意到我们已经从react-router-dom添加了useHistory。我们将需要useHistory来允许用户在创建新产品后浏览产品列表。我们需要Formiknotistack.snackbar

除此之外,我们还导入了FilesDropzoneQuillEditor以及服务{postProductAxios},,这允许我们在数据库中创建或添加新产品。

我们将使用由我们的产品对象的初始值组成的productDefaultValueyupProductValidation来验证创建新产品的需求。

接下来,让我们创建类型定义和一些本地状态;参见清单 8-20 。

const categories = [
  {
    id: 'shirts',
    name: 'Shirts',
  },
  {
    id: 'phones',
    name: 'Phones',
  },
  {
    id: 'cars',
    name: 'Cars',
  },
];

type Props = {
  className?: string;
};

const ProductCreateForm = ({ className, ...rest }: Props) => {
  const classes = useStyles();
  const history = useHistory();

  //we've deconstructed the snackbar to get just the enqueueSnackbar

  const { enqueueSnackbar } = useSnackbar();
  const [error, setError] = useState('');

Listing 8-20Creating the Type Alias and Local States of ProductCreateForm

接下来,让我们使用Formik属性,即initialValues, validationSchema,onSubmit事件处理程序,如清单 8-21 所示。

return (

    /*the required attributes or properties of Formik need to be initialized.
      initialValues, validationSchema, onSubmit.
    */
    <Formik
      initialValues={productDefaultValue}
      validationSchema={yupProductValidation}

      /*   The onSubmit, you can just initially write a function without anything inside.
Usually, I'd write an alert message first to trigger it as a proof of concept. */

      onSubmit={async (values, formikHelpers) => {
        try {
          await postProductAxios(values);

          formikHelpers.setStatus({ success: true });
          formikHelpers.setSubmitting(false);
          enqueueSnackbar('Product Created', {
            variant: 'success',
          });
          history.push('/dashboard/list-products');
        } catch (err) {
          alert('Something happened. Please try again.');
          setError(err.message);
          formikHelpers.setStatus({ success: false });
          formikHelpers.setSubmitting(false);
        }
      }}
    >

      {formikProps => (
        <form
          onSubmit={formikProps.handleSubmit}
          className={clsx(classes.root, className)}
          {...rest}
        >
          <Grid container spacing={3}>
            <Grid item xs={12} lg={8}>
              <Card>
                <CardContent>
                  <TextField
                    error={Boolean(
                      formikProps.touched.name && formikProps.errors.name,
                    )}
                    fullWidth
                    helperText={
                      formikProps.touched.name && formikProps.errors.name
                    }
                    label="Product Name"
                    name="name"
                    onBlur={formikProps.handleBlur}
                    onChange={formikProps.handleChange}
                    value={formikProps.values.name}
                    variant="outlined"
                  />
                  <Box mt={3} mb={1}>
                    <Typography variant="subtitle2" color="textSecondary">
                      Description
                    </Typography>
                  </Box>
                  <Paper variant="outlined">
                    <QuillEditor
                      className={classes.editor}
                      value={formikProps.values.description}
                      onChange={(value: string) =>
                        formikProps.setFieldValue('description', value)
                      }
                    />
                  </Paper>

                  {formikProps.touched.description &&
                    formikProps.errors.description && (
                      <Box mt={2}>
                        <FormHelperText error>
                          {formikProps.errors.description}
                        </FormHelperText>
                      </Box>
                    )}
                </CardContent>
              </Card>
              <Box mt={3}>
                <Card>
                  <CardHeader title="Upload Images" />
                  <Divider />
                  <CardContent>
                    <FilesDropzone />
                  </CardContent>
                </Card>
              </Box>
              <Box mt={3}>
                <Card>
                  <CardHeader title="Prices" />
                  <Divider />

                  <CardContent>
                    <Grid container spacing={3}>
                      <Grid item xs={12} md={6}>
                        <TextField
                          error={Boolean(
                            formikProps.touched.price &&
                              formikProps.errors.price,
                          )}
                          fullWidth
                          helperText={
                            formikProps.touched.price &&
                            formikProps.errors.price
                              ? formikProps.errors.price
                              : 'If you have a sale price this will be shown as old price'
                          }
                          label="Price"
                          name="price"
                          type="number"
                          onBlur={formikProps.handleBlur}
                          onChange={formikProps.handleChange}
                          value={formikProps.values.price}
                          variant="outlined"
                        />
                      </Grid>

                      <Grid item xs={12} md={6}>
                        <TextField
                          error={Boolean(
                            formikProps.touched.salePrice &&
                              formikProps.errors.salePrice,
                          )}
                          fullWidth
                          helperText={
                            formikProps.touched.salePrice &&
                            formikProps.errors.salePrice
                          }
                          label="Sale price"
                          name="salePrice"
                          type="number"
                          onBlur={formikProps.handleBlur}
                          onChange={formikProps.handleChange}
                          value={formikProps.values.salePrice}
                          variant="outlined"
                        />
                      </Grid>
                    </Grid>
                    <Box mt={2}>
                      <FormControlLabel
                        control={
                          <Checkbox
                            checked={formikProps.values.isTaxable}
                            onChange={formikProps.handleChange}
                            value={formikProps.values.isTaxable}
                            name="isTaxable"
                          />
                        }
                        label="Product is taxable"
                      />
                    </Box>

                    <Box mt={2}>
                      <FormControlLabel
                        control={
                          <Checkbox
                            checked={formikProps.values.includesTaxes}
                            onChange={formikProps.handleChange}
                            value={formikProps.values.includesTaxes}
                            name="includesTaxes"
                          />
                        }
                        label="Price includes taxes"
                      />
                    </Box>
                  </CardContent>
                </Card>
              </Box>
            </Grid>
            <Grid item xs={12} lg={4}>
              <Card>
                <CardHeader title="Organize" />
                <Divider />
                <CardContent>
                  <TextField
                    fullWidth
                    label="Category"
                    name="category"
                    onChange={formikProps.handleChange}
                    select
                    SelectProps={{ native: true }}
                    value={formikProps.values.category}
                    variant="outlined"
                  >
                    {categories.map(category => (
                      <option key={category.id} value={category.id}>
                        {category.name}
                      </option>
                    ))}
                  </TextField>

                  <Box mt={2}>
                    <TextField
                      error={Boolean(
                        formikProps.touched.productCode &&
                          formikProps.errors.productCode,
                      )}
                      fullWidth
                      helperText={
                        formikProps.touched.productCode &&.
                        formikProps.errors.productCode
                      }
                      label="Product Code"
                      name="productCode"
                      onBlur={formikProps.handleBlur}
                      onChange={formikProps.handleChange}
                      value={formikProps.values.productCode}
                      variant="outlined"
                    />
                  </Box>
                  <Box mt={2}>
                    <TextField
                      error={Boolean(
                        formikProps.touched.productSku &&
                          formikProps.errors.productSku,
                      )}
                      fullWidth
                      helperText={
                        formikProps.touched.productSku &&
                        formikProps.errors.productSku
                      }
                      label="Product Sku"
                      name="productSku"
                      onBlur={formikProps.handleBlur}
                      onChange={formikProps.handleChange}
                      value={formikProps.values.productSku}
                      variant="outlined"
                    />
                  </Box>
                </CardContent>
              </Card>
            </Grid>
          </Grid>

          {error && (
            <Box mt={3}>
              <FormHelperText error>{error}</FormHelperText>
            </Box>
          )}
          <Box mt={2}>
            <Button
              color="primary"
              variant="contained"
              type="submit"
              disabled={formikProps.isSubmitting}
            >
              Create product
            </Button>
          </Box>
        </form>
      )}
    </Formik>
  );
};

Listing 8-21Adding the Return Component of the ProductCreateForm

好的,那么清单 8-21 中发生了什么?我们已经把一切都包在Formik.下了

因为我们这样做了,,所以我们需要使用它的默认属性,即:

initialValues:我们正在通过productDefaultValues.

validationSchema:我们正在通过yupProductValidation.

一开始,我只是写一个函数,里面什么都没有。我只是设置了一个警报来触发它,并检查它是否正在发射。

Formik:这个组件发出formikProps(您可以随意命名它,但是我更喜欢这样命名,这样它的来源就一目了然了)。

formikProps,中,我们找到了 HTML 表单的FormonSubmit,这将触发Formik.onSubmit

这里,我们绑定了对象的名字。TextField来自于 Material-UI 核心。

先简单解释一下TextField里面的必备零件。如果您再次查看它,您会看到以下属性:

formikProps.touched.name && formikProps.errors.name,

label="Product Name"
name="name"
onBlur={formikProps.handleBlur}
onChange={formikProps.handleChange}
value={formikProps.values.name}

formikProps.touched.name:此时点击TextField of name.

当出现错误时,例如,您超出了允许的字符数或将其留空。

formikProps.handleBlur:当你离开TextField时触发,例如,点击后,你离开它去另一个字段。

formikProps.handleChange:每当你在键盘上敲击或输入什么的时候,这个就会更新名字的值。这将覆盖formikProps.values.name .中现有的将是我们将在现场看到的数据。

这看起来很复杂,但实际上,如果我们自己去做,事情会更复杂。Formik 在其网站 formik.org 上的行动号召是“在无泪的 React 中构建形式”,这是有原因的

如果你已经体验过编写带有验证和绑定的表单——在这种情况下你会看到输入的变化——那么你就会知道从头开始是一件相当痛苦且不好玩的事情。这是因为我们本质上需要做的是创建一个双向绑定。

然而,React 的主要问题是它是用单向数据流设计的。React 不同于 Svelte、Vue.js 和 Angular 等其他框架,在这些框架中,双向数据绑定很容易实现。

简单地说,在双向数据绑定中,当您将模型绑定到视图时,您会知道当您更改视图中的某些内容时,它会反映在模型中。所以基本上,在模型和视图之间有一个双向的数据流。

一个合理的用例是当用户更新或编辑他们的个人资料时,我们知道已经有来自 web 服务的数据输入。

当用户在轮廓输入表单中输入时,对象模型的值也被编辑。在 React 中,不编写大量代码很难做到这一点。

这就是为什么有 React 库可以用来创建表单,比如 React Forms、Formsy 和 Redux Form,但最流行的是 Formik,因为它非常容易使用和理解。

我强烈建议您使用 Formik 或任何这些表单库。尽可能不要实现或者从头开始写。

独自做这件事的另一个缺点是很难长期维护它,包括当你不得不把项目交给新的开发人员时。

大多数时候,最好使用流行的库,因为开发者可能已经知道如何使用或熟悉这些库,或者很容易从文档中理解或从在线社区团体获得帮助。

好了,独白到此为止。让我们回到TextField中的onChange,因为我想指出这里正在发生的另一件事。

您会注意到,我们只是将formikProps.handleChange放在onChange上,更改就会被触发。这是因为formikProps.handleChange签名与onChange期望的功能相匹配。

在这种情况下,这里的onChange发出一个事件。把你的鼠标悬停在它上面,你就会看到它。

但是寻找QuillEditor.下面的onChange,它发出一个字符串。所以它是不同的,这就是为什么handleChange在这里不工作:

            <QuillEditor
                      className={classes.editor}
                      value={formikProps.values.description}
                      onChange={(value: string) =>
                   formikProps.setFieldValue('description', value)
                      }
                    />

现在的问题是,我们怎么知道它不会起作用?当我们尝试它时,我们会知道,期待它工作,并得到一个错误。 是啊,我知道。但那是我的经验,意思是 handleChange, 在这种情况下不起作用。

但是如果你遇到这种问题,你正在使用的 onChange 可能会发出不同的类型。你需要做的是使用formik props . setfield value .这里我们需要传递两个参数:

string:哪一个是您的属性的名称

原始类型:描述的

你可以登记你的modelsproduct-type.ts.

每当你遇到onChange,时,首先做一个控制台日志,看看它发出的是什么类型或对象,尤其是使用 JavaScript 时。对于 TypeScript,您可以将鼠标悬停在上面查看。

img/506956_1_En_8_Figb_HTML.jpgTextField中需要注意的另一件事是,我们编写它的方式有一个模式,我们可以从TextField中创建一个抽象,并把它放在一个单独的文件中,以整理我们的ProductCreateForm.

运行或刷新您的应用。创建或添加新产品,并检查它是否显示在所有产品页面上。

img/506956_1_En_8_Figc_HTML.jpg

为了你的活动

  1. 创建一个删除一个或多个产品的新函数。

  2. 使用在selectedProducts中存储一个或多个产品 id 的handleSelectOneProduct功能。

  3. 通过创建一个接受 string 类型 id 的deleteProductAxios来更新postService.ts

  4. 这个新函数应该使用一个array.map进行循环,同时向json-server.发送一个删除请求,确保不仅在服务器中删除它,而且在 UI 中也删除它。

摘要

这一章我们已经学完了,我们在这里涉及了很多内容,特别是学习如何在 React 中使用 Formik 构建输入表单,以及表单上的 Yup 验证。最后,我们谈到了如何在 Material-UI 组件和其他库(如 Quill editor)的帮助下以智能的方式创建复杂的数据表。

在下一章,我们将学习一项重要的技能:状态管理,或者说如何使用 Redux 工具包 管理我们的状态。

九、通过 Redux 工具包使用 Redux 管理状态

在前一章中,我们构建了一个产品仪表板,并使用 Formik 作为输入表单,使用 Yup 库来验证用户的输入。创建和验证表单是任何前端开发人员都必须具备的基本和常见技能,但是现在,我们将继续学习更复杂的开发人员技能,即使用 Redux with Redux 工具包 管理应用的全局状态。

React 和 Redux 是状态管理的绝佳组合,尤其是在构建企业级应用时。但是配置 Redux 的复杂过程成了很多开发者的绊脚石。许多开发人员讨厌在 React 应用中设置 Redux 的复杂性。

于是,Redux 工具包 就诞生了。

正如其网站 redux-toolkit.js.org 所定义的,Redux 工具包 是“用于高效 Redux 开发的官方的、固执己见的、包含电池的工具集”以前称为 Redux Starter Kit,Redux 工具包 附带了有用的库,使 React 开发人员的生活更加轻松。

简而言之,Redux 工具包 现在是推荐使用 Redux 的方式。

在我们继续学习如何使用 Redux 工具包 所需的基本概念之前,我们先来谈谈 Redux。我们还将看看快速使用 Redux 工具包 所需的一些重要的 Redux 术语。最后,我们将通过使用 CodeSandbox 的快速 Redux 实现来完成本章。

Redux 概述

根据其官方网站,Redux 是“JavaScript 应用的可预测状态容器。”它主要用于管理单个不可变状态树(对象)中整个应用的状态。状态的任何变化都会创建一个新对象(使用操作和减速器)。我们将在本章后面详细讨论核心概念。

https://redux.js.org/ 的 Redux 网站如图 9-1 所示。

img/506956_1_En_9_Fig1_HTML.jpg

图 9-1

Redux 网站位于 redux. js. org/

如果我们正在构建一个大的 React 项目,我们通常会使用 Redux。对于较小的应用,我不认为你需要 Redux。现在让我们讨论一下为什么我们要在应用中使用 Redux。

为什么要用 Redux?

首先,我们的应用将有一个单独的状态存储。想象一下,商店拥有我们的组件需要到达的所有数据或状态。这非常方便,特别是对于大型应用,因为我们可以将大部分——不一定是全部——数据保存在一个位置,而不必担心必须将 props 发送到组件树的多个层次。

将数据从 React 组件树中的一个组件传递到另一个组件的过程通常有多层深,这种过程称为适当的钻取。是的,对于许多开发人员来说,这可能是一个相当头疼的问题。

这是一个常见的问题,您需要将数据从一个 React 组件传递到另一个组件,但是您必须通过许多其他不需要数据的组件才能到达您想要的目的地或组件。

是的,有大量的数据传递给各种组件,这些组件并不真正需要呈现数据,而只是将数据传递给下一个组件,直到数据到达需要它的组件。

假设您有这个 web 应用,并且有一个大的<div>或组件。

这个组件是许多组件的父组件,这些组件是子组件。这在图 9-2 中进行了说明,其中我们将容器作为父组件,并将两个组件(仪表板和顶栏)作为下一级组件。

img/506956_1_En_9_Fig2_HTML.jpg

图 9-2

React 中的适当钻孔

在仪表板下,我们有以下组件:

Sidebar ➤ Menu ➤ Component X.

在顶栏下,我们按级别深度顺序排列了以下组件:

Component Y ➤ Component ➤ Component

同步仪表板和顶栏组件可以很快完成——您只需要传递或发出一个事件,例如,从仪表板到容器,然后从容器到顶栏,反之亦然。

图 9-2 是 React 中支柱钻孔的图示。

公平地说,如果我们只是向下传递两层甚至三层数据,适当的钻探并不是那么糟糕。追踪数据流是容易做到的。但是,如果我们已经钻得太频繁,达到十层或更多层,问题就可能出现。

案例场景 :

如果您有一个四层深度的组件,并且您需要将数据共享或传递给另一个三层或四层深度的元素,该怎么办?

要解决的问题 : 你需要向组件 X 和组件 y 渲染或传递相同类型的数据,换句话说,生成的数据应该总是相同或同步的。如果组件 X 中有变化,它们也应该反映在组件 y 中。

这有点复杂,因为组件 X 和 Y 与它们的父组件(容器)是不同级别的。此外,随着应用的增长,还有一个可维护性的问题,因为最终,跟踪我们的应用发生的事情会变得更加困难。

在我们的应用中有一个解决这类问题或需求的方法。通常,这是状态管理的工作。

状态管理库通常允许组件访问存储。存储是状态的内存存储,任何组件都可以全局访问它。

存储还允许数据对其他组件进行 React。假设这个州有任何变化。在这种情况下,任何使用 状态的组件都会重新呈现 DOM 差异,或者无论组件的级别有多深,状态发生了什么变化都会反映在 UI 中,如图 9-3 所示,一个 React 式状态管理库。

img/506956_1_En_9_Fig3_HTML.jpg

图 9-3

React 状态管理存储

每个组件 都可以 直接访问商店;无需担心向上传递或返回到父组件并再次向下钻取以将数据或状态分配给另一个更深层次的组件。

可以将存储看作是内存中的本地存储,您可以使用它来访问状态。

状态管理本质上是一个大型 JavaScript 对象存储,是大型企业和现代 JavaScript 应用(如 Angular、Vue.js、React 和其他 JavaScript 框架)的流行解决方案,用于快速呈现不同组件中的状态。

为了更好地理解 Redux 中的状态管理,让我们看看 Redux 中使用的术语。

Redux的核心部件:

调度:触发动作的调度。对于接收方来说,接收一个动作,不能只是用一个普通的函数,然后把动作发送给 reducer。

您需要一个 dispatcher 函数将动作发送给 reducer。想象一下,调度员是你友好的 UPS 或 FedEx 快递员,负责给你送包裹。另一种思考调度员的方式是枪,而行动是子弹。枪需要击发,子弹才能释放到它的目标,也就是减速器。

Reducers : Reducers 的工作是修改店铺,也是唯一可以修改店铺的人。React-Redux 应用中可以有任意多的 reducers 来更新商店的状态。

存储:同步不同组件中所有状态的应用的全局状态。它是 React 式的;这就是为什么它可以同步各种组件中的所有状态。

选择器:选择器取一段状态呈现在 UI 中。本质上,选择器是从存储中获取状态的一部分或从存储中访问状态的函数。您不能只将整个商店导入组件中;您需要使用选择器从存储中获取状态。

现在我们已经对 Redux 有了一个大概的了解,也知道了为什么要在我们的应用中使用它,让我们从 Redux 工具包 (RTK)开始。

Redux 工具包

Redux 工具包 是一种自以为是的编写 Redux 的方式,因为社区意识到开发人员都有自己的 React-Redux 应用实现。

在创建 Redux 工具包(简称 RTK)之前,没有在 React 应用中实现或配置 Redux 的标准指南。它已经预装了有用的库,如 Redux、Immer、Redux Thunk 和 Reselect。

以下是每个库的简短描述:

Immer :处理店内不变性。

Redux :用于状态管理。

Redux Thunk :处理异步动作的中间件。RTK 提供默认选项,但如果您愿意,也可以使用 Redux-Saga。

重新选择:简化减速器功能。让我们能从全球商店中分得一杯羹。

图 9-4 如果想详细了解 RTK 的更多信息,是 Redux 工具包 的网站。

img/506956_1_En_9_Fig4_HTML.jpg

图 9-4

Redux 工具包网站

动作类型:避免我们的动作名称出现打字错误。

动作:携带修改店铺的说明。一个动作给 reducer 带来了关于如何处理存储内部状态的指令。

两种类型的操作:

  • 非异步动作、同步动作或没有副作用的动作:一个很好的例子是,如果你想在商店中保存一组使用复选框选择的项目。此操作不需要 HTTP 请求,因为您要保存在存储中的数据来自复选框的详细信息。

  • 异步动作或者有副作用的动作:这种动作通常需要一个 axios 函数来发送 HTTP 请求,例如,将 web 服务的响应保存在存储中,然后在 UI 中呈现。

  • 副作用:是响应一个 Redux 动作可能发生也可能不发生的过程。把它想象成一个行动,在你的行动得到回应之前,你不太确定接下来会发生什么。

例如,当你向 web 服务器发送一个请求时,你还不知道你将得到一个 200 OK 还是一个 404 还是一个错误 500。这种“走出去”并等待行动回应的过程被称为副作用,因为它本质上是我们无法“控制”或直到我们得到它才知道的东西。

为了更好或更全面地了解 Redux 状态管理是如何工作的,让我们看一下图 9-5——React 应用中 Redux 状态管理的流程。

冗余状态管理流程

img/506956_1_En_9_Fig5_HTML.jpg

图 9-5

React 应用内部的 Redux 状态管理流程

我们有从商店获取数据的组件。商店在初始设置时有一些默认属性,每个值都必须初始化,以免得到未定义的返回。

选择器:您会注意到选择器正在从存储中获取或选择一个状态。它是否是一个空的初始值并不重要,因为一旦存储加载了数据或状态,我们的组件就会被重新呈现,我们会在 UI 中看到新的值。

Dispatcher——异步和同步动作:组件可以通过 Dispatcher 发送一个动作。动作可以是异步的,也可以是同步的。

同步:有时也叫 非异步或非异步 ,意为同时发生。

异步:不同时发生。简单来说,两者的主要区别在于的等待时间。 在同步(非异步)代码过程中,有一个分步操作。

同时,异步代码通常是我们自己不处理的操作,比如发出 API 请求。我们提出一个请求;我们等待回应。但是在等待回复的同时,我们可以做另一个代码。当我们最终得到 API 响应时,就是我们处理它的时候。

减速器:同步或“非异步”动作将直接作用于减速器。缩减器然后将基于它们的动作修改或改变存储。

改变或修改可以意味着存储对象内的名字空间或模块的状态的改变。

那么什么是名称空间或者模块呢? 这些是彼此逻辑分离或状态分组。例如,您有一个配置文件的状态、一个付款状态、另一组对象的状态等。当我们开始实现 Redux 时,我们将在应用本身中讨论这一点。

异步:那么调度器发送一个异步动作呢?然后这个异步动作直接进入副作用。副作用会向服务发送请求。

副作用从 web 服务获得成功(例如,200 OK)或失败响应(例如,4xx 或 5xx)。无论我们从服务中得到什么样的 React 或行动,副作用都会把它发送给减少者。缩减者将再次决定如何处理他们收到的动作。

另一件要注意的事情是,副作用的动作可以根据服务的响应而改变。在操作中,我们需要使用 try-catch 块。例如,在 try 中,如果是 200,就这样做,如果是 400,就这样做,等等。

在 CodeSandbox 中使用 RTK

如果你想在应用中实现 RTK 之前先体验一下,请访问这个神奇的网站 https://codesandbox.io/s/redux-toolkit-matchers-example-e765q 来看看 Redux 的快速实现。

图 9-6 是 CodeSandbox 网站上 Redux 工具包 Matchers 示例的截图。

img/506956_1_En_9_Fig6_HTML.jpg

图 9-6

codesandbox.io/redux-toolkit-matchers-example-e765q 截图

在侧边栏菜单中,注意两个文件夹:应用和功能。这些是 Redux 实现,根据 Redux 工具包 的创建者,这就是我们应该如何构建 React-Redux 应用。

例如,创建放置名称空间的文件夹功能,并在 app 文件夹中设置您的商店。商店和减压器不同,但是在商店里你可以找到所有的减压器。

img/506956_1_En_9_Fig7_HTML.jpg

图 9-7

codesandbox.io 上的存储和 reducers 的屏幕截图

每个名称空间或模块都有它的缩减器。在流程的末端,也就是商店,是你组合所有减压器的地方。

在下一章,我们将使用 Redux DevTools。您将看到 React 以及 Angular 开发人员喜欢 Redux 的一些原因:

  • 时间旅行调试工具

  • 状态是可预测的,所以很容易测试

  • 集中式状态和逻辑

  • 灵活的用户界面适应性

顺便说一下,Angular 中还有一个 Redux 实现,叫做 NgRx。是 Redux 加 RxJS。还有一些其他的 Angular 应用的状态管理库,但我相信 NgRx 是目前最流行的一个。

摘要

在本章中,我们讨论了使用 Redux 和 Redux 工具包 进行状态管理。我们了解到 Redux 工具包 是为了简化 Redux 的设置而开发的,尤其是在大型 React 应用中。

我们还了解到,当我们需要将数据传递给几层深的组件和另一个组件树中的组件时,Redux 可能是一种有效而方便的方法来解决我们的正确钻取问题。最后,我们展示了 https://codesandbox.io/ 网站,在那里我们可以进行快速的网络开发,并在学习 RTK 时获得即时反馈。

在下一章中,我们将开始在项目应用中实现我们在这里讨论的内容。

十、设置 Redux 工具包并调度一个异步动作

在前一章中,我们学习了使用 Redux 工具包 管理状态的概念。我们讨论了 React 应用中的 prop drilling,并展示了在 React 中编写 Redux 时的模式。

现在,正如承诺的那样,在这一章中,我们在这里开始变脏:

  • 设置 Redux 工具包

  • 向缩减器分派异步动作

  • 将商店的状态呈现给我们的用户界面,特别是日历视图

创建日历视图组件

为此,我们现在将创建日历视图组件。

打开仪表板目录,我们将创建两个文件夹,calendarCalendarView,以及index.tsx文件:

dashboard ➤ calendar ➤ CalendarView ➤ index.tsx

打开index.tsx file,现在只添加一个 h1 标签,如清单 10-1 所示。

import React from 'react';

const Index = () => {
  return (
    <div>
      <h1>Calendar Works!</h1>
    </div>
  );
};

export default Index;

Listing 10-1Creating index.tsx of CalendarView

我们的下一个练习是更新路线,因为我们需要在 routes.tsx 中注册日历组件。

更新路线

转到routes.tsx,并注册CalendarView.我们可以把它放在ProductCreateView之后,如清单 10-2 所示。

<Route exact path={path + '/calendar'}
                  component={lazy(
                  () => import('./views/dashboard/calendar/CalendarView'),
                  )} />

Listing 10-2Registering the CalendarView in routes.tsx

更新仪表板边栏导航

在 routes 文件夹中注册日历后,我们将向仪表板侧栏导航添加一个日历图标。

转到dashboard-sidebar-navigation进行更新。首先,从 React Feather 添加日历图标。同样,我们将其重命名为CalendarIcon.

import { PieChart as PieChartIcon,
        ShoppingCart as ShoppingCartIcon,
        ChevronUp as ChevronUpIcon,
        ChevronDown as ChevronDownIcon,
        Calendar as CalendarIcon,
        List as ListIcon,
        FilePlus as FilePlusIcon,
        LogOut as LogOutIcon,
} from 'react-feather';

Listing 10-3Importing the Calendar Component to the dashboard-sidebar-navigation

既然我们已经将它添加到了DashboardSidebarNavigation组件中,让我们将另一个菜单放在 Create Product 下面,如清单 10-4 所示。

<ListSubheader>Applications</ListSubheader>
              <Link className={classes.link} to={`${url}/calendar`}>
              <ListItem button>
                <ListItemIcon>
                  <CalendarIcon/>
                </ListItemIcon>
                <ListItemText primary={'Calendar'} />
              </ListItem>
              </Link>

Listing 10-4Creating a Calendar Icon Menu in the dashboard-sidebar-navigation

刷新浏览器看到如图 10-1 所示的日历菜单。

img/506956_1_En_10_Fig1_HTML.jpg

图 10-1

在用户界面中显示日历

既然我们已经看到它正在工作,让我们为我们的日历建立模型。在 models 文件夹中,添加一个文件并将其命名为calendar-type.ts.我们将创建 CalendarView 的形状或模型类型,如清单 10-5 所示。

export type EventType = {
  id: string;
  allDay: boolean;
  color?: string;
  description: string;
  end: Date;
  start: Date;
  title: string;
};

//union type 

export type ViewType =
  | 'dayGridMonth'
  | 'timeGridWeek'
  | 'timeGridDay'
  | 'listWeek';

Listing 10-5Creating the Shape or Model Type of the CalendarView

好了,是时候让减压器进入商店了。记住 Redux 中的 reducers 是我们用来管理应用状态的。

还原剂

我们将首先进行一些重构,但我们将确保不会丢失 Redux 的任何核心功能。

打开reducers.tsx并用清单 10-6 所示的代码替换它。插入的注释是对每个问题的简要解释。

/* Combine all reducers in this file and export the combined reducers.
combineReducers - turns an object whose values are different reducer functions into a single reducer function. */

import { combineReducers } from '@reduxjs/toolkit';

/*  injectedReducers - an easier way of registering a reducer */
const injectedReducers = {
  //reducers here to be added one by one.
};

/* combineReducers requires an object.we're using the spread operator (...injectedReducers) to spread out all the Reducers */

const rootReducer = combineReducers({
  ...injectedReducers,
});

/* RooState is the type or shape of the combinedReducer easier way of getting all the types from this rootReduder instead of mapping it one by one. RootState - we can use the Selector to give us intelli-sense in building our components. */

export type RootState = ReturnType<typeof rootReducer>;
export const createReducer = () => rootReducer;

Listing 10-6Refactoring the reducers.ts

接下来,我们还需要更新商店并简化它。目前有 Saga 实现,但我们不需要它。我们将使用一个更简单的副作用 Thunk。

打开configureStore.ts并用下面的代码重构,如清单 10-7 所示。

/*Create the store with dynamic reducers */

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { forceReducerReload } from 'redux-injectors';

import { createReducer } from './reducers';

export function configureAppStore() {
  const store = configureStore({

    /*reducer is required. middleware, devTools, and the rest are optional */
    reducer: createReducer(),
    middleware: [
      ...getDefaultMiddleware({
        serializableCheck: false,
      }),
    ],
    devTools: process.env.NODE_ENV !== 'production',
  });

  /* Make reducers hot reloadable, see http://mxs.is/googmo istanbul ignore next */

  if (module.hot) {
    module.hot.accept('./reducers', () => {
      forceReducerReload(store);
    });
  }

  return store;
}

Listing 10-7Refactoring the configureStore.ts

让我们进一步检查清单 10-8 中发生了什么。

在商店设置中,我们使用来自Redux 工具包configureStoregetDefaultMiddleware

如果您将光标悬停在 getDefaultMiddleware 上,您将看到这条消息:“它返回一个包含 ConfigureStore()安装的默认中间件的数组。如果您希望使用自定义中间件阵列配置您的商店,但仍保持默认设置,这将非常有用。”

来自 redux-injectors 的是我们的热重装。

从 rootReducer 返回 combinedReducers 的函数。

是一组插件或中间件。

我们需要通过一个提供者将它注入到我们的组件中。

在那之后,我们去

 src ➤ index.tsx

在 React 中,如果您看到一个名称提供者作为后缀的组件,这意味着您必须将它包装在根组件中。

提供者组件提供对整个应用的访问。在清单 10-8 中,我们将根组件(index.tsx)包装在提供者组件中。

/*wrapping the root component inside a provider gives all the component an access
 to the provider component or the whole application */

const ConnectedApp = ({ Component }: Props) => (
  <Provider store={store}>
    <HelmetProvider>
      <Component />
    </HelmetProvider>
  </Provider>
);

Listing 10-8Wrapping the Root Component (index.tsx) Inside a Provider Component

该提供程序是从 React-Redux 派生的。这是样板文件为我们设置的。

注意,提供者有一个必需的属性商店,我们将在configureStore.ts.中创建的商店传递给它,这就是为什么我们从store/configureStore.中导入了configureAppStore

这使得store成为事实的单一来源——可用于我们应用中的所有组件。

接下来,我们需要更新根组件的 index.tsx,如清单 10-9 所示。请记住,这个 index.tsx 是应用的入口文件——仅用于设置和样板代码。

import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';
import 'react-quill/dist/quill.snow.css';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import * as serviceWorker from 'serviceWorker';
import 'sanitize.css/sanitize.css';

// Import root app
import { App } from 'app';
import { HelmetProvider } from 'react-helmet-async';
import { configureAppStore } from 'store/configureStore';

// Initialize languages
import './locales/i18n';

const store = configureAppStore();
const MOUNT_NODE = document.getElementById('root') as HTMLElement;

interface Props {
  Component: typeof App;
}

/*wrapping the root component inside a provider gives all the component an access
 to the provider component or the whole application */

const ConnectedApp = ({ Component }: Props) => (
  <Provider store={store}>
    <HelmetProvider>
      <Component />
    </HelmetProvider>
  </Provider>
);
const render = (Component: typeof App) => {
  ReactDOM.render(<ConnectedApp Component={Component} />, MOUNT_NODE);
};

if (module.hot) {

  // Hot reloadable translation json files and app
  // modules.hot.accept does not accept dynamic dependencies,
  // have to be constants at compile-time

  module.hot.accept(['./app', './locales/i18n'], () => {
    ReactDOM.unmountComponentAtNode(MOUNT_NODE);
    const App = require('./app').App;
    render(App);
  });
}

render(App);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Listing 10-9Updating the index.tsx of the Root Component

在这之后,让我们只是做一点清理。

清理时间

删除存储文件夹中的文件夹_tests_。我们还将取出types文件夹,因为我们已经有了一个RootState.

img/506956_1_En_10_Fig2_HTML.jpg

图 10-2

删除存储和类型文件夹中的 tests 文件夹

接下来,找到 utils 文件夹并删除除了bytes-to-size.ts文件之外的所有内容。

img/506956_1_En_10_Fig3_HTML.jpg

图 10-3

删除 utils 文件夹

更新 Axios

就这么定了。我们现在去 axios 更新端点,如清单 10-10 所示。

Open src ➤ api ➤ axios.ts

export default api;

export const EndPoints = {
  sales: 'sales',
  products: 'products',
  events: 'event',
};

Listing 10-10Updating the Endpoints in axios.ts

然后让我们添加另一组假数据,在db.json.产品后添加以下事件数据。事件数组包含七个事件对象。

复制清单 10-11 中的代码,并将其添加到 db.json 文件中。

"events": [
    {
      "id": "5e8882e440f6322fa399eeb8",
      "allDay": false,
      "color": "green",
      "description": "Inform about new contract",
      "end": "2021-01-01T12:00:27.87+00:20",
      "start": "2021-01-01T12:00:27.87+00:20",
      "title": "Call Samantha"
    },
    {
      "id": "5e8882eb5f8ec686220ff131",
      "allDay": false,
      "color": null,
      "description": "Discuss about new partnership",
      "end": "2021-01-01T12:00:27.87+00:20",
      "start": "2021-01-01T12:00:27.87+00:20",
      "title": "Meet with IBM"
    },

    {
      "id": "5e8882f1f0c9216396e05a9b",
      "allDay": false,
      "color": null,
      "description": "Prepare docs",
      "end": "2021-01-01T12:00:27.87+00:20",
      "start": "2021-01-01T12:00:27.87+00:20",
      "title": "SCRUM Planning"
    },
    {
      "id": "5e8882f6daf81eccfa40dee2",
      "allDay": true,
      "color": null,
      "description": "Meet with team to discuss",
      "end": "2020-12-12T12:30:00-05:00",
      "start": "2020-11-11T12:00:27.87+00:20",
      "title": "Begin SEM"
    },

    {
      "id": "5e8882fcd525e076b3c1542c",
      "allDay": false,
      "color": "green",
      "description": "Sorry, John!",
      "end": "2021-01-01T12:00:27.87+00:20",
      "start": "2021-01-01T12:00:27.87+00:20",
      "title": "Fire John"
    },
    {
      "id": "5e888302e62149e4b49aa609",
      "allDay": false,
      "color": null,
      "description": "Discuss about the new project",
      "end": "2021-01-01T12:00:27.87+00:20",
      "start": "2021-01-01T12:00:27.87+00:20",
      "title": "Call Alex"
    },

    {
      "id": "5e88830672d089c53c46ece3",
      "allDay": false,
      "color": "green",
      "description": "Get a new quote for the payment processor",
      "end": "2021-01-01T12:00:27.87+00:20",
      "start": "2021-01-01T12:00:27.87+00:20",
      "title": "Visit Samantha"
    }
  ]

Listing 10-11Adding the Events Object in db.json

实现 Redux 工具包

好了,现在让我们来做实现 Redux 工具包的有趣部分。

我们将在这个应用中使用两种实现,这样您将了解两者是如何工作的,并且您可以更容易地使用现有的 React–Redux 工具包 项目。

在您很快会遇到的许多不同的项目中,实现几乎是相同的;有时,这只是文件夹结构和创建的文件数量的问题。

在这里,我们将把所有的动作和 reducers 写在一个文件中,我们将把它命名为 calendarSlice.ts。

src目录中,创建一个新文件夹,并将其命名为features; this,这是我们将实现 Redux 的地方。

features,里面新建一个文件夹并命名为calendar.calendar里面新建一个文件名为calendarSlice.ts.

Redux 工具包 建议在名称空间中添加后缀 Slice。

img/506956_1_En_10_Figa_HTML.jpg

打开calendarSlice文件,让我们添加一些命名的导入(清单 10-12 )。

/*PayloadAction is for typings  */
import {
  createSlice,
  ThunkAction,
  Action,
  PayloadAction,
} from '@reduxjs/toolkit';

import { RootState } from 'store/reducers';
import { EventType } from 'models/calendar-type';
import axios, { EndPoints } from 'api/axios';

Listing 10-12Adding the Named Import Components in calendarSlice

接下来,让我们在 calendarSlice 中进行键入,如清单 10-13 所示。

/*typings for the Thunk actions to give us intlelli-sense */
export type AppThunk = ThunkAction<void, RootState, null, Action<string>>;

/*Shape or types of our CalendarState  */

interface CalendarState {
  events: EventType[];
  isModalOpen: boolean;
  selectedEventId?: string;     //nullable
  selectedRange?: {                       //nullable
    start: number;
    end: number;
  };

  loading: boolean;  //useful for showing spinner or loading screen

  error: string;
}

Listing 10-13Creating the Typings/Shapes in calendarSlice

仍然在我们的 calendarSlice 文件中,我们将在 initialState 中初始化一些值,如清单 10-14 所示。

/*initialState is type-safe, and it must be of a calendar state type.
  It also means that you can't add any other types here that are not part of the calendar state we’ve already defined.  */

const initialState: CalendarState = {
  events: [],
  isModalOpen: false,
  selectedEventId: null,
  selectedRange: null,
  loading: false,
  error: '',
};

Listing 10-14Adding the Default Values of the initialState

然后,我们继续创建namespacecreateSlice,,如清单 10-15 所示。我们将命名空间和 createSlice 添加到 calendarSlice。

const calendarNamespace = 'calendar';

/*Single-File implementation of Redux-Toolkit*/

const slice = createSlice({

  /*namespace for separating related states. Namespaces are like modules*/
  name: calendarNamespace,

  /*initialState is the default value of this namespace/module and it is required.*/

  initialState, // same as initialState: initialState

  /*reducers --  for non asynchronous actions. It does not require Axios.*/
  /* the state here refers to the CalendarState */

  reducers: {
    setLoading(state, action: PayloadAction<boolean>) {
      state.loading = action.payload;
    },
    setError(state, action: PayloadAction<string>) {
      state.error = action.payload;
    },
    getEvents(state, action: PayloadAction<EventType[]>) {
      state.events = action.payload;
    },
  },
});

/* Asynchronous actions. Actions that require Axios (HTTP client)
 or any APIs of a library or function that returns a promise. */

export const getEvents = (): AppThunk => async dispatch => {
  dispatch(slice.actions.setLoading(true));
  dispatch(slice.actions.setError(''));
  try {
    const response = await axios.get<EventType[]>(EndPoints.events);
    dispatch(slice.actions.getEvents(response.data));
  } catch (error) {
    console.log(error.message);
    dispatch(slice.actions.setError(error.message));
  } finally {
    dispatch(slice.actions.setLoading(false));
  }
};

export default slice.reducer;

Listing 10-15Adding the Namespace and createSlice

createSlice是一个大对象,要求我们在nameinitialStatereducers中放置一些东西。

这里的reducers是不需要 axios 或者不基于承诺的非异步动作(也称为同步动作)的对象。

非异步动作/同步动作

让我们检查一下我们在 calendarSlice 中的非异步操作或同步操作中写了什么:

有两个参数(状态和动作),但是你只需要传递一个布尔值 PayloadAction。

setError in reducers:同第一个参数状态;不需要传递任何东西,因为 Thunk 会在引擎盖下处理它。我们只需要传递一些东西或者更新一个字符串PayloadAction,

getEvents in reducers:payload action 是 EventType 的数组。

异步操作

下面是我们的异步操作:

getEvents :返回 AppThunk 的函数和调度函数。

dispatch(slice.actions.setLoading(true)) :将加载从默认假更新为真。

我们传递的只是一个空字符串,所以基本上,每当我们有一个成功的请求时,我们就将这里的错误重置为空。

在 try-catch 块中,我们使用了一个axios.get,,它从Endpoints.events.返回一个数组EventType

我们得到的response.data将被发送到商店,以便更新状态。

在创建了calendarSlice之后,我们现在将更新根 reducers。

更新根缩减器

再次打开reducers.ts文件,更新injectedReducers.

首先,我们需要从 features/calendar/calendar slice 导入 calendarReducer,如清单 10-16 所示。

import { combineReducers } from '@reduxjs/toolkit';
import calendarReducer from 'features/calendar/calendarSlice'

Listing 10-16Adding the Named Component in reducers.ts

然后,在同一个文件中,注入我们的第一个缩减器,如清单 10-17 所示。

const injectedReducers = {
  calendar: calendarReducer,
};

Listing 10-17Injecting the calendarReducer in injectedReducers

我们现在可以使用这个名称空间calendar从这个日历中获取所需的状态。但是我们稍后会在组件中这样做。

现在,我们准备在日历视图或页面的 UI 组件中编写我们的selectorsdispatchers

更新日历视图

但是首先,让我们通过进入日历视图组件来测试dispatch。打开CalendarViewindex.tsx进入.

首先,我们将更新 CalendarView 的 index.tsx,如清单 10-18 所示。

import React, { useEffect } from 'react';
import { getEvents } from 'features/calendar/calendarSlice';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from 'store/reducers';

const CalendarView = () => {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(getEvents());
  }, []);

Listing 10-18Updating index.tsx of CalendarView

现在,我们将在控制台中检查getEventsuseDispatch以查看我们是否成功获取了数据。

确保您的服务器正在运行http://localhost:5000/events,并单击浏览器http://localhost:3000/dashboard/calendar中的刷新按钮。

打开Chrome DevToolsNetworkResponse查看事件数据,如图 10-4 所示。

img/506956_1_En_10_Fig4_HTML.jpg

图 10-4

在 Chrome DevTools 上显示了事件数据的截图

我们的 Redux 正在工作的概念证明!状态在浏览器中,我们可以使用它。让我们回到我们的CalendarView组件,我们将添加useSelector.

useSelector需要一个带有发送和返回RootState的签名的函数,现在我们可以访问reducer。现在,我们只能访问或获取日历,因为这是我们到目前为止添加的内容,如图 10-5 所示。

img/506956_1_En_10_Fig5_HTML.jpg

图 10-5

通过根状态演示智能感知

我们通过使用RootState获得智能感知。如果您使用的是 JavaScript 而不是 TypeScript,那么您必须猜测或搜索您的 reducer 文件。想象一下,如果您有一个包含几十甚至几百个文件的大型应用。寻找它会很快变得令人厌倦。

这个智能特性是 TypeScript 的亮点之一。你可以只输入点(。),然后它会显示您可以使用的所有可用减速器。

好的,现在让我们在CalendarView.做一些映射

return (
    <div>
      <h1>Calendar Works!</h1>

      {loading && <h2>Loading... </h2>}
      {error && <h2>Something happened </h2>}
      <ul>

                 /*conditional nullable chain */

        {events?.map(e => (
          <li key={e.id}>{e.title} </li>
        ))}
      </ul>
    </div>
  );
};

export default CalendarView;

Listing 10-19Mapping the CalendarView in the UI

好了,让我们检查一下我们在清单 10-19 中做了什么。

loading &&:如果条件为真,& &之后的元素运行;否则,如果状态为 false,则忽略它。同样的逻辑也适用于error &&

刷新浏览器,检查是否可以在数据呈现之前看到加载。

img/506956_1_En_10_Fig6_HTML.jpg

图 10-6

在 UI 中呈现 CalendarView

摘要

在这一章中,我希望你已经对 React 应用中的 Redux 实现流程有了更好的理解,包括如何将一个异步动作分派给 reducer,以及如何将状态从存储渲染到 UI。

我们还使用了状态管理库 Redux 工具包,并实现了它的助手函数 createSlice。我们还扩展了样式组件,以包括 Material-UI 中的日历视图组件。

在下一章,我们将继续我们的 Redux 课程,使用 Redux 创建、删除和更新事件。

十一、使用 RTK 在 FullCalendar 上创建、删除和更新事件

在上一章中,我们设置了 Redux 工具包,并学习了如何将异步动作分派到存储中。我们还开始构建我们的日历组件。

我们将继续我们停止的地方——使用 Redux 工具包 在日历组件上创建、删除和更新事件。为此,我们将使用 FullCalendar 库添加用户表单来创建、删除和更新事件。

为了让您先睹为快应用的最终外观,图 11-1 和 11-2 ,以及清单 11-1 ,在本章末尾提供了我们应用的用户界面。

img/506956_1_En_11_Fig2_HTML.jpg

图 11-2

第十一章末尾添加事件截图

img/506956_1_En_11_Fig1_HTML.jpg

图 11-1

第十一章末尾完整日历截图

安装时刻和完整日历

让我们开始安装一些第三方库,我们将需要这些库来构建完整的日历组件。

打开您的终端并安装下面的包,如清单 11-1 所示。

npm i moment @date-io/moment@1 @fullcalendar/core
npm i @fullcalendar/daygrid @fullcalendar/interaction
npm i @fullcalendar/list @fullcalendar/react
npm i @fullcalendar/timegrid @fullcalendar/timeline

Listing 11-1Importing Additional Libraries

让我们快速回顾一下我们已经安装的每个库:

moment.js :一个 JavaScript 日期库,用于解析、验证、操作和格式化日期。关于这个著名的库的项目状态的说明:即使它现在处于维护代码中——这意味着创建者不打算向库添加更多的功能——补丁和错误修复将继续。它拥有超过 1800 万次下载,截至 2021 年初仍处于上升趋势。

还有很多 JavaScript 库可以用来操作日期。但是学习 moment.js 同样可以帮助您理解其他 React 应用,因为许多 React 库可能都在使用这个流行的日期库。

如果你去npmjs.org找这些库,你会看到它们的如下定义:

@ date-io/moment:date-io-mono repo 的一部分,包含了 moment 的统一接口。我们将需要版本 1。

@fullcalendar/core :提供核心功能,包括日历类。

@fullcalendar/daygrid :在月视图或日网格视图上显示事件。

@ full calendar/interaction:提供事件拖放、调整大小、日期点击和可选动作的功能。

@fullcalendar/list :以项目列表的形式查看您的活动。

@fullcalendar/react :是个连接器。它告诉核心 FullCalendar 包开始使用 react 虚拟 DOM Node 而不是它通常使用的 Preact Node 进行渲染,将 FullCalendar 转换为“真正的”React 组件。

@fullcalendar/timegrid :在时间段网格上显示你的事件。

@fullcalendar/timeline :在水平时间轴上显示事件(无资源)。

成功导入所有库和模块后,让我们更新根组件。

更新根组件

首先,让我们在index.tsx,中添加这些模块,如清单 11-2 所示。

import MomentUtils from '@date-io/moment';
import {MuiPickersUtilsProvider} from '@material-ui/pickers';

Listing 11-2Importing Modules in the Root Component index.tsx

在同一个根文件中,我们将使用MuiPickersUtils包装从SnackbarProvide r 开始的所有内容,如清单 11-3 所示。

export function App() {
  return (
    <BrowserRouter>

             /*required props called utils and we're passing the MomentUtils */
      <MuiPickersUtilsProvider utils={MomentUtils}>
      <SnackbarProvider dense maxSnack={3}>
        <Helmet
          titleTemplate="%s - React Boilerplate"
          defaultTitle="React Boilerplate"
        >
          <meta name="description" content="A React Boilerplate application" />
        </Helmet>

        <MainLayout>
          <Routes />
        </MainLayout>

        <GlobalStyle />
      </SnackbarProvider>
      </MuiPickersUtilsProvider>
    </BrowserRouter>
  );
}

Listing 11-3Adding MuiPickersUtilsProvider to the Root Component

更新日历切片

接下来让我们看看组件calendarSlice .我们将添加新的非异步或同步动作以及异步动作。

创建事件操作

我们将首先创建事件动作,如清单 11-4 所示。

createEvent(state, action: PayloadAction<EventType>) {
      state.events.push(action.payload);
    },
    selectEvent(state, action: PayloadAction<string>) {
      state.isModalOpen = true;
      state.selectedEventId = action.payload;
    },
    updateEvent(state, action: PayloadAction<EventType>) {
      const index = state.events.findIndex(e => e.id === action.payload.id);
      state.events[index] = action.payload;
    },
    deleteEvent(state, action: PayloadAction<string>) {
      state.events = state.events.filter(e => e.id !== action.payload);
    },

    /*{start: number; end: number} - this is the shape of the model that we can define here right away, although we can also write it separately in the models' folder. */

    selectRange(state, action: PayloadAction<{ start: number; end: number }>) {

              /*deconstructing the payload */
      const { start, end } = action.payload;

      state.isModalOpen = true;
      state.selectedRange = {
        start,
        end,
      };
    },

    openModal(state) {
      state.isModalOpen = true;
    },
    closeModal(state) {
      state.isModalOpen = false;
      state.selectedEventId = null;
      state.selectedRange = null;
    },

Listing 11-4Creating the Event Actions in calendarSlice.ts

让我们回顾一下清单 11-4 中发生了什么。

createEvent:createEvent的参数是EventType的一个对象,为了产生一个新的事件,我们将把它放入一个现有的数组中。这将在EvenType的数组中生成一个新对象。

获取一个字符串,我们在这里修改两个状态。

updateEvent:获取一个事件类型,然后我们需要得到我们正在经过的这个EventType的位置(findIndex)。这是更新现有对象。

我们正在传递一个字符串,然后我们正在做一个过滤器。过滤器返回一个没有我们选择的 id(字符串)的新数组。

selectRange:接受一个有开始和结束的对象——都是 number 类型。

openModal:不需要任何参数;它只是将状态更新为 true。

closeModel:不需要任何参数;它只是将状态更新回 false,selectedEventId,selectedRange更新回 null。

接下来,我们将在同一个文件 calendarSlice.ts 中导出一些非异步动作,如清单 11-5 所示

添加非异步操作

/* Export these actions so components can use them.  Non-asynchronous actions. HTTP client is not needed. */

export const selectEvent = (id?: string): AppThunk => dispatch => {
  dispatch(slice.actions.selectEvent(id));
};

export const selectRange = (start: Date, end: Date): AppThunk => dispatch => {
  dispatch(
    slice.actions.selectRange({
      start: start.getTime(),
      end: end.getTime(),
    }),
  );
};

export const openModal = (): AppThunk => dispatch => {
  dispatch(slice.actions.openModal());
};

export const closeModal = (): AppThunk => dispatch => {
  dispatch(slice.actions.closeModal());
};

Listing 11-5Adding non-async actions in calendarSlice.ts

同样,让我们看看清单 11-5 中发生了什么。

selectEvent:这是一个高阶函数,它接受一个 id 并返回一个 dispatch 以供执行。

id 来自于selectEvent.。函数selectEvent的名称与调度selectEvent相同,以避免在导入组件时产生混淆。

selectRange:该功能也与动作selectRange.同名

让我们继续在 calendarSlice.ts 中添加我们的异步事件动作,如清单 11-6 所示。

export const createEvent = (event: EventType): AppThunk => async dispatch => {

/* data – we deconstructed the response object */

const { data } = await axios.post<EventType>(EndPoints.events, event);

  dispatch(slice.actions.createEvent(data));
};

export const updateEvent = (update: EventType): AppThunk => async dispatch => {

  /*updating the state in the database */

  const { data } = await axios.put<EventType>(
    `${EndPoints.events}/${update.id}`,
    update,
  );

  /*updating the state in the UI */

  dispatch(slice.actions.updateEvent(data));
};

export const deleteEvent = (id: string): AppThunk => async dispatch => {
  /*deleting from the database */

  await axios.delete(`${EndPoints.events}/${id}`);

  /*deleting it from the UI */
  dispatch(slice.actions.deleteEvent(id));
};

Listing 11-6Adding Asynchronous Event Actions in calendarSlice.ts

在清单 11-6 中,我们添加了另外三个事件——create event、updateEvent 和 deleteEvent:

createEvent:我们正在导出和使用createEvent函数,它接受一个EventType对象。我们异步运行dispatch,因为我们等待axios.post,,它从 Endpoints.events 中获取EventType,。必需的 body 参数是一个event

我们解构了响应对象,因为我们只需要一个属性,即data

我们在createEvent动作中传递data并分派它。

updateEvent:它也更新EventType,异步运行dispatch,等待axios.put,,并返回一个EventType

在这里,我们只需要一个 id。在运行异步调度并将其从数据库中删除后,我们还将它从 UI 中过滤出来。

FOR YOUR ACTIVITY

如果您注意到,在 createEven t、 updateEventdeleteEvent 中没有 try-catch 块。

例如,在deleteEvent中,如果没有 try-catch 并且axios.delete失败,调度将继续运行并删除 UI 中的对象,即使数据库中的对象没有被删除。所以现在,UI 中的状态和数据库中的状态会不匹配。

对于您的活动,在三个异步事件中实现一个 try-catch。看看我们对getEvents做了什么。不要忘记实现 setLoading 和 setError 操作。

完成活动后,我们将更新CalendarViewindex.tsx

更新日历视图

我们将从 Material-UI Core 导入页面组件模板以及ContainermakeStyles模块,如清单 11-7 所示。

import { Container, makeStyles} from '@material-ui/core';
import Page from 'app/components/page';

Listing 11-7Importing Named Components in CalendarView

然后让我们用新导入的页面和容器替换 return。我们还将在高度和填充上添加一些样式,如清单 11-8 所示。

  const CalendarView = () => {
  const classes = useStyles();
  const dispatch = useDispatch();

  /* destructuring it because we only need the events, loading, error */

  const { events, loading, error } = useSelector(
    (state: RootState) => state.calendar,
  );

  useEffect(() => {
    dispatch(getEvents());
  }, []);

  return (
    <Page className={classes.root} title="Calendar">
      <Container maxWidth={false}>
        <h1>Calendar Works!</h1>
        {loading && <h2>Loading... </h2>}
        {error && <h2>Something happened </h2>}
        <ul>
          {events?.map(e => (
            <li key={e.id}>{e.title} </li>
          ))}
        </ul>
      </Container>
    </Page>
  );
};

export default CalendarView;

const useStyles = makeStyles(theme => ({
  root: {
    minHeight: '100%',
    paddingTop: theme.spacing(3),
    paddingBottom: theme.spacing(3),
  },
}));

Listing 11-8Updating the Styling of the CalendarView Component

之后,让我们为CalendarView添加一个 Header 组件。创建一个新文件Header.tsx:

calendar ➤ CalendarView ➤ Header.tsx

创建标题组件

在清单 11-9 中,我们导入了Header.tsx.的命名组件

import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import clsx from 'clsx';
import { PlusCircle as PlusCircleIcon } from 'react-feather';
import NavigateNextIcon from '@material-ui/icons/NavigateNext';
import {
  Button,
  Breadcrumbs,
  Grid,
  Link,
  SvgIcon,
  Typography,
  makeStyles,
  Box,
} from '@material-ui/core';

Listing 11-9Importing the Named Components in Header.tsx

我们将立即使用我们的 Header 函数组件来跟进,如清单 11-10 所示。

/*nullable className string and nullable onAddClick function

  */

type Props = {
  className?: string;
  onAddClick?: () => void;
};

/* using the Props here and ...rest operator  */

const Header = ({ className, onAddClick, ...rest }: Props) => {
  const classes = useStyles();

  return (
    <Grid
      className={clsx(classes.root, className)}
      container
      justify="space-between"
      spacing={3}
      {...rest}
    >
      <Grid item>
        <Breadcrumbs
          separator={<NavigateNextIcon fontSize="small" />}
          aria-label="breadcrumb"
        >
          <Link
            variant="body1"
            color="inherit"
            to="/app"
            component={RouterLink}
          >
            Dashboard
          </Link>
          <Box>
            <Typography variant="body1" color="inherit">
              Calendar
            </Typography>
          </Box>
        </Breadcrumbs>
        <Typography variant="h4" color="textPrimary">
          Here's what you planned
        </Typography>
      </Grid>

      <Grid item>
        <Button
          color="primary"
          variant="contained"
          onClick={onAddClick}
          className={classes.action}
          startIcon={
            <SvgIcon fontSize="small">
              <PlusCircleIcon />
            </SvgIcon>
          }
        >
          New Event
        </Button>
      </Grid>
    </Grid>
  );
};

Listing 11-10Creating the Header Component

最后是 Material-UI Core 的makeStyles中的useStyles,如清单 11-11 所示。

const useStyles = makeStyles(theme => ({
  root: {},
  action: {
    marginBottom: theme.spacing(1),
    '& + &': {
      marginLeft: theme.spacing(1),
    },
  },
}));

export default Header;

Listing 11-11Adding the Styling Margin for the Header Component

接下来,我们将在 CalendarView 的index.tsx中使用新创建的 Header 组件,如清单 11-12 所示。

import Header from './Header';
...
return (
    <Page className={classes.root} title="Calendar">
      <Container maxWidth={false}>
        <Header />
        <h1>Calendar Works!</h1>

Listing 11-12Using the Header Component in the index.tsx of the CalendarView

刷新浏览器,您应该会看到相同的内容。

img/506956_1_En_11_Fig3_HTML.jpg

图 11-3

使用 index.tsx 中的 Header 组件后的 UI 截图

所以婴儿再次迈步。我们可以看到它正在工作。我们现在可以继续添加编辑事件表单。在CalendarView文件夹中,创建另一个组件并将其命名为AddEditEventForm.tsx

使用 Formik 创建添加编辑事件表单

AddEditEventForm,中,我们将使用moment.js, Formik ,yup validation。在这种情况下,我们不需要为 Yup 验证创建单独的文件。

打开文件AddEditEventForm并添加以下命名组件,如清单 11-13 所示。

import React from 'react';
import moment from 'moment';
import * as Yup from 'yup';
import { Formik } from 'formik';
import { useSnackbar } from 'notistack';
import { DateTimePicker } from '@material-ui/pickers';
import { Trash as TrashIcon } from 'react-feather';
import { useDispatch } from 'react-redux';
import {
  Box,
  Button,
  Divider,
  FormControlLabel,
  FormHelperText,
  IconButton,
  makeStyles,
  SvgIcon,
  Switch,
  TextField,
  Typography,
} from '@material-ui/core';

/*the async actions we created earlier in the calendarSlice */

import {
  createEvent,
  deleteEvent,
  updateEvent,
} from 'features/calendar/calendarSlice';
import { EventType } from 'models/calendar-type';

Listing 11-13Adding the Named Components in AddEditEventForm

Material-UI 中新增加的是DateTimePicker。如果你去看看 Material-UI 网站,你会看到很多可以重用的日期时间选择器组件,你不必从头开始创建自己的组件。

接下来,让我们为我们的组件 AddEditEventForm 编写类型定义,如清单 11-14 所示。

/* the ? indicates it is a nullable type */

type Props = {
  event?: EventType;
  onAddComplete?: () => void;
  onCancel?: () => void;
  onDeleteComplete?: () => void;
  onEditComplete?: () => void;
  range?: { start: number; end: number };
};

Listing 11-14Creating the Type or Shape of the AddEditEventForm

在清单 11-14 中,我们有Props,并且我们在AddEditEventForm组件中使用它,如清单 11-15 所示。

const AddEditEventForm = ({
  event,
  onAddComplete,
  onCancel,
  onDeleteComplete,
  onEditComplete,
  range,
}: Props) => {
  const classes = useStyles();
  const dispatch = useDispatch();
  const { enqueueSnackbar } = useSnackbar();

 /*event is coming from the parent of the AddEditEventForm */

  const isCreating = !event;

  const handleDelete = async (): Promise<void> => {
    try {
      await dispatch(deleteEvent(event?.id));
      onDeleteComplete();
    } catch (err) {
      console.error(err);
    }
  };

Listing 11-15Creating the AddEditEventForm Component

正如您在清单 11-15 中注意到的,AddEditEventForm使用 Props,以及 dispatch Snackbar,而handleDelete是一个异步 fun c 操作,它调度deleteEvent动作并传递 event.id .

当然,我们还没有完成。接下来,让我们使用 Formik 创建我们的表单。因为我们使用的是 TypeScript,所以必须初始化以下三个 Formik 属性:initialValues、validationSchema 和 onSubmit。

我们将首先从清单 11-16 所示的initialValues和使用 Yup 的 ValidationSchema 开始。

return (
    <Formik
      initialValues={getInitialValues(event, range)}
      validationSchema={Yup.object().shape({
        allDay: Yup.bool(),
        description: Yup.string().max(5000),
        end: Yup.date().when(
          'start',
          (start: Date, schema: any) =>
            start &&
            schema.min(start, 'End date must be later than start date'),
        ),
        start: Yup.date(),
        title: Yup.string().max(255).required('Title is required'),
      })}

Listing 11-16Creating the Two Formik Props: initialValues and validationSchema

让我们看看我们在清单 11-16 中做了什么:

initialValues:清单 11-17 中增加了getInitialValues

通常,验证模式保存在另一个文件中,特别是如果它很长的话,但是在这个例子中,我们已经在这里编写了它,因为它只是一个小的验证对象。

简而言之,通常将 initialValues 和 validationSchema 放在一个单独的文件中,只在需要它们的组件中使用它们。

好,接下来,让我们添加另一个必需的 Formik 属性: onSubmit.

onSubmit={async (
        /*  where the input values (i.e. from TextField)  are being combined. */

        values,

         /* Formik helper deconstructed.*/

        { resetForm, setErrors, setStatus, setSubmitting },

      ) => {
        try {
          const data = {
            allDay: values.allDay,
            description: values.description,
            end: values.end,
            start: values.start,
            title: values.title,
            id: '',
          };

          if (event) {
            data.id = event.id;
            await dispatch(updateEvent(data));
          } else {
            await dispatch(createEvent(data));
          }

          resetForm();
          setStatus({ success: true });
          setSubmitting(false);
          enqueueSnackbar('Calendar updated', {
            variant: 'success',
          });

          if (isCreating) {
            onAddComplete();
          } else {
            onEditComplete();
          }
        } catch (err) {
          console.error(err);
          setStatus({ success: false });
          setErrors({ submit: err.message });
          setSubmitting(false);
        }
      }}
    >

      /*deconstructing here the Formik props  */

      {({
        errors,
        handleBlur,
        handleChange,
        handleSubmit,
        isSubmitting,
        setFieldTouched,
        setFieldValue,
        touched,
        values,
      }) => (

        /*this will trigger the onSubmit of Formik */

        <form onSubmit={handleSubmit}>
          <Box p={3}>
            <Typography
              align="center"
              gutterBottom
              variant="h3"
              color="textPrimary"
            >
              {isCreating ? 'Add Event' : 'Edit Event'}
            </Typography>
          </Box>

        /*TextField -- make sure to map everything to title */

          <Box p={3}>
            <TextField
              error={Boolean(touched.title && errors.title)}
              fullWidth
              helperText={touched.title && errors.title}
              label="Title"
              name="title"
              onBlur={handleBlur}
              onChange={handleChange}
              value={values.title}
              variant="outlined"
            />
            <Box mt={2}>

           /*TextFields -- make sure to map everything to description */

              <TextField
                error={Boolean(touched.description && errors.description)}
                fullWidth
                helperText={touched.description && errors.description}
                label="Description"
                name="description"
                onBlur={handleBlur}
                onChange={handleChange}
                value={values.description}
                variant="outlined"
              />
            </Box>

            /*Form Control Label  */

            <Box mt={2}>
              <FormControlLabel
                control={
                  <Switch
                    checked={values.allDay}
                    name="allDay"
                    onChange={handleChange}
                  />
                }
                label="All day"
              />
            </Box>

            /*DateTimePicker for Start date.
         onChange - we're using the setFieldValue because the onChange emits a date, not an event.
             */

            <Box mt={2}>
              <DateTimePicker
                fullWidth
                inputVariant="outlined"
                label="Start date"
                name="start"
                onClick={() => setFieldTouched('end')} // install the @date-io/moment@1.x
                onChange={date => setFieldValue('start', date)} // and use it in MuiPickersUtilsProvider
                value={values.start}
              />
            </Box>

             /*DateTimePicker for End date*/

            <Box mt={2}>
              <DateTimePicker
                fullWidth
                inputVariant="outlined"
                label="End date"
                name="end"

onClick={() => setFieldTouched('end')}
onChange={date => setFieldValue('end', date)}
                value={values.end}
              />
            </Box>

          /*FormHelperText - to show an error message */

            {Boolean(touched.end && errors.end) && (
              <Box mt={2}>
                <FormHelperText error>{errors.end}</FormHelperText>
              </Box>

            )}
          </Box>
          <Divider />
          <Box p={2} display="flex" alignItems="center">
            {!isCreating && (
              <IconButton onClick={() => handleDelete()}>
                <SvgIcon>
                  <TrashIcon />
                </SvgIcon>
              </IconButton>
            )}
            <Box flexGrow={1} />
            <Button onClick={onCancel}>Cancel</Button>
            <Button
              variant="contained"
              type="submit"

              disabled={isSubmitting}    ➤  /* this is to prevent double clicking */

              color="primary"
              className={classes.confirmButton}
            >
              Confirm
            </Button>
          </Box>
        </form>
      )}
    </Formik>
  );
};

export default AddEditEventForm;

Listing 11-17Creating the onSubmit on the AddEditEventForm

这里是列表 11-18 ,Formik 属性初始值的getInitialValues

const getInitialValues = (
  event?: EventType,
  range?: { start: number; end: number },
) => {
  if (event) {
    const defaultEvent = {
      allDay: false,
      color: '',
      description: '',
      end: moment().add(30, 'minutes').toDate(),
      start: moment().toDate(),
      title: '',
      submit: null,
    };
    return { ...defaultEvent, event };
  }

  if (range) {
    const defaultEvent = {
      allDay: false,
      color: '',
      description: '',
      end: new Date(range.end),
      start: new Date(range.start),
      title: '',
      submit: null,
    };
    return { ...defaultEvent, event };
  }

  return {
    allDay: false,
    color: '',
    description: '',
    end: moment().add(30, 'minutes').toDate(),
    start: moment().toDate(),
    title: '',
    submit: null,
  };
};

Listing 11-18Creating the getInitialValues of Formik 

在清单 11-18 中,我们有getInitialValues——一个接受事件和取值范围的函数。该函数显示一个默认事件或一系列事件。

在创建了 Formik 属性,之后,我们回到CalendarViewindex.tsx来做一些更新。

更新日历视图

让我们在calendarSlice,中导入closeModalopenModal,如清单 11-19 所示。

import {
  getEvents,
  openModal,
  closeModal,
} from 'features/calendar/calendarSlice';

Listing 11-19Importing Modules in calendarSlice

在 CalendarView 的同一个索引文件中,我们使用了下面的:useSelector.中的isModalOpenselectedRange我们还将创建一个handleAddClick和一个handleModalClose,,如清单 11-20 所示。

const { events, loading, error, isModalOpen, selectedRange } = useSelector(
    (state: RootState) => state.calendar,
  );

useEffect(() => {
    dispatch(getEvents());
  }, []);

  const handleAddClick = (): void => {
    dispatch(openModal());
  };

  const handleModalClose = (): void => {
    dispatch(closeModal());
  };

Listing 11-20Adding States and Handles in calendarSlice

更新标题

所以现在我们可以用handleClick函数更新Header,如清单 11-21 所示。

<Page className={classes.root} title="Calendar">
      <Container maxWidth={false}>
        <Header onAddClick={handleAddClick} />
        <h1>Calendar Works!</h1>

Listing 11-21Using the handleAddClick in the Header

更新日历视图

让我们在 CalendarView 的 index.tsx 中添加样式组件。我们将从 Material-UI 核心导入这些样式组件,如清单 11-22 所示。

import {
  Container,
  makeStyles,
     Dialog,   //a modal popup
     Paper,     //in Material Design, the physical properties of paper are translated to the screen.
  useMediaQuery,    // a CSS media query hook for React. Detects when its media queries change
} from '@material-ui/core';

Listing 11-22Adding Styling Components to the index.tsx of CalendarView

在同一个索引文件中,我们需要一个小函数来选择一个事件。

我们还将从models/calendar-typeAddEditEventForm中导入EventTypeViewType组件,如清单 11-23 所示。

import Header from './Header';
import { EventType, ViewType } from 'models/calendar-type';
import AddEditEventForm from './AddEditEventForm';

...
export default CalendarView;

const selectedEventSelector = (state: RootState): EventType | null => {
  const { events, selectedEventId } = state.calendar;

  if (selectedEventId) {
    return events?.find(_event => _event.id === selectedEventId);
  } else {
    return null;
  }
};

const useStyles = makeStyles(theme => ({
...

Listing 11-23Creating an Event Selector in the index.tsx of CalendarView

在清单 11-23 中,我们有selectedEventSelector——一个带stateRootStatecalendar的函数,我们从calendar传递的是变量eventsselectedEventId.

现在我们将调用useSelector并传递selectedEventSelector,如清单 11-24 所示

const selectedEvent = useSelector(selectedEventSelector);

Listing 11-24Using the useSelector in the index.tsx of CalendarView

还是在同一个索引文件中,我们将在Container.中做一些重构

我们将用Dialog, isModalOpen,AddEditEventForm替换当前的 h1 标签,如清单 11-25 所示。

<Container maxWidth={false}>
        <Header onAddClick={handleAddClick} />
        <Dialog
          maxWidth="sm"
          fullWidth
          onClose={handleModalClose}
          open={isModalOpen}
        >

          {isModalOpen && (
            <AddEditEventForm
              event={selectedEvent}
              range={selectedRange}
              onAddComplete={handleModalClose}
              onCancel={handleModalClose}
              onDeleteComplete={handleModalClose}
              onEditComplete={handleModalClose}
            />
          )}
        </Dialog>
      </Container>

Listing 11-25Adding Dialog and AddEditEventForm in index.tsx of CalendarView

在清单 11-25 中,我们有Dialog——一个材质-UI 模态组件——我们在这里定义大小并使用事件onCloseopen对话框。还有isModalOpen,如果为真,显示 AddEditEventForm。在 AddEditEventForm 属性中,我们传递 selectedEvent、selectedRange 和 handleModalClose

检查 CalendarView 的用户界面

让我们看看它在 UI 中是如何工作的。刷新浏览器,打开 Chrome DevTools。单击新建事件。您应该会看到名为“添加事件”的弹出模式对话框。

img/506956_1_En_11_Fig4_HTML.jpg

图 11-4

浏览器中模式对话框添加事件的屏幕截图

尝试创建一个事件,然后单击确认按钮。你会在 Chrome DevTools 中看到数据事件被成功返回,如图 11-5 所示。

img/506956_1_En_11_Fig5_HTML.jpg

图 11-5

Chrome DevTools 中的事件请求截图

检查 Chrome 开发工具

另请注意,模式对话框会自动关闭。在 Chrome DevTools 中,检查标题,您会看到创建了状态代码 201。这意味着我们能够创建一个对象并将其保存在数据库中。

检查 Redux 开发工具

接下来,打开 Redux DevTools。确保选择顶部下拉箭头上的日历–React 样本。

Redux DevTools 记录分派的动作和存储的状态。我们可以通过应用的时间旅行调试功能在每个时间点检查应用的状态,而无需重新加载或重启应用。

img/506956_1_En_11_Fig6_HTML.jpg

图 11-6

Redux DevTools 中事件请求的屏幕截图

我们对观察差异很感兴趣,你可以看到状态从setLoadingcreateEvents再到closeModal等等。

您可以看到可以从存储中访问的事件或事件数组,还可以查找任何错误消息等。所有的动作都被记录下来,我们可以通过 Redux DevTools 的时间旅行调试功能来回放。

创建工具栏

我们将在CalendarView文件夹下创建一个工具栏组件Toolbar.tsx,

首先,我们导入命名的组件,如清单 11-26 所示。

import React, { ElementType, ReactNode } from 'react';
import clsx from 'clsx';
import moment from 'moment';
import {
  Button,
  ButtonGroup,
  Grid,
  Hidden,
  IconButton,
  Tooltip,
  Typography,
  makeStyles,
} from '@material-ui/core';
import ViewConfigIcon from '@material-ui/icons/ViewComfyOutlined';
import ViewWeekIcon from '@material-ui/icons/ViewWeekOutlined';
import ViewDayIcon from '@material-ui/icons/ViewDayOutlined';
import ViewAgendaIcon from '@material-ui/icons/ViewAgendaOutlined';

import { ViewType } from 'models/calendar-type';

Listing 11-26Adding Named Components in Toolbar.tsx

在清单 11-26 中,我们从 Material-UI 核心导入了 moment 和标准样式模块。新的图标与材质界面图标不同。

我们还从模型/日历类型中导入了视图类型。

在同一个 Toolbar.tsx 文件中,我们将为模型ToolbarViewOption创建类型或模式,如清单 11-27 所示。

type ViewOption = {
  label: string;
  value: ViewType;
  icon: ElementType;
};

type Props = {
  children?: ReactNode;
  className?: string;
  date: Date;

   /* the ? means it's a nullable void function

  onDateNext?: () => void;
  onDatePrev?: () => void;
  onDateToday?: () => void;
  onAddClick?: () => void;

  /* takes a view object and returns nothing or void */

  onViewChange?: (view: ViewType) => void;
  view: ViewType;
};

Listing 11-27Creating the Type or Schema of Toolbar

在清单 11-27 中,我们有类型Props,,它有dateview作为必需的类型属性,而其余的都是可空类型。ViewOption型需要三个属性:label, value,icon.我们一会儿会用到ViewPoint。我们现在正在这里准备。

因此,我们现在将使用我们在工具栏组件中定义的Props,如清单 11-28 所示。

const Toolbar = ({
  className,
  date,
  onDateNext,
  onDatePrev,
  onDateToday,
  onAddClick,
  onViewChange,
  view,
  ...rest       // the rest parameter
}: Props) => {
  const classes = useStyles();

Listing 11-28Using the Props in the Toolbar Component

在清单 11-28 中,您会注意到静止参数。这允许我们在函数中接受多个参数,并将它们作为一个数组。

img/506956_1_En_11_Figa_HTML.jpg Rest 参数可用于函数、箭头函数或类中。但在函数定义中,rest 参数必须出现在参数表的最后;否则,TypeScript 编译器将会报错并显示错误。

接下来,我们将在同一个工具栏组件文件中创建 return 语句。

我们把所有东西都放在一个Grid中,并添加了ButtonGroup.

我们使用moment,格式化date,我们还映射了viewOptions中的四个对象,如清单 11-29 所示,并返回Tooltip key titleIconButton

return (
    <Grid
      className={clsx(classes.root, className)}
      alignItems="center"
      container
      justify="space-between"
      spacing={3}
      {...rest}
    >
      <Grid item>
        <ButtonGroup size="small">
          <Button onClick={onDatePrev}>Prev</Button>
          <Button onClick={onDateToday}>Today</Button>
          <Button onClick={onDateNext}>Next</Button>
        </ButtonGroup>
      </Grid>
      <Hidden smDown>
        <Grid item>
          <Typography variant="h3" color="textPrimary">
            {moment(date).format('MMMM YYYY')}
          </Typography>
        </Grid>

        <Grid item>
          {viewOptions.map(viewOption => {
            const Icon = viewOption.icon;

            return (
              <Tooltip key={viewOption.value} title={viewOption.label}>
                <IconButton
                  color={viewOption.value === view ? 'primary' : 'default'}
                  onClick={() => {
                    if (onViewChange) {
                      onViewChange(viewOption.value);
                    }
                  }}
                >
                  <Icon />
                </IconButton>
              </Tooltip>
            );
          })}
        </Grid>
      </Hidden>
    </Grid>
  );
};

export default Toolbar;

Listing 11-29Adding the Return Statement of the Toolbar Component

接下来,我们添加ViewOptionmakeStyles组件,如清单 11-30 所示。

const viewOptions: ViewOption[] = [
  {
    label: 'Month',
    value: 'dayGridMonth',
    icon: ViewConfigIcon,
  },
  {
    label: 'Week',
    value: 'timeGridWeek',
    icon: ViewWeekIcon,
  },
  {
    label: 'Day',
    value: 'timeGridDay',
    icon: ViewDayIcon,
  },
  {
    label: 'Agenda',
    value: 'listWeek',
    icon: ViewAgendaIcon,
  },
];

const useStyles = makeStyles(() => ({
  root: {},
}));

Listing 11-30Creating ViewOption and makeStyles components in Toolbar.tsx

又该更新 CalendarView 的 index.tsx 了。

设置日历视图的样式

我们将开始在 CalendarView 的 index.tsx 中添加新的样式组件,如清单 11-31 所示。

calendar: {
    marginTop: theme.spacing(3),
    padding: theme.spacing(2),
    '& .fc-unthemed .fc-head': {},
    '& .fc-unthemed .fc-body': {
      backgroundColor: theme.palette.background.default,
    },
    '& .fc-unthemed .fc-row': {
      borderColor: theme.palette.divider,
    },
    '& .fc-unthemed .fc-axis': {
      ...theme.typography.body2,
    },
    '& .fc-unthemed .fc-divider': {
      borderColor: theme.palette.divider,
    },
    '& .fc-unthemed th': {
      borderColor: theme.palette.divider,
    },
    '& .fc-unthemed td': {
      borderColor: theme.palette.divider,
    },
    '& .fc-unthemed td.fc-today': {},
    '& .fc-unthemed .fc-highlight': {},
    '& .fc-unthemed .fc-event': {
      backgroundColor: theme.palette.secondary.main,
      color: theme.palette.secondary.contrastText,
      borderWidth: 2,
      opacity: 0.9,
      '& .fc-time': {
        ...theme.typography.h6,
        color: 'inherit',
      },

      '& .fc-title': {
        ...theme.typography.body1,
        color: 'inherit',
      },
    },
    '& .fc-unthemed .fc-day-top': {
      ...theme.typography.body2,
    },
    '& .fc-unthemed .fc-day-header': {
      ...theme.typography.subtitle2,
      fontWeight: theme.typography.fontWeightMedium,
      color: theme.palette.text.secondary,
      padding: theme.spacing(1),
    },
    '& .fc-unthemed .fc-list-view': {
      borderColor: theme.palette.divider,
    },
    '& .fc-unthemed .fc-list-empty': {
      ...theme.typography.subtitle1,
    },
    '& .fc-unthemed .fc-list-heading td': {
      borderColor: theme.palette.divider,
    },
    '& .fc-unthemed .fc-list-heading-main': {
      ...theme.typography.h6,
    },
    '& .fc-unthemed .fc-list-heading-alt': {
      ...theme.typography.h6,
    },
    '& .fc-unthemed .fc-list-item:hover td': {},
    '& .fc-unthemed .fc-list-item-title': {
      ...theme.typography.body1,
    },
    '& .fc-unthemed .fc-list-item-time': {
      ...theme.typography.body2,
    },
  },

Listing 11-31Adding Styling Components in the index.tsx of CalendarView

清单 11-31 中的附加样式组件只是日历的几种边框颜色,以及一些边距和填充。

现在,在 CalendarView 的同一个索引文件中,我们将从 FullCalendar 库中导入 moment 库和模块,如清单 11-32 所示。

import moment from 'moment';
import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import interactionPlugin from '@fullcalendar/interaction';
import listPlugin from '@fullcalendar/list';
import timelinePlugin from '@fullcalendar/timeline';

Listing 11-32Importing Named Components in index.tsx of CalendarView

然后让我们添加并使用来自calendarSlice和 React 钩子的一些模块,如清单 11-33 所示。

import React, { useEffect, useState, useRef } from 'react';

import {
  getEvents,
  openModal,
  closeModal,
  selectRange,
  selectEvent,
  updateEvent
} from 'features/calendar/calendarSlice';

Listing 11-33Importing Additional Modules from calendarSlice and React Hooks

同样,在同一个索引文件中,我们将创建一些本地状态,如清单 11-34 所示。

const selectedEvent = useSelector(selectedEventSelector);

  const mobileDevice = useMediaQuery('(max-width:600px)');
  const [date, setDate] = useState<Date>(moment().toDate());
  const [view, setView] = useState<ViewType>(
    mobileDevice ? 'listWeek' : 'dayGridMonth',
  );

  const calendarRef = useRef<FullCalendar | null>(null);

  useEffect(() => {
    dispatch(getEvents());
  },

Listing 11-34Creating local states in index.tsx of CalendarView

让我们看看清单 11-34 中发生了什么。我们有useRef来访问 DOM 元素,并在后续或下一次渲染中保存值或状态。将鼠标悬停在useRef,上,您会看到这是一个React.MutableRefObject<FullCalendar>.,这意味着我们可以访问这个完整日历的 API 或接口。

我们用它来检测小的浏览器屏幕,比如移动设备的屏幕。

之后,我们将在handleModalClose,下面创建额外的句柄函数,如清单 11-35 所示。

/* calendarRef is a reference to the element FullCalendar*/

const handleDateNext = (): void => {
    const calendarEl = calendarRef.current;

/*the getApi here is part of FullCalendar. If you 'dot space' the  'calendarEl,' you'll see the interfaces or APIs available.  */

    if (calendarEl) {
      const calendarApi = calendarEl.getApi();

      calendarApi.next();
      setDate(calendarApi.getDate());
    }
  };

  const handleDatePrev = (): void => {
    const calendarEl = calendarRef.current;

    if (calendarEl) {
      const calendarApi = calendarEl.getApi();

      calendarApi.prev();
      setDate(calendarApi.getDate());
    }
  };

  const handleDateToday = (): void => {
    const calendarEl = calendarRef.current;

    if (calendarEl) {
      const calendarApi = calendarEl.getApi();

      calendarApi.today();
      setDate(calendarApi.getDate());
    }
  };

  const handleViewChange = (newView: ViewType): void => {
    const calendarEl = calendarRef.current;

    if (calendarEl) {
      const calendarApi = calendarEl.getApi();

      calendarApi.changeView(newView);
      setView(newView);
    }
  };

  /*the arg: any - could be a string or a number */

  const handleEventSelect = (arg: any): void => {
    dispatch(selectEvent(arg.event.id));
  };

  /*We have here a try-catch block because handleEventDrop is an async function */

  const handleEventDrop = async ({ event }: any): Promise<void> => {
    try {
      await dispatch(
        updateEvent({
          allDay: event.allDay,
          start: event.start,
          end: event.end,
          id: event.id,
        } as any),
      );
    } catch (err) {
      console.error(err);
    }
  };

  const handleEventResize = async ({ event }: any): Promise<void> => {
    try {
      await dispatch(
        updateEvent({
          allDay: event.allDay,
          start: event.start,
          end: event.end,
          id: event.id,
        } as any),
      );
    } catch (err) {
      console.error(err);
    }
  };

  const handleRangeSelect = (arg: any): void => {
    const calendarEl = calendarRef.current;

    if (calendarEl) {
      const calendarApi = calendarEl.getApi();

      calendarApi.unselect();
    }

    dispatch(selectRange(arg.start, arg.end));
  };

Listing 11-35Creating Additional Handle Events in the index.tsx of CalendarView

我们还没说完呢。我们需要添加来自 Material-UI 的纸张模块和 UI 样式的完整日历。

在 return 语句中找到Dialog标签;我们已经写了只有当isModalOpen为真时Dialog才可见。所以在 Header 组件之后和对话框之前,我们将放置 FullCalendar,如清单 11-36 所示。

return (
    <Page className={classes.root} title="Calendar">
      <Container maxWidth={false}>
        <Header onAddClick={handleAddClick} />
        <Toolbar
          date={date}
          onDateNext={handleDateNext}
          onDatePrev={handleDatePrev}
          onDateToday={handleDateToday}
          onViewChange={handleViewChange}
          view={view}
        />
        <Paper className={classes.calendar}>
          <FullCalendar
            allDayMaintainDuration
            droppable
            editable
            selectable
            weekends
            dayMaxEventRows
            eventResizableFromStart
            headerToolbar={false}
            select={handleRangeSelect}
            eventClick={handleEventSelect}
            eventDrop={handleEventDrop}
            eventResize={handleEventResize}
            initialDate={date}
            initialView={view}
            events={events}
            height={800}
            ref={calendarRef}
            rerenderDelay={10}
            plugins={[
              dayGridPlugin,
              timeGridPlugin,
              interactionPlugin,
              listPlugin,

              timelinePlugin,
            ]}
          />
        </Paper>
        <Dialog
          maxWidth="sm"
          fullWidth
          onClose={handleModalClose}
          open={isModalOpen}
        >

Listing 11-36Rendering the FullCalendar in the UI of the index.tsx of CalendarView

如果你注意到清单 11-36 中的一些属性(即allDayMaintainDuration, droppable editable等)。)没有等号=号;这意味着它们默认设置为真。

这是书写allDayMaintainDuration={true},的速记,这也意味着它们都是布尔型的。

但是对于headerToolbar,我们必须显式地声明 false 值。我们将它设置为 false,因为我们有工具栏组件,我们将很快添加。

在 UI 中检查完整日历

让我们在浏览器中测试一切。刷新它,您应该能够看到完整的日历和我们之前创建的测试事件。

img/506956_1_En_11_Fig7_HTML.jpg

图 11-7

完整日历的屏幕截图

点按显示的事件并尝试编辑它。您应该可以成功地进行更改,如图 11-8 所示。

img/506956_1_En_11_Fig8_HTML.jpg

图 11-8

编辑完整日历的事件表单

检查 Chrome 开发工具和 Redux 开发工具

看一眼 Redux DevTools,你会看到它也在更新,并在 Chrome DevTools 中看到 200 OK 状态码。

还要测试编辑事件表单左下方的删除图标,您应该能够删除所选择的事件。

一旦你删除了它,在 Chrome DevTools 中再次检查网络,查看请求方法:DELETE 和状态代码:200 OK。

img/506956_1_En_11_Fig9_HTML.jpg

图 11-9

删除事件

创建一个在连续两个月中有大约两周时间范围的事件怎么样?在图 11-10 中,我们可以看到我们已经成功地为多月日历添加了一个事件。

img/506956_1_En_11_Fig10_HTML.jpg

图 11-10

在多月日历中创建事件

我们能够创建从 2 月到 3 月的多月活动。但是,您会注意到我们无法导航到下个月。

这是因为我们还需要增加一个东西,就是工具栏。所以现在让我们在CalendarView.index.tsx中导入它

我们将导入工具栏组件,并在标题组件下使用它,如清单 11-37 所示。

import Toolbar from './Toolbar';

...

<Header onAddClick={handleAddClick} />
        <Toolbar
          date={date}
          onDateNext={handleDateNext}
          onDatePrev={handleDatePrev}
          onDateToday={handleDateToday}
          onViewChange={handleViewChange}
          view={view}
        />

Listing 11-37Adding the Toolbar Component in the index.tsx of CalendarView

检查 UI,您应该会看到如图 11-11 所示的变化。现在,您应该能够导航到之前或之后的月份。

img/506956_1_En_11_Fig11_HTML.jpg

图 11-11

添加工具栏后更新的 UI 的屏幕截图

摘要

在这一章中,我们继续构建我们的应用。我们安装了 FullCalendar 库,并学习了如何使用 Redux 工具包 在日历组件上创建、删除和更新事件。希望您现在对 Redux 工具包 的实现流程有了更好的理解。

在下一章,我们将构建登录和注册表单。我们将需要假 Node json-server 和 json-server-auth 的帮助,以及来自优秀 Material-UI 的更多样式组件。

十二、React 中的保护路由和认证

在上一章中,我们已经展示了如何使用 Redux 工具包 创建、删除和更新应用的事件。我们已经知道用我们的存储库进行 CRUD 是多么的高效和方便,它保存了我们应用的所有全局状态。

在这一章中,我们将为我们的应用建立一个登录和注册表单。我们将从伪 Node json-server 开始,我们已经在前一章中安装了它。json-server 允许我们发送 HTTP 方法或 HTTP 请求。

设置假服务器

设置假服务器只需要我们几分钟的时间,对构建我们的 UI 帮助很大;我们不需要等待我们的后端开发团队给我们 API。我们可以创建一个假的 API,并用它来测试 UI。

这就是我们在 json 服务器上所做的。我们还将使用json-server-auth,一个插件或模块,在 json-server 内部创建一个认证服务。

除了json-server-auth,之外,我们还使用了concurrently.

允许我们在一次运行中同时运行两个 npm 命令。

所以我们需要修改我们的脚本。

转到package.json ,编辑后端脚本并添加一个start:fullstack脚本,如清单 12-1 所示。

"backend": "json-server --watch db.json --port 5000 --delay=1000 -m ./node_modules/json-server-auth",
  "start:fullstack": "concurrently \"npm run backend\" \"npm run start\""

Listing 12-1Modifying the Scripts in package.json

同时运行多个命令。

现在我们已经设置好了,让我们试一试。取消所有正在运行的应用,然后在终端中键入以下命令:

npm run start:fullstack

db.json

一旦完成,让我们更新事件下面的 db. json .,我们将添加一个用户对象数组,如清单 12-2 所示。

  "users": [
    {
      "id": "7fguyfte5",
      "email": "demo@acme.io",
      "password": "$2a$10$Pmk32D/fgkig8pU.r1rGrOpYYJSrnqqpLO6dRdo88iYxxIsl1sstC",
      "name": "Mok Kuh",
      "mobile": "+34782364823",
      "policy": true
    }
  ],

Listing 12-2Adding the users Object in the db.json

我们稍后将使用它登录。用户的端点是用户对象的数组。它包含登录详细信息,包括哈希密码。

API:登录和注册

接下来,在 axios.ts 文件中,让我们更新端点,如清单 12-3 所示。

export const EndPoints = {
  sales: 'sales',
  products: 'products',
  events: 'events',
  login: 'login',
  register: 'register',
};

Listing 12-3Updating the Endpoints in axios.ts

“登录”和“注册”都是 json-server-auth 的一部分。如果您转到 npmjs.org 并搜索 json-server-auth,您会看到我们可以在认证流程中使用以下任何一条路线。

在这种情况下,我们使用登录和注册,如图 12-1 所示。

img/506956_1_En_12_Fig1_HTML.jpg

图 12-1

json-server-auth 中的认证流程

authService(认证服务)

我们现在可以更新服务了。在 services 文件夹中,创建一个名为authService.ts的新文件。

authService是一个包含我们使用 axios 的日志和注册服务的文件。

import axios, { EndPoints } from 'api/axios';

export type UserModel = {
  email: string;
  password: string;
};

/*The return object will be an object with an access token of type string. We're expecting an access token from the json-server-auth */

export async function loginAxios(userModel: UserModel) {
  return await axios.post<{ accessToken: string }>(EndPoints.login, userModel);
}

export type RegisterModel = {
  email: string;
  password: string;
  name: string;
  mobile: string;
  policy: boolean;
};

export async function registerAxios(registerModel: RegisterModel) {
  return await axios.post<{ accessToken: string }>(
    EndPoints.register,
    registerModel,
  );
}

Listing 12-4Creating the authService.ts

在清单 12-4 中,我们有登录信息——请求一个用户模型——我们在用户模型类型中定义它,它需要一个电子邮件和字符串类型的密码。我们还有registerAxios——请求 registerModel——我们在 register model 中描述它,它需要电子邮件、密码、姓名、手机和策略。

现在让我们继续创建登录页面。

在 views ➤页面文件夹中,创建一个新文件夹并将其命名为 auth,在 auth 中,添加另一个文件夹并将其命名为 components。

在 auth 文件夹中,创建一个新文件,并将其命名为 LoginPage.tsx :

app ➤ views ➤ pages ➤ auth ➤ LoginPage.tsx

在 components 文件夹中,创建一个新文件,并将其命名为 LoginForm.tsx :

app ➤ views ➤ pages ➤ auth ➤ components ➤ LoginForm.tsx

设置登录表单

让我们先建立逻辑关系。导入命名的组件,如清单 12-5 所示。

import React, { useState } from 'react';
import * as Yup from 'yup';
import { Formik } from 'formik';
import { Alert } from '@material-ui/lab';
import { useHistory } from 'react-router-dom';
import {
  Box,
  Button,
  FormHelperText,
  TextField,
  CardHeader,
  Divider,
  Card,
} from '@material-ui/core';

import { loginAxios } from 'services/authService';

Listing 12-5Importing Named Components of LoginForm.tsx

在清单 12-5 中,我们通常会怀疑命名导入。让我们看看这里还有什么新的东西:

Alert:我们这里是第一次从物料界面导入预警。Alert 用于显示简短而重要的消息,以便在不中断用户任务的情况下引起用户的注意。

这是我们从 React-Router-DOM 导入的一个钩子;这允许我们访问历史实例,并让我们向后导航。

我们还从the authService.中导入了loginAxios

然后让我们为LoginForm创建一个函数,并创建另一个函数来保存用户的身份验证细节。当然,我们将这样命名它,如清单 12-6 所示。

作为一种最佳实践,我们应该尽可能描述性地命名我们的函数和方法,以便于我们自己和其他开发人员阅读我们的代码。

const LoginForm = () => {
  const key = 'token';
  const history = useHistory();
  const [error, setError] = useState('');

  const saveUserAuthDetails = (data: { accessToken: string }) => {
    localStorage.setItem(key, data.accessToken);
  };

Listing 12-6Creating the Function for LoginForm.tsx

LoginForm:在这里,我们将“令牌”定义为密钥,并将useHistoryuseState用于错误。

saveUserAuthDetails:将用户资料保存在本地存储器的功能。local storage是浏览器的本地部分,所以我们可以访问它。它是一等公民支持的,所以我们不需要再进口它了。

接下来,让我们添加我们的LoginForm,的返回语句,它包含 Formik 及其所需的属性,如清单 12-7 所示。

return (
    <Formik
      initialValues={{
        email: 'demo@acme.io',
        password: 'Pass123!',
      }}
      validationSchema={Yup.object().shape({
        email: Yup.string()
          .email('Must be a valid email')
          .max(255)
          .required('Email is required'),
        password: Yup.string().max(255).required('Password is required'),
      })}

      onSubmit={async (values, formikHelpers) => {
        try {
          const { data } = await loginAxios(values);
          saveUserAuthDetails(data);
          formikHelpers.resetForm();
          formikHelpers.setStatus({ success: true });
          formikHelpers.setSubmitting(false);
          history.push('dashboard');
        } catch (e) {
          setError('Failed. Please try again.');
          console.log(e.message);
          formikHelpers.setStatus({ success: false });
          formikHelpers.setSubmitting(false);
        }
      }}
    >

      {/* deconstructed Formik props */}

      {({
        errors,
        handleBlur,
        handleChange,
        handleSubmit,
        isSubmitting,
        touched,
        values,
      }) => (
        <Card>
          <form noValidate onSubmit={handleSubmit}>
            <CardHeader title="Login" />
            <Divider />
            <Box m={2}>
              <TextField
                error={Boolean(touched.email && errors.email)}
                fullWidth
                autoFocus
                helperText={touched.email && errors.email}
                label="Email Address"
                margin="normal"
                name="email"
                onBlur={handleBlur}
                onChange={handleChange}
                type="email"
                value={values.email}
                variant="outlined"
              />

              <TextField
                error={Boolean(touched.password && errors.password)}
                fullWidth
                helperText={touched.password && errors.password}
                label="Password"
                margin="normal"
                name="password"
                onBlur={handleBlur}
                onChange={handleChange}
                type="password"
                value={values.password}
                variant="outlined"
              />
              <Box mt={2}>
                <Button
                  color="primary"
                  disabled={isSubmitting}
                  fullWidth
                  size="large"
                  type="submit"
                  variant="contained"
                >

                  Log In
                </Button>
              </Box>
              {error && (
                <Box mt={3}>
                  <FormHelperText error>{error}</FormHelperText>
                </Box>
              )}
              <Box mt={2}
                <Alert severity="info">
                  <div>
                    Use <b>demo@acme.io</b> and password <b>Pass123!</b>
                  </div>
                </Alert>
              </Box>
            </Box>
          </form>

        </Card>
      )}
    </Formik>
  );
};

export default LoginForm;

Listing 12-7Creating Formik in the LoginForm

让我们回顾一下我们在清单 12-7 中所做的一些事情:

initialValues:福米克必备属性。我们用电子邮件和密码的值初始化它。

一个有效的验证模式。我们将电子邮件定义为一个字符串,其中有效电子邮件地址的最大字符数为 255,密码的最大字符数为 255。

onSubmit:一个接受valuesformikHelpers.的异步函数,因为它是一个异步函数,我们把它包装在一个 try-catch 块中。

在尝试中,我们使用loginAxios来看看我们是否可以登录。我们需要的结果就是这个data,是一个大对象结果的析构。我们不需要获得这个巨大物体的所有属性。

然后,我们将data保存到saveUserAuthDetails,这意味着将它保存在我们的本地存储中。

然后我们有一组正在使用的formikHelpers,比如resetForm, setStatussetSubmitting

对于 catch,我们放置了setError以防登录失败。

我们使用 Material-UI 中的 Card 组件来设计登录 UI 的样式,并使用两个文本字段,分别用于电子邮件和密码。

创建注册表单

之后,我们需要在授权➤组件文件夹下创建另一个组件。姑且称之为RegisterForm .tsx.

同样,让我们先做命名的组件,如清单 12-8 所示。

import React, { useState } from 'react';
import * as Yup from 'yup';
import { Formik } from 'formik';
import { Alert } from '@material-ui/lab';
import {
  Box,
  Button,
  Card,
  CardContent,
  CardHeader,
  Checkbox,
  CircularProgress,
  Divider,
  FormHelperText,
  Grid,
  Link,
  TextField,
  Typography,
} from '@material-ui/core';
import { useHistory } from 'react-router-dom';
import { registerAxios } from 'services/authService';

Listing 12-8Importing Named Components in RegisterForm.tsx

除了 Material-UI 中的几个模块之外,注册表单需要与登录表单相同。我们还添加了来自authServiceregisterAxios

接下来,让我们创建函数来注册用户并在本地存储中保存他们的身份验证细节,如清单 12-9 所示。

const RegisterForm = () => {
  const key = 'token';
  const history = useHistory();
  const [error, setError] = useState('');
  const [isAlertVisible, setAlertVisible] = useState(false);

  const saveUserAuthDetails = (data: { accessToken: string }) => {
    localStorage.setItem(key, data.accessToken);
  };

Listing 12-9Adding the RegisterForm Function

以及用 Formik 包装的 return 语句,如清单 12-10 所示。

return
    <Formik
      initialValues={{
        email: 'johnnydoe@yahoo.com',
        name: 'John',
        mobile: '+34782364823',
        password: 'Pass123!',
        policy: false,
      }}
      validationSchema={Yup.object().shape({
        email: Yup.string().email().required('Required'),
        name: Yup.string().required('Required'),
        mobile: Yup.string().min(10).required('Required'),
        password: Yup.string()
          .min(7, 'Must be at least 7 characters')
          .max(255)
          .required('Required'),policy: Yup.boolean().oneOf([true], 'This field must be checked'),
      })}

      onSubmit={async (values, formikHelpers) => {
        try {
          const { data } = await registerAxios(values);
          saveUserAuthDetails(data);
          formikHelpers.resetForm();
          formikHelpers.setStatus({ success: true });
          formikHelpers.setSubmitting(false);
          history.push('dashboard');
        } catch (e) {
          setError(e);
          setAlertVisible(true);
          formikHelpers.setStatus({ success: false });
          formikHelpers.setSubmitting(false);
        }
      }}
    >
      {({

          errors,
          handleBlur,
          handleChange,
          handleSubmit,
          isSubmitting,
          touched,
          values,
        }) => (
        <Card>
          <CardHeader title="Register Form" />
          <Divider />
          <CardContent>
            {isAlertVisible && (
              <Box mb={3}>
 <Alert onClose={() => setAlertVisible(false)} severity="info">{error}!
 </Alert>
              </Box>
            )}

            {isSubmitting ? (
     <Box display="flex" justifyContent="center" my={5}>
          <CircularProgress />

       {/*for the loading spinner*/}

              </Box>
            ) : (
              <Box>
                <Grid container spacing={2}>
                  <Grid item md={6} xs={12}>
                    <TextField
            error={Boolean(touched.name && errors.name)}
                      fullWidth
            helperText={touched.name && errors.name}
                      label="Name"
                      name="name"
                      onBlur={handleBlur}
                      onChange={handleChange}
                      value={values.name}
                      variant="outlined"
                    />
                  </Grid>
                  <Grid item md={6} xs={12}>
                    <TextField
        error={Boolean(touched.mobile && errors.mobile)}
                      fullWidth
         helperText={touched.mobile && errors.mobile}
                      label="Mobile"
                      name="mobile"
                      onBlur={handleBlur}
                      onChange={handleChange}
                      value={values.mobile}
                      variant="outlined"
                    />
                  </Grid>
                </Grid>

                <Box mt={2}>
                  <TextField
          error={Boolean(touched.email && errors.email)}
                    fullWidth
              helperText={touched.email && errors.email}
                    label="Email Address"
                    name="email"
                    onBlur={handleBlur}
                    onChange={handleChange}
                    type="email"
                    value={values.email}
                    variant="outlined"
                  />
                </Box>
                <Box mt={2}>
                  <TextField
    error={Boolean(touched.password && errors.password)}
                    fullWidth
     helperText={touched.password && errors.password}
                    label="Password"
                    name="password"
                    onBlur={handleBlur}
                    onChange={handleChange}
                    type="password"
                    value={values.password}
                    variant="outlined"
                  />
                </Box>

 <Box alignItems="center" display="flex" mt={2} ml={-1}>
                  <Checkbox
                    checked={values.policy}
                    name="policy"
                    onChange={handleChange}
                  />
      <Typography variant="body2" color="textSecondary">
                    I have read the{' '}
         <Link component="a" href="#" color="secondary">
                      Terms and Conditions
                    </Link>
                  </Typography>
                </Box>
           {Boolean(touched.policy && errors.policy) && (
                  <FormHelperText error>{errors.policy}</FormHelperText>
                )}
                <form onSubmit={handleSubmit}>
                  <Button
                    color="primary"
                    disabled={isSubmitting}
                    fullWidth
                    size="large"
                    type="submit"
                    variant="contained"
                  >
                    Sign up
                  </Button>
                </form>
              </Box>
            )}
          </CardContent>
        </Card>
      )}
    </Formik>
  );

};

export default RegisterForm;

Listing 12-10Creating Formik in the RegisterForm.tsx

在 initialValues 中,您可以将其保留为空字符串或传递一个示例值。请注意,我们不在这里保存或存储密码。我们这样做只是为了演示的目的。

此外,initialValues 和 validationSchema 通常保存在一个单独的文件中,以获得更清晰的代码,尤其是一个长文件。

这就是注册表中的内容。我们稍后会测试它。让我们现在建立登录页面。

添加登录页面

现在让我们创建 LoginPage,我们将从导入我们需要的命名组件开始,如清单 12-11 所示。

import React, { useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import { Box, Button, Container, Divider } from '@material-ui/core';

import LoginForm from './components/LoginForm';
import RegisterForm from './components/RegisterForm';
import Page from 'app/components/page';

Listing 12-11Importing the Named Components in LoginPage.tsx

我们导入了刚刚创建的LoginFormRegisterForm。我们也有页面模板。

所以,接下来,让我们创建LoginPage组件函数,如清单 12-12 所示。

  const LoginPage = () => {
  const classes = useStyles();
  const [isLogin, setIsLogin] = useState(true);

Listing 12-12Creating the LoginPage Function

本地状态,它是一个布尔值,默认情况下设置为 true,因为我们正在登录。如果这是假的,我们显示注册表。我们如何做到这一点?这样做:{isLogin ? <LoginForm /> : <RegisterForm />}

所以现在,让我们接下来制作那个返回语句,如清单 12-13 所示。

return (
    <Page className={classes.root} title="Authentication">
      <Container>
        <Box
          my={5}
          display={'flex'}
          flexDirection={'column'}
          justifyContent={'center'}
          alignItems={'center'}
        >

          {/*if isLogin is true - show LoginForm, otherwise show RegisterForm */}

          {isLogin ? <LoginForm /> : <RegisterForm />}
          <Divider />
          <Box mt={5}>
            Go to{' '}
            {isLogin ? (
              <Button
                size={'small'}
                color={'primary'}
                variant={'text'}
                onClick={() => setIsLogin(false)}
              >
                Register Form
              </Button>

            ) : (
              <Button
                size={'small'}
                color={'primary'}
                variant={'text'}
                onClick={() => setIsLogin(true)}
              >
                Login Form
              </Button>
            )}
          </Box>
        </Box>
      </Container>
    </Page>
  );
};

const useStyles = makeStyles(() => ({root: {},}));

export default LoginPage;

Listing 12-13Adding the Return Statement of the LoginPage.tsx

好了,暂时就这样了。routes.tsx.更新时间到了

更新路线

AboutPage路线下方插入LoginPage路线,如清单 12-14 所示。

<Route
     exact
     path={'/login'}
     component={lazy(() => import('./views/pages/auth/LoginPage'))}
        />

Listing 12-14Adding the LoginPage Routes

让我们在浏览器中测试一下。点击刷新按钮或者转到你的localhost:3000/login,应该会看到登录页面,如图 12-2 所示。

img/506956_1_En_12_Fig2_HTML.jpg

图 12-2

登录页面的屏幕截图

点击注册表单,创建您的账户,如图 12-3 所示。

img/506956_1_En_12_Fig3_HTML.jpg

图 12-3

注册表单的屏幕截图

要检查成功的登录或注册,进入 Chrome DevTools 的网络,你应该在标题下看到状态代码 OK,在响应中,你会看到访问令牌。

复制访问令牌,让我们看看里面有什么。为此,我们将访问这个优秀的网站 jwt.io ,并在那里粘贴我们的访问令牌。

JSON Web 令牌(JWT)

JWT 有报头、有效载荷和签名。在标题中,您会看到alg或算法和typ或类型。在有效负载中,数据是访问令牌或 JWT 的解码值。解码后的值是我们在下一章的 React 应用中需要的。

img/506956_1_En_12_Fig4_HTML.jpg

图 12-4

检查来自服务器的访问令牌响应

点击 jwt.io/introduction 了解更多关于 JSON Web Token 结构的信息。

简单地说,JSON Web 令牌或 jwt 是用于访问资源的基于令牌的认证。基于令牌的身份验证是无状态的,不同于基于会话的身份验证,基于会话的身份验证是有状态的,需要一个 cookie,并将会话 ID 放在用户的浏览器中。

这是一个很大的话题,所以我建议你多读一些。

让我们回到应用的 Chrome DevTools,点击应用➤本地存储和本地主机。

img/506956_1_En_12_Fig5_HTML.jpg

图 12-5

存储在本地存储中的令牌的屏幕截图

令牌表示我们已经成功地在本地存储中存储了 JWT 或 JSON Web 令牌。

创建受保护的路由组件

接下来,我们需要保护我们的路由,以便未经身份验证的用户无法访问或看到仪表板。为此,让我们创建一个受保护的路由。

在应用目录中,转到 components,在其中创建一个新组件,并将其命名为protected-route.tsx:

app ➤ components ➤ protected-route.tsx

打开protected-route.tsx文件,复制下面的代码,如清单 12-15 所示。

import React from 'react';
import { Redirect, Route } from 'react-router-dom';

const ProtectedRoute = props => {
  const token = localStorage.getItem('token');

  return token ? (
    <Route {...props} />
  ) : (
    <Redirect to={{ pathname: '/login' }} />
  );
};

export default ProtectedRoute;

Listing 12-15Creating the protected-route.tsx

在清单 12-15 中,我们暂时保持它的简单,但是随着认证变得更加复杂,我们将在以后更新它。

我们从 React-Router-DOM 导入了重定向和路由。我们还有ProtectedRoute——一个接受属性并从localStorage中检索用户令牌的函数。

在 return 语句中,我们检查是否存在一个现有的令牌?如果这是真的,用户被定向到仪表板内的特定路径;否则,用户将被重定向到登录页面。

完成之后,我们现在可以使用ProtectedRoute组件来包装仪表板路线。

更新 Routes.tsx

转到 routes.tsx,我们将使用 ProtectedRoute 组件,如清单 12-16 所示。

import ProtectedRoute from './components/protected-route';
...
<ProtectedRoute
          path={'/dashboard'}
          render={({ match: { path } }) => (
            <Dashboard>
              <Switch>
                <Route
                  exact
                  path={path + '/'}
                  component={lazy(() => import('./views/dashboard/dashboard-default-content'),)/>

Listing 12-16Adding the ProtectedRoute in the routes.tsx

检查它是否工作。打开一个新窗口并转到localhost:3000/dashboard。由于令牌已经在我们的本地存储中,我们可以立即访问仪表板,而无需重定向到登录页面。

更新仪表板侧栏导航

在此之后,我们将需要更新注销。

dashboard-layoutdashboard-sidebar-navigation??。

我们将为注销创建一个新的句柄事件函数。把它放在handleClick函数的正下方,如清单 12-17 所示。

const handleLogout = () => {
    localStorage.clear();
  };

Listing 12-17Updating the dashboard-sidebar-navigation.tsx

handleLogout:通过删除所有存储值来清除localStorage的功能

在同一个文件中,转到注销,让我们在按钮上添加一个onClick事件,这样我们就可以触发它,如清单 12-18 所示。

<ListItem button onClick={handleLogout}>
                  <ListItemIcon>
                    <LogOutIcon />
                  </ListItemIcon>
                  <ListItemText primary={'logout'} />
                </ListItem>

Listing 12-18Adding an onClick Event for the handleLogout

我们来测试一下。

测试时间到了

转到仪表板,也打开你的 Chrome DevTools,点击应用。

单击 logout 按钮,浏览器应该会刷新,您会被定向到主页面;如果你看看 Chrome DevTools,这个令牌应该会被删除。

img/506956_1_En_12_Fig6_HTML.jpg

图 12-6

注销后删除令牌后本地存储的屏幕截图

这就是我们如何创建一个简单的认证。

随着我们的应用变得越来越复杂,我们将在接下来的章节中对此进行改进。

我想强调的是,我们了解身份认证的基础知识及其工作原理是至关重要的。但是现在,老实说,我强烈建议将身份验证作为一种服务或身份提供者。

我推荐第三方身份即服务的一些原因:

  1. 从应用中分散身份。用户的身份信息不会存储在您的数据库中。

  2. 允许开发人员专注于开发应用的业务价值,而不是花费数周时间构建身份验证和授权服务。

  3. 大部分第三方身份即服务公司比如 Auth0,我也是个人使用和推荐的,都是非常安全可靠的。Auth0 也有很好的文档、大量可以构建的开源项目和强大的社区支持。

    我尝试过的其他优秀的身份即服务提供商有 AWS Cognito、Azure AD 和 Okta。他们中的许多人提供了一个免费层程序,这是最适合小项目,所以你可以了解它是如何工作的。

(完全披露:我目前是一名授权大使。不,我不是公司的员工,也没有任何金钱报酬。每当我在会议上发言并提到它们时,我偶尔会得到一些奖品和其他极好的额外津贴。但我之所以成为认证大使,正是因为我以前就一直在使用它并推荐它们。)

  1. 最后,这些第三方身份提供者是由安全工程师或安全专家开发和维护的。他们更了解网络安全领域的最新动态,包括最佳实践、趋势和问题。

摘要

本章利用 Material-UI 组件的样式帮助构建了登录和注册表单。我们使用伪 Node json-server 中的 json-server-auth 库来模拟认证和保护我们的路由的实现流程。当我们在脚本中添加并发内容时,我们还使自己运行和构建应用变得更加容易。

在下一章中,我们将在 React 应用中构建更多的组件和功能。我们首先创建一个个人资料表单,然后将其同步到我们应用中的不同深层组件——所有这些都有 Redux 的强大帮助。

最后,我们将展示并非所有组件都需要绑定到 Redux。如果我们不需要增加复杂性,那么使用它就没有意义。有些人有这种错误的观念,认为一旦我们将 Redux 添加到我们的应用中,我们所有的组件都必须包含它。有需要就用;不然就不用了。

十三、编写配置文件表单并将其同步到组件

之前,我们展示了如何保护应用的某些部分免受未经身份验证或授权的人的攻击。本章将开始编写一个概要文件表单,并将该概要文件同步到各个组件。

由于这一部分相当长,我们将把它分成三个章节系列。在第一部分中,我们将更多地关注使用 Formik 和 JWT 进行身份验证来创建配置文件表单、注册表单和登录表单。在这里,我们将学习如何将配置文件表单同步到应用中的各个组件。

在第二部分中,我们将更新仪表板导航,并同步侧边栏导航和顶部导航栏之间的数据。在 Redux、Formik 和 Yup 验证模式的帮助下,我们将为应用添加更多的功能。

在本章系列的最后一部分,我们将继续巩固我们对 Redux 的了解,因为我们构建了完成应用 UI 所需的少数剩余组件。

这里的总体目标是从应用的不同层创建几个组件,并使用 Redux 将数据从一个组件无缝地传递到另一个组件——例如,使用 Redux 将个人资料数据同步到导航栏、侧栏和顶部导航栏。

在我们继续之前,让我在本章系列的最后向您展示完成的 UI。

图 13-1 到 13-4 显示了我们应用的完整 UI。

图 13-1 是设置页面。

img/506956_1_En_13_Fig1_HTML.jpg

图 13-1

设置页面

图 13-2 是订阅页面。

img/506956_1_En_13_Fig2_HTML.jpg

图 13-2

订阅表

图 13-3 是通知页面。

img/506956_1_En_13_Fig3_HTML.jpg

图 13-3

通知页面

图 13-4 为安全页面。

img/506956_1_En_13_Fig4_HTML.jpg

图 13-4

章节系列末尾的安全页面

创建索赔类型

好了,我们开始吧。首先,让我们为索赔创建一个模型或类型。

打开 models 文件夹,新建一个名为 claims-type.ts 的文件,复制清单 13-1 所示的代码。

export type ClaimsType = {
  readonly email: string;
  readonly iat: number;
  readonly exp: number;
  readonly sub: string;
};

Listing 13-1Creating the ClaimsType

ClaimsType:包含read-only email, iat, exp, and sub。这里的形状是根据解码后的访问令牌或 JWT 的有效载荷设计的。

iat - (issued at claim) :标识 JWT 的发行时间。

exp - (expiration time claim) :设置过期时间,在该时间或之后不得接受访问令牌进行处理。

sub - (subject claim):标识访问令牌或 JWT 的主题。

仍然有很多保留的 JSON Web Token 声明,如果你想了解更多,可以访问类似 iana.orghttps://tools.ietf.org/ 的网站。

接下来,让我们添加用户的形状。在 models 文件夹中,添加 user-type.ts 并复制代码,如清单 13-2 所示。

创建用户类型

export type Subscription = {
  name: string;
  price: number;
  currency: string;
  proposalsLeft: number;
  templatesLeft: number;
  invitesLeft: number;
  adsLeft: number;
  hasAnalytics: boolean;
  hasEmailAlerts: boolean;
};

export type UserType = {
  id: string;
  email: string;
  password: string;
  country: string;
  isPublic: boolean;
  phone: string;
  role: string;
  state: string;
  tier: string;
  name: string;
  avatar: string;
  city: string;
  canHire: boolean;
  subscription?: Subscription;
};

Listing 13-2Creating the UserType

添加 API:用户和用户数据库

然后我们需要再次更新我们的端点。所以转到 axios.ts 并再添加两个端点,如清单 13-3 所示。

export const EndPoints = {
  sales: 'sales',
  products: 'products',
  events: 'events',
  login: 'login',
  register: 'register',
  users: 'users',
  usersDb: 'users-db',
};

Listing 13-3Updating the Endpoints for Users and UsersDb

Users:用于编辑或更新用户密码。我们将允许用户更新或编辑他们的密码。

UsersDb:这是为了在我们的 models 文件夹中存储用户类型和订阅类型的详细信息。理想情况下,这应该放在一个单独的数据库中,就像在现实世界中,我们将用于用户身份验证的数据库与用户的配置文件或订阅分开一样。

创建 userDbService

让我们创建一个新的服务文件。转到 services 文件夹,添加一个名为 userDbService.ts 的新文件,如清单 13-4 所示。

import api, { EndPoints } from 'api/axios';
import { UserType } from 'models/user-type';

export async function getUserByIdFromDbAxios(id: string) {
  return await api.get<UserType>(`${EndPoints.usersDb}/${id}`);
}

export async function putUserFromDbAxios(user: UserType) {
  return await api.put<UserType>(`${EndPoints.usersDb}/${user.id}`, user);
}

Listing 13-4Creating UserDbService

userDbService有两个 HTTP 方法或函数。我们从模型中导入了UserType,我们将我们的第一个函数命名为尽可能具体和描述性的getUserByIdFromDbAxios,,返回类型是UserType.

另一个 axios 函数我们命名为putUserFromDbAxios,,它接受UserType,我们正在用UserType的预期响应进行更新。

更新授权服务

之后,我们将需要更新authService.打开authService .ts并添加以下函数,如清单 13-5 所示。

export type ChangePasswordModel = {
  email: string;
  password: string;
  id: string;
};

export async function changePassWordAxios(
  changePasswordModel: ChangePasswordModel,
) {
  return await axios.put<void>(
    `${EndPoints.users}/${changePasswordModel.id}`,
    changePasswordModel,
  );
}

Listing 13-5Adding changePasswordAxios in authService

changePasswordAxios是一个异步服务函数,它使用类型changePasswordModel来更改密码。我们使用 axios 发送一个 put 请求,用这个 id 更新特定用户的密码。

Redux 的另一种用法

接下来,我们将创建一个profileActionTypes,,我们将在这里使用 Redux。

这是 Redux 的另一种写法。在前面的章节中,我们把所有的东西都放在一个文件中的calendarSlice,中,但是这次我们将为动作和片做单独的文件。这样做的主要原因是关注点的分离和代码的可读性。

这是图 13-5 中文件夹结构的一个快照,我们计划将配置文件文件夹作为构建 React-Redux 应用的一种方式。

img/506956_1_En_13_Fig5_HTML.jpg

图 13-5

配置文件的文件夹结构

创建 profileactioinotys

在 features 文件夹中,我们创建一个新目录,并将其命名为 profile。

在配置文件文件夹中,添加一个文件并将其命名为profileActionTypes.ts:

features ➤ profile ➤ profileActionTypes.ts

在清单 13-6 中,我们正在创建 profileActionTypes。

import { UserType } from 'models/user-type';

export type ProfileStateType = {
  readonly profile: UserType;
  readonly loading: boolean;
  readonly error: string;
};

export const profileNamespace = 'profile';

/* action types */

export const ProfileActionTypes = {
  FETCH_AND_SAVE_PROFILE: `${profileNamespace}/FETCH_AND_SAVE_PROFILE`,
  UPDATE_PROFILE: `${profileNamespace}/UPDATE_PROFILE`,
};

Listing 13-6Creating the profileActionTypes.ts

创建配置文件操作

接下来,我们需要在概要文件文件夹中添加一个新文件。我们将其命名为profileAsyncActions.ts,如清单 13-7 所示。

import { createAsyncThunk } from '@reduxjs/toolkit';
import { UserType } from 'models/user-type';

import { ProfileActionTypes } from './profileActionTypes';
import {
  getUserByIdFromDbAxios,
  putUserFromDbAxios,
} from 'services/userDbService';

export const getProfileAction = createAsyncThunk(
  ProfileActionTypes.FETCH_AND_SAVE_PROFILE,
  async (id: string) => {
    return (await getUserByIdFromDbAxios(id)).data;
  },
);

export const putProfileAction = createAsyncThunk(
  ProfileActionTypes.UPDATE_PROFILE,
  async (user: UserType) => {
    return (await putUserFromDbAxios(user)).data;
  },
);

Listing 13-7Creating the profileAsyncActions

我们从 Redux 工具包 中导入模块createAsyncThunk来处理副作用。我们还有来自模型的UserType,动作类型profileActionTypes,以及来自userDbService.的两个 axios 函数

这是我们第一次异步操作。它来自于createAsyncThunk,的一个实例,我们需要传递一个字符串(FETCH_AND_SAVE_PROFILE)作为第一个参数,第二个参数是一个异步和等待函数。

string 类型的id参数与getProfileAction,相连,所以每当我们使用这个getProfileAction,时,我们都需要传递一个参数字符串。

然后我们将在getUserByIdFromDbAxios中使用那个id,用开-闭括号把它括起来,这样我们就可以用点(.)批注然后data.

**putProfileAction:用于更新个人资料。我们还有createAsyncThunk实例,然后将字符串指令传递给 reducers 来更新概要文件。第二个参数是基于承诺的匿名函数或异步和等待函数。

这是实现 Redux 工具包 的模式,即使在未来的 Redux 项目中,您也可以将它作为指导方针。

创建配置文件目录

之后,我们需要创建切片。在配置文件文件夹中,创建一个名为profileSlice .ts.的新文件

我们在下面有导入命名的组件,包括 profile state typeprofileNamespace,``profileActionTypes,和来自profileAsyncActionsgetProfileActionputProfileAction,如清单 13-8 所示。

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { UserType } from 'models/user-type';

import { profileNamespace, ProfileStateType } from './profileActionTypes';
import { getProfileAction, putProfileAction } from './profileAsyncActions';

/* profile state */
/* initial state or default state or initial values, it's up to you */
export const initialState: ProfileStateType = {
  profile: {} as UserType,
  loading: false,
  error: '',
};

/* profile store */

export const profileSlice = createSlice({

  /*
   name: is your feature or also called module, or namespace,
   or context, etc. The terminologies here can be interchangeable.
   This is required.
  */
  name: profileNamespace,

  /*initialState is the default value of this namespace/module and it is required.*/

  initialState,

  /*Non asynchronous actions. Does not require Axios.*/

  reducers: {},

  /*Asynchronous actions. Actions that require Axios.
    extraReducers - allows createSlice to respond not only to its own
     action type but other action types also*/

 /*state - is coming from the initialState; no need to define it because the Redux 工具包 can already infer what particular state it is.  */

  extraReducers: builder => {
    builder.addCase(
      getProfileAction.fulfilled,
      (state, action: PayloadAction<UserType>) => {
        state.profile = action.payload;
      },
    );

    builder.addCase(
      putProfileAction.pending,
      (state, action: PayloadAction) => {
        state.loading = true;
        state.error = '';
      },
    );
    builder.addCase(
      putProfileAction.fulfilled,
      (state, action: PayloadAction<UserType>) => {
        state.loading = false;
        state.profile = action.payload;
      },
    );
    builder.addCase(
      putProfileAction.rejected,
      (state, action: PayloadAction<any>) => {
        state.loading = false;
        state.error = 'Something wrong happened';
        console.log(action?.payload);
      },
    );
  },
});

export default profileSlice.reducer;

Listing 13-8Creating profileSlice.ts

这里我们没有使用任何非异步或同步动作。对于 Redux 中的异步动作,我们需要extraReducers,,它也是切片的一部分。

要使用extraReducers,,我们需要设置或添加一个名为builder.的函数签名

builder:返回我们将要构建的addCase

addCase:需要一个字符串,是动作类型。

如果你看看各种各样的addCases,你会发现它似乎只是一个大的 try-catch 块函数。

getProfileAction.fulfilled:例如,如果我们收到一个 2xx 状态代码,前面的代码块就会运行。

putProfileAction.pending:在被拒绝和被执行的功能之前运行。这是我们向任何 web 服务发送请求的时间。我们通过将 pending 设置为 true 来启用 spinner 或 loader。我们不需要挂起的有效负载,因为我们直接将 loading 从 false 改为 true。

在这里,我们得到状态代码 2xx,然后我们更新减速器。

putProfileAction.rejected:每当我们得到除 2xx 之外的状态码时就会运行,例如,4xx,表示未经授权,或 5xx,表示服务器有问题。如果发生这种情况,我们将 loading 设置为 false 并运行错误消息。

在底部,我们导出了缩减器: profileSlice.reducer

FOR YOUR ACTIVITY

getProfileAction.中再添加两个addCases添加待定和拒绝。按照putProfileAction.里的模式就行了

清单 13-9 是一个 ToDo 活动:在 getProfileAction 中创建 addCase pending 和 addCase rejected。

extraReducers: builder => {

   // todo activity: create addCase pending

    builder.addCase(
      getProfileAction.fulfilled,
      (state, action: PayloadAction<UserType>) => {
        state.profile = action.payload;
      },
    );

     // todo activity: create addCase rejected

Listing 13-9Activity for Chapter 13

一旦您完成了活动,现在让我们来更新根缩减器。去商店➤ reducers.ts

将 profileReducer 添加到异径管

我们需要从profileSlice a中导入profileReducer并将其添加到根减速器的injectedReducers中,如清单 13-10 所示。

import { combineReducers } from '@reduxjs/toolkit';
import calendarReducer from 'features/calendar/calendarSlice';
import profileReducer from'features/profile/profileSlice';

/* easier way of registering a reducer */

const injectedReducers = {
  calendar: calendarReducer,
  profile: profileReducer,
};

Listing 13-10Adding the profileReducer in the reducers.ts

我们店里有profileReducer;现在在每个应用组件中都可以访问到profileReducer

创建 authSlice

接下来,我们将创建另一个切片。我们需要authSlice将访问令牌和声明保存到全局存储中。

因此,在文件夹特性中,添加一个新文件夹并将其命名为 auth,并在其中创建一个名为d authSlice.ts:的新文件

features ➤ auth ➤ authSlice.ts

打开它,从 Redux 工具包 中导入createSlicePayloadAction,并从模型中导入ClaimsType。首先导入命名的组件,如清单 13-11 所示。

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ClaimsType } from 'models/claims-type';

Listing 13-11Importing Modules in the authSlice.ts

然后我们添加名称空间、AuthStateType 的形状和初始状态,如清单 13-12 所示。

const authNamespace = 'auth';

export type AuthStateType = {
  readonly accessToken: string;
  readonly claims: ClaimsType;
};

/*we are using the AuthStateType to type safe our initial state */

export const initialState: AuthStateType = {
  accessToken: '',
  claims: null,
};

Listing 13-12Adding the Namespace, Type, and initialState in authSlice.ts

接下来,让我们使用createSlice来接受namespace, initialState、充满了reducer函数的对象以及我们创建的片名,如清单 13-13 所示。

export const authSlice = createSlice({

  /*namespace for separating related states. Namespaces are like modules*/

  name: authNamespace,

  /* initialState is the default value of this namespace/module and it is required */

  initialState,

  /*Non asynchronous actions. Does not require Axios.*/

  reducers: {
    saveTokenAction: (state, action: PayloadAction<string>) => {
      state.accessToken = action?.payload;
    },
    saveClaimsAction: (state, action: PayloadAction<ClaimsType>) => {
      state.claims = action?.payload;
    },
  },

  /*Asynchronous actions. Actions that require Axios.*/

  extraReducers: builder => {},
});

/* export all non-async actions */

export const { saveClaimsAction, saveTokenAction } = authSlice.actions;

export default authSlice.reducer;

Listing 13-13Passing createSlice in our authSlice

在清单 13-13 中,我们有两个非异步动作,这意味着它们不需要使用 axios。

两个非异步动作—saveTokenActionsaveClaimsAction—不需要 axios 的帮助。

接下来,我们通过从 authSlice.actions 中提取这些非异步操作来导出它们。

最后,我们导出 authSlice.reducer,这样我们可以在根 reducer 中调用它。

将 authSlice 添加到 Reducers

转到reducers.ts,我们将authReducer注入到injectedReducers中,并导入命名组件,,如清单 13-14 所示。

import authReducer from '../features/auth/authSlice';

const injectedReducers = {
  calendar: calendarReducer,
  auth: authReducer,
  profile: profileReducer,
};

Listing 13-14Adding the authReducer in the reducers.ts

authReducer现在是组合减速器和存储的一部分。

安装 JWT 解码

接下来,我们需要安装一个流行的 JavaScript 库 jwt-decode。它解码 JWT 令牌,对浏览器应用很有用:

npm i jwt-decode

安装 jwt-decode 之后,让我们前往受保护的路由并对其进行改进:组件➤受保护的路由

更新受保护的路由

首先,让我们在 protected-route.tsx 中导入额外的命名组件,如清单 13-15 所示。

import { useDispatch } from 'react-redux';
import jwt_decode from 'jwt-decode';

import { saveClaimsAction } from 'features/auth/authSlice';
import { ClaimsType } from 'models/claims-type';

Listing 13-15Adding Named Components in protected-route.tsx

接下来,让我们更新 ProtectedRoute 组件,如清单 13-16 所示。

const ProtectedRoute = props => {
  const dispatch = useDispatch();
  const token = localStorage.getItem('token');

  /* this is cleaning up the localStorage and redirecting user to login */
  if (!token) {
    localStorage.clear();
    return <Redirect to={{ pathname: '/login' }} />;
  }

  const decoded: ClaimsType = jwt_decode(token);
  const expiresAt = decoded.exp * 1000;
  const dateNow = Date.now();
  const isValid = dateNow <= expiresAt;

  dispatch(saveClaimsAction(decoded));

  return isValid ? (
    <Route {...props} />
  ) : (
    <Redirect to={{ pathname: '/login' }} />
  );
};

export default ProtectedRoute;

Listing 13-16Updating the ProtectedRoute Component

我们需要useDispatch和来自localStorage的令牌。我们正在清理localStorage,如果它是假的或者没有令牌,我们会将用户重定向到登录页面。

我们将把 toke n 传递给jwt_decode来解码它。相对于dateNow或当前日期,解码的令牌将与expiresAt或令牌的到期日期进行比较。

dateNow应等于或小于expiresAt

接下来,我们将通过saveClaimsAction中的解码后的 token进行调度。

在 return 语句中,如果令牌isValid,则转到用户正在导航的地方;否则,将用户重定向到登录页面。

更新登录表单

我们现在将更新登录表单。首先,我们将添加一些命名的组件,如清单 13-17 所示。

import jwt_decode from 'jwt-decode';
import { useDispatch } from 'react-redux';

import { saveClaimsAction, saveTokenAction } from 'features/auth/authSlice';
import { loginAxios } from 'services/authService';
import { ClaimsType } from 'models/claims-type';

Listing 13-17Updating the Named Components in LoginForm

我们添加了来自 React Redux 的 useDispatch 和 jwt_decode。我们还需要 saveClaimsAction、saveTokenAction 和 ClaimsType。

接下来,我们需要更新 LoginForm 函数,如清单 13-18 所示。

const LoginForm = () => {
  const key = 'token';
  const history = useHistory();
  const dispatch = useDispatch();
  const [error, setError] = useState('');

Listing 13-18Hooks in Login Form

我们使用清单 13-18 中的useDispatch来调度一个动作,以保存对商店的访问令牌和声明,如清单 13-19 所示。

const saveUserAuthDetails = (data: { accessToken: string }) => {
    localStorage.setItem(key, data.accessToken);
    const claims: ClaimsType = jwt_decode(data.accessToken);
    console.log('Claims::', claims);    /*just to check it  */
    dispatch(saveTokenAction(data.accessToken));
    dispatch(saveClaimsAction(claims));
  };

Listing 13-19Updating the saveUserAuthDetails Function in the LoginForm

更新登记表

我们将对注册表进行类似的更新。我们开始导入命名的组件,如清单 13-20 所示。

import { useDispatch } from 'react-redux';

import { saveClaimsAction, saveTokenAction } from 'features/auth/authSlice';
import jwt_decode from 'jwt-decode';

import { ClaimsType } from 'models/claims-type';

Listing 13-20Updating the Named Components in the Register Form

在清单 13-20 中,我们添加了 useDispatch、saveClaimsAction、saveTokenAction、jwt_decode 和 ClaimsType。

接下来,我们更新 RegisterForm,如清单 13-21 所示。

const RegisterForm = () => {
  const key = 'token';
  const history = useHistory();
  const dispatch = useDispatch();
  const [error, setError] = useState('');
  const [isAlertVisible, setAlertVisible] = useState(false);

Listing 13-21Hooks in the Registration Form

在清单 13-21 中,我们还使用了 useDispatch 来调度或发送一个动作,以保存对商店的访问令牌和声明,如清单 13-22 所示。

const saveUserAuthDetails = (data: { accessToken: string }) => {
  localStorage.setItem(key, data.accessToken);
  const claims: ClaimsType = jwt_decode(data.accessToken);
  console.log('Claims::', claims);
  dispatch(saveTokenAction(data.accessToken));
  dispatch(saveClaimsAction(claims));
};

Listing 13-22saveUserAuthDetails Function

一旦保存在存储中,现在就可以在应用的任何地方访问访问令牌和声明。

让我们运行我们的应用:

npm run start:fullstack

刷新浏览器,打开 Redux DevTools,如图 13-6 所示。

img/506956_1_En_13_Fig6_HTML.jpg

图 13-6

Redux DevTools 的屏幕截图,没有本地存储中的数据

注意,在 auth 和 profile 状态下还没有数据。都是默认值。

接下来,让我们进入登录页面,让我们登录。当我们登录时,这将保存商店中的状态,如图 13-7 所示。

img/506956_1_En_13_Fig7_HTML.jpg

图 13-7

Redux DevTools 登录后的截图

在 Redux DevTools 中,我们可以看到saveClaimsActionsaveTokenAction.,我们有accessToken的数据和claims中的值。

我们现在已经验证了可以从服务器接收 accessToken 和声明。

配置文件仍然是空的,因为我们将使用声明 id (sub)来获取特定用户的数据。

创建标题配置文件

接下来,我们继续创建一个新的 React 组件,并将其命名为header-profile.tsx:

app ➤ components ➤ header-profile.tsx

让我们导入我们需要的命名组件,如清单 13-23 所示。

import React, { useState, MouseEvent } from 'react';
import clsx from 'clsx';
import { Theme, withStyles } from '@material-ui/core/styles';
import Menu, { MenuProps } from '@material-ui/core/Menu';
import { LogOut as LogOutIcon, Hexagon as HexagonIcon } from 'react-feather';
import { useSelector } from 'react-redux';
import { RootState } from 'store/reducers';
import { createStyles } from '@material-ui/styles';
import {

  Avatar,
  Box,
  Divider,
  ListItemIcon,
  ListItemText,
  makeStyles,
  MenuItem,
} from '@material-ui/core';

Listing 13-23Adding the Named Components of the HeaderProfile

好吧,有什么新鲜事吗?从 Material-UI 核心样式中,我们已经从 React Feather 中导入了菜单和 MenuProps 以及六边形。

我们还使用 Redux 中的 useSelector 和 RootState 进行类型化。

然后让我们添加 HeaderProfile 函数,如清单 13-24 所示。

const HeaderProfile = () => {
  const classes = useStyles();

  /*using the profile to render an avatar */
  const { profile } = useSelector((state: RootState) => state.profile);

  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);

  const handleClick = (event: MouseEvent<HTMLElement>) => {
    setAnchorEl(event.currentTarget);
  };

  const handleClose = () => {
    setAnchorEl(null);
  };

  const handleLogout = () => {
    localStorage.clear();
  };

Listing 13-24Creating the HeaderProfile Function Component 

在清单 13-24 中,我们使用 useSelector 来获取配置文件,并使用 profile.avatar 来呈现头像组件。参见清单 13-25 添加 HeaderProfile React 组件的返回语句。

return (
    <div>
      <Box display="flex" justifyContent="center" onClick={handleClick}>
        <Avatar
          variant={'circle'}
          alt="User"
          className={clsx(classes.avatar, classes.small)}
          src={profile.avatar}
        />
      </Box>
      <StyledMenu
        id="customized-menu"
        anchorEl={anchorEl}
        keepMounted
        open={Boolean(anchorEl)}
        onClose={handleClose}
      >

        <MenuItem>
          <ListItemText primary={profile.email} />
        </MenuItem>
        <Divider />
        <MenuItem>
          <ListItemIcon>
            <HexagonIcon />
          </ListItemIcon>
          <ListItemText primary="Partners" />
        </MenuItem>
        <a className={classes.link} href={'/'}>
          <MenuItem onClick={handleLogout}>
            <ListItemIcon>
              <LogOutIcon />
            </ListItemIcon>
            <ListItemText primary="Logout" />
          </MenuItem>
        </a>
      </StyledMenu>
    </div>
  );
};

export default HeaderProfile;

Listing 13-25Adding the Return Statement of the HeaderProfile

最后,添加样式组件,如清单 13-26 所示。

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    avatar: {
      cursor: 'pointer',
      width: 64,
      height: 64,
    },
    link: { textDecoration: 'none', color: 'inherit' },
    small: {
      width: theme.spacing(3),
      height: theme.spacing(3),
    },
  }),
);

const StyledMenu = withStyles({
  paper: {
    border: '1px solid #d3d4d5',
  },
})((props: MenuProps) => (
  <Menu
    elevation={0}
    getContentAnchorEl={null}
    anchorOrigin={{
      vertical: 'bottom',
      horizontal: 'center',
    }}
    transformOrigin={{
      vertical: 'top',
      horizontal: 'center',
    }}
    {...props}
  />
));

Listing 13-26Styling Components for the HeaderProfile

我们在这里使用的所有样式组件都来自 Material- UI。比如去网站 material-ui。并搜索菜单。

搜索您需要的特定菜单,复制代码,并根据您的喜好进行调整。你通常可以在网页底部看到 API。

更新导航栏

让我们前往导航栏。转到layoutsmain-layoutnavigation-bar.tsx,我们将对其进行更新。

当用户登录时,我们需要隐藏或删除菜单中的登录按钮。

首先,我们需要添加the useSelector, RootState,和HeaderProfile,useSelector用于访问或获取声明,如清单 13-27 所示。

import {useSelector} from 'react-redux';
import {RootState} from 'store/reducers';
import HeaderProfile from 'app/components/header-profile';

export default function NavigationBar() {
  const classes = useStyles();
  const { claims } = useSelector((state: RootState) => state.auth);

Listing 13-27Adding Import Components and Using useSelector

接下来,我们从 Auth 获取声明,并将使用这些声明来更新仪表板和登录。

我们将把仪表板按钮和登录按钮放在一些花括号内,然后添加声明,并在按钮仪表板下添加 HeaderProfile。

{claims ? (

              <Button color="inherit">
                <Link className={classes.link} to={'/dashboard'}>
                  Dashboard
                </Link>
              </Button>
              <HeaderProfile/>

          ) : (
            <Button color="inherit">
              <Link className={classes.link} to={'/login'}>
                Login
              </Link>
            </Button>
          )
          }

Listing 13-28Using the Claims to Wrap the Dashboard and Login Buttons in the Navigation Bar

因此,如果声明有效,就会呈现仪表板链接。否则,将呈现登录链接。

刷新浏览器,应该会在登录页面的右上角看到头像,如图 13-8 所示。

img/506956_1_En_13_Fig8_HTML.jpg

图 13-8

显示登录页面的仪表板头像

并且登录后看到仪表盘头像,如图 13-9 。

img/506956_1_En_13_Fig9_HTML.jpg

图 13-9

登录后显示仪表板头像

创建帐户视图

现在我们转到 views 文件夹,创建一个名为account的新文件夹,并在其中创建另一个名为AccountView的文件夹。在AccountView,里面加上index.tsx:

views ➤ account ➤ AccountView ➤ index.tsx

现在,我们将添加带有标签<h1> AccountView Page Works</h1>的标准页面模板。参见清单 13-29 。

import React from 'react';
import { Container, makeStyles } from '@material-ui/core';
import Page from 'app/components/page';

const AccountView = () => {
  const classes = useStyles();

  return (
    <Page className={classes.root} title="Settings">
      <Container maxWidth="lg">
        <h1>AccountView Page Works</h1>
      </Container>
    </Page>
  );
};

export default AccountView;

const useStyles = makeStyles(theme => ({
  root: {
    minHeight: '100%',
    paddingTop: theme.spacing(3),
    paddingBottom: theme.spacing(3),
  },
}));

Listing 13-29Creating the Standard Page for the AccountView

目前,帐户页面只是为了显示我们可以在这里导航;我们稍后将回到这一点来更新它。

添加图像

让我们在应用中添加更多的图像。转到图像➤产品。

你可以在我的 GitHub 中抓取这些图片:

来源: https://github.com/webmasterdevlin/practical-enterprise-react/tree/master/chapter-12/publimg/products

将这四张图片放在产品中:

product_extended.svg,
product_premium.svg,
product_premium—outlined.svg,
product_standard.svg.

最后,将这个头像添加到图片目录:

阿凡达 _6.png

你可以从这里抓取图像:

https://github.com/webmasterdevlin/practical-enterprise-react/blob/master/chapter-12/publimg/avatar_6.png

创建定价页面

接下来,让我们创建一个新组件,并将其命名为定价页面:

views ➤ pages ➤ pricing ➤ PricingPage.tsx

这个页面纯粹是为了美观或者设计。我们将从 Material-UI 导入标准的样式组件,如清单 13-30 所示。

import React from 'react';
import clsx from 'clsx';
import {

  Box,
  Button,
  Container,
  Divider,
  Grid,
  Paper,
  Typography,
  makeStyles,
} from '@material-ui/core';

import Page from 'app/components/page';

Listing 13-30Adding the Named Components of the PricingPage

然后我们添加将从 Material-UI 中重用的 PricingPage 组件。我们展示了我们通常在网站上看到的不同价格产品或定价选项,例如,标准选项、高级选项和扩展选项。参见清单 13-31 。

const PricingPage = () => {
  const classes = useStyles();

  return (
    <Page className={classes.root} title="Pricing">
      <Container maxWidth="sm">
        <Typography align="center"
         variant="h2" color="textPrimary">
          Start Selling!
        </Typography>
        <Box mt={3}>
          <Typography align="center"
         variant="subtitle1" color="textSecondary">
            Welcome to the best platform for selling products
          </Typography>
        </Box>
      </Container>
      <Box mt="160px">
        <Container maxWidth="lg">
          <Grid container spacing={4}>
            <Grid item md={4} xs={12}>
              <Paper className={classes.product}
               elevation={1}>
                <img
                  alt="Product"
                  className={classes.productImage}
                  src="images/products/product_standard.svg"
                />

                <Typography
                  component="h4"
                  gutterBottom
                  variant="overline"
                  color="textSecondary"
                >

                  Standard
                </Typography>
                <div>
                  <Typography
                    component="span"
                    display="inline"
                    variant="h4"
                    color="textPrimary"
                  >
                    $5
                  </Typography>
                  <Typography
                    component="span"
                    display="inline"
                    variant="subtitle2"
                    color="textSecondary"
                  >
                    /month
                  </Typography>
                </div>

                <Typography variant="overline"
                            color="textSecondary">
                  Max 1 user
                </Typography>
                <Box my={2}>
                  <Divider />
                </Box>
                <Typography variant="body2"
                            color="textPrimary">
                  20 proposals/month
                  <br />
                  10 templates
                  <br />
                  Analytics dashboard
                  <br />
                  Email alerts
                </Typography>
                <Box my={2}>
                  <Divider />
                </Box>

                <Button
                  variant="contained"
                  fullWidth
                  className={classes.chooseButton}
                >
                  Choose
                </Button>
              </Paper>
            </Grid>

            <Grid item md={4} xs={12}>
              <Paper
                className={clsx(classes.product,
                           classes.recommendedProduct)}
                elevation={1}
              >
                <img
                  alt="Product"
                  className={classes.productImage}
                  src="images/products/product_premium--outlined.svg"
                />
                <Typography
                  component="h4"
                  gutterBottom
                  variant="overline"
                  color="inherit"
                >
                  Premium
                </Typography>
                <div>
                  <Typography
                    component="span"
                    display="inline"
                    variant="h4"
                    color="inherit"
                  >
                    $29
                  </Typography>

                  <Typography
                    component="span"
                    display="inline"
                    variant="subtitle2"
                    color="inherit"
                  >
                    /month
                  </Typography>
                </div>
                <Typography variant="overline" color="inherit">
                  Max 3 user
                </Typography>
                <Box my={2}>
                  <Divider />
                </Box>
                <Typography variant="body2" color="inherit">
                  20 proposals/month
                  <br />
                  10 templates
                  <br />
                  Analytics dashboard
                  <br />
                  Email alerts
                </Typography>

                <Box my={2}>
                  <Divider />
                </Box>
                <Button

                  variant="contained"
                  fullWidth
                  className={classes.chooseButton}
                >
                  Choose
                </Button>
              </Paper>
            </Grid>
            <Grid item md={4} xs={12}>
              <Paper className={classes.product} elevation={1}>
                <img
                  alt="Product"
                  className={classes.productImage}
                  src="images/products/product_extended.svg"
                />
                <Typography
                  component="h4"
                  gutterBottom
                  variant="overline"
                  color="textSecondary"
                >
                  Extended
                </Typography>

                <div>
                  <Typography
                    component="span"
                    display="inline"
                    variant="h4"
                    color="textPrimary"
                  >
                    $259
                  </Typography>
                  <Typography
                    component="span"
                    display="inline"
                    variant="subtitle2"
                    color="textSecondary"
                  >
                    /month
                  </Typography>
                </div>
                <Typography variant="overline" color="textSecondary">
                  Unlimited
                </Typography>

                <Box my={2}>
                  <Divider />
                </Box>

                <Typography variant="body2"
             color="textPrimary">
                  All from above
                  <br />
                  Unlimited 24/7 support
                  <br />
                  Personalised Page
                  <br />
                  Advertise your profile
                </Typography>
                <Box my={2}>
                  <Divider />
                </Box>
                <Button
                  variant="contained"
                  fullWidth
                  className={classes.chooseButton}
                >
                  Choose
                </Button>
              </Paper>
            </Grid>
          </Grid>
        </Container>
      </Box>
    </Page>
  );
};

Listing 13-31Creating the PricingPage Component

接下来,我们需要添加样式组件,如填充、定位、过渡主题等。来自 Material-UI,如清单 13-32 所示。

const useStyles = makeStyles(theme => ({
  root: {
    minHeight: '100%',
    height: '100%',
    paddingTop: 120,
    paddingBottom: 120,
  },
  product: {
    position: 'relative',
    padding: theme.spacing(5, 3),
    cursor: 'pointer',
    transition: theme.transitions.create('transform', {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.leavingScreen,
    }),
    '&:hover': {
      transform: 'scale(1.1)',
    },
  },
  productImage: {
    borderRadius: theme.shape.borderRadius,
    position: 'absolute',
    top: -24,
    left: theme.spacing(3),
    height: 48,
    width: 48,
    fontSize: 24,
  },
  recommendedProduct: {
    backgroundColor: theme.palette.primary.main,
    color: theme.palette.common.white,
  },
  chooseButton: {
    backgroundColor: theme.palette.common.white,
  },
}));

export default PricingPage;

Listing 13-32Adding the Styling Components to the PricingPage

我们不会在这里添加任何功能;对于我们目前的应用来说,这只是为了美观。但是你可以摆弄它,根据你的喜好改变它。

更新路线

好了,现在是时候更新添加PricingPage and the AccountView.的路线了,将它们分别添加到登录路线路径和日历路线路径下,如清单 13-33 所示。

<Route
          exact
          path={'/pricing'}
          component={lazy(() => import('./views/pages/pricing/PricingPage'))}
        />

...

<Route
                  exact
                  path={path + '/account'}
                  component={lazy(
                    () => import('./views/dashboard/account/AccountView'),
                  )}
                />

Listing 13-33Updating the routes.tsx

摘要

在这一章中,我们已经学习了如何创建我们的 Profile 表单,并展示了在 Redux 的帮助下,我们如何轻松地将它同步到应用中的另一个组件。我们还使用了一个流行的 JavaScript 库 jwt-decode,它可以用来解码 jwt 令牌。我们还使用 Formik 和 JWT 创建了用于身份验证的登录和注册表单。

在本章的第二部分,我们将继续更新侧边栏导航。**

十四、更新仪表板侧栏导航

我们刚刚完成了配置文件、登录和注册表单的构建,并通过更新路线结束了第一部分。我们现在进入这个由三部分组成的章节系列的第二部分,更新仪表板侧边栏导航。

既然我们已经更新了路线,我们现在将继续到仪表板-侧栏-导航。

让我们从分别添加来自 React-Redux 和 reducers 的useDispatchuseSelector开始。见清单 14-1 。

import { useSelector, useDispatch } from 'react-redux';
import {RootState} from 'store/reducers';

Listing 14-1Updating the Named Components in the dashboard-sidebar-navigation

让我们使用DashboardSidebarNavigation component中的useDispatchuseSelector,如清单 14-2 所示。

const DashboardSidebarNavigation = () => {
  const classes = useStyles();
  const dispatch = useDispatch();
  const {profile} = useSelector((state: RootState) => state.profile);
  const {claims} = useSelector((state: RootState) => state.auth);
  const { url } = useRouteMatch();
  const [open, setOpen] = useState(false);

Listing 14-2Updating the DashboardSidebarNavigation

在清单 14-2 中,我们从auth reducer.得到profile减速器轮廓和claims

接下来,从 profileAsyncActions 导入命名模块 getProfileAction。在useEffect中,我们调用dispatch将带有claims.subgetProfileAction发送到减速器。这意味着我们需要导入清单 14-3 中所示的getProfileAction,

...
import { getProfileAction } from 'features/profile/profileAsyncActions';
...

useEffect(() => {
    dispatch(getProfileAction(claims.sub));

  }, []);

Listing 14-3Dispatching the getProfileAction in the DashboardSidebarNavigation

getProfileAction :我们调用传递the claims.sub并获取user's id的函数。

让我们从 Material-UI Core 添加头像和附加组件的样式——头像、盒子和字体,如清单 14-4 所示。

import { Collapse, Divider, ListSubheader, Avatar, Box, Typography} from '@material-ui/core';
...
const useStyles = makeStyles(theme =>
  createStyles({
    avatar: {
      cursor: 'pointer',
      width: 64,
      height: 64,
    },
...

Listing 14-4Adding the Avatar Style in the DashboardSidebarNavigation

现在,我们准备好更新到新的 UI。将包含徽标的工具栏替换为下面的工具栏,如清单 14-5 所示。

{/* check first if profile.name is true before rendering what's inside the Box, including the avatar */}
{profile.name && (
            <Box p={2}>
              <Box display="flex" justifyContent="center">
                <Avatar
                  alt="User"
                  className={classes.avatar}
                  src={profile.avatar}
                />
              </Box>
              <Box mt={2} textAlign="center">
                <Typography>{profile.name}</Typography>
                <Typography variant="body2" color="textSecondary">
                  Your tier: {profile.tier}
                </Typography>
              </Box>
            </Box>
          )}

Listing 14-5Updating

the DashboardSidebarNavigation

更新 db.json

好的,在我们刷新浏览器之前,我们需要首先更新db.json并添加 users-db。

目前,我们只有用于认证的用户。现在我们来补充一下。参见清单 14-6 。

"users-db": [
    {
      "id": "7fguyfte5",
      "email": "demo@acme.io",
      "name": "Mok Kuh",
      "password": "$2a$10$.vEI32nHFyG15ZACR7q/J.DNT/7iFC1Gfi2fFPMsG09LCPtwk0q/.",
      "avatar":img/avatar_6.png",
      "canHire": true,
      "country": "United States",
      "city": "NY",
      "isPublic": true,
      "phone": "+40 777666555",
      "role": "admin",
      "state": "New York",
      "tier": "Premium",
      "subscription": {
        "name": "Premium",
        "price": 29,
        "currency": "$",
        "proposalsLeft": 12,
        "templatesLeft": 5,
        "invitesLeft": 24,
        "adsLeft": 10,
        "hasAnalytics": true,
        "hasEmailAlerts": true
      }
    }
  ]

Listing 14-6Adding the users-db in the db.json

刷新浏览器,您应该会看到如图 14-1 所示的浏览器。

img/506956_1_En_14_Fig1_HTML.jpg

图 14-1

更新用户界面

你会注意到边栏导航和导航栏正在同步;他们在渲染同一个图像。它们从 Redux 存储中获得相同的状态。

检查 Redux DevTools 并单击状态,您将看到概要文件,它现在在应用的任何部分都可用,如图 14-2 所示。

img/506956_1_En_14_Fig2_HTML.jpg

图 14-2

检查 Redux 开发工具

img/506956_1_En_14_Figa_HTML.jpg

好了,现在我们知道我们可以在任何组件中同步用户的配置文件。但是我们还没有完成;我们仍然需要更新仪表板、帐户和定价菜单。

更新仪表板侧栏导航

让我们再次打开仪表板-侧栏-导航。让我们从 React Feather 导入几个图标,并在日历后添加帐户和定价菜单,如清单 14-7 所示。

...
  User as UserIcon,
  DollarSign as DollarSignIcon,
  LogOut as LogOutIcon,
} from 'react-feather';

...

<ListSubheader>Applications</ListSubheader>
              <Link className={classes.link} to={`${url}/calendar`}>
                <ListItem button>
                  <ListItemIcon>
                    <CalendarIcon />
                  </ListItemIcon>
                  <ListItemText primary={'Calendar'} />
                </ListItem>
              </Link>
              <ListSubheader>Pages</ListSubheader>
              <Link className={classes.link} to={`${url}/account`}>
                <ListItem button>
                  <ListItemIcon>
                    <UserIcon />
                  </ListItemIcon>
                  <ListItemText primary={'Account'} />
                </ListItem>
              </Link>
              <Link className={classes.link} to={`/pricing`}>
                <ListItem button>
                  <ListItemIcon>
                    <DollarSignIcon />
                  </ListItemIcon>
                  <ListItemText primary={'Pricing'} />
                </ListItem>
              </Link>
              <a className={classes.link} href={'/'}>
                <ListItem button onClick={handleLogout}>
                  <ListItemIcon>
                    <LogOutIcon />
                  </ListItemIcon>
                  <ListItemText primary={'logout'} />
                </ListItem>
              </a>
            </List>

Listing 14-7Adding the Account and Pricing Menus in the dashboard-sidebar-navigation

刷新浏览器,你会在侧边栏看到两个新的附加菜单。该帐户现在是空的,但是价格已经有了一些样式,这是由 Material-UI 组件提供的。见图 14-3 。

img/506956_1_En_14_Fig3_HTML.jpg

图 14-3

更新了侧边栏菜单

点击定价可以看到如图 14-4 所示的相同界面。

img/506956_1_En_14_Fig4_HTML.jpg

图 14-4

定价页面的屏幕截图

如果你点击了账户,你会看到这只是最低限度。所以让我们在这里做点什么。但在此之前,我们需要创建一个“是”验证。这是一个广泛的概要文件验证,所以最好把它写在一个单独的文件中。

创建 Yup 配置文件验证

新建一个文件:特色简介没错简介.验证. ts

让我们添加 Yup 配置文件验证,如清单 14-8 所示。

import * as Yup from 'yup';

const profileYupObject = Yup.object().shape({
  canHire: Yup.bool(),
  city: Yup.string().max(255),
  country: Yup.string().max(255),
  email: Yup.string()
    .email('Must be a valid email')
    .max(255)
    .required('Email is required'),
  isPublic: Yup.bool(),
  name: Yup.string().max(255).required('Name is required'),
  phone: Yup.string(),
  state: Yup.string(),
});

export { profileYupObject };

Listing 14-8Adding the Yup Profile Validation

之后,我们现在可以开始构建AccountView页面。

创建帐户视图页面

让我们在AccountView .下创建一个新文件夹,将新文件夹命名为 General,并在该文件夹下创建一个名为GeneralSettings.tsx:的新文件

account ➤ AccountView ➤ General ➤ GeneralSettings.tsx

让我们导入我们需要的命名组件,如清单 14-9 所示。

import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import clsx from 'clsx';
import { Formik } from 'formik';
import { useSnackbar } from 'notistack';
import Autocomplete from '@material-ui/lab/Autocomplete';
import {
  Box,
  Button,
  Card,
  CardContent,
  CardHeader,
  Divider,
  FormHelperText,
  Grid,
  Switch,
  TextField,
  Typography,
  makeStyles,
} from '@material-ui/core';

import { UserType } from 'models/user-type';
import { putProfileAction } from 'features/profile/profileAsyncActions';
import { profileYupObject } from 'features/profile/yup/profile.validation';

Listing 14-9Importing the Named Components of the GeneralSettings

这里有什么新鲜事?我们从 Material-UI 实验室得到了Autocomplete。对于我们的自动完成,我们将使用Country select组件。

除了其他常见的 Material-UI 组件,我们还导入了UserType, putProfileAction,profileYupObject.

接下来,我们将创建GeneralSettings的形状和我们将要返回的本地州,如清单 14-10 所示。

type Props = {
  className?: string;
  user: UserType;
};

const GeneralSettings = ({ className, user, ...rest }: Props) => {
  const dispatch = useDispatch();
  const classes = useStyles();
  const [error, setError] = useState('');
  const { enqueueSnackbar } = useSnackbar();

Listing 14-10Creating the Shape and Local States of the GeneralSettings

然后,让我们在 return 语句中使用 Formik,如清单 14-11 所示。

return (
    <Formik
      enableReinitialize
      initialValues={user}
      validationSchema={profileYupObject}
      onSubmit={async (values, formikHelpers) => {
        try {
          dispatch(putProfileAction(values));

          formikHelpers.setStatus({ success: true });
          formikHelpers.setSubmitting(false);
          enqueueSnackbar('Profile updated', {
            variant: 'success',
          });
        } catch (err) {
          setError(err);

          formikHelpers.setStatus({ success: false });
          formikHelpers.setSubmitting(false);
        }
      }}
    >
      {({
        errors,
        handleBlur,
        handleChange,
        handleSubmit,
        isSubmitting,
        touched,
        values,
        setFieldValue,
      }) => (

Listing 14-11Creating the Formik Props

在 Formik 中,我们使用enableReinitialize,它允许我们更新或编辑表单。当我们有一个现有的对象或数据,并且想用 Formik 编辑它时,我们需要使用 Formik prop。我们有这种双向数据绑定。

initialValues:我们正在传递来自GeneralSettings.的父组件的用户

validationSchema:我们正在传递 profileYupObject。

onSubmit:我们正在调度putProfileAction并传递用户输入的值。

现在让我们使用 Formik 属性来构建我们需要的不同的文本字段。

我们将为 Country 创建一个 TextField,我们将在其中集成自动完成功能。参见清单 14-12 。

<form onSubmit={handleSubmit}>
          <Card className={clsx(classes.root, className)} {...rest}>
            <CardHeader title="Profile" />
            <Divider />
            <CardContent>
              <Grid container spacing={4}>
                <Grid item md={6} xs={12}>
                  <TextField
           error={Boolean(touched.name && errors.name)}
                    fullWidth
            helperText={touched.name && errors.name}
                    label="Name"
                    name="name"
                    onBlur={handleBlur}
                    onChange={handleChange}
                    value={values?.name}
                    variant="outlined"
                  />
                </Grid>
                <Grid item md={6} xs={12}>
                  <TextField
          error={Boolean(touched.email && errors.email)}
                    fullWidth
                    helperText={
                      touched.email && errors.email
                        ? errors.email
               : 'We will use this email to contact you'
                    }
                    label="Email Address"
                    name="email"
                    onBlur={handleBlur}
                    onChange={handleChange}
                    required
                    type="email"
                    value={values?.email}
                    variant="outlined"
                  />
                </Grid>

                <Grid item md={6} xs={12}>
                  <TextField

         error={Boolean(touched.phone && errors.phone)}
                    fullWidth
            helperText={touched.phone && errors.phone}
                    label="Phone Number"
                    name="phone"
                    onBlur={handleBlur}
                    onChange={handleChange}
                    value={values?.phone}
                    variant="outlined"
                  />
                </Grid>
                <Grid item md={6} xs={12}>
                  <Autocomplete
                    id="country"
                    options={countries}
                    value={values?.country}
          getOptionLabel={option => option.toString()}
            renderOption={option => <>{option.text}</>}
                    onChange={(e: any) => {
         setFieldValue('country', e.target.innerText);
                    }}
                    renderInput={params => (
                      <TextField
                        {...params}
                        value={values?.country}
                        fullWidth
                        label="Country"
                        name="country"
                        onChange={handleChange}
                        variant="outlined"
                        inputProps={{
                          ...params.inputProps,
                          autoComplete: 'country',
                        }}
                      />
                    )}
                  />
                </Grid>

                <Grid item md={6} xs={12}>
                  <TextField
        error={Boolean(touched.state && errors.state)}
                    fullWidth
              helperText={touched.state && errors.state}
                    label="State/Region"
                    name="state"
                    onBlur={handleBlur}
                    onChange={handleChange}
                    value={values?.state}
                    variant="outlined"
                  />
                </Grid>
                <Grid item md={6} xs={12}>
                  <TextField

            error={Boolean(touched.city && errors.city)}
                    fullWidth
              helperText={touched.city && errors.city}
                    label="City"
                    name="city"
                    onBlur={handleBlur}
                    onChange={handleChange}
                    value={values?.city}
                    variant="outlined"
                  />
                </Grid>

                <Grid item md={6} xs={12}>
          <Typography variant="h6" color="textPrimary">
                    Make Contact Info Public
                  </Typography>
      <Typography variant="body2" color="textSecondary">Means that anyone viewing your profile will be able to see your contacts details
                  </Typography>
                  <Switch
                    checked={values?.isPublic}
                    edge="start"
                    name="isPublic"
                    onChange={handleChange}
                  />
                </Grid>
                <Grid item md={6} xs={12}>
          <Typography variant="h6" color="textPrimary">
                    Available to hire
                  </Typography>
        <Typography variant="body2" color="textSecondary">
 Toggling this will let your teammates know that you are
           available for acquiring new projects
                  </Typography>
                  <Switch
                    checked={values?.canHire}
                    edge="start"
                    name="canHire"
                    onChange={handleChange}
                  />
                </Grid>
              </Grid>

              {error && (
                <Box mt={3}>
         <FormHelperText error>{error}</FormHelperText>
                </Box>
              )}
            </CardContent>
            <Divider />
    <Box p={2} display="flex" justifyContent="flex-end">
              <Button
                color="secondary"
                disabled={isSubmitting}
                type="submit"
                variant="contained"
              >
                Save Changes
              </Button>
            </Box>
          </Card>
        </form>
      )}
    </Formik>
  );
};

const useStyles = makeStyles(() => ({
  root: {},
}));

export default GeneralSettings;

Listing 14-12Using the Formik Props in the GeneralSettings

文本字段绑定到属性名称,例如,姓名、电子邮件、电话、状态、州、地区、城市等。

自动完成功能与状态绑定在一起。我们将状态包装在自动完成中,我们从 Material-UI 的自动完成中得到这个 API,包括图 14-5 中的状态列表。

之后,我们将在所有状态列表的下方进行硬编码。以下只是它的截图。您可以通过此链接复制粘贴完整的列表:

https://github.com/webmasterdevlin/practical-enterprise-react/blob/master/chapter-12/src/app/views/dashboard/account/AccountView/General/GeneralSettings.tsx

img/506956_1_En_14_Fig5_HTML.jpg

图 14-5

在常规设置中添加自动完成状态选择

创建个人资料详细信息

不过,在通用文件夹下,我们将创建一个名为ProfileDetails .tsx.的新文件。这个新组件只是一个用户头像。

我们需要导入的命名组件如清单 14-13 所示。

import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import clsx from 'clsx';
import {
  Avatar,
  Box,
  Button,
  Card,
  CardActions,
  CardContent,
  Link,
  Typography,
  makeStyles,
} from '@material-ui/core';

import { UserType } from 'models/user-type';

Listing 14-13Adding the Named Components in the ProfileDetails

然后添加 ProfileDetails React 函数组件,如清单 14-14 所示。

type Props = {
  className?: string;
  user: UserType;
};

const ProfileDetails = ({ className, user, ...rest }: Props) => {
  const classes = useStyles();

  return (
    <Card className={clsx(classes.root, className)} {...rest}>
      <CardContent>
        <Box
          display="flex"
          alignItems="center"
          flexDirection="column"
          textAlign="center"
        >
          <Avatar className={classes.avatar} src={user?.avatar} />
          <Typography
            className={classes?.name}
            color="textPrimary"
            gutterBottom
            variant="h4"
          >
            {user?.name}
          </Typography>

          <Typography color="textPrimary" variant="body1">
            Your tier:{' '}
            <Link component={RouterLink} to="/pricing">
              {user?.tier}
            </Link>
          </Typography>
        </Box>
      </CardContent>
      <CardActions>
        <Button fullWidth variant="text">
          Remove picture
        </Button>
      </CardActions>
    </Card>
  );
};

const useStyles = makeStyles(theme => ({
  root: {},
  name: {
    marginTop: theme.spacing(1),
  },
  avatar: {
    height: 100,
    width: 100,
  },
}));

export default ProfileDetails;

Listing 14-14Adding the ProfileDetails Function Component

用户对象来自配置文件详细信息的父组件。调用用户来渲染avatarname,tier。我们也有 移除图片, 但是我们不打算在这里放任何功能。只是为了美观。

创建常规设置

现在我们继续在通用文件夹下创建一个index.tsx:

account ➤ AccountView ➤ General ➤ index.tsx

让我们首先添加命名的组件,如清单 14-15 所示。

import React from 'react';
import clsx from 'clsx';
import { useSelector } from 'react-redux';
import { Grid, makeStyles } from '@material-ui/core';

import ProfileDetails from './ProfileDetails';
import GeneralSettings from './GeneralSettings';
import { RootState } from 'store/reducers';

Listing 14-15Adding the Named Components in the index.tsx of the General Settings

然后,让我们创建组件的形状和useSelector来访问商店状态的一部分,如清单 14-16 所示。

type Props = {
  className?: string;
};

const General = ({ className, ...rest }: Props) => {
  const classes = useStyles();
  const { profile } = useSelector((state: RootState) => state.profile);

Listing 14-16Creating the Shape and useSelector in the index.tsx of the General Settings

在返回语句中,我们传递ProfileDetailsGeneral Settings' user props中的概要状态,如清单 14-17 所示。

return (
    <Grid
      className={clsx(classes.root, className)}
      container
      spacing={3}
      {...rest}
    >
      <Grid item lg={4} md={6} xl={3} xs={12}>
        <ProfileDetails user={profile} />
      </Grid>
      <Grid item lg={8} md={6} xl={9} xs={12}>
        <GeneralSettings user={profile} />
      </Grid>
    </Grid>
  );
};

const useStyles = makeStyles(() => ({
  root: {},
}));

export default General;

Listing 14-17Using the Profile to Pass to ProfileDetails and General Settings

创建标题

之后,我们需要回到AccountView来添加更多的组件:

account ➤ AccountView ➤ Header.tsx

让我们添加命名的组件,如清单 14-18 所示。

import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import clsx from 'clsx';
import {
  Typography,
  Breadcrumbs,
  Link,
  makeStyles,
  Box,
} from '@material-ui/core';
import NavigateNextIcon from '@material-ui/icons/NavigateNext';

Listing 14-18Adding the Named Components of Header.tsx in AccountView

这里有什么新鲜事吗?我们有来自 Material-UI 核心的 Breadcrumbs 和来自 Material-UI 图标的 NavigateNextIcon。

Breadcrumbs :让用户能够从一系列值中进行选择。

然后,让我们为 Header 组件添加 return 语句,如清单 14-19 所示。

type Props = {
  className?: string;
};

const Header = ({ className, ...rest }: Props) => {
  const classes = useStyles();

  return (
    <div className={clsx(classes.root, className)} {...rest}>
      <Breadcrumbs
        separator={<NavigateNextIcon fontSize="small" />}
        aria-label="breadcrumb"
      >
        <Link color="inherit" to="/app" component={RouterLink}>
          Dashboard
        </Link>
        <Box>
          <Typography variant="body1" color="inherit">
            Account
          </Typography>
        </Box>
      </Breadcrumbs>
      <Typography variant="h4" color="textPrimary">
        Settings
      </Typography>
    </div>
  );
};

const useStyles = makeStyles(() => ({
  root: {},
}));

export default Header;

Listing 14-19Creating the Header Component

摘要

在本章中,我们成功地同步了侧边栏导航和导航栏之间的数据,因为数据来自全局存储或 Redux 存储。

我们还更新了仪表板、帐户和定价菜单,并使用 Formik 和 Yup 验证模式来构建它们。

接下来,我们将继续构建这个由三部分组成的章节系列,并添加另外三个组件,通知、安全性和订阅页面,以完成我们的应用的 UI。

十五、创建通知、安全性和订阅页面

本系列的最后一部分将使用 Redux、Formik 和 Yup 验证模式制作通知、安全性和订阅页面。

总的来说,我们的目标是用现实世界中的基本功能完成 React 应用的 UI,并巩固我们对 Redux 如何工作以及如何在我们的应用中使用 Formik 和 Yup 验证模式的了解。

创建通知页面

我们将首先处理通知页面。这个页面只是为了应用的设计或整体 UI 外观。

在 AccountView 下,我们需要创建一个名为 Notifications.tsx 的文件:

account ➤ AccountView ➤ Notifications.tsx

下面是命名的导入组件,如清单 15-1 所示。

import React from 'react';
import clsx from 'clsx';
import {
  Box,
  Button,
  Card,
  CardContent,
  CardHeader,
  Checkbox,
  Divider,
  FormControlLabel,
  Grid,
  Typography,
  makeStyles,
} from '@material-ui/core';

Listing 15-1Adding the Named Components of the Notifications.tsx

以及通知组件的形状,如清单 15-2 所示。

type Props = {
  className?: string;
};

const Notifications = ({ className, ...rest }: Props) => {
  const classes = useStyles();

Listing 15-2Adding the Notifications Function Component

以及返回元素的 return 语句,如清单 15-3 所示。

return (
    <form>
      <Card className={clsx(classes.root, className)} {...rest}>
        <CardHeader title="Notifications" />
        <Divider />
        <CardContent>
          <Grid container spacing={6} wrap="wrap">
            <Grid item md={4} sm={6} xs={12}>
<Typography gutterBottom variant="h6" color="textPrimary">
                System
              </Typography>
              <Typography gutterBottom variant="body2" color="textSecondary">
                You will receive emails in your business email address
              </Typography>
              <div>
                <FormControlLabel
                  control={<Checkbox defaultChecked />}
                  label="Email alerts"
                />
              </div>
              <div>
                <FormControlLabel
                  control={<Checkbox />}
                  label="Push Notifications"
                />
              </div>

              <div>
                <FormControlLabel
                  control={<Checkbox defaultChecked />}
                  label="Text message"
                />
              </div>
              <div>
                <FormControlLabel
                  control={<Checkbox defaultChecked />}
                  label={
                     <>

                      <Typography variant="body1" color="textPrimary">
                        Phone calls
                      </Typography>
                      <Typography variant="caption">
                        Short voice phone updating you
                      </Typography>
                    </>

                  }
                />
              </div>
            </Grid>

            <Grid item md={4} sm={6} xs={12}>
              <Typography gutterBottom variant="h6" color="textPrimary">
                Chat App
              </Typography>
              <Typography gutterBottom variant="body2" color="textSecondary">
  You will receive emails in your business email address
              </Typography>

              <div>
                <FormControlLabel
                  control={<Checkbox defaultChecked />}
                  label="Email"
                />
              </div>
              <div>
                <FormControlLabel
                  control={<Checkbox defaultChecked />}
                  label="Push notifications"
                />
              </div>
            </Grid>
          </Grid>
        </CardContent>
        <Divider />
        <Box p={2} display="flex" justifyContent="flex-end">
 <Button color="secondary" type="submit" variant="contained">
            Save Settings
          </Button>
        </Box>
      </Card>
    </form>
  );
};

const useStyles = makeStyles(() => ({
  root: {},
}));

export default Notifications;

Listing 15-3Creating the Return Elements of the Notifications

我们这里的通知组件只是为了美观。在这一点上,我们不会使用任何额外的功能。一般设置应该有助于我们理解 Redux 的实现流程。

但是,您可以自己更新它,只是为了练习和巩固您的知识。

创建安全性页面

我们需要添加的另一个 React 组件是安全性。同样,在AccountView,下创建一个新文件,并将其命名为Security.tsx.

我们将在这里使用简单的 HTTP 请求,而不使用 Redux 工具包。我们创建这个是为了提醒你,我们不需要在每个 HTTP 请求中使用 Redux,就像我们在第六章中所做的那样。

img/506956_1_En_15_Figa_HTML.jpg

好了,现在让我们为安全性添加命名的组件,如清单 15-4 所示。

import React, { useState } from 'react';
import clsx from 'clsx';
import * as Yup from 'yup';
import { Formik } from 'formik';
import { useSnackbar } from 'notistack';
import { useSelector } from 'react-redux';
import {
  Box,
  Button,
  Card,
  CardContent,
  CardHeader,
  Divider,
  FormHelperText,
  Grid,
  TextField,
  makeStyles,
} from '@material-ui/core';

import { changePasswordAxios, ChangePasswordModel } from 'services/authService';
import { RootState } from 'store/reducers';

Listing 15-4Importing the Named Components in Security

我们使用的是 Yup 和 Formik 以及我们需要的其他常用组件。我们还从authService进口了changePasswordAxiosChangePasswordModel,从减速器进口了RootState。如您所见,没有任何操作会向存储发送请求或更新任何内容。

接下来,我们添加对象的类型并使用一些 React 钩子,如清单 15-5 所示。

type Props = {
  className?: string;
};

type PasswordType = {
  password: string;
  passwordConfirm: string;
};

const Security = ({ className, ...rest }: Props) => {
  const { claims } = useSelector((state: RootState) => state.auth);
  const classes = useStyles();
  const [error, setError] = useState('');
  const { enqueueSnackbar } = useSnackbar();

Listing 15-5Creating the Security Function Component

声明:我们通过 useSelector 从 auth reducer 获取声明。

以及安全组件的返回元素,如清单 15-6 所示。

return (
    <Formik
      initialValues={
        {
          password: '',
          passwordConfirm: '',
        } as PasswordType
      }

       {/*validation schema for the password */}

      validationSchema={Yup.object().shape({
        password: Yup.string()
          .min(7, 'Must be at least 7 characters')
          .max(255)
          .required('Required'),
        passwordConfirm: Yup.string()
          .oneOf([Yup.ref('password'), null], 'Passwords must match')
          .required('Required'),
      })}
      onSubmit={async (values, formikHelpers) => {
        try {

           {/*Checking if the password matches or not */}

          if (values.password !== values.passwordConfirm) {
            alert('Must match');
            return;
          }

          {/* If it matches, return this object with the
            following args to change password */}
          const args: ChangePasswordModel = {
            id: claims.sub,
            email: claims.email,
            password: values.password,
          };

          await changePasswordAxios(args);

          formikHelpers.resetForm();
          formikHelpers.setStatus({ success: true });
          formikHelpers.setSubmitting(false);
          enqueueSnackbar('Password updated', {
            variant: 'success',
          });
        } catch (err) {
          console.error(err);
          formikHelpers.setStatus({ success: false });
          formikHelpers.setSubmitting(false);
        }
      }}
    >
      {formikProps => (
        <form onSubmit={formikProps.handleSubmit}>
          <Card className={clsx(classes.root, className)} {...rest}>
            <CardHeader title="Change Password" />
            <Divider />
            <CardContent>
              <Grid container spacing={3}>
                <Grid item md={4} sm={6} xs={12}>
                  <TextField
                    error={Boolean(
                      formikProps.touched.password &&
                        formikProps.errors.password,
                    )}
                    fullWidth
                    helperText={
                      formikProps.touched.password &&
                      formikProps.errors.password
                    }
                    label="Password"
                    name="password"
                    onBlur={formikProps.handleBlur}
                    onChange={formikProps.handleChange}
                    type="password"
                    value={formikProps.values.password}
                    variant="outlined"
                  />
                </Grid>

                <Grid item md={4} sm={6} xs={12}>
                  <TextField
                    error={Boolean(
                      formikProps.touched.passwordConfirm &&
                        formikProps.errors.passwordConfirm,
                    )}
                    fullWidth
                    helperText={
                      formikProps.touched.passwordConfirm &&
                      formikProps.errors.passwordConfirm
                    }
                    label="Password Confirmation"
                    name="passwordConfirm"
                    onBlur={formikProps.handleBlur}
                    onChange={formikProps.handleChange}
                    type="password"
                    value={formikProps.values.passwordConfirm}
                    variant="outlined"
                  />
                </Grid>
              </Grid>
              {error && (
                <Box mt={3}>
                  <FormHelperText error>{error}</FormHelperText>
                </Box>
              )}
            </CardContent>
            <Divider />

            <Box p={2} display="flex" justifyContent="flex-end">
              <Button
                color="secondary"
                disabled={formikProps.isSubmitting}
                type="submit"
                variant="contained"
              >
                Change Password
              </Button>
            </Box>
          </Card>
        </form>
      )}
    </Formik>
  );
};

const useStyles = makeStyles(() => ({
  root: {},
}));

export default Security;

Listing 15-6Returning the Elements for the Security React Component

创建订阅页面

我们将在本章构建的最后一个组件是Subscription.tsx,,它仍然在AccountView文件夹下。

让我们首先添加命名的组件,如清单 15-7 所示。

import { Link as RouterLink } from 'react-router-dom';
import clsx from 'clsx';
import { useSelector } from 'react-redux';
import {
  Box,
  Button,
  Card,
  CardContent,
  CardHeader,
  Divider,
  Link,
  Paper,
  Typography,
  makeStyles,
} from '@material-ui/core';

import { RootState } from 'store/reducers';

Listing 15-7Adding the Named Components in the Subscription

接下来,让我们做对象的类型或形状,如清单 15-8 所示。

type Props = {
  className?: string;
};

const Subscription = ({ className, ...rest }: Props) => {
  const classes = useStyles();
  const {
    profile: { subscription },
  } = useSelector((state: RootState) => state.profile);

Listing 15-8Adding the Props and Using Hooks for Subscription.tsx

在清单 15-8 中,我们访问概要文件,然后在清单 15-9 中的 UI 中呈现管理您的订阅。

例如,我们可以访问subscription.currency, subscription.price,和subscription.name——因为subscription是剖析的对象。嵌套析构在这里是一种有效的语法。

清单 15-9 是订阅组件的返回语句。

return (
    <Card className={clsx(classes.root, className)} {...rest}>
      <CardHeader title="Manage your subscription" />
      <Divider />
      <CardContent>
        <Paper variant="outlined">
          <Box className={classes.overview}>
            <div>
              <Typography display="inline" variant="h4" color="textPrimary">
                {subscription.currency}
                {subscription.price}
              </Typography>
              <Typography display="inline" variant="subtitle1">
                /mo
              </Typography>
            </div>
            <Box display="flex" alignItems="center">
              <img
                alt="Product"
                className={classes.productImage}
                srcimg/product_premium.svg"
              />
              <Typography variant="overline" color="textSecondary">
                {subscription.name}
              </Typography>
            </Box>
          </Box>
          <Divider />

          <Box className={classes.details}>
            <div>
              <Typography variant="body2" color="textPrimary">
                {`${subscription.proposalsLeft} proposals left`}
              </Typography>
              <Typography variant="body2" color="textPrimary">
                {`${subscription.templatesLeft} templates`}
              </Typography>
            </div>
            <div>
              <Typography variant="body2" color="textPrimary">
                {`${subscription.invitesLeft} invites left`}
              </Typography>
              <Typography variant="body2" color="textPrimary">
                {`${subscription.adsLeft} ads left`}
              </Typography>
            </div>
            <div>
              {subscription.hasAnalytics && (
                <Typography variant="body2" color="textPrimary">
                  Analytics dashboard
                </Typography>
              )}
              {subscription.hasEmailAlerts && (
                <Typography variant="body2" color="textPrimary">
                  Email alerts
                </Typography>
              )}
            </div>
          </Box>
        </Paper>

        <Box mt={2} display="flex" justifyContent="flex-end">
          <Button size="small" color="secondary" variant="contained">
            Upgrade plan
          </Button>
        </Box>
        <Box mt={2}>
          <Typography variant="body2" color="textSecondary">
            The refunds don&apos;t work once you have the subscription, but you
            can always{' '}
            <Link color="secondary" component={RouterLink} to="#">
              Cancel your subscription
            </Link>
            .
          </Typography>
        </Box>
      </CardContent>
    </Card>
  );
};

Listing 15-9Adding the Return Statement of the Subscription Component

最后,这个组件的样式,如清单 15-10 所示。

const useStyles = makeStyles(theme => ({
  root: {},
  overview: {
    padding: theme.spacing(3),
    display: 'flex',
    alignItems: 'center',
    flexWrap: 'wrap',
    justifyContent: 'space-between',
    [theme.breakpoints.down('md')]: {
      flexDirection: 'column-reverse',
      alignItems: 'flex-start',
    },
  },
  productImage: {
    marginRight: theme.spacing(1),
    height: 48,
    width: 48,
  },
  details: {
    padding: theme.spacing(3),
    display: 'flex',
    alignItems: 'center',
    flexWrap: 'wrap',
    justifyContent: 'space-between',
    [theme.breakpoints.down('md')]: {
      flexDirection: 'column',
      alignItems: 'flex-start',
    },
  },
}));

export default Subscription;

Listing 15-10Styling Components for the Subscription.tsx

更新帐户视图

在我们结束本章之前,我们需要再次更新 AccountView。

首先,让我们更新导入命名组件,如清单 15-11 所示。

import React, { useState, ChangeEvent } from 'react';
import {
  Box,
  Container,
  Divider,
  Tab,
  Tabs,
  makeStyles,
} from '@material-ui/core';

import Header from './Header';
import General from './General';
import Subscription from './Subscription';
import Notifications from './Notifications';
import Security from './Security';
import Page from 'app/components/page';

Listing 15-11Import Named Components of AccountView

这里有什么新鲜事?标签页来自物料界面;具体来说,我们将使用简单的选项卡。

Tabs:允许我们在同一层级的相关内容组之间进行组织和导航。

接下来,让我们构建 AccountView React 函数组件,如清单 15-12 所示。

const AccountView = () => {
  const classes = useStyles();

  /*initialize the useState to 'general' - we will use that */

  const [currentTab, setCurrentTab] = useState('general');

 /*handleTabsChange -for setting or updating the value of the current tab */

  const handleTabsChange = (event: ChangeEvent<{}>, value: string): void => {
    setCurrentTab(value);
  };

  return (
    <Page className={classes.root} title="Settings">
      <Container maxWidth="lg">
        <Header />
        <Box mt={3}>
          <Tabs

            {/*handleTabsChange - for the clicking and selection of tabs */}

            onChange={handleTabsChange}
            scrollButtons="auto"
            value={currentTab}
            variant="scrollable"
            textColor="secondary"
          >

           {/*we're going to iterate or loop on the tabs here */}

            {tabs.map(tab => (
              <Tab key={tab.value} label={tab.label} value={tab.value} />
            ))}
          </Tabs>
        </Box>
        <Divider />
        <Box mt={3}>

          {/*current tab by default is the General component.
           The rest is not displayed until clicked or selected */}
          {currentTab === 'general' && <General />}
          {currentTab === 'subscription' && <Subscription />}
          {currentTab === 'notifications' && <Notifications />}
          {currentTab === 'security' && <Security />}
        </Box>
      </Container>
    </Page>
  );
};

const useStyles = makeStyles(theme => ({
  root: {
    minHeight: '100%',
    paddingTop: theme.spacing(3),
    paddingBottom: theme.spacing(3),
  },
}));

/* an array of objects with value. to be used in the
tabs for navigating between components*/
const tabs = [
  { value: 'general', label: 'General' },
  { value: 'subscription', label: 'Subscription' },
  { value: 'notifications', label: 'Notifications' },
  { value: 'security', label: 'Security' },
];

export default AccountView;

Listing 15-12Updating the index.tsx of the AccountView

恢复精神

现在是刷新浏览器的时候了。

在侧边栏导航中,单击 Account,您会看到默认设置页面是 General 页面。

单击其他选项卡,如订阅、通知和安全性。

要了解在我们的应用中使用 Redux 的强大功能,以及我们如何轻松地访问或共享一个组件到另一个组件的状态,请尝试在设置➤常规中编辑一些内容。

比如编辑名字改成 Mok Kuh JR 保存。一旦保存,你会在侧边栏导航中看到即时更新。

img/506956_1_En_15_Fig1_HTML.jpg

图 15-1

更新设置的屏幕截图

这就是 Redux 的强大之处——让整个应用对任何变化都做出 React。

摘要

这就是这个三部分系列的全部内容。在最后一章中,我们完成了应用的 UI,希望我们能够完全理解 Redux 是如何工作的,以及如何在必要的时候在应用中使用它。希望您也加深了对 Formik 和 Yup 验证模式的了解和理解,以便在应用中构建表单。

此外,请记住,Redux 是好的,但您不需要在应用中的每个地方都使用它,因为它仍然会增加复杂性。对于简单的 CRUD 应用或者不需要在其他组件中重用状态,就不需要使用 Redux。

但是,如果您打算构建一个大规模的应用或企业级应用,我建议从一开始就在您的应用中设置 Redux。

设置可能只需要一两个小时,这样做的好处是,只要你需要,它就可以随时使用。然后,您可以从任何组件访问 reducer 中的状态。

在下一章,我们将看到如何让我们的 React 应用对移动设备友好。

十六、让应用对移动设备友好

本章将在 2021 年第一季度把我们的 React 应用更新到最新的当前版本(在撰写本文时),然后使我们的应用对移动设备友好。这将确保我们的应用也能在移动设备或平板电脑等小屏幕上运行良好。

手机友好型应用是响应式设计或适应性设计应用的另一个术语。简而言之,显示页面会根据设备屏幕的大小自动调整。

入门指南

让我们转到 package.json,为了使用我们正在使用的每个库可用的最新版本,我们需要使用一个名为 Version Lens 的 VS 代码扩展。我们在前一章中安装了它,但是如果您还没有安装,现在可以随意安装。

img/506956_1_En_16_Fig1_HTML.jpg

图 16-1

使用版本镜头

那么如何查看我们正在使用的每个库的最新版本呢?打开 package.json,点击右上角的 V 图标,如图 16-2 所示。

img/506956_1_En_16_Fig2_HTML.jpg

图 16-2

版本镜头库

安全更新包库

我们将对 React 和我们正在使用的库进行安全升级。要升级,只需点击向上箭头符号,如图 16-3 所示。

img/506956_1_En_16_Fig3_HTML.jpg

图 16-3

库的安全更新

date-io/moment 的最新版本是 2.10.8,是我们正在使用的 1.3.13 版本的主要版本更新。我们不打算更新这个,因为主要版本意味着有潜在的突破性变化。 1.3.13 是与我们这里使用的日历组件兼容的版本。

  • 升级到主要版本时,请小心不要破坏更改。小版本和补丁升级通常没问题。

使用 Version Lens 升级你的应用中的所有次要版本和补丁,或者如果你想确定,首先将你的版本与我们在撰写本文时拥有的版本进行比较。您可以在我的 GitHub 中查看 package.json:

https://github.com/webmasterdevlin/practical-enterprise-react/blob/master/chapter-13/package.json

我们更新了这个 app 中的次要版本和补丁,包括以下主要版本,如清单 16-1 所示。

//major versions that were updated

@types/react
@types/react-dom
concurrently
prettier
react
react-dom
react-test-renderer
sanitize.css
ts-node
typescript

Listing 16-1Updated Major Versions

好的,完成之后,我们需要删除 package-lock.jsonNode 模块。然后做

npm install
npm start:fullstack

如果您在运行 npm start:fullstack 后遇到问题或错误,请检查您的 npm 版本。在撰写本文时,我们使用的是 NPM 版本 6,因为与版本 7 存在兼容性问题。

说到版本,React 17 中一个比较值得注意的变化是,在创建组件时,不需要显式地从' react' 键入 import React。试着删除其中一个组件,看看它是否还能工作。不过,目前我不建议删除它,因为一些 React 开发人员可能不熟悉这一变化。我只是提一下,这样如果您看到带有显式编写的 import React 语句的组件,就不会感到困惑。

此外,我们在 VS Code 或 WebStorm 中使用的代码片段仍然自动包含 import React 语句。然而,我们需要升级到 React 17,为 React 中即将到来的功能做准备。

一旦你检查你的应用仍然工作,我们现在可以开始使我们的应用移动友好。

更新主页

让我们从主页组件开始,让它具有响应性。我们将需要来自 Material-UI 核心的样式组件以及我们创建的页面模板,如清单 16-2 所示。

清单 16-2 制作移动友好的主页

import React from 'react';

import { Box, Container, Typography, useMediaQuery } from '@material-ui/core';

import Page from 'app/components/page';

const Home = () => {

const mobileDevice = useMediaQuery('(max-width:650px)');

return (

<Page title="Home">

<Container>

<Box

height={mobileDevice ? '50vh' : '100vh'}

display={'flex'}

flexDirection={'column'}

justifyContent={'center'}

alignItems={'center'}

>

<Typography variant={mobileDevice ? 'h4' : 'h1'}>

Welcome to Online Shop img/506956_1_En_16_Figa_HTML.gif

</Typography>

</Box>

</Container>

</Page>

);

};

export default Home;

这里有什么新鲜事?

useMediaQuery:React 的一个 CSS 媒体查询钩子。它将检测浏览器是否很小,如手机应用或平板电脑浏览器。我们将最大宽度设置为 650 像素,如果低于这个值,我们就将其设置为移动设备。

我们在返回元素中有来自 Material-UI 的页面模板、容器和盒子。

t:我们将高度设置如下:如果是移动设备,那么将高度设置为 50 vh(视图高度)或浏览器大小的 50 %;不然身高 100 vh。

排版:如果检测到移动设备,大小为 H4;否则,将其设置为 h1。

我们在这里也使用了一个购物袋表情符号。要获得这种表情符号并直接将其复制粘贴到您的代码中,请访问这个网站 emojipedia.org 。搜索“购物袋”,并将该表情符号复制并粘贴到您的代码中。

img/506956_1_En_16_Fig4_HTML.jpg

图 16-4

Emojis 来自血友病. org

刷新浏览器并拖动窗口使其变小。如果你在 Windows 上使用 Mac 或 Android Studio,你也可以从模拟器中检查它。

img/506956_1_En_16_Fig5_HTML.jpg

图 16-5

使用 MediaQuery 的主页的手机屏幕截图

更新“关于”页面

接下来,让我们更新 About 页面组件。我们将在这里做几乎相同的事情,如清单 16-3 所示。

清单 16-3 制作移动友好的关于页面

import React from 'react';

import { Box, Container, Typography, useMediaQuery } from '@material-ui/core';

import Page from 'app/components/page';

const AboutPage = () => {

const mobileDevice = useMediaQuery('(max-width:650px)');

return (

<Page title="About">

<Container>

<Box

height={mobileDevice ? '50vh' : '100vh'}

display={'flex'}

flexDirection={'column'}

justifyContent={'center'}

alignItems={'center'}

>

<Typography variant={mobileDevice ? 'h4' : 'h1'}>

About us img/506956_1_En_16_Figb_HTML.gif

</Typography>

</Box>

</Container>

</Page>

);

};

export default AboutPage;

除了表情符号之外,它实际上与主页相同,如图 16-6 所示。

img/506956_1_En_16_Fig6_HTML.jpg

图 16-6

使用 MediaQuery 的“关于”页面的手机屏幕截图

更新未找到的页面

我们将更新的下一个页面是“未找到”页面。同样,除了使用的表情符号,我们也在做同样的事情,如清单 16-4 所示。

import React from 'react';
import { Box, Container, Typography, useMediaQuery } from '@material-ui/core';
import Page from 'app/components/page';

const NotFoundPage = () => {
  const mobileDevice = useMediaQuery('(max-width:650px)');

  return (
    <Page title="Not Found Page">
      <Container>
        <Box
          height={mobileDevice ? '50vh' : '100vh'}
          display={'flex'}
          flexDirection={'column'}
          justifyContent={'center'}
          alignItems={'center'}
        >
          <Typography variant={mobileDevice ? 'h4' : 'h1'}>
            404 Page Not Found ☹
          </Typography>
        </Box>
      </Container>
    </Page>
  );
};

export default NotFoundPage;

Listing 16-4Updating the NotFoundPage Using Media Query

要测试它,只需进入一个不存在的localhost页面,例如“localhost:3000/not-found”,如图 16-7 所示。

img/506956_1_En_16_Fig7_HTML.jpg

图 16-7

使未找到的页面移动友好

使导航条对移动设备友好

我们将需要使仪表板侧边栏导航移动友好。在图 16-8 中,你会注意到侧边栏导航占据了超过 50%的屏幕大小。

img/506956_1_En_16_Fig8_HTML.jpg

图 16-8

使用媒体查询前用户界面的手机截图

让我们转到导航栏. tsx,我们将合并 useMediaQuery,如清单 16-5 所示。

//add the useMediaQuery
import { AppBar, Toolbar, Button, useMediaQuery, colors, } from '@material-ui/core';

...
//add the Media Query hooks
const mobileDevice = useMediaQuery('(max-width:650px)')

//return elements. we will hide the logo if its not a mobile device
return (
    <div className={classes.root}>
      <AppBar position="static">
        <Toolbar>
          <Link className={`${classes.link} ${classes.title}`} to={'/'}>
            {!mobileDevice && 'LOGO'}
          </Link

Listing 16-5Importing useMediaQuery in dashboard-sidebar-navigation

但是我们还没有完成。如果你在你的模拟器或移动设备上看这个应用,只有微小的变化。仪表板导航仍然没有响应,如图 16-9 所示,该图显示了移动设备中侧边栏的当前状态。

img/506956_1_En_16_Fig9_HTML.jpg

图 16-9

使用媒体查询的 UI 的移动屏幕截图

有几种方法可以让导航条有 React。首先,在移动设备上移除整个侧边栏导航,并在右上角创建一个下拉菜单列表或汉堡菜单,如图 16-10 所示。

img/506956_1_En_16_Fig10_HTML.jpg

图 16-10

下拉菜单列表

另一种方法是缩小仪表板侧边栏导航。我们将做第二个选择。

使仪表板侧边栏导航对移动设备友好

进入 dashboard-sidebar-navigation . tsx,从 Material-UI Core 添加 useMediaQuery,如清单 16-6 所示。

import {
  Collapse,
  Divider,
  Drawer,
  List,
  ListItem,
  ListItemIcon,
  ListItemText,
  ListSubheader,
  Avatar,
  Box,
  Typography,
  useMediaQuery,
} from '@material-ui/core';

...
//add the Media Query hooks

const mobileDevice = useMediaQuery('(max-width:650px)');

//add the styling components in the useStyles component
// mobile style
    drawerClose: {
      transition: theme.transitions.create('width', {
        easing: theme.transitions.easing.sharp,
        duration: theme.transitions.duration.leavingScreen,
      }),
      overflowX: 'hidden',
      width: theme.spacing(7) + 1,
      [theme.breakpoints.up('sm')]: {
        width: theme.spacing(9) + 1,
      },
    },

Listing 16-6Adding the Media Query Hooks and Style Components in Dashboard Sidebar Navigation

接下来,我们将更新 className 抽屉和 classes paper。同样,导入命名组件 clsx,如清单 16-7 所示。

...
import clsx from 'clsx';
...
<Drawer
          className={clsx(classes.drawer, mobileDevice && classes.drawerClose)}
          variant="permanent"
          classes={{
            paper: clsx(
              classes.drawerPaper,
              mobileDevice && classes.drawerClose,
            ),
          }}
          anchor="left"

Listing 16-7Updating the Drawer Elements

然后,我们将更改配置文件。姓名。如果头像在手机屏幕上,我们不会显示。我们为移动或更小的屏幕创建了另一个抽屉容器,如清单 16-8 所示。

{profile.name && !mobileDevice && (
            <Box p={2}>...

          )}
          <Divider />

          {/*drawer container for the mobile screen */}

          {mobileDevice ? (
           <div className={classes.drawerContainer}>
              <List>
             <Link className={classes.link} to={`${url}`}>
                  <ListItem button>
                    <ListItemIcon>
                      <PieChartIcon />
                    </ListItemIcon>
                  </ListItem>
                </Link>
                <Divider />
                <ListItem button onClick={handleClick}>
                  <ListItemIcon>
                    <ShoppingCartIcon />
                  </ListItemIcon>
                  {open ? <ChevronUpIcon /> : <ChevronDownIcon />}
                </ListItem>
                <Divider />
                <Collapse in={open} timeout="auto" unmountOnExit>
                  <List component="div" disablePadding>
                    <Link className={classes.link} to={`${url}/list-products`}>
                      <ListItem button className={classes.nested}>
                        <ListItemIcon>
                          <ListIcon />
                        </ListItemIcon>
                      </ListItem>
                    </Link>

                    <Link className={classes.link} to={`${url}/create-product`}>
                      <ListItem button className={classes.nested}>
                        <ListItemIcon>
                          <FilePlusIcon />
                        </ListItemIcon>
                      </ListItem>
                    </Link>
                  </List>
                </Collapse>
                <Divider />
                <Link className={classes.link} to={`${url}/calendar`}>
                  <ListItem button>
                    <ListItemIcon>
                      <CalendarIcon />
                    </ListItemIcon>
                  </ListItem>
                </Link>
                <Divider />
                <Link className={classes.link} to={`${url}/account`}>
                  <ListItem button>
                    <ListItemIcon>
                      <UserIcon />
                    </ListItemIcon>
                  </ListItem>
                </Link>
                <Divider />
                <Link className={classes.link} to={`/pricing`}>
                  <ListItem button>
                    <ListItemIcon>
                      <DollarSignIcon />
                    </ListItemIcon>
                  </ListItem>
                </Link>
                <Divider />

                <a className={classes.link} href={'/'}>
                  <ListItem button onClick={handleLogout}>
                    <ListItemIcon>
                      <LogOutIcon />
                    </ListItemIcon>
                  </ListItem>
                </a>
              </List>
              <Divider />
          ) : (

       {/*drawer container for the web browser */}

     <div className={classes.drawerContainer}>
              <List>
                ...
              </List>
            </div>
          )}
        </Drawer>
      </div>
    </>
  );
};

Listing 16-8Updating the Avatar Elements

现在我们有两个抽屉容器,一个用于移动屏幕,另一个用于网络浏览器。

我们将返回元素包装在 if-else 语句中。如果不是移动设备,就展示头像。否则,不要表现出来。

使仪表板布局移动友好

之后,我们还需要使用仪表板布局的 index.tsx 中的媒体查询挂钩。我们还需要导入 clsx。

最后,在 useStyles 中添加另一个样式属性,如清单 16-9 所示。

//import the useMediaQuery

import { Grid, useMediaQuery} from '@material-ui/core';
import clsx from 'clsx';
...

//add the Media Query hooks

const mobileDevice = useMediaQuery('(max-width:650px)')

//update the className
<DashboardSidebarNavigation />{' '}
      <div className={classes.wrapper}>
        <div className={classes.contentContainer}>
          <div
            className={clsx(classes.content, mobileDevice && classes.leftSpace)}
          >
            {children}
          </div>
        </div>
      </div>

//add a new style element

leftSpace: {
    paddingLeft: '3rem',
  },

Listing 16-9Making the Dashboard Layout Mobile-Friendly

现在重新检查你的手机屏幕。现在应该是手机友好了,如图 16-11 所示。

img/506956_1_En_16_Fig11_HTML.jpg

图 16-11

移动友好仪表板

FOR YOUR ACTIVITY

移动屏幕现在是移动友好的。然而,仍然有改进的余地。为了你的活动

  1. 使用 useMediaQuery 挂钩,在移动设备上查看时,可以调整导航栏和仪表板页面内容之间的足够空间。

  2. Figure 16-12 is a screenshot of the Dashboard (L) and the Dashboard Calendar (R).

    img/506956_1_En_16_Fig12_HTML.jpg

    图 16-12

    仪表板(L)和仪表板日历(R)的屏幕截图

摘要

总而言之,我们在 2021 年第一季度将我们的 React 应用更新到了最新的当前版本(在撰写本文时)。然后,在媒体查询挂钩的帮助下,我们使我们的应用对移动设备友好。

在下一章,我们将讨论 React 组件的各种流行的样式方法。

十七、React 组件的样式方法

在前一章中,我们借助媒体查询钩子使我们的应用对移动设备友好。现在我们的应用几乎可以部署了。然而,在我们开始之前,我认为我们应该简单地关注一下 React 组件的其他样式方法。

在整个应用中,我们使用 Material-UI 库作为我们的样式首选项。但是还有其他几种流行的方法。我们不会深入探讨每一种造型方法,而是告诉你你的其他选择。

我们可以用多种方式来设计 React 组件的样式。对于我们中的许多人来说,选择哪一个取决于不同的因素,例如我们当前项目的架构或设计目标、特定的用例,当然还有个人偏好。

例如,在某些情况下,当您只需要在特定文件中添加一些样式属性时,内联样式可能是最好的选择。如果您发现自己在同一个文件中重用了一些样式属性,那么样式化组件是完美的。对于其他复杂的应用,您可以查看 CSS 模块,甚至是常规的 CSS。

内嵌样式

开发人员通常使用内联样式来构建组件原型或测试组件的 CSS 样式。这种样式也是一种强力处理元素的方式,可以查看我们编写和删除的任何 CSS 内联样式的结果。内联样式可能是我们可以使用的最直接的样式方法,尽管不推荐用于大规模应用。

需要记住的一点是,在 React 中,内联样式被指定为对象,而不是字符串。键值是 CSS 属性名,应该用 camelCase 编写,样式的值通常是字符串。

所以让我们试试。

在 components 文件夹中,我们创建一个新文件,并将其命名为InlineStyle.tsx.

import React from "react";

const heading = {
  color: "orange",
  fontSize: "50px",
};

function InlineStyle() {
  return (
    <div>
      {/* style attribute to equal to the object it is calling */}
      <h1 style={heading}> Inline Style</h1>
    </div>
  );
}

export default InlineStyle;

Listing 17-1InlineStyle.tsx

普通 CSS

这只是你标准的普通 CSS。简单易用。没有依赖性,并具有本机浏览器支持。然而,这通常不用于 React 项目,尤其是大型项目。

让我们用这种样式方法做一个按钮的例子。我们正在创建一个具有以下属性的按钮类。我们还为 hover 类添加了背景色。

创建一个文件并将其命名为 Plain.css,如清单 17-2 所示。

.button {
    align-items: center;
    display: inline-flex;
    justify-content: center;
    padding: 6px 16px;
    border-radius: 3px;
    font-weight: 50;
    background: rgb(43, 128, 77);
    color: rgb(241, 240, 240);
    border: 1px solid rgb(249, 200, 200);
    box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 2px;
    width: auto;
    margin-top: 500px;
    margin-bottom: 1px;
    cursor: pointer;;
}
.button:hover {
    background-color: #d4bd54;
}

Listing 17-2Plain CSS Styling

出于美观的目的,我添加了一些样式属性,但主要目的是创建 CSS 文件。我们有使用类的命名元素。这允许我们重用组件中的元素。

然后让我们使用这个按钮样式。我们只是导入了 CSS 文件,然后使用了按钮类。由于这是 React,我们需要使用 className,因为单词 class 是 JavaScript 中的保留关键字。

import React from "react";
import "./Plain.css";

const Button = () => {
  return (
     <>

      <Container>
        <button className="button"> Log in </button>
      </Container>
    </>

  );
};

Listing 17-3Plain CSS Styling in your React component

全球 CSS

本质上,它们和普通的 CSS 写的一样。主要区别在于,全局 CSS 非常适合使用共享布局组件,如标题组件、导航栏、仪表板和其他共享站点。

我们还可以在我们的根索引目录中创建全局 CSS,如下例所示。我们制作了一个 index.css 并将其导入到我们的根索引文件中。

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',  'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
  'Helvetica Neue', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

Listing 17-4Global CSS in index.css

然后将其导入 index.tsx 以供全局使用。

.GlobalButton-root {
    background-color: #792b78;
    box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
    padding: 7px 14px;
}
.GlobalButton-root:hover {
    background-color: #d49254;
}
.GlobalButton-label {
    color: #fff;
}

Listing 17-5Global CSS in index.tsx

在这种情况下,我们在所有应用组件上应用 index.css 的样式。

CSS 模块

默认情况下,所有的类名都是本地范围的,或者只适用于特定的组件。这意味着每个 React 组件都有自己的 CSS 文件,该文件的范围局限于该文件和组件,从而防止名称冲突或特殊性问题。

对于依赖项,使用 css-loader。Create-react-app 支持现成的 CSS 模块。

.button {
  background-color: #406040;
  box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
  padding: 7px 14px;
}
.button:hover {
  background-color: #a08884b;
}

Listing 17-6Button.module.css

让我们使用 button 创建另一个样式表,并将其命名为another-stylesheet.css,,如清单 17-7 所示。

.button {
    color: #f4f466;
}

Listing 17-7another-stylesheet.css

我们可以在应用的不同文件中使用相同的 CSS 类名,而不用担心名称冲突。让我们看看它是如何工作的,如 ButtonModule.tsx 中的清单 17-8 所示。

import React from 'react';

// import css modules stylesheets as styles

import styles from './Button.module.css';
import "./another-stylesheet.css";

export default function ButtonModule() {
  return (
    <div>

      <button className={`${styles.button} button`}> Button Module</button>
    </div>
  );
}

Listing 17-8ButtonModule.tsx

我们使用模板文字或反勾号加美元符号来放置 modules.css 中的样式对象。

见图 17-1 。

img/506956_1_En_17_Fig1_HTML.jpg

图 17-1

按钮模块界面截图

CSS-in-JS

样式化组件允许我们样式化 React 组件和重新样式化现有组件。如果我们想改变对象的样式,我们就使用属性。

另一方面,React 中的 Emotion 同时支持字符串和对象样式。语法也更像 CSS。

样式组件库

样式化组件允许我们编写常规的 CSS,并在应用中传递函数和属性。我们可以对任何组件使用样式化组件,只要它接受类名属性。

样式化组件使用带标签的模板文字 CSS 代码写在两个反斜线之间——来样式化组件。

import styled from "styled-components";

//you can rename your classes however you want.

export const ButtonStyled = styled("button")`
  align-items: center;
  display: inline-flex;
  justify-content: center;
  padding: 6px 16px;
  border-radius: 3px;
  font-weight: 50;
  background: rgb(49, 85, 77);
  color: rgb(241, 240, 240);
  border: 1px solid rgb(249, 200, 200);
  box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 2px;
  width: auto;
  margin-top: 500px;
  margin-bottom: 1px;
  cursor: pointer;

  &:hover {
    background-color: #95503a;
  }
`;

export const Container = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  height: 1px;
  color: #5a3667;
`;

Listing 17-9ButtonStyled Style Class

接下来,使用按钮样式类。组件和样式之间的映射被移除;这意味着您只需创建一个标准的 React 组件并将您的样式附加到它上面——您只需直接使用而不是使用带有类名的

我们还可以使用属性来设计我们的样式化组件,比如属性如何被传递给常规的 React 组件。

import React from "react";
import { ButtonStyled, Container } from "./styles";

const Button = () => {
  return (
     <>

      <Container>
        <ButtonStyled>Log in</ButtonStyled>
      </Container>
     </>

  );
};

export default Button;

Listing 17-10Button.tsx Using ButtonStyled Styling Class

React 中的情绪

我必须在这里强调“React 中的情感”,因为有两种使用情感的方法——一种是框架不可知的,另一种是 React。

这意味着安装也不同。有关这方面的更多信息,您可以查看这里的官方文档:

https://emotion.sh/docs/introduction

当然,我们将关注 React 方法。

但是在 React 方法中,也有两种主要的样式化元素的方法——使用 CSS prop 或者使用样式化组件。我们将只举后者的一个例子。

在 Emotion 中使用 styled-components,这个包是@emotion/styled.

对于 CSS 属性,在这里阅读更多的文档: https://emotion.sh/docs/css-prop .

import styled from "@emotion/styled";

export const ButtonEmotion = styled("button")`
  align-items: center;
  display: inline-flex;
  justify-content: center;
  padding: 6px 16px;
  border-radius: 3px;
  font-weight: 50;
  background: rgb(85, 49, 74);
  color: rgb(241, 240, 240);
  border: 1px solid rgb(249, 200, 200);
  box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 2px;
  width: auto;
  margin-top: 500px;
  margin-bottom: 1px;
  cursor: pointer;

  &:hover {
    background-color: #134f0e;
  }
`;

export const Container = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  height: 1px;
  color: #5a3667;
`;

Listing 17-11Emotion Styling Component

styled-component 使用styled.div样式 API 来创建组件。

摘要

正如我们在本章开始时所说的,有不同的方式或方法来设计 React 组件。选择其中一个有很多因素。我们在这里只概述了其中的一些,从最基本的如内联样式到 CSS 模块和流行的样式库,包括样式组件和情感。

下一章本质上是我们的项目应用的高潮,因为我们使用两种不同的方式部署它:Netlify 和 Docker。

十八、在 Netlify 和 Docker 中部署 React

在前一章中,在媒体查询钩子的帮助下,使我们的应用对移动设备友好,现在我们准备部署我们的前端应用。

我们将以两种不同的方式部署我们的应用。

首先,我们将使用 Netlify 来构建、部署和托管我们的静态站点或应用。开发人员喜欢 Netlify,因为它的拖放界面允许从 GitHub 或 Bitbucket 进行持续集成和交付。在这种情况下,我们将使用 GitHub 部署到 Netlify。

我们的下一个部署策略是使用一种叫做 Docker 的流行容器技术。使用 Docker 的最大优势之一是将我们的应用打包在“容器”中。因此,我们的应用对于任何运行 Windows 操作系统或 Linux 操作系统的系统都是“可移植的”。

请记住,我们只是在使用一个假的本地服务器(使用运行在终端中的外部 CLI 工具来创建一个假的服务器),所以我们实际上并没有为后端服务编译后端代码。这意味着我们一直使用的本地服务器或本地主机将无法与 Netlify 或 Docker 一起工作。

但是,我们仍然可以看到应用的实况。我们的目标是学习如何部署我们的前端应用,而不是后端,特别是使用 Netlify 和 Docker。

现在,让我们转到 package.json,因为我们需要更新我们的构建脚本。编辑构建脚本,如清单 18-1 所示。

"scripts": {
    "start": "react-scripts start",
    "build": "CI= react-scripts build NODE_ENV=production",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "test:generators": "ts-node --project=./internals/ts-node.tsconfig.json ./internals/testing/test-generators.ts",
    "cypress:open": "cypress open",
    "start:prod": "npm run build && serve -s build",
    "checkTs": "tsc --noEmit",
    "eslint": "eslint --ext js,ts,tsx",

Listing 18-1Updating the Build Script in package.json

现在,在开始部署我们的应用之前,让我们开始设置我们需要的工具。第一个上来的是 GitHub。

开源代码库

在开始之前,我们先来谈谈 GitHub 以及它的用途。

要了解 GitHub 是怎么回事,你应该知道 Git,这是一个开源的版本控制系统。版本控制系统允许我们有效地存储文件,更有效地与其他开发人员协作,进行更改,并上传最新的修订版。

那么 GitHub 是什么呢?Git 是一个命令行工具,但 GitHub 中的“中枢”是 Git 所关注的所有东西汇集在一起的地方,也是开发人员存储他们的项目和其他文件的地方。要部署到 Netlify,我们需要满足一些要求。我们可以有几种方法,但是我们会选择最直接的途径来通过我们的 GitHub。

如果您还没有帐户,请访问此网站 www.github.com 创建您的帐户。

img/506956_1_En_18_Fig1_HTML.jpg

图 18-1

GitHub 网站

您可以将所有示例代码或示例项目保存在您的 GitHub 帐户中;不需要将它们保存在本地机器中。

转到您的项目应用,并将其保存在 GitHub 帐户中。

为此,您需要单击初始化存储库。

img/506956_1_En_18_Fig2_HTML.jpg

图 18-2

初始化存储库或发布到 GitHub

提交任何更改;如果有,保存,然后发布到 GitHub。选择选项私有存储库。

img/506956_1_En_18_Fig3_HTML.jpg

图 18-3

提交 GitHub 私有存储库

接下来,进入你的 GitHub 账户,检查你的项目是否已经保存在你的私有存储库中。确认后,前往 www.netlify.com 并创建一个账户,如果你还没有的话。

网易

Netlify 是一个 web 开发托管平台,允许开发人员构建、测试和部署网站。主要功能包括托管、无服务器功能、表单、分割测试、持续部署和其他附加功能。

img/506956_1_En_18_Fig4_HTML.jpg

图 18-4

Netlify 网站

您也可以使用 GitHub 帐户登录。

img/506956_1_En_18_Fig5_HTML.jpg

图 18-5

Netlify 的登录选项

创建帐户后,我们将从 Git 创建一个新站点。

img/506956_1_En_18_Fig6_HTML.jpg

图 18-6

添加新站点

点击“从 Git 新建网站”按钮后,您将看到以下内容。选择 GitHub 进行我们的持续部署。

img/506956_1_En_18_Fig7_HTML.jpg

图 18-7

选择 GitHub 进行持续部署

然后,您将看到“创建新站点”页面。

img/506956_1_En_18_Fig8_HTML.jpg

图 18-8

搜索您的回购

搜索您的存储库的名称。

一旦你找到它,只需点击它,你会被引导到以下。检查您是否有相同的设置,然后单击“部署站点”按钮。

img/506956_1_En_18_Fig9_HTML.jpg

图 18-9

部署网站页面

点击按钮后,这个过程需要几分钟,所以只要坐下来放松一会儿。您将看到消息“部署您的站点”,这是第一步。

img/506956_1_En_18_Fig10_HTML.jpg

图 18-10

第一步:部署站点

如果部署成功,您应该会看到消息“您的站点已部署”

img/506956_1_En_18_Fig11_HTML.jpg

图 18-11

站点已成功部署

成功部署后,您应该会看到以下内容。

img/506956_1_En_18_Fig12_HTML.jpg

图 18-12

Netlify 上的应用

现在应用已经部署好了,点击 Netlify 给你的免费域名。是的,它是免费的,但你不能自定义或更改它,虽然有付费选项,其中您可以自定义您的网址。

码头工人

首先,我们讨论一下 Docker 是什么。简单地说,它是一个工具,使开发人员能够通过容器创建、部署和运行应用。在容器中,我们可以打包我们的应用及其所有部分——从库到依赖项——然后在一个包中部署它。

接下来,我们来试试 Docker。

我们将首先从 Windows 上的 Docker 桌面开始。您将在这里看到系统要求,并检查您的机器是否兼容。

img/506956_1_En_18_Fig13_HTML.jpg

图 18-13

在 Windows 上安装 Docker 桌面。来源: https://docs.docker.com/docker-for-windows/install/

点击从 Docker Hub 下载按钮后,您将被重定向到以下页面。单击 Get Docker 按钮开始安装过程。

img/506956_1_En_18_Fig14_HTML.jpg

图 18-14

获取 Docker 按钮。来源: https://hub.docker.com/editions/community/docker-ce-desktop-windows

对于 Mac,安装过程基本相同。

在 Mac 上安装 docker 桌面

img/506956_1_En_18_Fig16_HTML.jpg

图 18-16

获取 Mac 上的坞站桌面

img/506956_1_En_18_Fig15_HTML.jpg

图 18-15

在 Mac 上安装 Docker 桌面。来源: https://docs.docker.com/docker-for-mac/install/

下面是如何在 Ubuntu 上下载 Docker 引擎。

img/506956_1_En_18_Fig17_HTML.jpg

图 18-17

在 Ubuntu 上安装 Docker 引擎。来源: https://docs.docker.com/engine/install/ubuntu/

Ubuntu 和另外两个,Mac 和 Windows 的唯一区别就是 Ubuntu 没有 Docker 客户端。Docker 客户端是 Docker 管理容器的 GUI 或 UI。

安装 Docker 后,图 18-18 显示了 Docker 客户端在 Windows 或 Mac 上的仪表板外观,如果你有容器运行的话。否则,您将看到一条指示板消息:没有容器在运行。

img/506956_1_En_18_Fig18_HTML.jpg

图 18-18

Docker 客户端仪表板

Docker 忽略

接下来,让我们看看源代码,因为我们需要添加 Docker ignore 文件。在根目录中,创建。dockerignore.

img/506956_1_En_18_Fig19_HTML.jpg

图 18-19

Docker 忽略文件

我们忽略或者不提交测试和 Node 模块的 cypress。

nginx 配置

之后,我们需要创建 NGINX 服务器配置。我们需要 NGINX 服务器在 Docker 内部运行,并为浏览器提供 React 应用。

但是 NGINX 是什么?发音像“engine-ex”,NGINX 是一个免费、开源、高性能的 HTTP 或 web 服务器,也可以充当反向代理、电子邮件代理和负载平衡器。

本质上,它通过异步和事件驱动的方法提供了低内存使用、负载平衡和高并发性,允许同时处理许多请求。

在根目录中,创建 Nginx.conf 并添加以下配置。

server {

  listen 80;

  location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
  }

  error_page   500 502 503 504  /50x.html;

  location = /50x.html {
    root   /usr/share/nginx/html;
  }

}

Listing 18-2NGINX Config

Dockerfile

接下来,让我们添加 Dockerfile 文件。

# Stage 1
FROM node:15-alpine as build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json ./
COPY package-lock.json ./
RUN npm install
COPY . ./
RUN npm run build

# Stage 2
FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Listing 18-3Dockerfile

在清单 18-3 中,我们使用 Alpine 版本 15 构建 Docker 图像和其他小型的类似容器的应用。 WORKDIR (工作目录)类似于“cdying”或者转到 Docker 容器中的 cd 目录。

我们还指定了 ENV 路径。我们正在复制 package.json 和包-lock.son,并将它们转储到 Docker 中。这是在副本中完成的。

左侧(package.json)是您的存储库的一部分,而右侧是 Docker 的一部分。我们只是复制 package.json 和 package-lock.json,并将它们转储到根文件中。/码头工人的。

运行 npm 安装

这不是在您的目录中运行,而是在 Docker 容器的应用目录中运行。app 目录与我们转储 package.json 和 package-lock.json 文件副本的根目录相同。

收到。。/

请注意点和点斜线之间的空格。第一个点与应用的整个存储库有关,它被复制并粘贴到。/或应用目录。

如果你要问我为什么我们要复制整个应用,为什么我们需要在早期复制 package.json 和 package-lock.json?

这是因为我们需要 npm 先运行它,另一个原因与应用的优化有关。

Docker 为每个命令创建层,如复制、添加、运行等。按照目前的配置,Docker 不需要 npm 安装或重建所有层,包括 package.json。每当我们的源代码发生变化时,这样做将花费大量的时间和资源,这可能会经常发生。

只有在 package.json 中有更改时,才会执行 npm 安装,比如删除或添加某些内容。

同时,在阶段 2 中,注意副本中的三个参数,前两个参数被复制或转储到第三个或最后一个参数中。

坞站集线器

现在,让我们试试 Docker 部署。但在此之前,请登录您的 Docker Hub 帐户,这样您就可以上传或推送您的 Docker 图片,并将其发布到您的 Docker Hub。

img/506956_1_En_18_Fig20_HTML.jpg

图 18-20

dock hub sign up/log in page-对接集线器登入/登入页面。资料来源: https://hub.docker.com/

Docker 命令

现在,让我们开始 Docker 部署过程。以下是从 docker 登录开始的 Docker 命令的屏幕截图。

img/506956_1_En_18_Fig21_HTML.jpg

图 18-21

Docker 命令

确保您的 Docker 正在运行,然后编写 Docker 构建。下面是一个成功的 Docker 构建的截图(已完成)。根据您的计算机,这可能需要几分钟才能完成。

img/506956_1_En_18_Fig22_HTML.jpg

图 18-22

码头工人建造

一旦你成功地构建了它并把它推送到 Docker Hub 的仓库,我认为最好先在我们的本地机器上运行它。运行 Docker 命令:

$ docker run -p 8080:80 yourDockerUserName/react-docker:1.0.0

以下是 Docker 在我机器上运行的截图。你的用户名会不同。

img/506956_1_En_18_Fig23_HTML.jpg

图 18-23

码头运行

检查 Docker 客户端,看看它是否在您指定的端口 8080 上运行。hungry_raman 这个名字是由两个单词随机生成的,中间用下划线连接。简而言之,它是容器的 UUID 或通用唯一标识符。

img/506956_1_En_18_Fig24_HTML.jpg

图 18-24

Docker 客户端仪表板中的容器

转到 localhost:8080 来检查您的应用。

img/506956_1_En_18_Fig25_HTML.jpg

图 18-25

在端口 8080 上运行

好了,现在我们已经看到一切都在工作,我们可以把它推到我们的 Docker Hub:

-$ docker push yourDockerUsername/react-docker:1.0.0

img/506956_1_En_18_Fig26_HTML.jpg

图 18-26

码头推送

之后,打开你自己的 Docker Hub,这样你就可以看到你的应用了。

img/506956_1_En_18_Fig27_HTML.jpg

图 18-27

成功部署后的 Docker Hub

你可以在任何地方下载或部署它,例如,在 Kubernetes、Azure 或 AWS 中使用它们的服务或容器实例。

如果你想了解更多关于 Kubernetes 的信息,可以去他们的网站: https://kubernetes.io/

如果你想了解更多关于 Azure 的信息,可以去他们的网站: https://azure.microsoft.com/

如果你想了解更多关于 AWS 的信息,可以去他们的网站: https://aws.amazon.com/

摘要

这就是我们的应用在 Netlify 和 Docker 中的部署过程。Netlify 非常适合静态网站,我们已经看到了将 Netlify 与我们的 GitHub 存储库连接以获取源代码并允许我们进行持续集成和部署是多么容易。

另一方面,如果我们需要在任何 Linux 机器上运行我们的应用,那么将我们的应用部署到 Docker 容器中就是一种方法。还有,记住 Docker 是开源的,所以任何人都可以贡献和扩展它来满足他们的需求,比如添加功能等等。

下一章只是额外的一章,在这一章中,我们将了解如何重用我们的 React 知识,以及如何将我们在构建项目应用时学到的概念和技能扩展到其他相关的平台和框架。

posted @ 2024-10-01 21:06  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报