React-应用-2-创建快速启动指南-全-

React 应用 2 创建快速启动指南(全)

原文:zh.annas-archive.org/md5/492e037739f7604d9ec34e65b63f34f2

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Create React App 在我看来是合适的引导工具的终极示例。而不是花费数小时进行乏味的调整和配置,希望一切恰到好处且没有遗漏任何东西,您反而得到了一个工具和一个项目,从您想要开始项目的那一刻起,它就已经构建好并准备好了。现在,启动一个功能齐全的项目所需的时间从数小时,甚至可能几天,缩短到几分钟,没有任何东西能阻止您构建您梦想中的 React 项目!

这本书是为那些想要深入了解 Create React App 工具的人准备的。我们将涵盖所有命令,所有 2.0 版本的新增功能,我们还将从头开始构建项目,并在过程中涉及所有关键概念,以确保您能够充分利用这个工具。

这本书的适用对象

这本书是为那些有一定 JavaScript 知识,并希望更好地利用 Create React App 引导工具开始构建自己的精彩 React 项目的人设计的。如果您想提高 JavaScript 技能,并考虑最佳实践来构建项目,这本书也会适合您!即使您是 React 的老手,但从未或很少使用 Create React App 来开始项目,这本书也会给您带来很多学习和工作的机会!

为了充分利用这本书

您需要在您的计算机上具备一些设置 Node.js 的经验以及编程经验,并且您还需要一个兼容的代码编辑器,最好是能显示您项目目录结构的编辑器。

下载示例代码文件

您可以从www.packt.com的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packt.com登录或注册

  2. 选择 SUPPORT 标签

  3. 点击代码下载与勘误

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明

文件下载完成后,请确保使用最新版本的软件解压或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

这本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Create-React-App-2.0-Quick-Start-Guide。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富的书籍和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

下载彩色图像

我们还提供了一个包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781789952766_ColorImages.pdf

约定

本书使用了多种文本约定。

CodeInText: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“在App.js文件的顶部,我们将添加我们的import语句。”

代码块设置如下:

const App = () => {
  return <div className="App">Homepage!</div>;
};

任何命令行输入或输出都按照以下方式编写:

$ yarn add bootstrap@4 reactstrap@6.5.0

粗体: 表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“从管理面板中选择系统信息。”

警告或重要提示看起来像这样。

小贴士和技巧看起来像这样。

联系我们

我们欢迎读者的反馈。

一般反馈: 如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并给我们发送电子邮件至customercare@packtpub.com

勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您能向我们报告。请访问www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版: 如果您在互联网上发现我们作品的任何非法副本,我们将非常感激您能提供位置地址或网站名称。请通过copyright@packt.com与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

评论

请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论呢?潜在的读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

如需更多关于 Packt 的信息,请访问packt.com

第一章:介绍 Create React App 2

本书将指导你如何有效地使用 Create React App 2 (CRA) 来创建一个新的 React 项目并构建你梦想中的网络项目!我们将涵盖许多主题,包括如何安装 Create React App、默认获得的项目结构,以及如何将代码和库添加到你的项目中。我们将探索你构建复杂、现代网络应用所需的一切,包括最新的最常见 React 项目配置和编译。

然而,要能够做到这一点,我们首先需要花一点时间来谈谈项目本身,它的谦逊起点,以及它最终试图解决的问题。通过了解历史和意图,我们可以更好地理解如何充分利用我们提供的工具集,并了解限制存在的位置和原因。

在本章中,我们将讨论 Create React App 是什么以及它带来了什么。当你完成本章时,你会理解为什么 Create React App 如此重要,以及它是如何帮助开发者以更短的时间完成更多工作的。我们还将讨论本书将涵盖哪些主题,其格式本身,以及如何最好地跟随。

在本章中,我们将探讨以下主题:

  • 启动一个新的 Create React App 项目

  • 启动和停止你的服务器的命令

  • 运行测试的命令

  • 创建生产就绪构建的命令

  • 退出 Create React App 的命令仅限于进一步调整和配置你的项目

什么是 Create React App?

许多不同的编程语言、工具和框架都有许多不同的方式来启动它们特定工具集的开发。有时这需要下载大量的库,或者使用为正确架构、操作系统或其他配置预构建的二进制文件或压缩存档开始。有时它有一个很好的预构建路径来开始,这样可以最大限度地减少挫折,但可能限制了可用的选项。

然而,问题在于,对于许多 JavaScript 框架来说,并没有类似的选项。使用任何特定的框架或技术实际上是一个明显难以解决的问题,因为每个人的技能组合、操作系统和任务都有所不同。

Create React App 是 JavaScript 对那些不存在于任何特别易于接触方式中的入门工具集的回应。一般来说,要开始一个项目,你必须在真正开始之前学习大量的支持技术和技巧。你必须了解配置框架,如 Babel、Webpack、Brunch 或 Gulp。此外,你还必须知道如何在 JavaScript 中建立项目结构。在你弄清楚所有其他事情之后,你必须学习如何设置一个开发服务器,该服务器可以自动重新加载更改。在所有这些之后,你仍然需要弄清楚如何设置你的测试框架、React 以及你想要的任何附加库。

这最终会变成一大堆工作,而这只是入门。由于你正在开发的每一个框架和配置可能都不会转移到你的下一份工作中,所以这个问题会变得更加复杂!

相反,Create React App 旨在做些不同的事情:将配置和设置变成一个一步到位的过程。这让你可以更早地开始并构建你的应用程序,而不用担心更深入的工作部分。你可以把更多的时间花在编写代码上,而不是在配置一个出色的开发环境上。在你的 Create React App 应用程序中,环境本身就是一个出色的开发环境,这对新人和有经验的开发者来说都是一个巨大的障碍和障碍的消除!

命令行界面CLI)工具提供了一个极佳的开发环境,鼓励快速迭代和测试驱动技术。我们有很多配置和特定库的问题已经为我们解决了,所以我们不必做这些基础工作。此外,你永远不会被你做出的任何选择所束缚。Create React App 团队包含了一个eject选项,它将你正在开发的应用程序的全部内容提取出来,并将其转换为标准的 webpack 或 babel 构建,例如,可以与Node 包管理器NPM)兼容。你不必担心需要重复大量工作来将你的代码从 Create React App 转移到自己的项目或工作环境中的特定配置和设置;你只需创建一些可以安全、干净、顺利转移的东西。你甚至可以在这一步(eject 之后)调整配置,并进一步使这个应用程序成为你自己的!

Create React App 的历史是什么?

为了更好地理解 Create React App 的成功之处,我们必须了解 JavaScript 开发世界的起点。我们需要看到系统的瑕疵,以了解为什么某些事情被修复以及如何被修复。让我们简单谈谈 JavaScript 开发的历史以及开发者经常遇到的一些主要问题!

JavaScript 开发的早期

首先,你需要回顾一下在前端处理 JavaScript 代码的过去。长期以来,你可能会下载一些来自某个内容分发网络CDN)的 JavaScript 文件,将它们扔到你的前端代码中,然后在前面写一大堆额外的 JavaScript 代码,然后就算完成了。

这种方式在某种程度上是不错的,因为你的依赖项被锁定到了你下载的版本,并固定在服务器上,所以你部署的内容开发起来相对容易,因为所有依赖项都已经存在并准备好使用。不幸的是,它以许多其他方式引入了大量问题。首先,你可能会不断遇到问题,其中一个你下载的库与另一个特定库的特定版本完全不兼容,而且这通常是一个复杂且困难的过程。大多数人解决这个问题的方法被分为几个阵营:

  • 逐一检查并修复所有不兼容性

  • 编写复杂的粘合代码,通过包装其中一个库并提供两个库协同工作的手段,使这些库能够一起工作

  • 只为另一个库下载不同版本的库并将它们分别存储,当加载网页时导致巨大的 JavaScript 包,因为你可能正在下载两到三个不同版本的某个东西,比如 jQuery

是的,最后一个要点是一个真实存在的事情,真正的开发者确实这么做了!你可能明白为什么开发者们尽可能快地想要摆脱这种做法。

压缩包时代

为了解决这个问题,解决依赖性问题至关重要。转向基于 Node.js 的工具,如npm,对此有很大帮助,因为现在你的依赖项将从一个集中位置拉取,版本控制成为 JavaScript 开发的头等公民,这真是太棒了!

然而,当你需要将这个问题应用到浏览器代码和富 Web 应用中时,情况就不那么美妙了。通常,这意味着你需要理解哪些库适用于哪些项目。如果你想使用 React 与 JSX(我们稍后会详细讨论这个话题),以及最新的 JavaScript 语法,你需要确切知道要包含 React 和 Babel 的哪些版本。你还需要了解你需要哪些 Babel 插件来支持你使用的 JavaScript 语法的任何草案。

你想在你的 React 项目中使用一些 CSS 转换器或其他任何语言帮助,比如 TypeScript 或 Flow 吗?如果是这样,构建和配置你的项目就会变得明显更加困难,而且我们还没有涉及到如何将这段代码组合起来以便在浏览器上使用的问题!现在,你只需要开始一个项目,就需要有广泛的知识面,而对于如何设置和配置像 Webpack、Bundler、Grunt、Gulp 或 Brunch 这样的东西,则需要深入的知识!

这是我们之前在命令行工具和配置实用程序普及之前的发展阶段,所以让我们通过讨论 Create React App 解决的问题来深入了解这一点!

CRA 解决了哪些问题?

Create React App 旨在解决在开始开发时需要理解大量不同的工具、脚本、配置实用程序、配置语言和文件类型的问题。所以现在,这并不是在你项目高级阶段和学习时需要解决的问题。这也不是在你成为专家并试图优化包以最小化最终用户在想要使用你的花哨 Web 应用时需要下载的内容时需要解决的问题!

记住,我们在这里讨论的问题不是专家独自解决的问题:这个问题存在于所有技能水平的发展中,对每个人来说都是如此。更糟糕的是,每次你开始一个新的项目时,这些问题都会重复出现。作为开发者,我们讨厌重复和浪费精力,所以 Create React App 团队着手消除这些障碍!

Create React App 允许你在任何技能水平、任何舒适度和熟悉 JavaScript 及其生态系统的水平上开始。你可以通过单个命令行工具启动一个项目,并获取你需要的所有东西,包括测试工具和框架。

这并不是懒惰。这是效率

这并不是过度简化。这是消除障碍

安装 Create React App 的先决条件

首先也是最重要的,你需要在你的工作电脑上安装npm。没有这个,就无法安装先决条件的库和项目。你可以在nodejs.org下载 Node 和npm用于你的项目,然后执行以下步骤:

  1. nodejs.org找到适合你的电脑和操作系统的 Node 和 NPM 的适当安装程序包,并遵循安装程序提供的说明。

  2. 安装一个合适的代码编辑器或交互式开发环境IDE)。我在 Visual Studio Code 上有了最好的体验,所以这是我的个人推荐,但你可以使用任何你感到舒适的东西!

  3. 一旦你安装了 Node 和npm(如果你还没有的话),你就可以开始了!

现在一切都已经设置好、运行正常,并且安装到了我们需要的版本,我们可以开始迭代了!学习一个项目的最快方式之一就是在我们学习的过程中开始构建它并对其进行迭代,所以我们将这样做。

创建我们的第一个 Create React App 项目

您应该首先选择一个主要的开发目录,您希望所有关于这本书的开发工作都发生在这个目录中。无论这个目录在哪里(我总是喜欢在我的home文件夹或Documents文件夹中的某个地方创建一个开发目录),然后您将创建一个新的项目。这将是一个一次性项目,因为我们将专注于尝试使用 Create React App 并熟悉从空白项目开始。让我们创建一个新的项目,我们将称之为homepage

对于这个一次性项目,我们将假装我们正在编写一个花哨的homepage替代品。您实际上可以选择任何类型的项目,但我们将在这个章节之后丢弃这个初步项目。在您构建项目后,您应该看到以下输出:

 $ npx create-react-app homepage

 Creating a new React app in [directory]/homepage.

 Installing packages. This might take a couple of minutes.
 Installing react, react-dom, and react-scripts...

 [... truncating extra text]

 Done in 13.65s.

 Success! Created hello-world at [directory]/homepage
 Inside that directory, you can run several commands:

 yarn start
 Starts the development server.

 yarn build
 Bundles the app into static files for production.

 yarn test
 Starts the test runner.

 yarn eject
 Removes this tool and copies build dependencies, configuration files
 and scripts into the app directory. If you do this, you can't go back!

 We suggest that you begin by typing:

 cd homepage
 yarn start

 Happy hacking!

在成功创建项目后,我们看到的那些说明对于我们在 Create React App 中的工作流程至关重要。默认情况下,Create React App 中捆绑了四个主要命令(以及大量选项)。如果您使用的是npm而不是 Yarn,请注意,Create React App CLI 帮助页面中的许多注释和输出主要指的是 Yarn。现在,这些命令(startbuildtesteject)相对容易理解,但仍然很重要,要进一步深入了解它们。

快速查看 CRA 的选项

在我们能够使用 Create React App 构建我们梦想中的应用程序之前,我们必须首先分析 Create React App 中的每个命令以及它们各自的功能,以及何时使用每个命令!

每个命令都与软件开发生命周期的特定部分相对应:构建应用程序、运行开发服务器、运行测试以及深度定制和配置。让我们更详细地探索每个命令。

Yarn start 命令

这个命令的功能是启动开发服务器

在您的 Create React App 项目上运行start会将您的项目从代码转换到您的网页浏览器。也就是说,它将编译项目的所有代码。从那里,它将加载一个带有默认起始模板的开发服务器。关于这一点,还有另一个好处是,它实际上会捕捉到您对任何代码所做的任何更改(假设您保存了这些代码),因此您不必不断地进行更改、保存文件、重新启动服务器、刷新浏览器;相反,您将对所做的任何更改立即获得反馈。

从一个完全全新的 Create React App 项目开始,并运行start将产生以下结果:

yarn build 命令

这个命令的功能是将应用程序打包成用于生产的静态文件*。

运行 build 将应用程序转换成更适用于生产的版本。这意味着什么?好吧,如果你已经对 webpack 和 brunch 等工具如何将它们转换为生产网站相当熟悉,那么你基本上已经知道这能完成什么。另一方面,如果你觉得这一切都极其令人困惑,我将花一点时间,用稍微不那么模糊的术语来解释它。

从本质上讲,大多数浏览器默认情况下无法处理为 Create React App 项目编写的代码。需要对代码进行大量工作,将其转换为对浏览器更有意义的形式,确保它不需要依赖帮助来解释一切。然后,代码还会被压缩!通过重命名函数和变量、尽可能删除空白,以及在这里和那里进行一些小的优化,直到代码被缩减到一个非常干净且可用的版本。所有内容都被压缩,文件尽可能缩小,以减少下载时间(如果你针对的是可能没有良好互联网速度的移动受众,这一点很重要)。

压缩的意思就是听起来那样。它是将代码压缩成更小的值,使人类难以阅读,但对计算机来说却非常易于消化!

yarn test 命令

这个函数启动测试运行器

运行 test 正如你所期望的那样:运行你应用程序的所有测试。默认情况下,当你使用 Create React App 启动一个新项目时,你的项目将包括许多额外的工具,所有这些工具都应该准备好让你开始测试。如果你选择以更测试驱动的方式处理项目,这特别有用,在前端开发世界中这可能非常有用。

第一次运行 test 时,你应该在屏幕上看到一些输出,可能看起来有点像这样:

 PASS  src/App.test.js
 renders without crashing (18ms)

 Test Suites: 1 passed, 1 total
 Tests:       1 passed, 1 total
 Snapshots:   0 total
 Time:        1.976s
 Ran all test suites related to changed files.

 Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press q to quit watch mode.
 › Press Enter to trigger a test run.

不仅如此,所有这些构建和提供给你的选项和附加工具都非常好!在应用程序中设置测试框架并使其持续运行可能会非常痛苦,所以这一切都为你解决,让你的生活变得容易一千倍。除此之外,Create React App 项目附带的测试监视器还会监视并实时重新加载对任何相关文件所做的更改,类似于 start 命令。

这些只是默认设置。值得看看一些与我们的测试监视器一起提供的额外命令:

  • 按 a 键运行所有测试:正如命令所说,如果你在那个窗口按A键,它就会决定从零开始运行你项目中所有的单个测试,并输出结果。当你需要它并需要在任何时间验证一个完全绿色的测试套件时,请使用此功能。

  • 按 f 键只运行失败的测试:当我提到过去设置这样的东西时,我是在个人经验的基础上说的,这是一项绝对令人讨厌的任务。当你采取红色、绿色、重构的项目方法,只想运行上次失败的测试,并试图让这些测试通过时,这真是太好了。你可以将此作为你的开发方法的一部分,逐渐清除应用程序失败测试的冗余,直到所有测试都通过!

红色、绿色、重构:这指的是一种常见的开发模式,你首先编写测试,目的是让它们失败,然后编写最少的代码让它们通过,然后重构代码直到它们再次失败,然后重复这个循环。虽然这通常在面试环境中而不是实际开发中使用,但这个过程本身是一个非常现实的过程。

  • 按 p 键通过文件名正则表达式模式过滤:这是一个非常酷的功能。假设你修改了一些代码,影响了所有与用户相关的功能,但你有一个巨大的测试套件,不想测试整个套件。你可以通过按P键来针对所有用户代码,然后输入user并查看哪些测试运行。

  • 按 t 键通过测试名称正则表达式模式过滤:类似于前面的选项,但它更进一步,通过查看你的测试是如何命名的(更多内容将在后面的章节中介绍),并根据这些描述而不是测试文件所在的文件名来运行测试。

  • 按 q 键退出监视模式:这里没有太多要解释的;这将退出测试监视器。

  • 按 Enter 键触发测试运行:按下Enter键将重新运行你最后的测试,这在使用正则表达式选项时非常有帮助,但你不想每次都重新输入模式。

yarn eject 命令

移除 Create React App 脚本和预设配置,并将构建依赖项、配置文件和脚本复制到应用目录中。如果你这样做,你就不能回到在你的项目中使用 Create React App 了!

对于这个命令,值得看看文档中对此的说明。用通俗易懂的话来说,这个命令将你的项目从 Create React App 项目转换成 Webpack 配置,并移除了很多 Create React App 和 React Scripts 项目的细节,这些细节实际上隐藏了一些信息。一个类似的概念是购买预装好的电脑与自行组装(或重新组装)电脑。你可能一开始希望所有东西都为你准备好,但也许有一天你会想要添加更多的 RAM 或更换显卡,这时你将不得不打开之前是黑盒的部分,以便进一步配置!

如果你需要超出从基础项目获得的标准项目结构和配置的局限,你可能会这样做。这将允许你将其转换成标准的 Webpack 项目,添加新的库,更改默认和基线细节,或者更进一步,替换其他核心组件。

让我们探索创建的项目

最后,我们应该花一点时间看看究竟创建了什么,并将其放入你的项目中。

Create React App 将为你的项目生成一个名为README.md的 README 文件。这是一个 Markdown 格式的文件,告诉其他人如何有效地使用你的项目(或者,如果你像我一样,几个月后会提醒你如何使用项目中实施的所有工具和技巧)。

你还会得到一个favicon,这是显示在地址栏中你的网站路径旁边的小图标,并且用于后续对应用程序的任何书签。接下来,我们有公共的或index.html文件,这是主要的执行文件,包含了你所有的花哨的 React 代码,更重要的是,它告诉网络浏览器在哪里渲染你的 React 应用程序;在我们的例子中,我们有一个div元素,它作为 React 渲染的主要目标。文件的来源默认如下:

  <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-
         fit=no">
        <meta name="theme-color" content="#000000">
        <!--
          manifest.json provides metadata used when your web app is added to the
          homescreen on Android. See 
          https://developers.google.com/web/fundamentals/web-app-manifest/
        -->
        <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
        <!--
          Notice the use of %PUBLIC_URL% in the tags above.
          It will be replaced with the URL of the `public` folder during the build.
          Only files inside the `public` folder can be referenced from the HTML.

          Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
          work correctly both with client-side routing and a non-root public URL.
          Learn how to configure a non-root public URL by running `npm run build`.
        -->
        <title>React App</title>
      </head>
      <body>
        <noscript>
          You need to enable JavaScript to run this app.
        </noscript>
        <div id="root"></div>
        <!--
          This HTML file is a template.
          If you open it directly in the browser, you will see an empty page.

          You can add webfonts, meta tags, or analytics to this file.
          The build step will place the bundled scripts into the <body> tag.

          To begin the development, run `npm start` or `yarn start`.
          To create a production bundle, use `npm run build` or `yarn build`.
        -->
      </body>
    </html>

我提到了一个作为主要 React 渲染目标的div元素:具有idrootdiv元素作为我们的主要渲染目标,是使你的应用程序正常工作的关键组件。移除它后,你的应用程序在浏览器中将无法正确渲染!接下来是package.json清单文件。它存储了你的项目使用的所有依赖项,以及一些用于描述项目的元数据(可能包括名称、版本、描述或其他元数据)。如果你使用 Yarn,我们还有一个yarn.lock文件,它以防止项目在某个库更新时随机崩溃的方式锁定项目使用的库和依赖项列表。

您项目的所有依赖项、库以及使项目在幕后运作的东西都位于node_modules目录中。这也带我们进入了src目录,这可以说是我们整个项目结构中最重要的目录!它将存放我们即将进行的所有工作——所有源代码。

在该目录内,我们有index.js文件,它处理 React 的主要渲染调用,由名为ReactDOM的包支持。这个包接收我们的App.js组件,这是我们主要的根级组件,并告诉 React 将其渲染到之前向您展示的index.html文件中。

我们还默认获得一些样式,通过index.css文件。这是我们的项目将使用的基级样式表,我们将在其基础上进行配置。

在我们的非测试代码方面,App.js是通过 Create React App 默认得到的最终组件。里面的内容对我们来说并不特别重要,因为我们只是会删除该文件中的所有代码,然后从头开始!App.css存储该组件的样式表,这使我们能够确保每个组件包含的任何样式都可以独立存储和配置。我们还以可缩放矢量图形SVG)文件的形式提供了 React 标志,即 React 标志(logo.svg)。我们不需要它,所以请随意删除它!

serverWorker.js是一个文件,它告诉我们的应用如何作为一个渐进式 Web 应用的服务工作者存在/工作,但我们将在这个章节的后面深入探讨,届时我们将专注于渐进式 Web 应用!

最后,我们有一个为我们预构建的测试。App.test.js文件包含针对我们的App.js组件的测试套件(我想不是套件,因为它只有一个测试,但随着时间的推移,它将变成套件)。这就是全部!这就是我们的 Create React App 项目的默认项目结构!

向我们的项目中添加代码

理解默认项目结构的最简单方法之一就是实际进入其中并开始尝试修改,所以让我们这么做!我们将删除项目附带的一些默认代码,并自己构建一些内容,以获得项目应该如何结构化的良好感觉,并学习每个文件如何交互,以便我们在开始玩耍和更改文件结构时了解这些。

创建我们的第一个组件

要创建我们的第一个组件,请按照以下步骤操作:

  1. 在您最喜欢的文本编辑器中打开新创建的项目,并在该项目中运行start命令,以便打开浏览器窗口查看我们沿途所做的任何更改的结果。

  2. 让我们做所有开发者都爱做的事情:删除旧代码!

  3. 一旦代码中有了,我们可以通过 Babel 中包含的最新 JavaScript 语法变化来了解与 React 交互的主要方法。鉴于这一点,让我们看看与 React 类交互的方法。我们可以使用函数,或者我们可以使用类将 React 代码引入我们的代码库。我们将从仅使用函数开始,随着时间的推移,也将引入类,并且我们将讨论如何、何时以及为什么选择每种方法。当然,无论实现方法如何,都需要 React,因此我们需要在代码的开始处实际导入它。

  4. App.js 文件的顶部,我们将添加我们的 导入 语句:

import React from 'react';

这一行代码告诉 JavaScript 我们想要 导入 React 库,并且我们可以从 react npm 库(Create React App 显然已经为我们包含了它)中找到 React 类。这为我们提供了所需的 React 支持,并且还增加了对 JSX 模板的支持,以及我们编写基础级 JavaScript 所需要的一切!

  1. 在处理完导入之后,让我们编写我们的第一段代码:
const App = () => {
 return <div className="App">Homepage!</div>;
};

在这里,我们将进一步探讨一些新的 JavaScript 语法,如果你来自较旧的 JavaScript 世界,你可能不太熟悉。上一行负责创建一个称为常量函数的东西,这限制了我们在事后重新定义或修改 App 函数的能力。我们正在编写的这个函数不接受任何参数,并且总是返回相同的内容。这是一个函数组件,因此我们需要编写 return 语句来返回一个 JSX 模板,告诉 React 如何将我们的 React 组件渲染到浏览器中。我们还确保告诉 React,我们的主要组件应该有一个名为 App 的 CSS 类名。

className 而不是 class!在 JavaScript 中,class 是一个保留关键字,这就是为什么 React 需要这个小陷阱!

  1. 在这段代码的末尾,我们需要添加一个 export 语句,以便其他文件(例如我们的 index.js 文件,具体来说)能够将正确的模块导入到我们的代码库中:
export default App;

我们最终的结果是,当我们的浏览器刷新时,我们应该在屏幕上看到 Homepage! 弹出!

等等,什么是 JSX?

你可能不知道 JSX 是什么,但如果你知道,请随意跳过。否则,我将为你提供一个非常快速的总结!

简单来说,JSX 就是 JavaScript 和 HTML 的混合,本质上是一个模板语言。这是一个简化的解释;JSX 实际上是围绕 React.createElement 调用的智能语法包装器,但以更接近 HTML 的方式组合在一起。这样,我们可以编写与 HTML 极其相似的界面代码,这使得开发人员、设计师和其他人可以与我们的代码一起工作(假设他们已经熟悉 HTML),但我们也可以访问一些额外的功能,因为它是基于 JavaScript 的模板语言。

我们获得的第一项功能是,我们实际上可以在任何 JSX 中嵌入任何 JavaScript 语句,只需将其包裹在花括号中!不过,需要注意的是,我们需要记住 JSX 是 JavaScript,因此这里有一些单词和语法是保留的(类是这个例子中的主要例子),所以在编写 JSX 时需要使用特定的变体(例如 className)。

在我们的组件中嵌入样式表

使用 React 和创建这些基于浏览器的界面很棒,但没有任何样式,整体看起来会很单调。好消息是 Create React App 也为你提供了一个很好的框架来清理你的界面!目前,由于我们删除了大量代码,我们目前应该有一个完全空的 App.css 文件。我们需要回到 App.js 文件,并在顶部添加以下行以确保它包含我们的新 App 组件样式表:

import "./App.css";

这将告诉 React 确保将 App 样式表包含为组件样式表的一部分。如果 App.css 保持为空,那么这不会产生太大影响,所以让我们也将我们的默认样式表更改为更有趣的内容:

 .App {
  border: 1px solid black;
  text-align: center;
  background: #d5d5f5;
  color: black;
  margin: 20px;
  padding: 20px;
}

保存文件,回到你的浏览器窗口,你应该会看到以下类似的内容:

图片

好的,我们现在有一些可以工作的代码,这是我们在应用程序中开始的好地方,所以我们将跳转到 index.js 并快速弄清楚组件是如何进入浏览器的。打开 src/index.js

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

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

    // 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: http://bit.ly/CRA-PWA
    serviceWorker.unregister();

到目前为止,我们已经看到了 import React。上一行导入 ReactDOM(其中包含主要的 render() 函数),我们需要它来告诉 React 要渲染哪个组件,以及在哪里渲染它!这来自于独立的 react-dom npm 模块。

之后,我们还包括了另一个样式表,这次是 index.css。这将成为我们的全局、基线 CSS 文件。之后,我们使用 import 语句导入 App 组件(记得我们之前写的 export 语句?)使用 import App from './App'。注意,我们可以完全省略 .js 扩展名,并在文件名前包含一个点和斜杠;这告诉 Node 我们正在从本地文件系统导入,而不是从 NPM 模块导入!App.js 作为 local 文件位于我们的 src 目录中,所以本地包含就足够了。

我们以一个新的行结束,import registerServiceWorker from './registerServiceWorker',这允许我们访问在 Create React App 中实现渐进式网络应用的 Service Workers。渐进式网络应用略超出了本教程系列的范畴。

render() 是一个函数调用,它接受两个简单的参数:

  • 要渲染哪个组件

  • 在哪里渲染该组件

由于我们的组件名称被导入为 App,并且因为我们使用 JSX,我们可以将 App 当作一个 HTML 标签来处理:

<App />

记住,所有 JSX 中的标签都需要关闭,无论是通过简写语法,如前面的示例,还是通过较长的语法,如下所示:

<App></App>

我们渲染难题的最后一部分是我们需要确定 React 组件需要渲染到的 DOM 中的位置,通过 document.getElementById('root') 这一行。这告诉 JavaScript 需要在页面上找到一个具有 idroot 的元素,这最终将成为我们的渲染目标!

就这样!我们承认这是一个基础但完整的 React 应用程序,我们几乎在没有任何时间的情况下就写出来了,而且在设置开发服务器、确定我们需要哪些库、使代码和浏览器窗口自动重新加载,或者,嗯,你懂的,我们都没有压力或头痛。

严肃地说,开发者还能要求什么更多呢?

展望未来——我们将做什么?

我们还能要求什么更多?实际上,还有很多!在接下来的章节中,我们将随着对 Create React App 提供的开发工作流程的熟悉,更深入地探讨。让我们探索我们将要构建的这个项目的计划(因为“hello-world”应用程序只是让我们有机会玩玩,而且不会成为我们未来的最终项目)。

项目计划

在本书的整个过程中,我们将使用 Create React App 完全构建一个应用程序,涵盖许多常见的现代 React 开发技术、方法和最佳实践。我们将花时间探索可用的不同库,以及如何在我们的 Create React App 项目中最佳地利用它们,而且还要尽可能少花力气!我们将构建一个项目,将充分利用现代 JavaScript 开发的最佳之处,利用 Babel 和 React 的最新功能。我们将利用 JavaScript 中最新的语法变化来发挥其全部作用!我们将确保我们的应用程序经过全面测试且坚不可摧,使用 CSS 模块和 Syntactically Awesome Style SheetsSASS)保持其美观,甚至模拟后端服务器,这样我们甚至不需要一个单独的后端服务器来开发!

最后,我们将探索如何通过服务工作者使我们的应用程序能够在在线或离线状态下工作,然后通过使其生产就绪来完善我们的应用程序,使其最小化、整洁且可部署!

摘要

在本章中,我们探讨了在启动 Create React App 项目时我们所拥有的选项。我们还花了很多时间探索了 Create React App 之前的前端开发历史,甚至抽出一些时间坐下来,通过探索默认项目结构来构建一个漂亮的小型入门级应用程序。

你现在应该对 Create React App 的默认项目结构和将使我们能够在后续章节中完成更多工作的语言结构感到更加舒适,所以无需多言,让我们奋勇前进,开始构建一个更复杂的应用程序,这将成为我们项目剩余部分每个章节的基础。

第二章:创建我们的第一个 Create React App 应用程序

在第一章《介绍 Create React App 2》中,我们首先仔细研究了 Create React App 及其提供的一些选项。我们甚至开始了create-react-app的学习,并从非常基础的 React 知识开始学习。然而,我们现在需要开始构建一个将作为添加更多功能和测试 Create React App 极限框架的应用程序。

为了让事情更加简单,我们将构建一个简单的待办事项列表,因为这是一个几乎每个人都能理解的应用程序。它甚至有一个很好的非数字类似物,这使得它很容易进行推理!

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

  • 设计一个网络应用程序

  • 构建一个简单的项目结构

  • 通过 props 向组件传递值

  • 通过 props 将函数传递给子组件

  • 使用包含的 CSS 进行基本组件样式设计

创建我们的第一个主要项目

现在是我们向前推进并开始构建一个真正项目的时候了,这个项目将作为我们想要在这个书中探索的所有其他功能的基准项目。不过,为了做到这一点,让我们先简要地谈谈我们想要如何设计我们的应用程序。

设计我们的应用程序

当你不确定最初要构建什么时,构建项目是非常困难的,对吧?当我们谈论如何设计、构建和规划一个应用程序时,我们需要讨论我们最初试图解决的问题。考虑到这一点,让我们来谈谈我们应用程序的理论愿景。

首先,我们将构建一个介于番茄计时器和待办事项列表之间的混合体。这将给我们一个普通的待办事项列表的功能,同时允许我们在通过列表时跟踪每个项目花费的时间。我们将保持这个应用程序的整体设计相当简单;没有必要深入一个庞大的应用程序。一个相对较小且简单的应用程序将教会你如何快速开始使用 Create React App。

本章也将是唯一一个真正只关注 React 方面,而较少关注 Create React App 的章节。如果你在开始使用任何工具后不知道如何构建东西,那么能够开始使用任何工具基本上是没有用的,这就是本章旨在帮助你解决的问题。

我们的应用程序将有一个简单的待办事项界面,每个待办事项都有一个“标记为完成”按钮。对于每个项目,你可以在添加后将其标记为完成,并且它将更改该项目的显示,让你知道它已完成。实际上并没有什么特别复杂的事情,只是一个非常简单的应用程序设计。让我们看看这个设计的可能样子(从非常高的层面来看):

图片

为我们的应用程序构建基准功能

现在我们已经了解了设计,我们将直接开始创建项目并构建。虽然应用程序本身并不特别复杂,但整体上具有一定的复杂性,并且我们将最终需要编写相当多的代码。为了使一切正常工作,我们将分离应用程序的关注点,并确保我们在有限范围内构建的内容仍然非常类似于在现实世界应用程序中构建的内容!

创建我们的项目

与上一个项目类似,我们将使用 Create React App 创建一个新的项目,我们将称之为 todoifier

$ create-react-app todoifier

在项目创建完成后,我们将通过在项目上运行 start 来验证一切是否正确设置并运行:

$ yarn start

初始化我们的组件以构建在之上

在构建你的项目时,你应该努力使顶级组件(在我们的例子中是 src/App.js)尽可能简单,并尽可能少地将其中的代码放入其中。在我们的例子中,我们将移除所有内容(类似于我们在上一章第一章,介绍 Create React App 2)中完成的工作,并用一个简单的替换标题和一些其他内容来替换它:

import React from 'react';
import './App.css';

const App = () => (
 <div className="App">
   <h2>Todoifier</h2>
 </div>
);

export default App;

我们还希望从 App.css 中移除所有内容,因为我们在这个项目中也将采取空白石板的方法!

在单独的文件中构建独立的组件

Create React App 最令人愉快的事情之一是它使导入其他文件作为它们自己的独立 React 组件的过程变得非常简单,而无需你真正思考 Webpack 如何组织一切。我们将从构建一个新的简单组件开始。让我们创建一个 Todo 组件来跟踪我们将需要添加的每个 Todo 项目。

src/Todo.js 中,我们希望复制 App.js 中的所有内容(除了 className 属性中的字符串和函数名):

import React from 'react';
import './Todo.css';

const Todo = () => <div className="Todo">I am an item</div>;

export default Todo;

这里没有什么令人兴奋的内容可以讨论,所以我们将继续前进!我们还应该创建一个 Todo.css 文件,以确保我们的组件不被未设置样式:

.Todo {
  border: 2px solid black;
  text-align: center;
  background: #f5f5f5;
  color: #333;
  margin: 20px;
  padding: 20px;
}

如果我们不做任何事情,我们将看不到我们刚刚创建的华丽新 Todo 组件的结果,因此我们需要回到 src/App.js 并更改代码。我们将从在顶部添加一个 import 语句来导入 Todo 组件开始!记住,我们是从本地文件系统加载这个文件,而不是从安装的依赖中加载:

import Todo from './Todo';

我们还需要在源代码中包含 Todo 组件,以便在重新渲染页面时显示:

const App = () => (
  <div className="App">
    <h2>Todoifier</h2>
    <br />
    <Todo />
  </div>
);

我们在这里添加的只是 Todo 组件,它被渲染在 App 组件的主根 div 中。当浏览器刷新(假设你已经保存),你应该能看到 Todo 组件出现并准备好使用!

这个整个过程最令人兴奋的部分是,我们已经通过这种方式引入了更好的代码标准和可复用性。Todo组件已经被完全提取出来,所以如果我们想在App组件中包含多个Todo组件,我们只需复制粘贴几行代码,而不需要做任何更复杂的事情。

这听起来相当不错,所以让我们自己尝试一下,验证一切是否如我们所预期。回到App组件,添加一些更多的Todo组件作为 JSX 标签:

const App = () => (
  <div className="App">
    <h2>Todoifier</h2>
    <br />
    <Todo />
    <Todo />
  </div>
);

当我们在App组件的根目录中声明两次Todo时,我们应该看到这两个组件出现:

有了这个,我们得到了相当多的可复用性,而且几乎不需要付出任何努力!然而,存在的问题是这里没有变化。组件只是盲目地重复,我们更希望它能够做些不同的事情,比如为每个Todo显示不同的内容。我们可以通过引入两个新概念来实现这一点:状态属性!我们稍后会谈到状态,让我们先从属性开始,以尽可能简单的方式实现所有这些。

介绍 props

那么,什么是 props?Props 是属性的缩写,正如你可以猜到的,它们定义了我们 React 组件内部的属性。一般来说,这些属性是从父组件传递过来的,尽管实际上它们可以从任何地方传递过来。

现在,我们只是使用一个简单的函数组件,而这个函数在其签名中并没有指定任何参数,所以如果我们想开始使用 props,我们首先需要改变这一点。

让我们打开Todo组件在src/Todo.js中的代码,并将函数声明更改为传递一个props参数:

const Todo = props => {

这大致相当于我们用纯 JavaScript 编写以下内容:

function Todo(props) {

接下来,我们必须更改显示文本,以便实际使用props参数中的某个内容,所以我们将添加对{props.description}的引用:

const Todo = props => <div className="Todo">{props.description}</div>;

保存文件,因为我们现在需要回到我们的主要App组件(src/App.js)并开始将description作为属性传递给我们的Todo组件:

const App = () => (
  <div className="App">
    <h2>Todoifier</h2>
    <br />
    <Todo description="Do the thing" />
    <Todo description="Do another thing" />
  </div>
);

保存文件并看到浏览器窗口刷新后,我们应该期望看到我们刚刚输入的属性现在显示在浏览器中,如下所示:

就这样!可复用、可修改的组件,几乎不需要任何努力就完成了!

更好的部分是,对props的任何更改都会触发 React 重新渲染该组件(取决于更改了什么以及在哪里更改)。这非常有用,尤其是在考虑到旧世界需要你检查更改,然后尝试在飞行中删除和重新创建元素,或者试图在不删除所有内容的情况下悄悄地更改更改的情况下。

Props 总体来说很棒,但如果我们想要做一些更持久的事情,或者更好地存储随时间变化的东西,我们需要引入状态的概念。与 props 不同,状态是用来表示不断变化的东西的,通常局部于单个组件;你将通过 props 将状态传递给需要它的子组件。

问题在于我们目前使用的是函数组件,这对于现在来说是可以的,但当我们想要开始跟踪任何类型的内部状态时,我们就需要切换到创建 React 组件的不同方法。

编写基于类的组件

ECMAScript 6ES6)中,我们第一次在 JavaScript 中尝到了真正的面向对象编程的滋味,那就是。类的声明方式与我们的函数组件有根本的不同,但大部分核心原则仍然相同,我们不需要学习太多就可以开始使用它们。

我们首先需要做的是对src/Todo.js中的import语句进行一些小的修改。我们需要import的不只是 React 本身:我们还需要importReact 中指定的一个命名导出,称为Component让我们看看新的import语句是什么样子的:

import React, { Component } from 'react';

我们已经导入了Component,所以让我们看看声明class的语法:

class Todo extends Component { /* ... */ }

这告诉 JavaScript 我们正在构建一个新的Todo类,它继承自Component的功能(因此有extends关键字)。接下来,任何作为 ES6 类构建的 React 组件都需要声明一个render()函数。要在类中声明一个函数,你只需在类定义中写出名称、参数和函数体:

functionName(argument1, argument2) { /* ... */ }

React 特别要求我们声明一个不带参数的render()函数,正如我们之前提到的。我们的return语句与我们在之前的函数组件中的相同,所以将所有内容组合起来,我们应该得到类似以下的内容:

class Todo extends Component {
  render() {
    return <div className="Todo">{this.props.description}</div>;
  }
}

在这里,我们编写我们的render() { … }函数,它基本上没有变化,除了一个小变化:props.description现在变成了this.props.description

原因在于 props 不再是函数的简单参数了。它实际上是类特定属性的一部分,因此我们需要告诉 JavaScript 当我们说props时,我们实际上是指这个类的局部 props。我们只是用this.props来简写它!这样我们就解决了这个问题,接下来我们可以更深入地探索状态的世界!

将状态引入我们的组件

声明状态到类组件的一部分是开始一个初始或默认状态。如果我们不告诉 JavaScript 当我们的类实际实例化时应该做什么,我们就无法做到这一点,所以我们的类需要一个constructor来处理这项工作。在我们的Todo类中,我们将构建我们的constructor函数,它将props作为其单个参数接收:

  constructor(props) {
    super(props);
    this.state = {
      description: props.description,
      done: false
    };
  }

JavaScript 知道使用constructor()作为我们的构造函数,因为这是一个语言构造,我们知道它需要接收props。由于我们是在扩展 React 的Component类,我们需要在constructor()的第一行代码中调用super()。这告诉 JavaScript 使用Componentconstructor()中的代码来设置它需要设置的内容。接下来,我们通过声明一个新变量并将其附加到我们的类上,命名为(有点无聊地)this.state来设置状态。我们将其设为一个带有description键的普通对象,它只是存储在props参数上传递的描述。它还有一个名为done的属性,初始值为false(因为我们不应该创建已经完成的任务)。仅此代码本身并不会做任何事情,所以让我们也改变我们的render()函数以利用我们的state

  render() {
    return <div className="Todo">{this.state.description}</div>;
  }

目前还没有什么变化。相反,我们需要添加某种形式的交互性,以真正了解使用state的好处!

通过状态修改添加交互性

我们将在我们的Todo组件中添加一个非常简单的button,命名为Mark as Done。当点击时,这个button应该将这个Todo项的state done状态改为true。现在,我们想要确保只有在这个组件上改变,而不是所有组件上改变,这是使用内部状态的一个重要部分!让我们首先构建我们的markAsDone()函数:

markAsDone() {
  this.setState({ done: true });
}

做完这些后,我们可以继续实现我们的功能,包括我们的Mark as Done按钮:

  render() {
    return (
      <div className={'Todo' + (this.state.done ? ' Done' : '')}>
        {this.state.description}
        <br />
        <button onClick={this.markAsDone}>Mark as Done</button>
      </div>
    );
  }

现在,如果我们只是保存,等待刷新,然后尝试点击markAsDone按钮,我们最终会得到一个错误信息:

图片

让我们更深入地探索这个错误信息。我们得到了一个 TypeError: this is undefined 的消息,这绝对不是世界上最清晰的错误信息,当然。这是使用 ES6 类与任何类型的 React 组件以及 JavaScript 事件处理器结合使用的一个缺点。所以在这种情况下,当我们的onClick调用this.markAsDone时,函数进入markAsDone的主体,它试图调用this.setState,但它实际上并不理解this试图引用什么!这仅发生在事件处理器中,所以我们不需要总是担心这个问题。好消息是,有一个简单的方法可以解决这个问题。让我们在我们的constructor中再添加一行,如下所示:

this.markAsDone = this.markAsDone.bind(this);

这告诉 JavaScript,如果它看到markAsDone函数内部对this的引用,它是对Todo类的特定引用。保存文件并点击按钮——它工作了!好吧,你还不能确定它是否真的工作。我们需要添加一些视觉指示来表明它已经工作。

使用 CSS 表示我们的状态

我们的工作几乎已经完美无缺,但我们还缺少一些代码来告诉我们的 React 组件何时使用某些 CSS 类。让我们先添加一个新函数 cssClasses(),它将返回一个包含在我们组件中的 CSS 类列表:

  cssClasses() {
    let classes = ['Todo'];
    if (this.state.done) {
      classes = [...classes, 'Done'];
    }
    return classes.join(' ');
  }

这里的特别之处仅在于使用了 JavaScript 扩展运算符(即 ...classes 部分)。这仅仅是我们以安全的方式向数组末尾添加内容的一种方法。接下来,我们将改变声明我们组件的 className 的逻辑,以便使用这个新函数:

  render() {
    return (
      <div className={this.cssClasses()}>
// ...

最后,在 src/Todo.css 中添加新的 .Done CSS 类定义:

.Done {
  background: #f58888;
}

现在,我们可以看到点击标记为完成的按钮时的结果:

图片

进一步迭代我们的项目

好的,每个组件中都有一些状态和属性;我们有可以按需使用和重用的组件,我们可以看到一点交互性,并且我们组件之间的分离做得很好。这让我们离编写更复杂和困难的 React 组件更近了一步。更重要的是,我们正在构建一个更大、更复杂的应用程序,这将需要我们作为 Create React App 工具集一部分的一些功能。

构建列表组件

让我们进一步扩展我们的 Todo 组件,并实际创建一个动态的组件列表,我们可以将其添加到列表中!我们需要首先添加一个新的 TodoList,它负责渲染我们的 Todo 组件列表!

我们将首先添加两个新文件来处理我们的 Todo 列表:src/TodoList.jssrc/TodoList.css。在我们的 src/TodoList.js 文件中,我们将从一个相当标准的 React 框架开始(你将经常编写类似的内容,所以这会很快成为你的第二天性):

import React, { Component } from 'react';
import Todo from './Todo';
import './TodoList.css';

class TodoList extends Component {

}

export default TodoList;

注意,我们类的主体目前是空的。接下来,我们需要添加一个 render() 函数,让我们直接跳到那里:

render() {
  return (
    <div className="TodoList">
      <Todo description="Item #1" />
      <Todo description="Item #2" />
    </div>
  );
}

我们还需要修改 TodoList 的样式表,使其不仅仅是默认的:

.TodoList {
 margin: 20px;
 padding: 20px;
 border: 2px solid #00D8FF;
 background: #DDEEFF;
}

将 TodoList 添加到我们的 App 中

目前,我们只是通过复制粘贴组件多次来显示 Todo 项的列表,但这并不有趣,也不是好的编程实践!相反,让我们将我们的 List 组件添加到 App 中,并使其负责处理多个项。我们将首先从本地文件系统导入新的 TodoList 组件到 src/App.js

import TodoList from './TodoList';

我们还需要修改 render() 函数,以便使用 TodoList 组件,而不是直接使用两个 Todo 组件:

const App = () => (
  <div className="App">
    <h2>Todoifier</h2>
    <br />
    <TodoList />
  </div>
);

一切看起来几乎都一样,但因为我们稍微改变了样式表,所以整个列表周围应该有一个干净的蓝色框。这有助于我们区分每个组件及其周围的父组件。

为 TodoList 添加状态

在我们能够做更多事情之前,我们需要在 src/TodoList.js 中添加一些 state,所以我们将创建一个初始的 state,它可能不是那么令人兴奋,但能完成任务。给 TodoList 组件添加一个 constructor,并给它以下内容:

constructor(props) {
  super(props);
  this.state = { items: ['Item #1', 'Item #2'] };
}

创建和使用辅助的 render() 函数

创建和初始化状态,但不使用它对我们帮助不大,所以我们要确保所有的 JSX 都是在我们状态的帮助下构建的!我们必须遍历存储在我们状态中的每个 Todo 项目,我们将它命名为 this.state.items,对于每个项目,我们将渲染 Todo 组件,并使用属性传递该 Tododescription

我们将在这里特别使用 map 函数,因为 map 将遍历每个项目,执行一个函数,然后将结果存储为数组。JSX 期望我们返回一个单一的 JSX 元素或一个 JSX 元素的数组,所以这非常适合我们的需求。我们还将把这个任务委托给一个新的函数,称为 renderItems(),以确保我们的每个函数都服务于单一的小目标:

renderItems() {
  return this.state.items.map(description => (
    <Todo key={description} description={description} />
  ));
}

这里唯一的新增是添加了 key 属性。这是在 React 中通过 JSX 添加多个项目的一个重要部分:React 必须以某种独特的方式知道如何引用相关项目。如果 React 要更改某些内容、删除它或以其他方式影响 DOM,它必须有一些东西可以用来引用特定的项目。

在这里,我们并没有对名字列表做出任何保证;如果我们最终出现任何重复,这将会给我们带来问题,但这是我们现在的天真实现。

返回到 render() 函数,我们将添加对新创建的 renderItems() 函数的引用,而不是对 Todo 的多次调用:

render() {
  return <div className="TodoList">{this.renderItems()}</div>;
}

为了更加确信,让我们也在 constructor 中添加第三个项目到我们的初始状态。如果我们也能验证这一点,那么我们就知道我们已经正确地实现了所有内容:

constructor(props) {
  super(props);
  this.state = { items: ['Item #1', 'Item #2', 'Item #3'] };
}

就这样!三个项目,都按预期工作,并且完全依赖于 state!这是一个相当好的进度指标!

创建一个新的 Todo 组件

现在我们已经对动态状态影响我们的 DOM 有了一个良好的初始尝试,是时候创建一个新的组件,允许我们向 TodoList 添加额外的 Todo 项目了。我们将称之为 NewTodo!按照惯例,首先创建 src/NewTodo.jssrc/NewTodo.css。然后在 src/NewTodo.css 中,给它一些默认样式:

 .NewTodo {
  margin: 20px;
  padding: 20px;
  border: 2px solid #00FFD8;
  background: #DDFFEE;
  text-align: center;
 }

然后,是我们构建 NewTodo 组件的时候了!我们开始于我们经常使用的 React 模板代码:

import React, { Component } from 'react';
import './NewTodo.css';

class NewTodo extends Component {
}

export default NewTodo;

接下来,我们将构建我们的 constructor() 函数:

  constructor(props) {
    super(props);
    this.state = { item: '' };
    this.handleUpdate = this.handleUpdate.bind(this);
  }

我们开始于对 super() 的调用,就像往常一样。接下来,我们将设置一个初始状态,其中包含一个 item 属性,它一开始是空的(关于这一点我们稍后再说)。我们还需要编写一些处理更新的内容,所以我们将在一个 handleUpdate() 函数上编写一个 bind 语句(我们将在下一个步骤中编写):

  handleUpdate(event) {
    this.setState({ item: event.target.value });
  }

因此,当 handleUpdate() 被调用时,它将接收一个 DOM 事件,如果我们想获取正在更改的输入的值,我们可以通过 event.target.value 来获取它。最后,让我们调用我们的 render() 函数:

render() {
  return (
    <div className="NewTodo">
      <input type="text" onChange={this.handleUpdate} />
      &nbsp;&nbsp;
      <button>Add</button>
    </div>
  );
}

这段代码大部分都很普通,但请注意,我们这里有一个 input,它是一个 text 类型,每次输入值改变时都会通过委托处理程序到我们已编写的 handleUpdate() 函数来响应!

是时候回到我们的 TodoList,导入 NewTodo 组件,并将其添加到 render() 调用的顶部。在 src/TodoList.js 的顶部添加以下内容:

import NewTodo from './NewTodo';

然后,将 NewTodo 添加到 render() 函数中:

render() {
  return (
    <div className="TodoList">
      <NewTodo />
      {this.renderItems()}
    </div>
  );
}

将函数作为属性传递

这引入了一个非常有趣的“鸡生蛋,蛋生鸡”的场景:我们如何从子组件向父组件添加组件?Todo 项的列表位于 TodoList 中,而我们需要添加新 Todo 的组件是一个独立的组件,它位于 TodoList 内部!NewTodo 中没有 Todo 列表的内部状态,那么我们如何让它工作呢?

简单!我们将在 TodoList 中创建一个函数,该函数可以修改其组件列表,然后将其传递给我们的 NewTodo 组件。所以,在 src/TodoList.js 中,我们需要添加一个名为 addTodo() 的新函数,并确保它包含一个 bind() 语句,这样无论该函数在哪里,它都知道如何处理对 this 的引用。在 constructor 中添加我们的 bind 语句:

this.addTodo = this.addTodo.bind(this);

让我们继续编写我们的 addTodo() 函数。我们将接受一个单独的字符串,它将是我们添加的描述。好消息是这个函数非常容易编写:

addTodo(item) {
  this.setState({ items: [...this.state.items, item] });
}

我们在这里使用了一些新的 JavaScript 语法,一个数组展开。这允许我们本质上通过添加新项目来简化操作!本质上,我们想要将新项目添加到 state 中的项目列表中,但我们希望以一种非破坏性的方式做到这一点。这将创建一个修改后的项目列表副本并保留原始列表。我们将项目列表设置为这个新修改后的数组,这就完成了!我们接下来要做的就是只需将这个新的 addTodo 函数作为属性传递给 NewTodo

render() {
  return (
    <div className="TodoList">
      <NewTodo addTodo={this.addTodo} />
      {this.renderItems()}
    </div>
  );
}

让我们回到 src/NewTodo.js。我们需要复制我们的函数名,所以我们在 NewTodo 中添加一个 addTodo 函数。这个函数将通过 JavaScript 事件处理程序来调用,所以我们需要在 constructor 中为它添加一个 bind 语句:

this.addTodo = this.addTodo.bind(this);

注意以下内容,关于我们的 addTodo() 函数体:

addTodo() {
  this.props.addTodo(this.state.item);
  this.setState({ item: '' });
}

记得我们通过属性传递下来的 addTodo() 函数吗?我们需要通过对象上的属性调用该函数,并将 state 中的 item 属性传递进去。记住,item 是通过我们的 onChange 事件处理程序不断更新的值!最后,让我们修改 render() 来将其全部组合起来:


  render() {
    return (
      <div className="NewTodo">
        <input
          type="text"
          onChange={this.handleUpdate}
          value={this.state.item}
        />
        &nbsp;&nbsp;
        <button onClick={this.addTodo}>Add</button>
      </div>
    );
  }

我们需要添加一个新的值属性,并将其设置为stateitem属性的当前值。如果不这样做,我们就无法看到当我们清除stateitem属性时发生了什么。最后,我们添加了一个新的onClick事件处理程序,它只是调用addTodo,就像我们准备的那样!

尝试一下,我们就有了:交互性!

删除项目同样重要

如果我们在添加项目,我们也应该删除它们,所以我们在TodoList中实现一个removeTodo()函数,然后它将传递到每个Todo中。这与我们在NewTodo组件中所做的是非常相似的。我们需要遵循相同的步骤:添加一个bind语句,编写removeTodo()函数,并在Todo组件中实现调用它。

首先,src/TodoList.js中的bind如下所示:

  constructor(props) {
    super(props);
    this.state = { items: ['Item #1', 'Item #2', 'Item #3'] };

    this.addTodo = this.addTodo.bind(this);
    this.removeTodo = this.removeTodo.bind(this);
  }

接下来,我们将实现removeTodo()函数。我们将filter掉任何匹配我们要删除的项目的新 Todos 列表:

  removeTodo(removeItem) {
    const filteredItems = this.state.items.filter(description => {
      return description !== removeItem;
    });
    this.setState({ items: filteredItems });
  }

我们需要做的最后一件事是更改renderItems()调用,以便将这个新函数传递给每个Todo

  renderItems() {
    return this.state.items.map(description => (
      <Todo
        key={description}
        description={description}
        removeTodo={this.removeTodo}
      />
    ));
  }

最后,我们准备在子组件中实现这个功能。打开src/Todo.js,我们将在Todo组件内部实现一个同名removeTodo()函数。我们还需要一个bind,所以我们将从constructor开始这个实现:

this.removeTodo = this.removeTodo.bind(this);

然后,我们将编写removeTodo()函数:

removeTodo() {
  this.props.removeTodo(this.state.description);
}

我们需要做的最后一件事是通过一个button和一个onClick事件处理程序添加一个调用,并调用组件的removeTodo()函数:

render() {
 return (
 <div className={this.cssClasses()}>
 {this.state.description}
 <br />
 <button onClick={this.markAsDone}>Mark as Done</button>
 <button onClick={this.removeTodo}>Remove Me</button>
 </div>
 );
}

保存后刷新浏览器,你现在应该能够即时添加和删除项目!完全的交互性!请参考以下截图:

摘要

到现在为止,你应该对 React 有了深刻的理解,包括 React 的工作原理、如何编写良好的 JSX 和 React 代码,以及你可能会遇到的各种问题和关注点。我们在完成工作的同时涵盖了所有这些内容,而且从未需要离开 Create React App。现在,我们应该有以下几点:

  • 一个正在工作的 Create React App 项目

  • 更复杂的应用程序结构

  • 理解如何通过传递变量和函数作为属性来影响父结构

  • 如何绑定可能从事件处理程序内部调用的函数

随着我们继续本章的学习,我们将更深入地探讨 Create React App 的其他功能和它所支持的功能。对我们来说,重要的是我们的应用程序已经构建并准备好,这样我们就有空间迭代和探索 Create React App 的真正深度。现在我们已经准备好了,我们可以尽情地玩转各种东西!

第三章:创建 React App 和 Babel

如果你来自一个可能已经很久没有使用 JavaScript 的背景,或者你可能对 JavaScript 和 Create React App 是使用它的途径,你可能会在本书的各个部分以及探索他人的项目时看到很多语法,并发现没有对正在发生的事情有彻底的了解,代码很难阅读。本章旨在为那些新接触 Babel 的人架起桥梁,并描述它为我们的 Create React App 项目带来了什么。

本章的另一个目标是针对已经熟悉 JavaScript 但对其引入的功能和默认在 Create React App 项目中启用的 Babel 感兴趣的人,这样你就可以在你的项目中充分利用所有的生活质量改进和更简洁的语法规则,真正为你的项目增添最后一笔。目标是教授你生产就绪的代码,以便你能够在你的 React 项目中达到最高水平的贡献。

随着我们学习本章,你可以期待掌握以下主题:

  • 常量变量

  • 新的箭头函数语法

  • 数组和对象解构

  • 数组和对象扩展操作符

  • React 片段

Babel 和最新的 JavaScript 语法

我们一直在构建这个应用程序作为我们的基础,在这个过程中,我们引入了许多可能与你习惯编写的 JavaScript 不同的语法。例如,我们使用这种语法编写了一些函数:

const foo = () => {
  doSomething();
  doSomethingElse();
}

这里的语法并不特别复杂,你可能能够弄清楚发生了什么,但也许你并不完全理解所有这些最终是如何变成一个函数的。你可能更习惯于按照以下类似模式编写函数:

var foo = function() {
  doSomething();
  doSomethingElse();
}

或者可能更类似于没有变量的函数声明,如下面的函数:

function foo() {
  doSomething();
  doSomethingElse();
}

事实上,随着 JavaScript 的发展,有新的、更高效的方法来编写各种不同的语言结构。一些提供了有用的快捷方式,或者可能为开发者提供了更好的生活质量改进。由于 Create React App 在 Node.js 上运行,我们在这里得到了一些语法改进,但一般来说,Node 以更慢的速度将新功能和语法整合到其标准库中。

在本章的进程中,我们将更深入地探讨如何在 Create React App 项目中编写、整合以及最重要的,理解 现代 JavaScript 代码。我们将查看当前在 Create React App 中支持哪些功能,并学习如何充分利用每一个功能!

什么是 Babel?

Babel 旨在弥合JavaScript 将拥有的功能Node.js 目前支持的功能之间的差距,并将生成的代码转换为 Node.js 可以理解的形式。这意味着即使 Node 选择不支持某些功能(或者可能由于与现有系统的不兼容性而无法支持),你仍然有解决方案!

理解历史

要理解为什么 Babel 默认集成到 Create React App 项目中,你需要了解一点它的历史(类似于为什么了解 Create React App 被转化为工具的历史是有帮助的)。在方便的 CLI 工具出现之前,很多配置都是手动完成的,而且,通常这些项目要么是针对 vanilla Node.js 构建的,要么是直接在浏览器上构建的。你想要做的任何 JavaScript 都局限于在 Node.js 的每个版本中支持的最小功能集,或者是在你的应用程序中选择的每个浏览器上支持的功能集。

因此,你最终基本上对任何有趣或生活质量的提升都没有支持。想象一下以下表格:

浏览器 功能 A 功能 B 功能 C 功能 D
Internet Explorer
Firefox
Chrome
Safari
我的代码可以支持的功能

以之前的表格为例,我们可以在项目中支持功能 B,而绝对不支持其他任何功能!知道你可能会支持你编程语言中一些非常棒的功能,但由于一些用户会有负面或完全糟糕的体验,却无法使用它们,这是最令人沮丧的事情。

也许你和你的公司决定想要减少官方支持的浏览器数量,以便可以使用这些新功能。现在你需要做出决定,牺牲你的用户群,疏远新旧用户,以便使用更新的语言功能。也许这会给其他用户带来更好的体验,最终证明这是值得的,但那些决定需要谨慎做出,并考虑有多少用户在使用什么。例如,如果你决定只支持 Safari,你将疏远每一个 Windows 用户,而不仅仅是每一个 Internet Explorer 用户。

这些决定很重大,对应用程序的健康状况有长期影响。在项目生命周期的开始就疏远用户群可能意味着它根本无法恢复!

Babel 在这个谜题中扮演什么角色?

Babel 在这里伸出援手,说:“嘿,我们会给你那些你想要使用的语言特性,但不是所有浏览器都支持。”当你开始使用越来越大的代码库,并绕过一些你过去必须用来构建大型 JavaScript 应用程序的糟糕方式时,这会带来巨大的安慰!现在,如果你想使用导入、新语法以及其他任何东西,你都可以这样做!

Babel 作为一个转换器,这是一个非常华丽的说法,意思是它将你的 JavaScript 代码(可能不是所有内容都能被理解)转换成可以被理解的 JavaScript 代码!根据不同的配置、设置和所谓的阶段,Babel 将允许你选择使用各种新的语法和语言特性,并确保你的代码能在大多数现代浏览器上运行!当然,没有任何东西是万无一失的,你当然会发现一些特定浏览器不支持的场景。不幸的是,你不可能赢得所有比赛!

使用 Babel 探索现代 JavaScript

在本节中,我们将探讨 Babel 允许我们作为代码主要部分的现代 JavaScript 技巧和技术。我们将查看我们可以实现各种不同代码和模式的不同方式,探讨 JavaScript 标准方式执行某些技巧与 Babel 允许我们使用的简写语法之间的差异。首先是变量声明的添加,特别是如果你很久没有编写现代 JavaScript 的话,比如constlet

let变量允许我们声明一个具有非常具体作用域规则的变量。虽然var的作用域是最近的函数块,并因此被使用,但let的作用域是最近的块,并且在其声明之前不能使用。你也不能用let重新声明具有相同名称的变量。

const变量允许我们声明一个具有与let相同作用域规则的常量。总的来说,最佳实践是尽可能多地使用这两个变量,而不是使用var。事实上,如果我知道我正在处理的代码支持constlet,我个人从来不会使用var

现在,让我们继续到我们作为应用程序代码遇到的第一件更复杂的事情:JSX!

JSX

让我们看看一个非常简单的 JSX 代码示例;与我们已经编写过的代码非常相似。我们最好从简单开始,逐步构建,这样你就可以看到 JSX 实际上是如何帮助我们更快、更智能地编写代码的。

首先,这是一个简单的 React 中的HelloWorld div

const HelloWorld = () => (<div>Hello World</div>);

正如我说的,目前还没有什么特别复杂或困难的东西。让我们来看看这个的纯 JavaScript 版本:

function HelloWorld() {
  return React.createElement('div', null, 'Hello World');
}

最终它做的都是同样的事情:它创建了一个 HelloWorld React 组件,然后该组件本身包含一个带有 HelloWorld 主体文本的单个 div。当您必须开始包括子组件时,事情开始变得复杂。使用我们之前的 HelloWorld 组件,让我们扩展它,并使我们要问候的人可配置:

const HelloWorld = props => (<div>Hello {props.name}</div>);

在常规 JavaScript 中的类似操作如下:

function HelloWorld(props) {
  return React.createElement("div", null, `Hello ${props.name}!`);
}

让我们打开我们的项目,并尝试改变现有项目中的一些语法,以使用创建 React 组件和元素的非 JSX 方法。我们的目标是,我们应该达到在 TodoList 组件中的每个 Todo 项目之间添加一个小分隔符的程度。我们不需要做太多修改就能使它工作,但我们将使用非 JSX 方法来构建 Divider 组件。我们将从创建 src/Divider.jssrc/Divider.css 开始,然后首先编写 src/Divider.js

import React from "react";
import "./Divider.css";

function Divider() {
 return React.createElement(
 "div",
 { className: "Divider" },
 React.createElement("hr")
 );
}

export default Divider;

在这里我们并没有做太多额外的事情;我们创建了一个具有 Divider 类的 div 容器(你从这里可以看到为什么 class 在 React JSX 中不可用;这没有意义,因为 class 是我们用来声明类(如我们的基于类的有状态组件)的)。在我们的函数中,它不接受任何额外的属性,我们返回 React.createElement() 函数的结果。React.createElement() 调用有三个参数:我们正在创建的主要元素(可以是 HTML 标签,如 divhr,或者函数或变量的完全限定名称,如 Todo),然后是一个对象,包含传递给该元素的属性,最后是应该存在于该组件内部的子元素数组。

接下来,我们将用一些花哨的 CSS 来填充我们的 src/Divider.css,使我们的 hr 变成基于渐变的分隔线:

hr {
  border: 0;
  height: 1px;
  background-image: linear-gradient(
    to right,
    rgba(0, 0, 0, 0),
    rgba(0, 0, 0, 0.8),
    rgba(0, 0, 0, 0)
  );
}

接下来,进入 src/TodoList.js,我们将 import 我们的新 Divider 并修改一些代码以包含新的分隔符。首先,我们从顶部开始 import

import Divider from "./Divider";

然后,我们将实际上将 Divider 放置在代码中。我们需要进入 renderItems() 函数,并将主体改为将 Todo 包裹在一个 div 容器中(我们可以通过使用 React Fragments 来解决这个问题,但我们会稍后再详细讨论),然后在底部包含 Divider 组件。此外,请注意,在下面的 JSX 代码中的每个关键属性中,我们都在 "description" 前缀加上我们正在构建的组件的简要描述,以避免冲突:

  renderItems() {
    return this.state.items.map(description => (
      <div key={"div-" + description}>
        <Todo
          key={description}
          description={description}
          removeTodo={this.removeTodo}
        />
        <Divider key={"divide-" + description}/>
      </div>
    ));
  }

保存并重新加载,我们应该有一些新的分隔线来分隔我们的 Todo 项目:

图片

然后,我们就有了,一个完全用纯 JavaScript 编写的 Divider

下一个代码片段只是作为一个示例,展示一个完整的、更复杂的函数在没有 JSX 的情况下会是什么样子。你实际上不需要做任何这项工作!

为了好玩,让我们看看在 TodoList 中的两个 render() 函数调用在没有 JSX 的情况下会是什么样子:

  renderItems() {
    return this.state.items.map(description =>
      React.createElement("div", { key: "div-" + description }, [
        React.createElement(Todo, {
          key: description,
          description: description,
          removeTodo: this.removeTodo
        }),
        React.createElement(Divider, { key: "divider-" + description })
      ])
    );
  }
  render() {
    return React.createElement("div", { className: "TodoList" }, [
      React.createElement(NewTodo, { addTodo: this.addTodo }),
      this.renderItems()
    ]);
  }

所有的功能都将保持完全相同,所以如果你想要追求这个,并且你更喜欢这种语法而不是 JSX,这仍然是你的一种选择。

函数语法

让我们也花一点时间来谈谈在使用 Babel 和 Create React App 配对时可以利用的不同方式来编写函数。在我们之前写的代码中,我们已经对此进行了一些讨论,并展示了几个替代函数语法的示例,但现在我们将更深入地探讨所有这些内容。

最终,在 JavaScript 中,有一些标准的方式来声明函数,没有任何花哨的东西。我们可以选择使用 function 关键字声明函数的方法,或者将其声明为一个变量。让我们看看一些例子:

function sayHello(name) {
  console.log(`Hello ${name}!`);
}

我们也可以这样写,不使用 Babel 的任何花哨功能,通过以下代码:

var sayHello = function(name) {
  console.log(`Hello ${name}!`);
}

在构建函数之后,我们调用该函数的方式只是简单地通过以下方式:

sayHello('Mason');

现在,回想一下我们在前两章中编写的一些其他函数。我们经常使用 const 语句来定义函数,这会把我们之前写的 function 转换成以下形式:

const sayHello = name => {
 console.log(`Hello ${name}!`);
};

虽然这实际上与旧 JavaScript 中声明函数的变量方法非常相似,但在语法上有细微的差别,这是值得指出的,那就是在代码中声明函数签名的方式。

在之前,在我们的变量声明之后,我们会写一个函数,然后是带有参数的括号。在现代 JavaScript 中,我们可以使用一种叫做 箭头函数 的东西。箭头函数是一种简写语法快捷方式,在 this 绑定方面有一个额外的优势。具体来说,函数声明时的 this 上下文是函数声明时的 this 上下文。而不是函数自己获取并定义自己的 this 上下文,它使用当前作用域中的 this

声明箭头函数的规则相当简单:

  • 如果你没有参数,你必须使用圆括号声明函数,然后是粗箭头 (=>):
const foo = () => { return "Hi!"; }
  • 如果你有一个参数,你可以选择性地包含圆括号:
const foo = name => { return `Hi ${name}!` };
  • 如果你有两个或更多参数,你必须包含圆括号:
const foo = (greeting, name) => { return `${greeting} ${name}!`; } 
  • 如果你作为单行函数返回某个东西,你不需要使用花括号或 return 语句:
const foo = (greeting, name) => `${greeting} ${name}`;
  • 如果你作为多行函数返回某个东西,你必须使用花括号和 return 语句:
const foo = (greeting, name) => {
    const message = greeting + " " + name + "!";
    return message;
};

解构赋值

现代 JavaScript 也给了我们更好的解构访问。解构是一种匹配数据结构(例如,在数组或对象中)的模式,并且能够将这些模式转换为函数参数或变量声明中的单个变量。让我们通过几个不同的例子来玩一玩,以更好地了解解构是如何工作的,以及我们如何更好地利用它。打开src/App.js,我们将多次使用解构。在我们做出更改之前,App函数应该看起来像以下代码:

const App = () => (
  <div className="App">
    <h2>Todoifier</h2>
    <br />
    <TodoList />
  </div>
);

目前还没有什么激动人心的东西,所以让我们让这段代码变得有趣!我们首先允许你重命名你的应用,因为也许你并不觉得Todoifier是一个很好的应用名称!我们将在代码上方添加一个简单的数据结构:

const details = {
  header: "Todoifier",
  headerColor: "red"
};

接下来,我们将这个数据结构解构到一个单独的变量名中。我们将在声明我们的details数据结构之后添加以下行:

const { header } = details;

我们在这里所做的就是重新编写我们在details变量中创建的数据结构的结构,然后说我们想要它取details变量中header键的值,忽略其他所有内容,然后将它抛入header变量中。最终结果是,我们应该在header变量中看到Todoifier。为了确保这一点,让我们抛出一个console.log语句来验证结果:

console.log("appName is " + appName);

如果一切顺利,我们应该在我们的浏览器 JavaScript 控制台中看到它:

就这样!现在我们知道这可以工作,让我们回到App组件,并添加对header变量的引用:

const App = () => (
  <div className="App">
    <h2>{appName}</h2>
    <br />
    <TodoList />
  </div>
);

当我们的页面刷新时,我们应该看到你在details变量中的header值所抛出的任何值!让我们让它更简洁一些,更接近你通常在生产代码中期望看到的样子,因为现在我们写的代码有点冗余。删除对appName的引用和console.log语句,我们将编写一个新的函数用于我们的组件:

const headerDisplay = ({ header: title, headerColor: color }) => (
  <h2 style={{ color: color }}>{title}</h2>
);

我们实际上在这里使用了一些单独的技巧!我们使用了新的函数声明语法和简单的函数return语法,我们还使用了解构来使我们的代码超级简单和整洁!我们解构传入的参数以提取titleheaderColor,并将它们分别存储在titlecolor变量中!

我们然后将这些传递给h2标签,以设置 CSS 的color样式和应用的显示title!最后一步是我们需要将这个组件连接到使用我们刚刚定义的新header函数:

const App = () => (
  <div className="App">
    {headerDisplay(details)}
    <br />
    <TodoList />
  </div>
);

就这样!有了这段代码,我们应该看到一个红色的标题,上面写着 Todoifier!让我们看看:

实际上,我们也可以解构数组!例如,假设我们有一些独特的选项想要作为列表的起始项。我们可以通过数组解构捕获这些选项,我们还可以利用我们稍后将要学习的其他一些语法技巧,比如数组展开!让我们看看src/TodoList.js,并更改我们的构造函数以使用数组解构:

const [item1, item2, ...rest] = [
  "Write some code",
  "Change the world",
  "Take a nap",
  "Eat a cookie"
];
this.state = {
  items: [item1, item2, rest.join(" and ")]
};

数组解构仅基于位置;这里唯一的技巧是在匹配item1item2之后,我们将把数组的其余部分扔到一个叫做rest的变量中,我们将用一些空格和单词"and"将其连接起来。让我们看看结果:

图片

可选参数

为函数设置可选参数是一个相对简单的任务!如果你想使一个function参数可选,你只需要在参数名称后添加一个等号,并给它一个默认值。例如,让我们回顾一下本章稍早前我们编写的sayHello函数:

const sayHello = name => {
  console.log(`Hello ${name}!`);
};

现在,让我们修改一下,如果有人没有指定name,函数调用将不会失败或抛出错误:

const sayHello = (name = "Unknown") => {
  console.log(`Hello ${name}!`);
};

注意,由于我们正在使用可选变量作为参数列表,我们需要再次将其括在括号中!现在,如果有人调用该函数而不指定任何参数,我们预计在控制台看到的是 Hello Unknown!,类似于以下内容:

sayHello();

既然如此,让我们将其写入我们之前的headerDisplay函数中。这确实会有些杂乱,但了解如何有效地使用它是非常好的,因为这是一种在项目中实现防御性编程的绝佳方式:

const headerDisplay = ({
  header: title = "Todo List",
  headerColor: color = "blue"
}) => <h2 style={{ color: color }}>{title}</h2>;

现在,如果我们回到并更改我们的App组件对header()函数的调用,只传递一个空对象,我们预计标题将改为显示TodoList,带有蓝色的标题:

const App = () => (
  <div className="App">
    {headerDisplay({})}
    <br />
    <TodoList />
  </div>
);

在我们将更改回header函数并改回传递details变量之前,让我们看看结果:

图片

展开运算符

记得在章节开头一点,当我们写了一个省略号然后一个变量名的时候吗?每次你这样做,你都是在告诉 JavaScript 将未匹配的其余部分放入这里,并将其与当前的数据结构连接起来:

    const [item1, item2, ...rest] = [
      "Write some code",
      "Change the world",
      "Take a nap",
      "Eat a cookie"
    ];

这行代码告诉 JavaScript,第一个位置的项目进入item1变量,第二个位置的项目进入item2变量,然后所有其他的项目都进入rest变量。当我们想要以非破坏性的方式向数组中添加项目时,我们也可以利用这一点。还记得位于src/TodoList.js中的addTodo()函数吗?让我们更详细地看看,看看我们如何在其他地方使用数组展开:

  addTodo(item) {
    this.setState({ items: [...this.state.items, item] });
  }

这行代码告诉 JavaScript 将组件的 state 中的 items 键设置为当前 this.state.items 的值,然后将 item 添加到该列表的末尾。代码与以下内容相同:

this.setState({ items: this.state.items.concat(item) });

你也可以使用 Babel 在 Create React App 中的最新更新在 JavaScript 代码中这样做,这对于 state 修改来说非常好,因为 state 修改只是改变对象!让我们回到 src/App.js 并编写一段示例代码,这段代码也会为我们的 header 设置背景颜色。我们将从对象展开开始,并设置一个新的变量 moreDetails

const moreDetails = {
  ...details,
  header: "Best Todoifier",
  background: "black"
};

我们只是获取 details 数据结构,然后在上面添加新的键或替换现有键的值。接下来,我们需要修改 headerDisplay 函数,使其能够处理传入的背景颜色:

const headerDisplay = ({
  header: title = "Todo List",
  headerColor: color = "blue",
  background: background = "none"
}) => <h2 style={{ color: color, background: background }}>{title}</h2>;

这个过程的最后一步是将 App 组件中的调用改为传递 moreDetailsheader 而不是 details 或一个空对象:

const App = () => (
  <div className="App">
    {headerDisplay(moreDetails)}
    <br />
    <TodoList />
  </div>
);

保存并重新加载后,你应该会看到以下内容:

Object 展开的代码行相当于我们编写以下内容:

const moreDetails = Object.assign({}, details, {
  header: "Best Todoifier",
  background: "black"
});

这只是稍微简洁一些,更容易阅读,所以非常感谢 Create React App 团队和 Babel 团队在最新版本中支持这一点!

React Fragments

本章我们将讨论的最后一件事是 React Fragments 的新支持!React Fragments 是一个全新的但重要的特性。之前,如果你想在同一级别包含多个组件,你 总是 必须有一个根组件,即使对于像多行表这样的东西,这实际上也没有太多意义;你必须在 <div> 内部嵌套 <td> 标签,这看起来很奇怪。

结果是你不得不在技术上无效的 HTML 和符合 React 代码之间做出选择,这往往只是为了绕过限制而编写尴尬或糟糕的代码。现在,相反,我们可以编写包含特殊 Fragment 标签(<Fragment></Fragment>》)的代码,分别表示片段的开始和结束。我们可以将这些引用为 <React.Fragment>(如果我们选择在 import Component的地方import Fragment,例如在以下代码行中),或者作为 <>` 的快捷方式:

import React, { Fragment, Component } from "react";

关于使用 <></> 简写语法的快速警告:如果你在构建片段列表的代码中使用片段,你不能使用简写语法并指定 key 属性;你将不得不使用 React.FragmentFragment

如果我们回到 src/TodoList.js,在我们的 renderItems() 函数中,我们可以看到将多余的 <div> 替换为 Fragment 的完美位置:

 renderItems() {
 return this.state.items.map(description => (
 <Fragment key={"item-" + description}>
  <Todo
   key={description}
   description={description}
   removeTodo={this.removeTodo}
  />
 <Divider key={"divide-" + description} />
  </Fragment>
 ));
 }

在函数顶部,我们通过import Component作为从React的命名import导入组件,我们还需要包含Fragment,类似于本节稍高一点的代码行。

最终结果在其他方面是相同的;主要区别在于,我们不再无理由地将每个 Todos 和 Dividers 放在额外的div中,它们都可以并排坐在 DOM 树中,让你的代码更加整洁,尤其是在处理 HTML 表格时,引入额外的div实际上只会破坏你的代码!

快速回顾

在结束本章之前,让我们看看我们所编写代码的最终状态。我们的src/TodoList.js已经扩展,并包含了很多新技巧:

import React, { Fragment, Component } from "react";
import Todo from "./Todo";
import "./TodoList.css";

import NewTodo from "./NewTodo";
import Divider from "./Divider";

class TodoList extends Component {
  constructor(props) {
    super(props);
    const [item1, item2, ...rest] = [
      "Write some code",
      "Change the world",
      "Take a nap",
      "Eat a cookie"
    ];
    this.state = {
      items: [item1, item2, rest.join(" and ")]
    };

    this.addTodo = this.addTodo.bind(this);
    this.removeTodo = this.removeTodo.bind(this);
  }
  addTodo(item) {
    this.setState({ items: [...this.state.items, item] });
  }
  removeTodo(removeItem) {
    const filteredItems = this.state.items.filter(description => {
      return description !== removeItem;
    });
    this.setState({ items: filteredItems });
  }
  renderItems() {
    return this.state.items.map(description => (
      <Fragment key={"item-" + description}>
        <Todo
          key={description}
          description={description}
          removeTodo={this.removeTodo}
        />
        <Divider key={"divide-" + description} />
      </Fragment>
    ));
  }
  render() {
    return (
      <div className="TodoList">
        <NewTodo addTodo={this.addTodo} />
        {this.renderItems()}
      </div>
    );
  }
}

export default TodoList;

我们的src/App.js组件也显著扩展了:

import React from "react";
import "./App.css";

import TodoList from "./TodoList";

const details = {
 header: "Todoifier",
 headerColor: "red"
};

const moreDetails = {
 ...details,
 header: "Best Todoifier",
 background: "black"
};

const App = () => (
 <div className="App">
 {header(moreDetails)}
 <br />
 <TodoList />
 </div>
);

const header = ({
 header: title = "Todo List",
 headerColor: color = "blue",
 background: background = "none"
}) => <h2 style={{ color: color, background: background }}>{title}</h2>;

export default App;

摘要

我们覆盖了很多内容,但非常高效!我们讨论了很多如何在 Create React App 2 项目中充分利用 Babel 提供的更好、更干净的语法!

即使这样,这也只是触及了现代 JavaScript 所能做到的一小部分,但它涵盖了你在整本书中都会看到的许多常见模式和技巧。我们希望这本指南能给你提供一切所需,以便能够执行项目,并在最高水平上理解和贡献!

我们将在未来的章节中探索很多这些代码技巧,所以请确保在你继续前进之前,你已经牢固掌握了本章中我们讨论的所有内容!

第四章:使用测试和 Jest 保持你的应用程序健康

任何应用程序开发者的生活中一个重要的部分是确保他们的应用程序第一次运行就正确无误,每次都如此。随着应用程序变得更加复杂,这可能是一个困难的事情去做;之前可能只需要几分钟和一行代码来清理或修复的问题,现在可能需要数小时、数天,甚至数周(或更长)来尝试修复!此外,如果你试图构建你的应用程序并使其工作干净、有效。

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

  • 测试 React 应用程序的历史

  • 运行 React 测试的不同方式

  • Jest 的简要介绍

  • 测试监视器的简要介绍

  • 如何编写测试

测试的“为什么”和“何时”

你可能想知道为什么我们在应用程序开发的这么晚才开始处理测试。一般来说,当你刚开始你的项目时,你可能会等待一段时间,看看你的应用程序如何发展,然后再开始用测试来验证其行为。但话虽如此,我们现在也已经到了一个很好的时机,开始巩固我们的项目,使其成为我们可以自信地部署到生产环境中的东西!

测试 React 的历史

再次强调,了解工具集的历史对于理解为什么使用某些功能或库是非常有帮助的。当人们刚开始使用 React 时,关于测试的想法和标准四处散落。有些人使用各种各样名字的库的组合,比如 night-something、mocha 或其他随机的框架。你必须弄清楚模拟库、测试框架和用户界面测试框架。

这绝对是 令人筋疲力尽的,不可避免的是,你使用的任何框架和设置都会失去青睐或过时,你不得不每隔几周就重新开始学习一个新的框架!更糟糕的是,如果你问了很多不同的人他们的特定测试设置,你会得到至少那么多不同的答案!

关于 Jest

Jest 是 React 团队针对“我们应该为我们的 React 应用程序使用什么测试框架?”这一持续问题的回答。你得到的答案总是因人而异,有时你不得不使用多个框架才能获得你想要的功能,这导致了大量的冗余和开发者对于在任何给定时间应该使用哪个框架的困惑!

分析 Jest 测试的结构

我们开始的最佳地点是查看任何新的 Create React App 项目中附带的标准测试,那就是对我们 App 组件的测试,位于 src/App.test.js

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

it('renders without crashing', () => {
  const div = document.createElement('div');
  ReactDOM.render(<App />, div);
  ReactDOM.unmountComponentAtNode(div);
});

嗯,从任何想象的角度来看,这不是一个特别令人兴奋的测试。正如其名所示,这个测试只是验证了App组件可以在页面上渲染,没有任何大的故障或错误。这个测试可能看起来有点多余,但它实际上扮演了一个极其重要的角色:它作为你应用程序的理智检查!

想想看:如果你的主要应用程序或组件在没有其他更复杂的检查的情况下无法渲染,那么你的项目可能非常、非常糟糕!这有助于我们检查我们是否意外地排除了某些内容,或者以保持我们所有人理智的方式对我们的项目引入了重大的破坏性更改!让我们也验证一下,即使现在我们有了一些更改,这也确实按照我们预期的样子工作:

$ yarn test

当我们运行这个时,我们应该在我们的控制台窗口中看到一些输出,它会告诉我们更多关于运行了哪些测试,哪些测试通过,哪些失败,以及关于我们的测试运行的其他元数据:

图片

就这样,干净且可工作的测试!现在,这本身真的很酷,但让我们花几分钟来分析之前的输出窗口!

探索测试输出

目前,如果我们没有上下文,只看之前的输出可能不太有意义!让我们从查看测试之后的第一个行开始,它讨论了我们的测试套件:

Test Suites: 1 passed, 1 total

Jest 允许我们以多种不同且有用的方式运行和组织我们的测试!例如,我们的测试可以通过套件运行;套件是测试的更大分组。稍后当我们有不止一个测试时,我们会进一步探讨这一输出行,但现在我们只有一个测试套件,所以我们只运行了一个测试套件:

Tests: 1 passed, 1 total

我们运行了一个测试,并且这个测试通过了!这使我们能够跟踪随着应用程序代码的变化,运行了哪些测试;当我们进行小改动时,我们可能不需要在每个套件中运行每个测试!相反,我们可以专注于受更改影响的测试,因此你可能看到这少于套件中的总测试数:

Snapshots: 0 total

在 Jest 中,快照是一个完全不同的特性。快照是一种告诉 Jest 的方式:给定输入 X,渲染的组件应该看起来完全像这样。我们稍后会更详细地探讨这一点!以下行指示了我们的测试运行所需的时间:

Time: 3.986s

这点很重要,因为有时你可能会向你的应用程序中引入一些代码,使得应用程序变得极其缓慢!这可以帮助你捕捉到应用程序中那些偷偷破坏性能的代码片段。你的测试将基本上是你对性能不佳算法的早期预警系统!我们将得到以下输出:

Ran all test suites.
Watch Usage: Press w to show more.

最后,我们得到了一些输出,告诉我们一些关于运行了哪些测试以及我们可以做些什么的信息。我们也可以在这里按下 w 键,以获取更多关于我们可以使用测试运行器和监视器的选项:

使用 F 键的开发测试工作流程

当我们按下 w 键时,我们会得到一个新列表的命令,我们可以运行这些命令来继续我们的测试套件并对其进行更多操作。我们已经探索了这些选项的一些内容,但值得重申的是,我们在开发期间可以(和应该)做的工作,以及谈谈何时使用这些测试!

第一个主要的问题是以下内容:

Press f to run only failed tests

我们会大量使用这个测试。通常在开发复杂的应用程序时,您可能会引入破坏测试套件的变化,或者您可能会采用更多的测试驱动开发方法,在这种情况下,您将使用破坏的测试,然后在应用程序或测试中进行修复,直到测试再次通过!如果这种情况发生,您将大量使用这个命令,所以最好早点熟悉它!如果我们现在在没有失败的测试的情况下尝试运行它,我们会得到以下输出:

No failed test found.
Press `f` to quit "only failed tests" mode.

Watch Usage: Press w to show more.

这是有道理的。我们没有失败的测试,所以当然找不到任何测试,因此不会运行任何测试。不过,我们可以通过故意引入一个失败的测试,然后运行我们的完整测试套件,接着只运行失败的测试来使这为我们所用!我们可以通过回到 src/App.js 并取消注释代码底部的 export 语句来快速模拟这个过程:

// export default App;

现在如果您重新启动测试,您将得到一个失败的测试!我们应该能够按下 F 键,然后它会重新运行单个失败的测试(我们也可以反复执行这个过程)。现在如果我们取消注释那一行并保存文件,如果我们重新运行测试套件(无论是自动还是手动使用 F 键),我们应该能够回到一个完全工作的测试套件!

有另一个原因需要您熟悉重新运行失败的测试:您应该熟悉编写故意失败的测试,直到您使测试通过,或者通过注释或故意破坏代码来使测试通过!如果您做出的代码更改应该破坏您的测试,但测试仍然通过,这意味着您的测试实际上并没有正确地测试您应用程序的行为!

是时候添加一些新的测试了!

我们对我们的初始测试有相当好的理解,但通过编写新的测试,我们可以学到更多!我们将从测试我们迄今为止编写的最简单的组件开始:我们的 Todo 组件!您会注意到我们如何命名测试以便 Jest 正确地捕获它们:我们将为 Todo 组件(在 src/Todo.js 中)创建一个测试,命名为 src/Todo.test.js!我们几乎总是希望从模仿 App.test.js 中的结构开始我们的测试,所以我们将开始做几乎相同的事情:

import React from "react";
import ReactDOM from "react-dom";
import Todo from "./Todo";

it("renders without crashing", () => {
 const div = document.createElement("div");
 ReactDOM.render(<Todo />, div);
 ReactDOM.unmountComponentAtNode(div);
});

在重新运行我们的测试套件之后(你可能需要使用Q退出测试监视器并重新运行yarn test),你应该会得到以下输出:

图片

因此,它运行了我们刚刚添加的src/Todo.test.js测试,而且它还是一个全新的测试套件!让我们扩展我们的测试,因为到目前为止,它们实际上并没有做什么。然而,为了做到这一点,我们需要向我们的测试套件中添加几个更多的库!我们希望添加enzyme(用于浅渲染);enzyme是 React v16.x 的适配器,以及 React 的测试renderer到我们的应用程序。我们可以通过一个快速的yarn命令来实现:

$ yarn add --dev react-test-renderer enzyme enzyme enzyme-adapter-react-16

enzyme为我们的测试套件增添了大量的功能,使得与之工作变得更加容易,所以将enzyme作为测试套件的基础确实是值得的!实际上,它如此有用,以至于它被包含在一些默认的 React/Jest 测试文档中!现在,仅仅包括这些还不足以完成我们所需的一切,所以我们也需要创建一个测试设置文件来初始化 Enzyme。创建src/setupTests.js并给它以下内容:

// setup file
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

在完成这些之后,我们可以开始处理一些真正的测试代码!我们需要打开src/Todo.test.js,在那里我们可以在顶部添加一些代码,这将使我们能够利用enzyme的浅渲染renderer!我们还需要renderer()函数的react-test-renderer,因为我们将使用它来创建快照:

import React from "react";
import ReactDOM from "react-dom";
import { shallow } from "enzyme";
import renderer from "react-test-renderer";

import Todo from "./Todo";

现在我们已经拥有了开始编写测试所需的一切。在我们开始编写测试之前,我通常首先将所有的测试都抛入一个大的describe函数中,所以让我们从移动我们已经在describe块内部编写的测试开始:

describe(Todo, () => {
  it("renders without crashing", () => {
    const div = document.createElement("div");
    ReactDOM.render(<Todo />, div);
    ReactDOM.unmountComponentAtNode(div);
  });
});

重新运行测试,我们应该回到绿色,两个套件和两个测试:

图片

describe是一种将相关测试组合在一起的方式,无论是通过功能、概念还是其他方式。你有几种方法来声明一个describe块。你可以使用一个字符串来指定测试的名称,或者你可以使用一个有效的ClassComponent名称。

现在,如果我们想使我们的测试变得更加复杂,我们还需要做一些基本的设置工作,因为如果你记得我们的Todo组件,我们有一些函数需要传递到我们的子组件中。让我们看看Todo的默认props

    this.state = {
      done: false
    };

以及removeTodo属性的函数体:

  removeTodo() {
    this.props.removeTodo(this.props.description);
  }

描述很简单;那只是一个我们需要传递的字符串。另一方面,removeTodo(...)要复杂得多。它是一个不作为此组件一部分存在的函数;相反,它存在于父组件中,并被传递进来!那么,我们如何处理这种情况呢?

简单来说,我们可以用 Jest 来 模拟 一个函数!模拟一个函数基本上创建了一个跟踪其何时被调用的假函数。我们还需要对组件进行 浅渲染 以验证它在 DOM 中的整体外观。我们稍后会详细讨论这个问题,但现在请将以下内容添加到 describe 块的顶部:

  const description = "New Todo";
  const mockRemoveTodo = jest.fn();
  const component = shallow(
    <Todo description={description} removeTodo={mockRemoveTodo} />
  );

如前例所示,jest.fn() 允许我们创建一个模拟函数。正如之前提到的,模拟一个函数并没有做任何特别的事情。它假装是一个函数,看到谁试图使用该函数,并跟踪一些事情,比如伪造的参数或为函数设置伪造的返回值。如果我们想验证我们的 props 中的 removeTodo 确实做了些什么,但我们不关心它执行的具体行为,这会很有用。

编写一个通用的快照测试

谈论编写测试是一回事,但让我们实际开始实现我们的测试。我通常以以下方式处理测试编写:

  • 编写一个通用的快照测试

  • 编写一些子组件特定的测试

  • 检查内容

  • 检查交互

快照测试可以用来验证初始状态渲染以及满足特定条件后的渲染。这是通过获取组件的表示形式,存储它,然后用于未来的测试来实现的。当你的组件不断变化时,这可能会很棘手,但当你的组件稳定且不应经常修改时,这会非常方便。让我们编写我们的 snapshot 测试:

  it("renders and matches our snapshot", () => {
    const component = renderer.create(<Todo description="Yo" />);
    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });

记得我们之前从 react-test-rendererimportrenderer 吗?这就是我们使用它的地方!我们通过 JSX 创建一个 Todo 组件,并将其传递给 renderer.create() 函数。然后我们获取组件结构,将其转换为 JSON,并验证它是否与之前运行中适当的快照匹配。这是另一个早期预警系统,有助于捕捉到有人更改了组件但未对测试进行任何更新的情况!让我们来看看我们的测试套件中的结果:

图片

为内容编写测试

我们还需要确保当传递的属性会修改用户看到的内容时,这些属性实际上已经进入了完全渲染的函数!我们已经在 describe 块的顶部设置了 description 变量。在我们跳得太远之前,我们需要为我们的浅渲染编写一个快速测试,以确保它也正常工作!

什么是浅渲染?

浅渲染基本上就是组件的一个假渲染,其中只渲染根级组件,不渲染其他内容。这是任何组件在测试中可以使用的最简版本,因此我们应该在跳入其他任何内容之前始终使用它!让我们首先编写我们的浅渲染测试:

  it("renders a Todo component", () => {
    expect(component.contains(<div className="Todo" />));
  });

这是一个简单的测试。它只是确保我们渲染出一个带有Todo CSS 类的div,这是当我们使用 JSX 实例化我们的Todo组件时渲染的根div。否则,这里没有太大的惊喜!在此之后,我们需要编写一些测试来确保这些传入的props确实进入了组件

  it("contains the description", () => {
    expect(component.text()).toContain(description);
  });

我们已经创建了组件的浅渲染版本,所以我们正在获取这个组件,分析如果它是一个真实渲染,将会添加到 DOM 中的渲染文本,然后确保我们放入描述中的任何内容都包含在内!

测试交互

最后一步是测试组件中的实际交互!我们需要能够定位我们的每个按钮。如果你回想起我们的Todo组件的render()函数,有两个按钮被创建:

  render() {
    return (
      <div className={this.cssClasses()}>
        {this.state.description}
        <br />
        <button onClick={this.markAsDone}>Mark as Done</button>
        <button onClick={this.removeTodo}>Remove Me</button>
      </div>
    );
  }

没有任何修改,实际上对我们来说很难具体地定位任何一个实际按钮。我们希望有一种方法可以单独地定位每个按钮,所以让我们进入Todo组件,并为每个按钮添加一个唯一的className!在src/Todo.js的渲染函数中,将MarkDone className添加到第一个button,将RemoveTodo className添加到第二个button,就像以下代码所示:

  render() {
    return (
      <div className={this.cssClasses()}>
        {this.props.description}
        <br />
        <button className="MarkDone" onClick={this.markAsDone}>
          Mark as Done
        </button>
        <button className="RemoveTodo" onClick={this.removeTodo}>
          Remove Me
        </button>
      </div>
    );
  }

保存这段代码,测试将重新运行并且失败?但是为什么?我们还没有修改测试!参考以下截图:

图片

这实际上正是它应该做的!我们对组件进行了一些修改,改变了组件的渲染方式。在我们的例子中,我们对这些修改完全满意,所以我们将使用另一个测试监视器命令来更新快照!按u键,我们的快照将更新,并且测试将重新通过!最后,我们可以回到完成我们的交互测试!

完成我们的交互测试

现在我们已经可以隔离每个按钮,我们可以测试当每个按钮被点击时会发生什么!我们需要首先验证我们的MarkDone button是否将Todo标记为完成,这也可以通过检查状态来完成!让我们看看测试,然后我们再讨论它做了什么:

  it("marks the Todo as done", () => {
    component.find("button.MarkDone").simulate("click");
    expect(component.state("done")).toEqual(true);
  });

思考这些测试的最简单方法就是大声说出来。如果我们像人类一样测试这个行为,我们会说,找到标记Todo为完成的按钮,点击那个按钮,然后我们应该期望Todo完成!我们的代码正是这样做的!它通过 CSS 选择器找到了组件,该选择器抓取了附有MarkDone CSS 类的按钮(记得我们之前更改的render()函数)。然后我们模拟了一个发送到该button"click"事件,该事件针对onClick处理程序。最后,我们必须使用state()函数从组件的状态中获取一个值,对我们来说就是state中的"done"属性!如果现在是true,那么我们就成功了,我们的测试就通过了!

返回到我们的模拟函数

我们谈了很多关于函数模拟的内容,但后来又关注了一堆其他的测试;现在是我们重新审视我们的模拟函数并实际测试它的时候了!基本上,我们只需要使用一个辅助函数来验证我们的模拟是否被调用:

  it("calls the mock remove function", () => {
    component.find("button.RemoveTodo").simulate("click");
    expect(mockRemoveTodo).toHaveBeenCalled();
  });

我们之前在 describe 块顶部放回的mockRemoveTodo函数:

const mockRemoveTodo = jest.fn();

我们在之前的测试中已经看到了simulate的调用,所以我们只需要创建一个期望,即我们的模拟函数已被调用,这就足够了!有了这个,我们就有了针对Todo组件的非常全面的测试套件,从这里开始,我们所做的一切都是相同测试的略微复杂的变化!总共七个测试,两个测试套件,以及一个快照测试——一切运行得都很完美!参考以下截图:

在我们继续之前,让我们验证src/Todo.test.js的完整测试套件:

import React from "react";
import ReactDOM from "react-dom";
import { shallow } from "enzyme";
import renderer from "react-test-renderer";

import Todo from "./Todo";

describe(Todo, () => {
  const description = "New Todo";
  const mockRemoveTodo = jest.fn();
  const component = shallow(
    <Todo description={description} removeTodo={mockRemoveTodo} />
  );

  it("renders without crashing", () => {
    const div = document.createElement("div");
    ReactDOM.render(<Todo />, div);
    ReactDOM.unmountComponentAtNode(div);
  });

  it("renders and matches our snapshot", () => {
    const component = renderer.create(<Todo description="Yo" />);
    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });

  it("renders a Todo component", () => {
    expect(component.contains(<div className="Todo" />));
  });

  it("contains the description", () => {
    expect(component.text()).toContain(description);
  });

  it("marks the Todo as done", () => {
    component.find("button.MarkDone").simulate("click");
    expect(component.state("done")).toEqual(true);
  });

  it("calls the mock remove function", () => {
    component.find("button.RemoveTodo").simulate("click");
    expect(mockRemoveTodo).toHaveBeenCalled();
  });
});

现在我们也给TodoList添加一些测试吧!

为 TodoList 添加测试

我们将首先添加一个框架来放置我们其余的TodoList测试!我们需要标准的导入和渲染、快照和浅渲染组件的测试。我们将从以下src/TodoList.test.js的脚手架开始:

import React from "react";
import ReactDOM from "react-dom";
import { shallow } from "enzyme";
import renderer from "react-test-renderer";

import TodoList from "./TodoList";
import NewTodo from "./NewTodo";
import Todo from "./Todo";

describe(TodoList, () => {
  const component = shallow(<TodoList />);

  it("renders without crashing", () => {
    const div = document.createElement("div");
    ReactDOM.render(<TodoList />, div);
    ReactDOM.unmountComponentAtNode(div);
  });

  it("renders and matches our snapshot", () => {
    const component = renderer.create(<TodoList />);
    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });

  it("renders a TodoList component", () => {
    expect(component.contains(<div className="TodoList" />));
  });
});

我们之前已经覆盖了这些测试,所以没有太多需要深入的内容,但我们想确保,由于我们的render()调用包括一个NewTodo组件,并且我们在文件顶部导入了该component,我们有一个测试来验证树中只有一个NewTodo

  it("includes a NewTodo component", () => {
    expect(component.find(NewTodo)).toHaveLength(1);
  });

我们还需要验证TodoList中有多少Todo组件,但这个测试有一个更复杂的问题需要解决。如果你记得,TodoList中状态的"items"属性决定了哪些Todo应该出现,所以我们将检查状态与组件的find函数是否同步:

  it("renders the correct number of Todo components", () => {
    const todoCount = component.state("items").length;
    expect(component.find(Todo)).toHaveLength(todoCount);
  });

我们的component已经通过shallow()调用被渲染出来,所以我们将使用state()调用来验证项目的长度,并找到相同数量的Todo组件。我们还需要测试TodoListaddTodo函数:

  it("adds another Todo when the addTodo function is called", () => {
    const before = component.find(Todo).length;
    component.instance().addTodo("A new item");
    const after = component.find(Todo).length;
    expect(after).toBeGreaterThan(before);
  });

这里有一些新的功能可能稍微复杂一些,所以让我们简单谈谈!我们首先找出已经存在的 Todos 数量,因为在我们添加另一个项目后,我们应该期望这是我们从前的数量加上一个!之后,我们将在组件上调用addTodo(),但为了做到这一点,我们需要跳入component的实际生活上下文。我们可以通过instance()调用来实现,这允许我们调用component上的任何函数,而无需模拟任何按钮点击!在调用addTodo之后,我们抓取所有存在的 Todos 列表并期望它比我们最初开始时更多!这是一种非常重要且非常好的编写测试的方法;我们从不硬编码 Todos 的数量或其他任何东西;相反,我们在事件发生后检查相对值!这消除了某些奇怪的场景,即有人更改了组件的默认或初始状态,并直接导致我们的测试失败!

最后,我们需要实现一个removeTodo测试,这正好是我们之前写的测试的反向操作:

  it("removes a Todo from the list when the remove todo function is called", () => {
    const before = component.find(Todo).length;
    const removeMe = component.state("items")[0];
    component.instance().removeTodo(removeMe);
    const after = component.find(Todo).length;
    expect(after).toBeLessThan(before);
  });

唯一值得注意的区别是removeTodo函数需要实际的项目来移除,因此我们必须从列表中抓取一个项目,并通过将此值传递给removeTodo函数来具体移除它!

一切都结束后,我们应该有一个完整的TodoList.test.js测试套件,如下所示:

import React from "react";
import ReactDOM from "react-dom";
import { shallow } from "enzyme";
import renderer from "react-test-renderer";

import TodoList from "./TodoList";
import NewTodo from "./NewTodo";
import Todo from "./Todo";

describe(TodoList, () => {
  const component = shallow(<TodoList />);

  it("renders without crashing", () => {
    const div = document.createElement("div");
    ReactDOM.render(<TodoList />, div);
    ReactDOM.unmountComponentAtNode(div);
  });

  it("renders and matches our snapshot", () => {
    const component = renderer.create(<TodoList />);
    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });

  it("renders a TodoList component", () => {
    expect(component.contains(<div className="TodoList" />));
  });

  it("includes a NewTodo component", () => {
    expect(component.find(NewTodo)).toHaveLength(1);
  });

  it("renders the correct number of Todo components", () => {
    const todoCount = component.state("items").length;
    expect(component.find(Todo)).toHaveLength(todoCount);
  });

  it("adds another Todo when the addTodo function is called", () => {
    const before = component.find(Todo).length;
    component.instance().addTodo("A new item");
    const after = component.find(Todo).length;
    expect(after).toBeGreaterThan(before);
  });

  it("removes a Todo from the list when the remove todo function is called", () => {
    const before = component.find(Todo).length;
    const removeMe = component.state("items")[0];
    component.instance().removeTodo(removeMe);
    const after = component.find(Todo).length;
    expect(after).toBeLessThan(before);
  });
});

添加对新 Todo 的测试

最后,我们可以添加我们的最终测试套件并确保NewTodo也被覆盖。大部分时间,我们将使用我们之前已经使用过的相同的框架。创建src/NewTodo.test.js并给它以下框架:

import React from "react";
import ReactDOM from "react-dom";
import { shallow } from "enzyme";
import renderer from "react-test-renderer";

import NewTodo from "./NewTodo";

describe(NewTodo, () => {
  const mockAddTodo = jest.fn();
  const component = shallow(<NewTodo addTodo={mockAddTodo} />);

  it("renders without crashing", () => {
    const div = document.createElement("div");
    ReactDOM.render(<NewTodo addTodo={mockAddTodo} />, div);
    ReactDOM.unmountComponentAtNode(div);
  });

  it("renders and matches our snapshot", () => {
    const component = renderer.create(<NewTodo addTodo={mockAddTodo} />);
    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });

  it("renders a Todo component", () => {
    expect(component.contains(<div className="NewTodo" />));
  });
});

我们还希望修改我们编写的检查内容的测试,因为我们至少应该确保文本字段和button作为表单的一部分仍然存在:

it('contains the form', () => {
  expect(component.find('input')).toHaveLength(1);
  expect(component.find('button')).toHaveLength(1);
});

我们还希望测试我们的模拟addTodo函数:

  it("calls the passed in addTodo function when add button is clicked", () => {
    component.find("button").simulate("click");
    expect(mockAddTodo).toBeCalled();
  });

这基本上与我们之前在Todo组件套件中所做的是一样的。我们需要为我们的handleUpdate函数编写一个测试,该测试应将"item"状态属性修改为伪造的input值:

  it("updates the form when keys are pressed", () => {
    const updateKey = "New Todo";
    component.instance().handleUpdate({ target: { value: updateKey } });
    expect(component.state("item")).toEqual(updateKey);
  });

handleUpdate参数的结构有点奇怪,因此我们需要确保我们传递的对象与我们所编写的handleUpdate函数兼容,该函数如下所示:

  handleUpdate(event) {
    this.setState({ item: event.target.value });
  }

然后,我们使用state函数来验证"item"现在是否与我们传递的相匹配!我们将通过验证当点击添加项目的按钮时,"item"状态键的值重置为空白来结束我们的测试编写之旅:

 it("blanks out the Todo Name when the button is clicked", () => {
 const updateKey = "I should be empty";
 component.instance().handleUpdate({ target: { value: updateKey } });
 expect(component.state("item")).toEqual(updateKey);
 component.find("button").simulate("click");
 expect(component.state("item")).toHaveLength(0);
 });

我们需要通过确保组件首先有一个值然后重置为空白来验证我们的测试是否彻底。如果我们不这样做,我们就无法验证我们的测试是否按预期工作!

完整的测试套件如下所示:

import React from "react";
import ReactDOM from "react-dom";
import { shallow } from "enzyme";
import renderer from "react-test-renderer";

import NewTodo from "./NewTodo";

describe(NewTodo, () => {
 const mockAddTodo = jest.fn();
 const component = shallow(<NewTodo addTodo={mockAddTodo} />);

 it("renders without crashing", () => {
 const div = document.createElement("div");
 ReactDOM.render(<NewTodo addTodo={mockAddTodo} />, div);
 ReactDOM.unmountComponentAtNode(div);
 });

 it("renders and matches our snapshot", () => {
 const component = renderer.create(<NewTodo addTodo={mockAddTodo} />);
 const tree = component.toJSON();
 expect(tree).toMatchSnapshot();
 });

 it("renders a Todo component", () => {
 expect(component.contains(<div className="NewTodo" />));
 });

 it("contains the form", () => {
 expect(component.find("input")).toHaveLength(1);
 expect(component.find("button")).toHaveLength(1);
 });

 it("calls the passed in addTodo function when add button is clicked", () => {
 component.find("button").simulate("click");
 expect(mockAddTodo).toBeCalled();
 });

 it("updates the form when keys are pressed", () => {
 const updateKey = "New Todo";
 component.instance().handleUpdate({ target: { value: updateKey } });
 expect(component.state("item")).toEqual(updateKey);
 });

 it("blanks out the Todo Name when the button is clicked", () => {
 const updateKey = "I should be empty";
 component.instance().handleUpdate({ target: { value: updateKey } });
 expect(component.state("item")).toEqual(updateKey);
 component.find("button").simulate("click");
 expect(component.state("item")).toHaveLength(0);
 });
});

如果您一直跟随我们的测试,最终的结果应该是以下测试套件结果:

图片

摘要

测试对于您应用程序的整体健康至关重要!它确保了您的开发周期是合理的,您的部署不会非常危险。您的行为可以被测试和验证,您可以在任何时间点对应用程序正在执行的操作有信心,而无需打开浏览器!

这曾经是一件令人头疼的事情。React 测试设置是一个人们普遍讨厌的功能,因为它需要投入大量的时间,而且在设置完成后变得非常挑剔。一旦出现失误或配置更改不当,整个测试框架可能会完全崩溃!

如果你编写的是一款生产就绪的 React 应用程序,那么准备写大量的测试是非常重要的!在编写代码时,这是一个非常好的软件工程实践,React 也不例外!

在下一章中,我们将深入探讨使用最新版本的 Create React App 中内置的新 CSS 模块和 SASS 支持来清理我们项目的视觉设计,并且我们还将引入一个主要的 CSS 框架!

第五章:将现代 CSS 应用于创建 React App 项目

在我们一直在工作的项目中,我们非常重视功能,但总体来说,我们对实际的外观可能关注得稍微少一些!虽然这对于建立功能和在项目开始时使一切流畅是不错的,但任何参与项目的设计师在这个时候可能会大喊大叫!

让我们的设计团队满意,无论是实际团队还是我们自己,只需花一点时间清理项目的视觉吸引力!就我们当前的设计而言,虽然它并不一定难看,但确实有很多地方需要改进!

那么,我们如何以安全可靠的方式改进应用程序的设计呢?嗯,以前在 Create React App 中,你实际上没有很多选项来从视觉上清理东西。你经常受随机层叠样式表CSS)项目维护者的摆布,试图让其他库、框架或预处理器参与项目编译过程通常是一场噩梦。

在 Create React App 的上下文中,预处理器基本上是构建过程中的一个步骤。在这种情况下,我们谈论的是一些样式代码(CSS 或其他格式),将其编译成基本的 CSS,并将其添加到构建过程的输出中。

在本章中,我们将涵盖涉及样式相关功能的各个方面,并突出我认为是 Create React App 中最好的新功能之一:支持 CSS Modules 和 SASS。具体来说,我们将涵盖以下主题:

  • 将 CSS 引入我们的项目的不同方法

  • Create React App 项目中 CSS 的简要历史

  • 介绍 CSS Modules

  • 将 SASS 引入我们的项目

  • 将 CSS Modules 和 SASS 混合使用

  • 将 CSS Modules 和 SASS 集成到我们的项目中

  • 将 CSS 框架集成到我们的项目中

  • 在修改设计后清理我们的测试

有哪些工具可用?

默认情况下,Create React App 支持我们以多种不同的方式将 CSS 引入我们的应用程序。

我们可以通过编写一个style属性并给它一些任意的 CSS 来直接将 CSS 引入我们的组件中,如下面的代码所示:

const Example = () => {
  return (
    <div className="Example" style="border: 1px solid red;">
      Hello
    </div>
  );
};

这将给我们一个包含单词Hellodiv,周围有一个单像素的红线作为边框。虽然技术上可以这样做,但一般来说,你应该避免这样做。使用像前面示例那样的内联样式声明会使你的样式难以组织,并且在格式化出错时难以追踪。此外,如果设计师或其他非开发人员需要更新外观和感觉(例如,如果某些标准颜色发生变化),他们必须搜索以找到这个随机的一个像素红边的来源!

我们还可以创建.css文件,然后通过以下类似语句将它们导入到我们的项目中:

import "./someStyle.css";

这是一个你之前已经看到过并且我们在我们的应用程序中大量使用的技术。它是有用的,当然,它允许我们稍微分离代码和样式,但它并不能给我们提供我们需要的所有东西。事实上,它实际上引入了一个新的问题,我们现在必须解决,这个问题可能会随着时间的推移使修复项目和清理项目的视觉显示变得极其令人沮丧和困难:CSS 冲突!

CSS 冲突可能会破坏你的应用程序

什么是 CSS 冲突?基本上,当你将 CSS 文件导入到你的组件中时,它并不会真正限制它只针对一个特定的文件;它会被添加到全局 CSS 定义中。这意味着如果你在一个地方定义了特定的样式,它可能会覆盖或与另一个地方的不同样式发生冲突。这些样式表按照特定的顺序导入,这取决于代码如何加载到你的应用程序中,最终在浏览器加载所有内容时,所有内容都会添加到一个巨大的样式表中。

如你所想,如果所有内容都被添加到一个巨大的文件中,而且不同文件之间以及加载方式之间没有真正的区分,你可能会定期遇到一些被粗心命名的东西最终破坏一切的问题!

CSS 冲突的快速示例

理解这个问题的最简单方法就是看到它在实际操作中的效果。大部分时间,我们都非常聪明和谨慎地命名了我们的 CSS 文件,但我们确实遇到了一个巨大的问题:我们的Divider组件为所有hr标签定义了一个全局样式,无论它们出现在哪里。让我们回到src/Todo.js,并更改我们的render函数,在descriptionbutton之间放置一个hr标签:

  render() {
    return (
      <div className={this.cssClasses()}>
        {this.state.description}
        <br />
        <hr />
        <button className="MarkDone" onClick={this.markAsDone}>
          Mark as Done
        </button>
        <button className="RemoveTodo" onClick={this.removeTodo}>
          Remove Me
        </button>
      </div>
    );
  }

注意,我们还没有对这个文件添加任何样式!保存文件并重新加载,尽管我们从未在Todo组件中为hr标签定义过样式,但我们仍然会发现它继承了Divider组件的样式!请参考以下截图:

截图

但这并不是我们想要的!虽然这是一个相当不错的分隔符,但我们可能希望我们的分隔符有不同的颜色!为了比较,我们可以说我们希望Todo组件内的分隔符是实心的红色线条,但我们希望其他的保持不变。我们将在src/Todo.css中添加以下 CSS,将我们的hr标签改为红色,通过更改border颜色:

hr {
  border: 2px solid red;
}

保存并重新加载,但什么都没有发生?这很奇怪。代码是正确的,并且它确实已经正确地将 CSS 导入到我们的应用程序中。为了确保一切正常,我们将hr标签改为div标签,以确保它为我们的Todo div标签添加一个红色边框:

div {
  border: 2px solid red;
}

保存并重新加载,你应该现在能看到这个:

截图

哎呀,这可不是我们想要的!它给页面上的每一个div都添加了边框,而不是仅仅在我们的Todo组件中的div标签上!好吧,至少我们已经弄清楚代码没有问题,只是 CSS 加载的方式有些问题。这很容易解决;我们只需在src/Todo.css文件的hr定义的末尾加上一个!important标志,然后就算完成了!

!important标志是一种强制 CSS 优先执行此指令而不是其他指令的方法。它也是一个让你的应用程序随着时间的推移变得难以维护的绝佳方式;尽可能避免使用它!

src/Todo.css中,我们将通过在hr块的末尾添加一个!important标志来犯下我们的 CSS 罪行:

hr {
  border: 2px solid red !important;
}

好了!保存并重新加载,我们将看到以下输出:

图片

现在,我们搞砸了一切。哎呀!希望我们的设计团队不会因此完全抛弃我们,对吧?他们真的很擅长 CSS,所以他们会修复问题的!嗯,他们会在我们因为代码中破坏了网站的布局而大喊大叫之后修复问题,这对非开发者来说很难追踪到。

好消息是,有一种不同的方式来处理这种情况,效果非常好,可以防止未来发生这种类型的场景!这对那些一直在做共享前端开发项目的人来说是一个绝对的福音,这些项目可能需要搜索多个不同的 CSS 文件,才能找到导致主要设计头痛的单个 CSS 文件!

介绍 CSS Modules

这些方法中的第一个是 CSS Modules,在 Create React App 2 及以上版本中,你不需要做任何事情就可以立即开始利用它。CSS Modules 允许你以防止引入全局、重叠命名空间的方式模块化你导入的任何 CSS 代码,尽管最终结果仍然是一个巨大的 CSS 文件。

话虽如此,如果你不打开任何东西或者稍微更好地组织一下代码,这东西也不会立即在你的项目中工作。目前,我们一直把所有的代码直接放入src/目录,导致root文件夹会随着时间的推移不断增大,最终变得庞大而难以管理,以至于你永远找不到任何东西。

更好的项目组织

让我们从更好地清理我们的项目目录结构开始。有无数种不同的方法来做这件事,而且说实话,它们都有自己的优点和缺点。由于这个项目实际上不会非常大,我们现在将采用一个非常简单的结构,因为用这个结构保持简单和构建起来非常容易。我们将要做的是将每个具有 CSS 和 JavaScript 代码的组件分开到它们自己的文件夹中。我们将从创建NewTodoTodoAppTodoListDivider文件夹开始,并将所有相关代码放在每个文件夹中。我们还需要在每个这些目录中创建一个名为index.js的新文件,它将只负责导入和导出适当的组件。例如,App索引文件(src/App/index.js)将看起来像这样:

import App from "./App";
export default App;

新的Todo索引文件(src/Todo/index.js)将看起来像这样:

import Todo from "./Todo";
export default Todo;

你可能可以猜到NewTodoTodoListDivider索引文件的样子,根据这个模式!

接下来,我们需要更改所有引用这些文件的地方,以便更容易地导入所有文件。这无疑会是一点点的体力劳动,但无论如何我们都需要这样做,以确保在过程中不会破坏任何东西。

首先,在src/App/App.js中,将TodoList导入组件更改为以下内容:

import TodoList from "../TodoList";

对于Divider组件,我们不需要做任何事情,因为它是一个没有导入的组件。NewTodoTodo类型相似,所以我们可以跳过它们。另一方面,src/TodoList/TodoList.js有很多东西需要处理,因为它是我们最高级别的组件之一,并且导入了大量内容:

import Todo from "../Todo";
import NewTodo from "../NewTodo";
import Divider from "../Divider";

但这还不是全部。我们的测试文件src/TodoList/TodoList.test.js也需要修改,以包含这些新路径,否则我们的测试将会失败!我们需要几乎与之前相同的导入列表:

import TodoList from "./TodoList";
import NewTodo from "../NewTodo";
import Todo from "../Todo";

现在,当你重新加载你的应用程序时,你的代码应该仍然工作得很好,所有的测试都应该通过,而且一切都应该被干净地分离出来!这使我们生活变得更加容易,原因有很多,但当我们与其他开发者或设计师一起工作时,这对他们来说就像天赐之物,因为他们可以准确地找出在需要修复东西时应该修改哪些 CSS!我们的完整项目结构现在应该看起来像这样:

src/
  App/
    App.css
    App.js
    App.test.js
    index.js
  Divider/
    Divider.css
    Divider.js
    index.js
  NewTodo/
    NewTodo.css
    NewTodo.js
    NewTodo.test.js
    index.js
  Todo/
    Todo.css
    Todo.js
    Todo.test.js
    index.js
  TodoList/
    TodoList.css
    TodoList.js
    TodoList.test.js
    index.js
  index.css
  index.js
  setupTests.js
  ... etc ...

如何使用 CSS 模块

是时候让我们直接将 CSS 模块整合到我们的项目中了。目前,我们还没有设置任何东西来开始使用 CSS 模块,所以我们需要做一些更改才能使其工作。回想一下我们的Todo CSS 冲突,引入一些冲突的 CSS 命名空间和关于使用!important标志的糟糕选择,导致了一个噩梦般的情况。

相反,让我们开始利用 CSS Modules!我们实际上可以混合旧的方式和新方式,但更进一步,完全使用 CSS Modules 会更好。

将 CSS Modules 引入我们的应用程序

如果我们要使用 CSS Modules,有一些简单的指南我们需要遵循。首先,我们需要将我们的文件命名为 [ whatever ].module.css,而不是 [ whatever ].css。接下来,我们需要确保我们的样式命名简单且易于引用。让我们从遵循这些约定并重命名我们的 CSS 文件 Todosrc/Todo/Todo.module.css 开始,然后我们会稍作修改:

.todo {
  border: 2px solid black;
  text-align: center;
  background: #f5f5f5;
  color: #333;
  margin: 20px;
  padding: 20px;
}

.done {
  background: #f5a5a5;
}

CSS Module 指南建议您使用 camelCase 命名约定,所以 DoneTodo 将分别变为 donetodo。类似 NewTodo 的东西也将变为 newTodo

接下来,我们将打开 src/Todo/Todo.js 文件,以利用 CSS Modules。我们在 Todo 组件中创建了一个辅助函数 cssClasses(),它返回我们在组件中应该使用的样式,而且我们不需要做太多修改就能让这一切与之前完全一样。我们还需要更改顶部的 import 语句,因为我们重命名了文件,并且正在更改我们的 CSS 被加载到代码中的方式!请看以下内容:

import styles from "./Todo.module.css";

这使得我们的代码能够通过将它们作为 styles.[className] 引用来利用 Todo.module.css 中定义的任何类名。例如,在上一个文件中,我们定义了两个 CSS 类名:tododone,因此我们现在可以通过 styles.Todostyles.done 在我们的组件中引用它们。我们需要更改 cssClasses() 函数以使用它,所以现在让我们进行这些精确的更改。在 src/Todo/Todo.js 中,我们的 cssClasses() 函数现在应该如下所示:

  cssClasses() {
    let classes = [styles.todo];
    if (this.state.done) {
      classes = [...classes, styles.done];
    }
    return classes.join(' ');
  }

保存并重新加载,我们的应用程序应该恢复正常!不过,我们还可以做更多的事情,所以让我们回到我们的冲突场景。如果你还记得,问题是我们需要能够在 todo 组件内部更改 hr 标签以拥有自己的样式和效果,同时不影响其他一切,并且如果可能的话,避免使用 !important 标志。回到 src/Todo/Todo.module.css 文件,并为我们的 hr 标签添加以下块,我们将给它一个新的类名 redDivider

.redDivider {
  border: 2px solid red;
}

最后,回到我们的 render() 函数,它在 src/Todo/Todo.js 文件中,并将我们的 render() 函数的 hr 标签包含更改为以下内容:

<hr className={styles.redDivider} />

保存并重新加载,现在我们应该有了完全分区的 CSS 代码,无需担心冲突和全局命名空间!请参考以下截图:

图片

与 CSS Modules 的组合性

这并不是 CSS Modules 给我们的所有东西,尽管这确实是 CSS Modules 的一个伟大部分,我们可以立即获得,而且没有任何麻烦(真的,我们写了零配置来让所有这些发生;这只是一些代码)。我们还获得了 CSS 可组合性,这是从其他类继承 CSS 类的能力,无论它们是否在主文件中!当你设置更复杂的嵌套组件,所有这些组件都需要处理稍微不同的样式表,但彼此之间并不完全不同时,这可以非常有用。让我们假设我们想要有标记一些组件为 critical 而不是只是常规 Todos 的能力。

我们不想对组件做太多改动;我们希望它继承所有其他 Todos 的相同基本规则。我们需要设置一些代码来实现这一点。回到 src/Todo/Todo.js,我们将对允许新的 critical 状态属性的代码进行一些修改。我们将在 constructor 组件中开始,我们将添加新的 state 属性和一个用于函数的 bind 标签:

  constructor(props) {
    super(props);
    this.state = {
      done: false,
      critical: false
    };

    this.markAsDone = this.markAsDone.bind(this);
    this.removeTodo = this.removeTodo.bind(this);
    this.markCritical = this.markCritical.bind(this);
  }

在我们的 state 属性中,我们添加了一个新的 critical 属性,将其设置为默认值 false,然后我们还引用了一个(我们还没有编写的)名为 markCritical 的函数,并绑定 this,因为我们将在事件处理程序中稍后使用它。接下来,我们将处理 markCritical() 函数:

  markCritical() {
    this.setState({ critical: true });
  }

我们还需要修改我们的 cssClasses() 函数,使其能够响应这个新的 state 属性。为了演示 CSS Modules 的可组合性功能,我们将设置 classes 初始为一个空数组,然后第一个项目要么变为 critical,要么变为 todo,具体取决于项目是否被标记为 critical

  cssClasses() {
    let classes = [];
    if (this.state.critical) {
      classes = [styles.critical];
    } else {
      classes = [styles.todo];
    }
    if (this.state.done) {
      classes = [...classes, styles.done];
    }
    return classes.join(' ');
  }

最后,在我们的 render 函数中,我们将创建 button 标签来标记项目为 critical

  render() {
    return (
      <div className={this.cssClasses()}>
        {this.props.description}
        <br />
        <hr className={styles.hr} />
        <button className="MarkDone" onClick={this.markAsDone}>
          Mark as Done
        </button>
        <button className="RemoveTodo" onClick={this.removeTodo}>
          Remove Me
        </button>
        <button className="MarkCritical" onClick={this.markCritical}>
          Mark as Critical
        </button>
      </div>
    );
  }

虽然我们已经完成了至少 90%,但我们还没有完全完成。我们还需要回到 src/Todo/Todo.module.css 并为 critical 类名添加一个新的块,同时我们也会使用我们的可组合属性:

.critical {
  composes: todo;
  border: 4px dashed red;
}

要使用组合,你只需要添加一个新的 CSS 属性 composes 并给它一个类名(或多个类名),你想要它组合的。在这里,组合是一个花哨的说法,意思是它继承了其他类名的行为,并允许你覆盖它们。在前面的例子中,我们说的是 critical 是一个由 todo 模型作为基础组合的 CSS 模块类,并添加了一个大红色虚线边框组件,因为,嗯,我们只能说这意味着它是 critical。这段之前的代码相当于我们编写以下内容:

.critical {
  text-align: center;
  background: #f5f5f5;
  color: #333;
  margin: 20px;
  padding: 20px;
  border: 4px dashed red;
}

和往常一样,保存并重新加载,你应该能够标记项目为“标记为完成”、“标记为关键”或两者兼而有之,或者通过点击“移除我”来移除它们,如下面的截图所示:

图片

大概这就是我们对 CSS 模块的简要介绍了!当然,随着时间的推移,你可以涵盖更多内容,但这个文档更倾向于作为一个快速入门指南,我们可能只需要再写一本书就能涵盖 CSS 技巧和库了!

在继续之前,你还需要快速更新你的测试快照,在 yarn test 界面中按 U 键!

将 SASS 引入我们的项目

这还不是 Create React App 2 部分添加的所有支持。根据非常受欢迎的需求,Create React App 团队还添加了对 SASS 预处理的支持!通常,你几乎可以保证,一旦你想在你的项目中开始使用任何 SASS,你就需要将项目推出。

什么是 SASS?

让我们简单谈谈 SASS 实际上是什么,因为它是一个非常重要的内容。否则,你就不会知道为什么这值得付出努力(尽管,公平地说,将其集成到你的 Create React App 项目中几乎不需要付出什么努力)。SASS 实质上是具有扩展功能支持的 CSS。当我说 扩展功能支持 时,我确实是这个意思!SASS 支持以下功能集,这是 CSS 中缺失的,包括以下内容:

  • 变量

  • 嵌套

  • 部分 CSS 文件

  • 导入支持

  • 混合

  • 扩展和继承

  • 运算符和计算

仅这个功能集就使得在几乎任何复杂的客户端项目中包含 SASS 都是有价值的,而且说实话,在使用了很长时间的 SASS 之后,没有它就很难再想回到纯 CSS。那么,让我们开始在我们的项目中添加一些 SASS 吧!

安装和配置 SASS

好消息是,在 Create React App 项目中使 SASS 支持工作极其简单!我们首先需要通过 yarnnpm 安装它。我们之前一直使用 yarn,所以我们将继续使用它:

$ yarn add node-sass

我们会看到大量的输出,但假设没有错误并且一切顺利,我们应该能够重新启动我们的开发服务器,并开始使用一些 SASS。让我们创建一个更通用的实用 SASS 文件,它将负责存储我们将在整个应用程序中使用的标准化颜色,以及存储我们可能想要在其他地方使用的整洁渐变 hr 模式。

我们还将更改一些我们正在使用的颜色,以便根据项目是否关键、已完成或都不是,分别使用红色、绿色和蓝色。此外,我们还需要稍微调整我们的项目,并添加一个新文件来有一个共享样式和颜色的概念。那么,让我们开始吧:

  1. 在我们的项目中创建一个名为 src/shared.scss 的新文件,并给它以下内容:
$todo-critical: #f5a5a5;
$todo-normal: #a5a5f5;
$todo-complete: #a5f5a5;
$fancy-gradient: linear-gradient(
  to right,
  rgba(0, 0, 0, 0),
  rgba(0, 0, 0, 0.8),
  rgba(0, 0, 0, 0)
);
  1. 接下来,转到 src/Divider/Divider.css 并将其重命名为 src/Divider/Divider.scss。接下来,我们将更改 src/Divider/Divider.js 中对 Divider.css 的引用,如下所示:
import "./Divider.scss";
  1. 现在,我们需要更改 Divider.scss 中的代码,以导入共享变量文件并使用变量作为其一部分:
@import "../shared";

hr {
  border: 0;
  height: 1px;
  background-image: $fancy-gradient;
}

因此,我们在 src/ 中导入新的共享 SASS 文件,然后 background-image 的值仅引用我们创建的 $fancy-gradient 变量,这意味着我们现在可以在需要时重新创建那个复杂的渐变,而无需反复重写它!

  1. 保存并重新加载,您应该看不到任何重大变化!

这是一个很好的例子,展示了如何引入 SASS 来替换我们的标准 CSS,但当我们开始引入 CSS Modules 时会发生什么呢?

混合 SASS 和 CSS Modules

好消息是,在 Create React App 中引入 SASS 到 CSS Modules 并没有更复杂。实际上,步骤几乎完全相同!所以,如果我们想开始混合这两种技术,我们只需要重命名一些文件并更改我们的导入方式。让我们看看实际操作:

  1. 首先,回到我们的 src/Todo/Todo.module.css 文件,进行一个非常小的修改。具体来说,让我们将其重命名为 src/Todo/Todo.module.scss。接下来,我们需要更改 src/Todo/Todo.js 中的 import 语句,否则整个项目都会崩溃:
import styles from "./Todo.module.scss";
  1. 现在,我们应该让 SASS 在 Todo 组件中为 CSS Modules 工作,让我们开始利用它。同样,我们还需要将 shared 文件导入到这个 SASS 文件中。注意以下在 src/Todo/Todo.module.scss 中的内容:
@import '../shared';
  1. 接下来,我们需要开始更改对各种背景颜色的引用。我们将常规待办事项的背景更改为 $todo-normal。然后,我们将完成的 Todo 背景更改为 $todo-complete。最后,我们希望将 critical 项目更改为 $todo-critical
.todo {
  border: 2px solid black;
  text-align: center;
  background: $todo-normal;
  color: #333;
  margin: 20px;
  padding: 20px;
}

.done {
  background: $todo-complete;
}

.hr {
  border: 2px solid red;
}

.critical {
  composes: todo;
  background: $todo-critical;
}
  1. 保存并重新加载我们的项目,让我们确保新的配色方案被尊重:

图片

实话实说,从这一点开始,就是越来越深入地探索 SASS 特定的语法,而且,这又超出了本书的范围。然而,正如您从前面的截图中所见,我们能够将 SASS 引入我们的 CSS Modules 代码中,而且没有遇到任何真正的困难。

更好的是,我们现在已经引入了一种新的方法来更改项目中的主题和皮肤,而无需做很多额外的工作,如果我们的设计师想要快速进入并更改,例如,所有待办事项的背景以及整体配色方案,他们可以通过快速进入 shared.scss 文件并做一些颜色更改来实现,而无需更改其他太多内容!

我们可以保留 CSS Modules 和 SASS 的可爱代码模块化和以开发者为中心的特性,同时为设计师和其他非开发者提供修改设计元素和样式元素的入口!更好的是,通过添加两个新功能,我们的代码变得更加易于维护,而不是让我们的项目变得复杂指数级增长!

添加 CSS 框架

在前端项目中工作时会遇到的一个非常常见的用例是集成某种第三方 CSS 框架。这是我几乎在接触过的每一个前端开发项目中都必须做的事情,而且有很大可能性你需要运行相同的流程!

我们将坚持使用最常见的一种,因为它将为你提供一个很好的框架使用介绍,所以我们将从将 bootstrap 集成到我们的项目中开始!我们的项目从有点丑陋到真正有点体面不会花费太多时间!正如我们在使用 Create React App 进行开发过程中遇到的大多数其他事情一样,这同样简单易行!我们将从将bootstrapreactstrap都添加到我们的项目中开始,reactstrap是一个预制的 React 组件,充分利用了 Twitter Bootstrap!

我们将首先通过yarn添加bootstrapreactstrap

$ yarn add bootstrap@4 reactstrap@6.5.0

目前,如果你使用的是bootstrap 4.x 版本,你需要包含reactstrap,但在 6.5.x 版本中,为了避免错误信息!

我们应该在项目文件夹中看到许多东西被安装,但希望没有错误!在所有内容都成功安装后,我们就可以通过打开src/index.js并添加一个单独的import语句来将基线bootstrap引入我们的项目:

import 'bootstrap/dist/css/bootstrap.css';

清理我们的设计,从头部开始

接下来,让我们清理我们丑陋的头部,这也将要求我们移除我们之前为了实验 JavaScript 语法而编写的部分代码!目前,我们的header是我们自己预制的,但它现在看起来并不好看。我们将利用reactstrap作为其标准导出的一部分提供的NavbarNavbarBrand组件!打开src/App/App.js,我们将开始对该文件进行相当大的修改:

  1. 我们将首先在顶部添加NavbarNavbarBrand的导入:
import { Navbar, NavbarBrand } from "reactstrap";
  1. 接下来,我们可以移除所有的header配置对象,因为我们编辑完这个文件后不再需要它们。相反,我们将用单个headerTitle变量来替换它:
const headerTitle = "Todoifier";
  1. 接下来,我们需要替换我们的headerDisplay函数,因为它将使用新的reactstrap组件而不是我们之前放置的代码:
const headerDisplay = (title) => (
  <Navbar color="dark" dark expand="md">
    <NavbarBrand href="/">{title}</NavbarBrand>
  </Navbar>
);

注意,现在header只接受传入的标题,而不是我们之前使用的巨大的配置对象。这显著简化了我们的代码!我们还需要更改App组件中对header函数的调用:

const App = () => (
  <div className="App">
    {headerDisplay(headerTitle)}
    <br />
    <TodoList />
  </div>
);
  1. 保存它,我们应该在我们的项目中有一个显著更干净的header!参考以下截图:

图片

清理 NewTodo 组件

我们还想要清理我们的 NewTodo 组件,因为它现在非常简单!我们将基本上想要更新代码中任何出现 ButtonInput 函数的地方,以确保我们的应用程序在各个地方都有一个干净、一致的设计!

  1. 首先,在 src/NewTodo/NewTodo.js 的顶部,我们想要添加我们的 reactstrap 导入!我们需要 ButtonInputInputGroup,所以让我们从 reactstrap 中添加它们作为命名导入:
import { Button, Input, InputGroup } from "reactstrap";
  1. 接下来,我们需要正确清理 InputButton 的文本显示,所以让我们将我们的文本字段和 Button 包裹在 InputGroup 组件中,以保持它们在一起!我们将文本 Input 更改为 reactstrap Input 组件,并将 Button 标签更改为 reactstrap Button 组件,并为输入项添加一个 placeholder 文本。此外,请注意,我们在 Button 标签上设置了一个新属性 color,其设置为 "primary"。这给了我们一个蓝色的按钮,而不是默认的丑陋灰色按钮!我们的 render() 函数现在应该看起来像这样:
  render() {
    return (
      <div className="NewTodo">
        <InputGroup>
          <Input
            type="text"
            onChange={this.handleUpdate}
            value={this.state.item}
            placeholder="Input item name here..."
          />
          <Button onClick={this.addTodo} 
          color="primary">Add</Button>
        </InputGroup>
      </div>
    );
  }
  1. 保存并重新加载,我们的输入应该看起来好多了;类似于以下内容:

清理我们的 Todo 组件

我们的 Todo 组件仍然看起来有点丑,所以让我们也给它们同样的处理。在这之后,我们将足够好地完成我们的项目,使其看起来更漂亮,但要达到这个目标,我们需要更多的导入:

  1. 我们需要将 importButtonButtonGroup 导入到我们的 Todo 组件中,因为我们想要清理的只有我们的按钮!为此,使用以下代码,将其添加到 src/Todo/Todo.js
import { Button, ButtonGroup } from "reactstrap";
  1. 接下来,直接进入 src/Todo/Todo.js 中的 render() 函数,我们将把我们的按钮包裹在 ButtonGroup 组件中,并将每个 button 标签更改为 Button 组件:
  render() {
    return (
      <div className={this.cssClasses()}>
        {this.props.description}
        <br />
        <hr className={styles.hr} />
        <ButtonGroup>
          <Button className="MarkDone" onClick={this.markAsDone}
          color="success">
            Mark as Done
          </Button>
          <Button className="RemoveTodo" onClick={this.removeTodo}
          color="warning">
            Remove Me
          </Button>
          <Button className="MarkCritical" onClick={this.markCritical}  
           color="danger">
            Mark as Critical
          </Button>
        </ButtonGroup>
      </div>
    );
  }
  1. 保存并重新加载,现在我们应该看到我们的项目看起来像这样:

我们还没有修复的一件事是我们的测试!现在我们应该看到大量的失败测试,所以我们需要进去并专门修复它们!

让我们的测试再次通过

由于我们更改了许多 inputbutton 标签,并且我们有针对它们的特定测试,因此我们需要首先进入 src/NewTodo/NewTodo.test.js,并将每个 .find("input").find("button") 实例更改为 .find("Input").find("Button")。我们将从我们的第一个测试开始,该测试测试表单:

  it("contains the form", () => {
    expect(component.find("Input")).toHaveLength(1);
    expect(component.find("Button")).toHaveLength(1);
  });

我们还想要修改下一个依赖于模拟 button 点击的测试:

  it("calls the passed in addTodo function when add button is clicked", () => {
    component.find("Button").simulate("click");
    expect(mockAddTodo).toBeCalled();
  });

我们几乎完成了这个文件!我们还有一个地方正在尝试模拟一个 button 点击,所以我们需要清理这个测试!我们可以这样做:

  it("blanks out the Todo Name when the button is clicked", () => {
    const updateKey = "I should be empty";
    component.instance().handleUpdate({ target: { value: updateKey } });
    expect(component.state("item")).toEqual(updateKey);
    component.find("Button").simulate("click");
    expect(component.state("item")).toHaveLength(0);
  });

保存并重新加载后,我们应该看到失败的测试更少,然后我们可以继续进行下一个失败的测试套件!我们可以这样做:

 FAIL src/Todo/Todo.test.js
 - Todo › marks the Todo as done

 Method “simulate” is meant to be run on 1 node. 0 found instead.

 34 |
 35 | it("marks the Todo as done", () => {
 > 36 | component.find("button.MarkDone").simulate("click");
 | ^
 37 | expect(component.state("done")).toEqual(true);
 38 | });
 39 |

 at ShallowWrapper.single (node_modules/enzyme/build/ShallowWrapper.js:1875:17)
 at ShallowWrapper.simulate (node_modules/enzyme/build/ShallowWrapper.js:1080:21)
 at Object.simulate (src/Todo/Todo.test.js:36:39)

 - Todo › calls the mock remove function

 Method “simulate” is meant to be run on 1 node. 0 found instead.

 39 |
 40 | it("calls the mock remove function", () => {
 > 41 | component.find("button.RemoveTodo").simulate("click");
 | ^
 42 | expect(mockRemoveTodo).toHaveBeenCalled();
 43 | });
 44 | });

 at ShallowWrapper.single (node_modules/enzyme/build/ShallowWrapper.js:1875:17)
 at ShallowWrapper.simulate (node_modules/enzyme/build/ShallowWrapper.js:1080:21)
 at Object.simulate (src/Todo/Todo.test.js:41:41)

 PASS src/TodoList/TodoList.test.js
 PASS src/App/App.test.js

Test Suites: 1 failed, 3 passed, 4 total
Tests: 2 failed, 19 passed, 21 total

从之前的代码片段中,我们可以看到其他失败的测试套件位于 src/Todo/Todo.test.js 中,所以我们也以同样的方式修复它!滚动到文件的底部并更改两个失败的测试,这些测试正在寻找 button 标签而不是 Button 组件:

  it("marks the Todo as done", () => {
    component.find("Button.MarkDone").simulate("click");
    expect(component.state("done")).toEqual(true);
  });

  it("calls the mock remove function", () => {
    component.find("Button.RemoveTodo").simulate("click");
    expect(mockRemoveTodo).toHaveBeenCalled();
  });

保存文件,当测试重新开始(你可能需要按 U 更新 Snapshots,别忘了),我们应该再次看到一个完全绿色的测试套件,如下所示:

 PASS src/Todo/Todo.test.js
 PASS src/NewTodo/NewTodo.test.js
 PASS src/App/App.test.js
 PASS src/TodoList/TodoList.test.js

Test Suites: 4 passed, 4 total
Tests: 21 passed, 21 total
Snapshots: 3 passed, 3 total
Time: 4.386s
Ran all test suites.

Watch Usage: Press w to show more.

我们 几乎 到达目的地了,但记得我们为项目添加的新功能,用于标记某些 Todo 项为 critical 吗?我们从未为它添加新的测试!好消息是,我们只需要再写一个测试!

这个测试应该几乎与查看 Todo 项被点击并标记为完成的测试相同;但是,这次我们正在寻找 Mark Critical 按钮,我们将模拟点击该按钮。按钮点击后,我们应该期望看到 componentcritical 属性从 false 变为 true,这也意味着我们在点击按钮之前,将在测试中进行一个健全性检查,以确保 critical 属性开始时为 false,并在按钮点击后变为 true!如下所示:

 it("marks the Todo as critical", () => {
 expect(component.state("critical")).toEqual(false);
 component.find("Button.MarkCritical").simulate("click");
 expect(component.state("critical")).toEqual(true);
 });

在你的测试中养成编写这些健全性检查的习惯,因为这将帮助你避免编写错误地假设默认状态的测试,并在未来导致无用的测试!

就这样!我们的设计很干净,我们的测试套件是绿色的,我们的项目正以惊人的速度前进!我们将再次运行测试套件,以确保一切仍然绿色,但如果它是的话,那么我们可以安全地继续到下一个挑战:

 PASS src/NewTodo/NewTodo.test.js
 PASS src/TodoList/TodoList.test.js
 PASS src/Todo/Todo.test.js
 PASS src/App/App.test.js

Test Suites: 4 passed, 4 total
Tests: 22 passed, 22 total
Snapshots: 3 passed, 3 total
Time: 4.969s
Ran all test suites.

Watch Usage: Press w to show more.

摘要

我们为项目添加了一些很棒的新功能,但真的不必参与很多头痛和设置(我想,还有心痛)的工作,这些工作伴随着向项目中添加两个新的 CSS 处理器!我们在 Create React App 项目中让 CSS Modules 和 SASS 顺利地协同工作,并且只需要安装一个新依赖项。我们甚至让它们 一起 顺利工作,这是一个更大的成就!

我们稍微清理了一下代码,并开始将事物分组在一起,引入了如共享 SASS 文件等概念来存储颜色和值等变量,这使得我们可以在一个地方更改颜色,例如,它将影响变量出现的所有地方!

我们的代码更干净,我们的设计师很高兴,我们也很高兴,我们可以继续推进我们的项目,而无需放慢速度。我们从未需要深入研究配置文件,或做任何比安装依赖项更复杂的事情!这是 Create React App 的又一个巨大胜利!

在下一章中,我们将深入了解如何通过 Create React App 的非常棒的代理 API 功能构建前端项目的模拟后端!

第六章:使用代理 API 模拟你的后端

在我们构建应用程序的过程中,我们已经做了大量的出色工作,但几乎所有的工作都处于一种奇怪的状态,数据完全存在于我们的 React 应用程序内部。但现实是,对于你将要工作的多数项目来说,这不会是事实,因此我们想要做一些工作,尝试将我们的 Create React App 项目的状态置于 React 本身之外。为此,我们可以利用 Create React App 世界中的另一个特性:代理后端!

此外,我们还需要花一点时间讨论如何将数据从后端服务器实际获取到我们的 Create React App 项目中!没有前端的后端并不特别有用,反之亦然!同样,一个对这两个方向都没有理解的开发者将处于一个困难的情况,他们设计系统时没有意识到它们需要如何相互交互!

我们将要构建的模拟后端服务器将作为我们前端开发人员遵循的设计文档。它并不是我们最终将使用的最终后端,而是作为一个框架,其他开发人员可以在此基础上工作,更好地理解如何正确地与我们的前端交互,以及如何构建一个不需要我们完全重建前端的后端 API!

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

  • 了解代理功能

  • 实现一个占用空间小的快速Express.js服务器

  • 学习一点关于 React 组件方法的知识

  • 通过 Fetch 从我们的后端代理获取或发送数据

  • 更新我们的测试

使用代理 API 模拟后端服务器

我们一直在使用 Create React App 构建我们的应用程序,但我们几乎将其完全保持为纯前端应用程序。这当然很好,但现实是,你可能会构建一个后面有服务器的应用程序!好消息是,在 Create React App 项目中创建模拟后端仍然尽可能简单,同时还能让你对现实世界场景或项目中的事物转换有一个良好的感觉!

设置后端 API

如前所述,假装我们的 Create React App 项目有一个后端需要付出非常少的努力,并且能让我们非常快地回到开发状态。为了利用这一点,我们首先需要在package.json文件中设置一个"proxy"。代理是一种让我们告诉 Create React App,我们发出的任何请求都应该发送到这个其他服务器,即使它看起来像是在本地发出请求。让我们从配置我们的项目开始,然后继续实现这一功能:

"proxy": "http://localhost:4000"

我们还希望添加一个命令来运行和执行我们的服务器,这样我们就可以通过 yarn 容易地运行它,因此我们还需要更新 package.json 中的 "scripts" 部分,以包含一个新的命令。我们将随意命名为 "backend",这样当我们准备好运行我们的服务器时,我们只需运行 yarn backend 命令即可:

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "backend": "node server.js"
  },

仅此还不够。我们需要在幕后实现一个 server,它能够对我们在过程中发出的任何 API 调用做出响应和反应。不过,由于我们本身无法模拟 API,我们还需要在我们的项目中添加另一个库来做这件事。我个人更喜欢使用 Express.js 来做这件事,因此我们将选择它作为我们项目的库。在我们的命令窗口中,我们将通过 yarn 添加 express:

$ yarn add express

然后,我们在 Create React App 项目的根目录中创建一个新的文件,名为 server.js。这将成为服务所有我们应用程序将发出的请求的代码,以模拟现实世界的场景。我们的服务器实际上不会是状态化的;相反,我们将通过从我们的小型、虚假 API 返回的静态数据来模拟这种行为。

这个部分将比 React 更侧重于 Node.js 和 Express 概念,因为我们需要能够设置一个快速的服务器来充当我们的模拟后端!

我们将从为我们的服务器创建一个基本的骨架开始,这将执行 express 从我们这里要求的一些设置工作。在 server.js 中,在我们的项目根目录中,我们将有以下内容:

const express = require('express');
const app = express();
const port = 4000;

app.use(express.json());

app.get('/', (req, res) => res.json({}));

app.listen(port, () =>
  console.log(`Simulated backend listening on port ${port}!`)
);

我们将首先将 Express 加载到我们的服务器项目中。由于我们将通过 Node 运行它,我们需要通过 require() 语句而不是 import() 语句将其拉入我们的项目。接下来,我们需要在我们的应用程序变量中构建我们的服务器,这是 express.js 代码的实例化。我们还将设置我们将运行的端口,这是我们在代理配置中指定的端口。我倾向于选择 1,000 的倍数作为端口,但你可以使用你个人偏好的任何端口号!

接下来,我们需要告诉我们的应用程序使用 express JSON 中间件,这将使我们能够对 JSON post body 语句做出反应和响应(当我们去创建服务器中的新 Todo 项目时,我们将需要这样做)。之后,我们在我们的 express 服务器中有了第一个路由的例子。当有人对我们的代理 API 后端根目录发起请求时,它只是返回一个空的 JSON 主体。

让我们花几分钟时间更详细地讨论一下 Express 中路由的结构。虽然我们不会深入细节并从头到尾写出来,但至少了解什么是路由以及如何在 Express 中编写路由是值得的,以避免以后产生任何混淆。

路由是结构化和编写的,形式为(express()变量的名称).(HTTP 方法)("path", (请求变量, 响应变量))然后是一个基于该路径的函数。每个路由都应该以向请求者发送某些内容结束,在我们的情况下(因为我们正在编写一个小型 API),应该发送一些 JSON。

这就足够我们目前使用了;我们将在本章后面的内容中进一步扩展这个配置!

中间件是一系列函数,它们在服务器看到请求、请求进入正确的路由,然后返回给发送者的过程中执行。在这种情况下,所有 JSON 体都被转换为 JSON,这样我们就可以在不添加大量额外代码的情况下每次都从中读取。

最后,我们需要设置一个监听器来监听我们的应用程序。我们将监听我们之前指定的端口,然后指定一个函数,当应用程序完成对该端口的监听时执行。在我们的情况下,我们只有一个简单的消息,它会记录下来,让我们知道服务器已设置并正确运行!

这段代码非常简洁,但由于它的简洁性,它也需要一些维护。例如,每次你更改代码时,你都需要重新启动你的后端服务器(尽管不是 Create React App 项目)。

确定我们的后端请求

为了确定我们将如何构建我们的 API,重新查看 UI 以识别可能发生的不同类型的函数很有帮助:

图片

看看那个用户界面,你能弄清楚我们需要什么吗?我们需要Todo项的表示以及获取当前项列表的方法,我们还需要一种改变它们状态、删除项或添加新项的方法。我们将从实现Todo索引开始,但这需要先确定我们将要使用的数据结构。首先,我们需要为每个todo创建一个对象。每个todo都需要与它们的内部对象相同的表示:

{ description: String, done: Boolean, critical: Boolean }

我们还需要一些内部id实例的表示,所以我们的新对象将是以下形式:

{ id: Number, description: String, done: Boolean, critical: Boolean }

让我们从在服务器中构建这个开始,这样我们就可以在后面的操作中更好地了解如何处理它!

构建 Todos 索引 API 请求

首先,回到server.js,我们需要整理我们的Todo项列表。你可能会从之前使用的Todo项中认出这些项,除了我们现在在项中也有一些id之外。这更接近我们在服务器中通常看到的形式,其中项不仅包含它们的正常属性,还包含主键的形式。我们将这些存储为const,因为我们不希望不小心在以后覆盖它们:

const todos = [
  { id: 1, description: 'Write some code', done: false, critical: false },
  { id: 2, description: 'Change the world', done: false, critical: false },
  { id: 3, description: 'Eat a cookie', done: false, critical: false }
];

要通过服务器get到这些 Todos,我们需要创建一个指向端点的新的路由,/api/todos

app.get('/api/todos', (req, res) => res.json({ todos: todos }));

首先,你需要使用 yarn backend 启动后端。接下来,如果我们通过某些网络工具发送 HTTP 请求(例如,使用 Postman),我们应该能够通过向 http://localhost:4000/api/todos 发送 GET 请求来验证结果:

图片

我们还可以删除 app.get("/"...) 行,因为我们不再需要那段代码。

有很多方法可以模拟对后端的请求。类似于代码编辑器的选择,Postman 是我手动发送 HTTP 请求以验证结果的工具,或者你可能已经拥有可用的工具,例如 CURL!无论你使用什么,都应该没问题。

构建添加待办事项的 API 请求

我们还需要能够创建新的待办事项。为此,我们将实现我们 API 的下一部分,在 server.js 中创建一个能够处理接收带有正文的 HTTP 请求的路由。记得之前的那行代码,我们指示 express 使用 JSON 中间件?这个代码也不复杂。我们告诉 express 我们可以接受一个 HTTP 请求,并给它标准的 reqres 参数。

从那里开始,由于我们的 Todo 列表中只有几个条目,我们给新的 Todo 分配一个等于列表长度的 id,然后使用对象展开运算符填充用户传递的其余正文!如下所示:

app.post("/api/todos", (req, res) => {
  const body = { id: todos.length + 1, ...req.body };
  res.json({ todos: [...todos, body] });
});

再次,为了验证我们是否正确地做了事情,我们将通过 Postman 发送一个快速的测试,带有 Todo 正文,并验证我们是否收到了待办事项列表,以及我们发布的新的一个!参考以下截图:

图片

那就是我们添加新 Todo 所需要做的所有事情,所以现在我们可以继续到下一个功能:删除 Todo

构建删除待办事项的 API 请求

删除一个 Todo 也很简单!就像接受一个 post 路由一样,我们必须接受一个 delete 路由并在正文中指定一个 id。这将允许我们使用类似 /api/todos/3 的 URL,其中 3 将是我们想要删除的 Todoid!我们可以通过 req.params.[参数名称] 访问 URL 中的 params。在以下情况下,我们指定的参数名称是 :id,这意味着我们可以访问的变量是 req.params.id

从那里,我们只需 filter 出不匹配的 id 实例,然后结束!记住,任何 params URL 都是以字符串形式传递的;这就是为什么我们在 filter 函数之前对 id 进行快速 parseInt() 的原因!如下所示:

app.delete("/api/todos/:id", (req, res) => {
  const todoId = parseInt(req.params.id);
  res.json({ todos: todos.filter(t => t.id !== todoId) });
});

然后,我们将在 Postman 中运行它并验证结果:

图片

React 组件的生命周期(挂载)

我们还需要了解 React 组件的生命周期,以便挂载组件,因为我们稍后需要将其作为将一切重新连接到应用程序的一部分。处理 React 组件时,有两个主要的阶段。第一个是 渲染阶段,在这个阶段,React 关注的是如何创建初始组件并将其渲染到页面上。你应该保持这些阶段干净,没有副作用,例如,我们不会包含任何调用后端以填充数据或页面上组件的调用。这里最常用的函数是 constructor()render()

然而,第二个阶段是我们可以在事后修改事物的阶段。这里要提到的主函数是 componentDidMount(),我们可以在其中(并将会)向我们的后端服务器发起请求。这些调用的图示可能看起来像这样:

图片

API 请求的放置位置

基于此,我们将不得不创建一个 componentDidMount() 函数,它将包含我们需要向我们的后端服务器发起请求的代码,而不是将它们放在 constructor() 或其他函数中!这确保了我们的组件将以合理的方式更新和挂载!

使用 React 与你的代理服务器通信

现在我们已经编写了服务器端所需的所有代码,我们需要修改我们的 TodoList 组件,以便真正调用我们编写的服务!好消息是,再次,我们需要的所有东西都已经存在于我们的 Create React App 项目中!

这也是一个很好的机会,让我们来谈谈另一个现代 JavaScript 函数:Async/Await

使用 Async/Await

如果我们想从服务器获取数据,最适合我们使用的工具就是通过一些新的 JavaScript 语法来处理异步操作和调用!

这被称为 Async/Await,是一种执行不需要应用程序完全挂起但会在处理其他数据的同时等待结果的操作方法。如果你熟悉承诺,你可能熟悉看起来像这样的代码:

doSomethingAsync().then(doSomething()).then(doSomethingElse())

这是可以的,但还有更好的方法来做这件事。随着代码的增长,这段代码可能会变得特别混乱,尤其是如果我们引入了不同的分支或需要考虑的失败标准时。最终,一串承诺可能会变成在出错时难以追踪的代码,我们希望尽量避免这种情况。

介绍 Fetch

好消息是,Fetch 也是一个极其简单的库,易于使用!它的语法简单明了,应该很容易快速理解其功能。当然,这引发了一个问题:什么是 Fetch?

简短的回答是,Fetch 是一个HTTP请求机制,它已经被提升为 JavaScript 世界中的第一公民。更长的回答是,Fetch 是 JavaScript 社区和贡献者尝试以标准化的方式处理 JavaScript 代码中的HTTP请求的尝试,而不是数百(甚至可能是数千)种不同的选择,每种选择都需要自己的实现、模式、配置等等。

Fetch 的实用性源于其无处不在,并且是 JavaScript 语言标准的一部分,尽管目前并非所有浏览器都完全支持它。因此,我们不必选择其他随机的库来工作,而是更容易依赖于标准,因为我们深知,你在使用 Fetch 时学到的技能应该可以很好地转移到你可能选择并开始使用的任何其他库中。

从服务器获取待办事项列表

要获取待办事项,我们需要改变我们构建状态的方式。如果你回想起我们最早的TodoList组件实现,我们是将所有的待办事项作为一个字符串数组构建的,然后允许我们的Todo组件完成其余的工作。当我们实际上从服务器获取信息时,这种模型并不理想,因此我们希望从依赖于简单的字符串来存储待办事项信息过渡到一个模型,其中待办事项的数据结构与其组件实现和服务器端的数据表示相匹配。

为了开始修改TodoList以适应我们的新世界,我们希望从一个空的待办事项列表和一个标志开始,这个标志用来捕获数据是否已经加载。在src/TodoList/TodoList.js中添加以下代码:

  constructor(props) {
    super(props);

    this.state = {
      items: [],
      loaded: false
    };

    this.addTodo = this.addTodo.bind(this);
    this.removeTodo = this.removeTodo.bind(this);
  }

这个loaded标志实际上非常重要;没有它,我们可能会遇到一个场景,即当页面首次加载时,页面看起来是空的,或者显示无项,而实际上它只是还没有加载完整的项列表!为了创造更好的用户体验,我们希望依赖于一个标志来告诉应用程序它是否已经完成加载,并向用户显示有关此的信息,而不是依赖于items 状态属性中是否有值。

如果你记得我们的服务器,Todo项现在将从我们在服务器文件中创建的数据结构中填充,因此它们不再只是描述。这将要求我们重构一些代码,所以我们将回过头来修复因这一变化而损坏的代码。首先,让我们添加一个新项:

  addTodo(description) {
    const newItem = {
      description: description,
      done: false,
      critical: false
    };
    this.setState({
      items: [...this.state.items, newItem]
    });
  }

newItem拆分成一个单独的变量然后传递给setState()调用对我们来说更容易,否则我们可能会使这一行代码变得非常长,而且对任何可能发生的数据结构变化来说也非常脆弱:

  removeTodo(removeItem) {
    const filteredItems = this.state.items.filter(todo => {
      return todo.description !== removeItem;
    });
    this.setState({ items: filteredItems });
  }

我们还对 removeTodo 调用执行了类似的操作。我们将根据描述移动过滤后的项目列表。接下来是 renderItems() 调用,它也将检查新的状态变量以确定数据是否已从服务器加载。此外,我们还将向 Todo 组件传递一些新属性,使其遵守我们的数据结构。具体来说,我们将传递 iddonecritical 标志,以便在 Todo 中作为传入的 props 部分设置:

  renderItems() {
    if (this.state.loaded) {
      return this.state.items.map(todo => (
        <Fragment key={'item-' + todo.description}>
          <Todo
            id={todo.id}
            key={todo.id}
            description={todo.description}
            removeTodo={this.removeTodo}
            done={todo.done}
            critical={todo.critical}
          />
          <Divider key={'divide-' + todo.description} />
        </Fragment>
      ));
    } else {
      return <p>Still Loading...</p>;
    }
  }

注意,我们向 Todo 组件传递了一些新的属性,这意味着我们需要修改它们以允许通过 props 设置 state,以便确定一个 Todo 是否已完成以及一个 Todo 是否是关键的。打开 src/Todo/Todo.js,我们将在 constructor() 函数中迅速处理这个问题:

  constructor(props) {
    super(props);
    this.state = {
      done: props.done,
      critical: props.critical
    };

    this.markAsDone = this.markAsDone.bind(this);
    this.removeTodo = this.removeTodo.bind(this);
    this.markCritical = this.markCritical.bind(this);
  }

回到 src/TodoList/TodoList.js,让我们开始编写我们的 Async/Await 功能。我们将创建一个新的函数,它是 React 标准组件生命周期的一部分,componentDidMount(),我们将将其声明为一个 async 函数。记住,我们想要在代码中使用 await 的任何地方,都必须在已经声明为 async 的函数内部这样做!我们将从一个简单的主体开始,以便首先验证它的工作方式,然后我们会对其进行更多扩展:

  async componentDidMount() {
    this.setState({ loaded: true });
  }

接下来,我们需要使用 fetch 向我们的模拟后端发起请求,我们将 await fetch 的结果:

const res = await fetch('/api/todos', { accept: 'application/json' });

记住,我们通过 Create React App 代理请求到不同的后端!因此,我们不需要指定端口或主机,因为它假装是相同的端口/主机。

这个后端服务器旨在作为当你在一个与代码运行相同的应用程序或服务上构建后端时的占位符,以实现最小延迟。当你正在构建某个前端但后端尚未完全构建时,这是一个非常好的模型!

如果我们想要对这些结果进行任何操作,我们还需要将它们转换为 JSON 格式,这也是一个 async 调用:

const json = await res.json();

最后,别忘了我们的服务器以以下形式返回数据:

{
  todos: [ ...todo1, ...todo2, ...etc ]
}

所以最后,我们需要将项目的状态替换为新状态:

this.setState({ items: json.todos, loaded: true });

就这样!现在当页面刷新时,你应该看到与之前相同的组件列表,但现在数据来自我们的模拟后端!这应该让这个函数的完整体看起来像以下代码片段:

async componentDidMount() {
  const res = await fetch('/api/todos', { accept: 'application/json' });
  const json = await res.json();
  this.setState({ items: json.todos, loaded: true });
}

在服务器上创建一个新的 Todo

我们还希望能够通过使用HTTP post 来创建一个新的Todo。这也会是一个async函数,因为我们将会对fetch进行async调用以发送数据。由于通过HTTP post 比获取更复杂,我们还需要在我们的fetch调用中指定一些选项。具体来说,我们可以通过指定HTTP方法(在我们的情况下是POST),头部(仅一个接受header的 JSON 数据,这与我们通常与任何 JSON API 通信时使用的是相同的),以及我们要发送到服务器的正文来配置调用,这只是一个新的Todo的数据结构。如果成功,我们将新的Todo添加到我们的状态中,然后结束。回到src/TodoList/TodoList.js,如下所示:

  async addTodo(description) {
    const res = await fetch('/api/todos', {
      method: 'POST',
      headers: { accept: 'application/json', 'content-type': 'application/json'  },
      body: JSON.stringify({ description: description, critical: false, done: false })
    });
    if (res.status === 200) {
      const newItem = {
        id: this.state.items.length + 1,
        description: description,
        done: false,
        critical: false
      };
      this.setState({
        items: [...this.state.items, newItem]
      });
    }
  }

删除待办事项

删除Todo的过程将与创建Todo非常相似,所以这里实际上没有太多需要描述的。最重要的事情是方法设置为DELETE,以及我们想要删除的Todoid将通过 URL 传递:

  async removeTodo(removeItemId) {
    const res = await fetch(`/api/todos/${removeItemId}`, {
      method: 'DELETE',
      headers: { accept: 'application/json', 'content-type': 'application/json' }
    });
    if (res.status === 200) {
      const filteredItems = this.state.items.filter(todo => {
        return todo.id !== removeItemId;
      });
      this.setState({ items: filteredItems });
    }
  }

我们还需要更改从src/Todo/Todo.js中调用removeTodo函数的方式,所以打开该文件,并将它传递的参数更改为 Todo 的id而不是其描述!如下所示:

  removeTodo() {
    this.props.removeTodo(this.props.id);
  }

这应该能让我们获得我们想要的绝大部分功能,但我们目前遇到的问题是我们的代码并不像我们希望的那样简单易测试。事实上,我们实际上有几个失败的测试需要修复!

回到通过测试

测试失败了。我们将首先按u来更新我们的快照,然后继续与我们的代码一起工作,修复其余的测试。好消息是,我们的Todo组件的测试很容易修复!记住,我们的Todo组件现在接受一些其他属性来初始化;根据我们的测试,它实际上只接受descriptionremoveTodo属性。

修复待办事项测试

我们需要更改我们的浅渲染组件的初始化,以接受idcriticaldone属性!在src/Todo/Todo.test.js中,我们将通过将const component语句更改以包含这些额外的属性来更改第一个失败的测试套件:

  const component = shallow(
    <Todo
      description={description}
      removeTodo={mockRemoveTodo}
      critical={false}
      done={false}
      id={1}
    />
  );

重新运行测试,现在我们应该只剩下单个失败的测试套件!不幸的是,这将也是最难修复的测试套件!

通过重构修复我们最后的失败的测试套件

不幸的是,我们的代码现在让我们陷入了一个场景,我们的组件实际上很难进行适当的测试。我们的代码必须从后端获取数据,与初始化我们的组件的代码混合,以及与按钮点击时的行为混合!这不行,所以我们需要修复这个问题!

好消息是,我们修复这个问题的最简单方法不需要大量的努力;相反,它只需要我们将一些代码移动到使其更易于扩展的方式。我们首先需要执行的操作是将所有与后端交互的代码移动到它自己的独立服务库中!

服务库模式是一个非常好的模式,当你需要将行为和与外部服务的交互锁定到更简单的 API 中时,这将使你的代码或他人的代码更容易高效地与后端服务器交互!

构建服务库

我们将从将所有 API 调用移动到一个新文件 src/TodoService.js 开始。我们将从最简单的调用开始,即从服务器获取 fetchTodos 项目:

const fetchTodos = async () => {
  const res = await fetch("/api/todos", { accept: "application/json" });
  const json = await res.json();
  return { status: res.status, todos: json.todos };
};

在这里,我们将我们的 fetchTodos() 函数编写为一个 async 函数,大部分功能与最初相同。这里唯一的重大不同是,我们将 return 语句改为不仅从服务器发送回 todos 列表,还发送服务器的 HTTP 状态码!

接下来,我们将实现创建服务器上的 Todo 的调用:

const createTodo = async description => {
  const res = await fetch("/api/todos", {
    method: "POST",
    headers: { accept: "application/json" },
    body: JSON.stringify({
      description: description,
      critical: false,
      done: false
    })
  });
  const json = await res.json();
  return { status: res.status, todos: json.todos };
};

再次强调,这篇文章几乎与你之前看到的一样,只是对 return 语句进行了修改。最后,我们将继续进行服务器上的删除调用:

const deleteTodo = async todoId => {
  const res = await fetch(`/api/todos/${todoId}`, {
    method: "DELETE",
    headers: { accept: "application/json" }
  });
  const json = await res.json();
  return { status: res.status, todos: json.todos };
};

我们添加了一些函数,所以我们将它们在服务库的末尾 export。我们只需要将这些三个函数作为命名函数 export

export { fetchTodos, createTodo, deleteTodo };

在 TodoList 中实现服务库

现在我们有了 TodoService 服务库,我们必须回到我们的 src/TodoList/TodoList.js 文件,找到我们曾经在我们的组件中写入 fetch 代码的所有区域。我们需要从文件的顶部开始,导入来自 TodoService 的这三个命名函数:

import { fetchTodos, createTodo, deleteTodo } from "../TodoService";

接下来,我们需要进入我们的 componentDidMount() 函数,我们将对其进行修改以调用 fetchTodos() 函数:

  async componentDidMount() {
    const { todos } = await fetchTodos();
    this.setState({ items: todos, loaded: true });
  }

看看这个函数现在多么干净漂亮!这绝对是一个向好的方向发展的举措!现在,让我们继续到我们的 addTodo() 函数调用:

  async addTodo(description) {
    const { status } = await createTodo(description);
    if (status === 200) {
      const newItem = {
        id: this.state.items.length + 1,
        description: description,
        done: false,
        critical: false
      };
      this.setState({
        items: [...this.state.items, newItem]
      });
    }
  }

最后,我们将修改我们的 removeTodo() 函数:

  async removeTodo(todoId) {
    const { status } = await deleteTodo(todoId);
    if (status === 200) {
      const filteredItems = this.state.items.filter(todo => {
        return todo.id !== todoId;
      });
      this.setState({ items: filteredItems });
    }
  }

这将使我们大部分回到修复失败的测试,但我们还需要在失败的测试本身做一些工作。

最后修复我们失败的测试套件

首先,前往失败的测试套件,src/TodoList/TodoList.test.js,在那里我们需要在 Jest 中创建一个 mock 库。mock 库基本上是我们告诉 Jest 我们需要伪造一个特定的 import 模块的行为,这样,无论何时使用它,我们的伪造 mock 函数都将被使用。这将允许我们伪造我们编写的整个服务库的行为,使我们能够测试我们的组件并验证功能,而无需实际调用某个后端 API 的测试!

我们将在测试文件的顶部,在import语句之下,添加库模拟和三个mock函数:

jest.mock("../TodoService", () => ({
  fetchTodos: jest.fn().mockReturnValue({ status: 200, todos: [] }),
  createTodo: jest.fn().mockReturnValue({ status: 200, todos: [] }),
  deleteTodo: jest.fn().mockReturnValue({ status: 200, todos: [] })
}));

我们编写这些代码的方式是,函数始终会工作,始终返回一个空的todos列表,并且始终返回一个假的HTTP状态码200!处理完这些之后,我们可以清理失败的测试。

失败的两个测试是因为当我们处理非异步测试且涉及async功能时,行为是随机的!我们可以将我们的测试写成async函数,类似于我们写其他函数调用一样!想想测试声明的结构:

it("does some thing", () => {
  // Do some work here
});

如果我们想要使那个测试异步化,我们就会将测试声明写成如下:

it("does some thing", async () => {
  // Do some work here
});

考虑到这一点,让我们看看修正后的函数:

  it("adds another Todo when the addTodo function is called", async () => {
    const before = component.find(Todo).length;
    await component.instance().addTodo("New Item");
    component.update();
    const after = component.find(Todo).length;
    expect(after).toBeGreaterThan(before);
  });

与我们之前进行的测试相比,并没有太大的不同,只是增加了对addTodo()的调用需要await语句。现在让我们来看看我们对removeTodo()的测试:

 it("removes a Todo from the list when the remove Todo function is called", async () => {
 const before = component.find(Todo).length;
 const removeMe = component.state("items")[0];
 await component.instance().removeTodo(removeMe.id);
 component.update();
 const after = component.find(Todo).length;
 expect(after).toBeLessThan(before);
 });

你可能会收到关于第一个测试的错误信息,即尝试渲染组件而不崩溃的测试。由于我们新的async/await添加,这个测试不再可行,所以只需删除它!现在运行测试,我们应该看到以下结果:

 PASS src/Todo.test.js
 PASS src/App/App.test.js
 PASS src/TodoList/TodoList.test.js
 PASS src/NewTodo/NewTodo.test.js
 PASS src/Todo/Todo.test.js

Test Suites: 4 passed, 4 total
Tests: 21 passed, 21 total
Snapshots: 3 passed, 3 total
Time: 5.596s
Ran all test suites.

我们又回到了一个完全通过测试的套件!

摘要

在本章中,我们花了不少时间探索了在 Create React App 项目旁边制作、模拟和运行后端服务器的选项。这使我们能够将项目转交给其他团队,他们知道在前后端开发团队之间作为开发过程一部分需要存在的隐式数据结构合约。

我们还花了不少时间探索了从 React 项目内部检索数据到服务器的选项!这只是许多实现方式中的一种;一般来说,JavaScript 项目往往有大量的(并且每种方式都有其自身的优点)实现许多常见功能的方法。使用 Fetch 和库服务只是实现这一点的可能方式之一,但这是我个人发现非常成功并且倾向于坚持的方式,除非我看到需要做更复杂的事情。

在下一章中,我们将探讨一些更先进的选择,以支持以支持移动用户和互联网连接较差的用户的方式构建 Web 应用程序:渐进式 Web 应用程序!Create React App 自带构建渐进式 Web 应用程序的出色支持,因此我们将深入探讨构建渐进式 Web 应用程序,它为我们提供了哪些功能和机会,以及我们如何利用这些功能来制作一个真正现代的 React 应用程序,同时仍然保持在 Create React App 项目的范围内!

第七章:构建渐进式 Web 应用

构建现代 Web 应用的美丽之处在于能够利用诸如渐进式 Web 应用PWA)之类的功能!但它们可能有点复杂。就像往常一样,Create React App 项目让这一切对我们来说变得容易得多,但这次也有一些重要的注意事项需要我们思考。

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

  • 检查 PWA 及其添加的内容

  • 学习如何将我们的 Create React App 项目配置为自定义 PWA

  • 修改和调整manifest文件

  • 探索 Service Worker、其生命周期以及如何与 Create React App 一起使用

  • 探索使用 Create React App 构建 PWA 的注意事项

理解和构建 PWA

PWAs 是那些听起来非常酷但理解其何时、为什么以及如何构建它们却很复杂的功能之一。让我们花点时间揭开它们的神秘面纱,帮助你理解为什么它们是我们 Create React App 项目中如此强大(且受欢迎)的组成部分,以及我们如何开始使用它们!

什么是 PWA?

让我们简单谈谈什么是 PWA,因为关于 PWA 确切做什么的信息和误解很多!

下面是一个简短、可能不太有用的 PWA 功能版本;它仅仅是一个执行以下操作的网站:

  • 只使用HTTPS

  • 添加一个 JSON manifest(web 应用 manifest)文件

  • 有 Service Worker

虽然是的,PWA 必须具备这些行为,但仍然很难理解 PWA 是什么,或者它最终能给你带来什么。例如,这告诉你关于它在不同浏览器中做什么的信息吗?在不同窗口大小下呢?关于它的可访问性或当网络慢或不存在时如何工作呢?

我们如何定义 PWA?

对于我们来说,PWA(Progressive Web Application,渐进式 Web 应用)是一个可以在移动设备或桌面电脑上安装/运行的 React 应用。本质上,它只是你的应用,但具有一些使其更先进、更有效、更能抵御网络不佳/无网络的能力。PWA 通过一些原则、技巧和需求来实现这些功能,这是我们希望遵循的:

  • 应用必须可供移动和桌面用户使用

  • 应用必须通过HTTPS运行

  • 应用必须实现一个 web app JSON manifest文件

  • 应用必须实现 Service Worker

现在,第一个问题是设计问题。你的设计是否是响应式的?如果是的话,恭喜你,你已经迈出了拥有 PWA 的第一步!下一个问题也是一个可能不太相关的实现问题:当你将应用部署到生产环境时,你是否只使用了HTTPS?我希望答案是肯定的,但仍然是一个值得问的问题!

然而,接下来的两个是我们可以在 Create React App 项目中实现的事情,我们将把这些作为本章的重点!

在 Create React App 中构建 PWA

如果我们想在 Create React App 中开始构建 PWA,如前所述,我们需要开始针对我们提出的两个要求进行实施。我们将从最简单的问题开始解决:为我们的 PWA 实现一个manifest文件!

从我们的清单文件开始

好吧,所以我确定了我们需要构建的两个项目来使这一切发生:JSON manifest文件和服务工作者!简单,对吧?实际上,比这还简单。你看,Create React App 默认会为我们创建一个 JSON manifest文件,作为项目创建的一部分!这意味着我们已经完成了这一步!让我们庆祝一下,回家,脱掉鞋子,因为我们现在已经完成了,对吧?

嗯,差不多吧。我们应该看看那个默认的manifest文件,因为我们不太可能希望我们的TodoList项目被称为"Create React App Sample"。让我们看看位于public/manifest.jsonmanifest文件:

{
 "short_name": "React App",
 "name": "Create React App Sample",
 "icons": [
 {
 "src": "favicon.ico",
 "sizes": "64x64 32x32 24x24 16x16",
 "type": "image/x-icon"
 }
 ],
 "start_url": ".",
 "display": "standalone",
 "theme_color": "#000000",
 "background_color": "#ffffff"
}

其中一些键非常直观,或者至少你可以从中推断出它们的功能。然而,一些其他的键可能有点奇怪。例如,"start_url"是什么意思?我们可以选择哪些不同的显示选项?什么是"theme_color""background_color"?这些不都是由我们应用的 CSS 决定的吗?

还不止这些。实际上,让我们深入这个 JSON manifest文件的世界,并使其变得更有用!

使用 Chrome 查看我们的清单文件的实际效果

首先,为了能够测试这个,我们应该有一个可以验证我们更改结果的地方。我们将从 Chrome 开始,如果你进入开发者工具部分,你可以导航到应用标签,并直接进入服务工作者部分!让我们看看我们的应用看起来是什么样子:

探索清单文件选项

如果清单文件没有解释不同的键和选项的含义,那么它并不很有帮助,所以让我们深入地,键接键,查看我们可用的每个配置选项以及我们可以为每个选项使用的可能值。

name 和 short_name

我们拥有的第一个键是short_name。这是当,例如,标题只能显示比完整的应用或站点名称更小的文本时可能显示的名称的简短版本。与之相对应的是name,这是你应用的完整名称。一个很好的例子可能是这样的:

{
 "short_name": "Todos",
 "name": "Best Todoifier"
}

图标

下一个要查看的键是 "icons" 键,它是一个子对象的列表,每个子对象都有三个键。这包含 PWA 应该使用的图标列表,无论是用于桌面、手机主屏幕还是其他地方。每个 "icon" 对象都应该包含一个 "src",这是一个指向图标图像文件的链接。接下来,你有 "type" 键,它应该告诉 PWA 你正在处理什么类型的图像文件。例如,如果你正在使用 .png 文件,你将在这里列出 "image/png" 作为类型。最后,我们有 "sizes" 键,它告诉 PWA 图标的尺寸。为了获得最佳效果,你应该至少有一个 "512x512" 和一个 "192x192" 的图标。PWA 将负责在必要时调整大小。

start_url

start_url 键用于告诉应用程序相对于你的服务器,它应该在应用程序的哪个位置开始。虽然我们现在没有使用它,因为我们有一个单页、无路由的应用程序,但在一个更大的应用程序中可能会有所不同,所以你可能只想让 start_url 键表示你希望它们从哪里开始。另一个选项是在 url 的末尾添加查询字符串,例如跟踪链接。一个例子可能是这样的:

{
 "start_url": "/?source=AB12C"
}

background_color

这是应用程序首次启动时显示启动屏幕时使用的颜色。这类似于你第一次从手机上启动应用程序时;在应用程序加载期间临时弹出的那个小页面就是启动屏幕,这将是它的背景。这可以是颜色名称,就像你在 CSS 中使用的那样,也可以是颜色的十六进制值。

display

display 影响应用程序启动时浏览器的 UI。有方法可以使应用程序全屏显示,隐藏一些 UI 元素等。以下是可能的选项及其说明:

描述。
browser 正常的网页浏览器体验。
fullscreen 没有浏览器 UI,并占据整个显示区域。
standalone 使网络应用程序看起来像原生应用程序。它将在自己的窗口中运行,并隐藏很多浏览器 UI,使其看起来和感觉更像原生应用程序。

在我们的例子中,我们将使用 standalone 作为显示设置!

方向

如果你想要在横向方向上制作你的应用程序,你将在这里指定它。否则,你将省略这个选项,不将其包含在 manifest 中:

{
  "orientation": "landscape"
}

scope

范围有助于确定你的 PWA 在网站中的位置以及它不在的位置。这可以防止你的 PWA 尝试加载运行范围之外的内容。"start_url" 必须位于你的范围内部才能正常工作!这是可选的,在我们的例子中我们将省略它。

theme_color

这设置了工具栏的颜色,再次使其感觉和外观更接近原生。如果我们指定了元主题颜色,我们会将其设置为与该指定相同。就像背景颜色一样,这可以是 CSS 中使用的颜色名称,也可以是颜色的十六进制值。

自定义我们的清单文件

现在我们已经是manifest文件的专家了,让我们自定义我们的清单文件!我们在这里和那里做一些更改,但不会进行任何重大更改。为了这本书的目的,我们不会担心与图像一起工作,所以我们会暂时保留它们。让我们看看我们在public/manifest.json中是如何设置manifest文件的:

{
 "short_name": "Todos",
 "name": "Best Todoifier",
 "icons": [
 {
 "src": "favicon.ico",
 "sizes": "64x64 32x32 24x24 16x16",
 "type": "image/x-icon"
 }
 ],
 "start_url": "/",
 "display": "standalone",
 "theme_color": "#343a40",
 "background_color": "#a5a5f5"
}

因此,我们将short_namename键设置为与实际应用程序匹配。我们完全保留了icons键,因为我们实际上不需要对它做太多任何事情。

接下来,我们将start_url更改为仅"/",因为我们假设这个应用程序是这个域上唯一运行的东西。我们将display设置为standalone,因为我们希望我们的应用程序能够被添加到某个人的主页上,并被识别为真正的 PWA。

最后,我们将主题颜色设置为#343a40,这与导航栏的颜色相匹配,并将为 PWA 提供更流畅的外观和感觉。我们还设置了background_color键,这是为我们启动屏幕设置的,颜色为#a5a5f5,这是我们的正常Todo项的颜色!

如果你回顾一下键的解释,你会记得我们还需要更改public/index.html文件中的元主题标签,所以我们将打开它并快速进行更改:

<meta name="theme-color" content="#343a40" />

就这样!我们的manifest文件已经自定义了!如果我们一切都做得正确,我们应该能够在 Chrome 开发者工具中再次验证这些更改:

图片

连接服务工作者

为我们的应用程序创建一个可工作的 PWA 的另一个必要组件是构建服务工作者。服务工作者是 PWA 中那些组件之一,其理解程度因人而异,而且它们的效用也不立即明显。尽管我们为我们的 PWA 实现服务工作者所需的工作量非常小(可能甚至比我们的manifest文件还要少),但我们将花一些时间探索和理解服务工作者。

服务工作者是什么?

Service Worker 被定义为浏览器在幕后运行的脚本,与主浏览器线程分开。它可以拦截网络请求,与缓存(存储或从缓存检索信息)交互,或监听并传递推送消息。这也带来了一些注意事项。它是完全异步的,这意味着需要同步操作的所有内容,例如 XMLHttpRequestXHR)或与 localStorage 操作,都不能在 Service Worker 代码内部使用。它还可以执行一些其他巧妙的小技巧,例如即使在应用程序本身不活跃的情况下也能接收推送消息,允许你在应用程序未打开时向用户显示通知!

由于它可以拦截网络请求并从服务器存储/检索信息,因此它还可以在离线状态下运行,这使得你的应用程序可以立即启动并使用,并从服务器逐步获取更新或定期在后台更新!

Service Worker 生命周期

Service Worker 的生命周期相当简单。主要有三个主要阶段:

  • 注册

  • 安装

  • 激活

每个这些阶段都很直观易懂。

注册 是让浏览器知道 Service Worker 所在位置以及如何将其安装到后台的过程。注册的代码可能看起来像这样:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js')
  .then(registration => {
    console.log('Service Worker registered!');
   })
  .catch(error => {
    console.log('Error registering service worker! Error is:', error);
   });
}

安装 是在 Service Worker 已注册后发生的过程,并且仅在 Service Worker 还未安装或自上次以来已更改的情况下才会发生。

service-worker.js 文件中,你可以添加以下内容来监听这个 event

self.addEventListener('install', event => {
  // Do something after install
});

最后,激活 是在所有其他步骤完成后发生的步骤。Service Worker 已注册并安装,现在是 Service Worker 开始执行其任务的时候了:

self.addEventListener('activate', event => {
  // Do something upon activation
});

我们如何在我们的应用程序中使用 Service Worker 呢?

那么,我们如何在我们的应用程序中使用 Service Worker 呢?嗯,使用 Create React App 来做这件事很简单,但有一个主要的注意事项:不幸的是,你无法在不推出项目的情况下配置 Create React App 默认生成的 service-worker.js 文件!然而,并非所有希望都破灭了;你仍然可以通过使用默认的 Create React App 生成的 Service Worker 来利用一些 PWA 和 Service Worker 的亮点。

要启用此功能,请跳转到 src/index.js,并在最后一行将 Service Worker 的 unregister() 调用更改为 register()

serviceWorker.register();

现在我们正在选择加入我们的 Service Worker!接下来,为了真正看到结果,你需要运行以下命令:

$ yarn build

我们将创建一个 生产 版本(我们将在第八章 准备应用程序投入生产中更详细地介绍)。你会看到一些我们希望作为此过程一部分跟踪的输出:

The build folder is ready to be deployed.
You may serve it with a static server:

 yarn global add serve
 serve -s build

根据指示,我们将全局安装 serve,并按照指示运行命令。当我们运行这个命令时,我们应该看到以下输出:

$ serve -s build

我们将得到以下输出:

现在请在您的本地浏览器中打开 http://localhost:5000,您将能够在 Chrome 开发者工具中看到,您的应用程序的服务工作者正在运行:

注意,当您在 localhost 上运行时,要求服务工作者和 PWA 使用 HTTPS 的规则并不适用!

摘要

希望我们至少已经探索了足够多的渐进式网络应用程序,至少它们的部分神秘性已经被揭开!构建 PWA 时的许多困惑和麻烦往往源于没有好的起点来构建一个 PWA。要从 PWA 中获得价值,您必须拥有一个 PWA,但通常需要证明它们的价值!真是一场旋风!

Create React App 在我们如何实现服务工作者方面有限制,这确实限制了我们的 PWA 的功能和实用性。但这并不是说它束缚了我们,我们可以通过预缓存网络和 API 响应等有趣的小技巧来提高我们的应用程序的加载速度,即使加载的浏览器最初是离线的。话虽如此,这就像 Create React App 中的许多其他事情一样:一个惊人的垫脚石,也是未来开始使用 PWA 的绝佳方式!

在下一章和最后一章中,我们将整理我们项目的几个 loose ends,并简要讨论一下生产构建和从 Create React App 中退出!我们将探讨如何将我们的代码带入生产环境。我们还将讨论将一些其他主要库导入我们的 Create React App 项目中,例如 Redux!

第八章:为您的应用准备生产环境

在这本书的篇幅内,我们已经在 Create React App 中构建了一个相当不错的入门级应用。虽然它可能不会赢得任何奖项,但你现在有了建立在这个基础之上的能力,并将其转变为下一个世界领先的番茄工作法追踪器,或者可能是为开发者开发的新任务管理应用!在这个过程中,我们涵盖了构建过程和引导过程,但尚未关注构建任何应用的最终部分:将其部署到生产环境!

我们已经花费了很多时间讨论 Create React App 中的不同重要特性,并在其基础上构建了一个应用。我们对应用做了很多扩展;我们将一些从零开始的东西变成了一个功能性的应用,使用了最新的最先进的现代 JavaScript 编程技术,并且使用了在以任何专业环境使用 Create React App 时你会看到的那些技术。

我们需要进一步扩展并介绍我们还可以运行的剩余 Create React App CLI 命令:buildeject。我们在一些地方添加了一些额外的库,主要是作为实用库,但我们也应该花一些时间与一些额外的库一起工作,以便了解这对工作流程的影响。因此,我们将讨论一些常见的库,包括它们的用法、影响以及如何了解更多关于它们的信息。

到本章结束时,你将了解以下主题:

  • 将其他库添加到我们的应用中

  • 使用 Create React App eject 及其对应用的影响

  • 使用 Create React App build 创建生产构建

添加其他库

正如我们之前提到的,我们将添加一些更多的库。如果你对交换这些库中的任何一个不感兴趣,你可以安全地跳过这一部分,但有一些可能代表你在团队环境中编写 JavaScript 时会遇到的情况的项目要讨论。例如,Redux 在某个时刻被认为是构建任何具有一定复杂性的 React 项目的(如果不是必需的)重要部分。最近,这种心态有所减弱,一般来说,人们只有在更必要的时候才会选择使用像 Redux 这样的库,当状态管理是它们应用中更困难的部分时。无论如何,我们应该有在应用中添加它的经验,这样我们就可以准备好为任何可能使用它的项目做出贡献。还有许多其他常见的库与 React 一起使用,我们将在快速介绍它们之后,再转到使用 Redux 在 Create React App 项目中的快速示例!

其他流行的 React 库

在使用 React 的时候,还会经常遇到其他库。这些可能是帮助您使用浏览器地址栏来确定要渲染为主应用的组件的库,或者可能是为 React 中的任何表单元素提供更多原生功能并减少更多模板代码的库。

React Router

React Router 是一个旨在帮助您将请求路由到特定组件的库。其理念是,当有人访问您的项目时,他们可能想要与一个特定的组件进行交互。这有助于您管理这些请求并找出将用户直接带到该组件的正确方式。好消息是,在 Create React App 项目中安装 React Router 并使用它实际上非常简单!要安装它,您只需运行以下命令:

$ yarn add react-router-dom

然后,您可以将 src/App/App.js 文件修改为开始向组件添加新路由!React Router 与 Redux 类似,它本身是一个非常复杂的工程;因此,如果您想了解更多,可以访问 reacttraining.com/react-router/

React Final Form

如果您正在处理任何形式的交互式网络应用,您可能需要能够以一致和高效的方式处理网页表单。好消息是,由于这是一个非常普遍的问题,因此有多个流行的库可以高效地处理 React 中的表单处理。其中最受欢迎的是 React Final Form,您可以通过运行以下命令将其添加到项目中:

$ yarn add react-final-form final-form

从那里,您可以使用 React Final Form 的任何功能来替换应用程序中的表单!有关如何充分利用此库的更多信息,您可以访问 github.com/final-form/react-final-form

添加 Redux 进行状态管理

Redux 是一个用于管理应用程序状态的常见库,尤其是在应用程序稍微复杂一些,并且您想要控制前端项目中事件流的时候!Redux 提供了一种方式,让您可以紧密控制应用程序的整个状态如何受到不同类型事件的影响,例如按钮点击或表单更改。如果您的应用程序变得非常大且复杂,管理影响应用程序的众多不同事件流变得过于困难,那么在项目中引入类似 Redux 这样的工具将是一个很好的时机!

好消息是,与之前的库类似,您也可以轻松地将 Redux 添加到您的 Create React App 项目中!您需要添加 reduxreact-redux,以确保您拥有连接一切所需的所有绑定:

$ yarn add redux react-redux

这应该会安装你开始使用 Redux 所需的所有内容。类似于我们提到的其他库,Redux 是一个极其复杂的话题,单独一本书都足以涵盖。如果你想了解更多关于 Redux 的信息,请访问redux.js.org/

创建一个生产构建

最后,我们准备好将所有这些打包起来,为生产部署做好准备!我们的代码完成了,代码运行正常,一切都很完美;那么,我们现在该做什么呢?

这很简单;我们只需运行yarn build,然后就算完成了!嗯,那基本上就是整个过程。你看,当你运行yarn build时,Create React App 会尝试找出最佳、最高效的方式来捆绑你在 Create React App 项目中所有的工作,并最小化/生产化你所写的每一行代码——你添加的每一张图片和资产,需要包含的每一个库——所有这些。

如何创建一个生产构建

要创建一个生产构建,我们只需要运行yarn build。就这么简单!请看以下内容:

$ yarn build
yarn run v1.12.3
$ react-scripts build
Creating an optimized production build...
Compiled successfully.

File sizes after gzip:

69.67 KB (+4.53 KB) build/static/js/1.877dc59d.chunk.js
21.85 KB build/static/css/1.4cb1c3c1.chunk.css
2.32 KB (+46 B) build/static/js/main.1f5d1459.chunk.js
763 B build/static/js/runtime~main.229c360f.js
507 B build/static/css/main.af121e46.chunk.css

The project was built assuming it is hosted at the server root.
You can control this with the homepage field in your package.json.
For example, add this to build it for GitHub Pages:

"homepage" : "http://myname.github.io/myapp",

The build folder is ready to be deployed.
You may serve it with a static server:

yarn global add serve
serve -s build

Find out more about deployment here:

http://bit.ly/CRA-deploy

Done in 26.70s.

现在,你应该在你的应用程序的build/目录中拥有一个生产就绪的文件构建!这是一个高度优化的构建,准备好复制和部署,无论你在哪里可以部署你的应用程序!

需要注意的重要一点是,这个过程输出的结果是静态文件(完全编译的 HTML/JS/CSS),可以从任何可以服务静态文件回传给用户的地方部署和运行——所以,本质上,任何你已有的内容分发网络CDN)或网页服务器都可以。

这个过程高度依赖于 Webpack 的构建过程和多个不同的构建和优化插件,它们优化了构建过程的几乎每一个部分。文件被压缩,根据它们的用途和范围进行分块(以减少导入我们不需要的文件),并设置为允许浏览器缓存,以尽可能减少每次有人重新访问你的网站时的努力!

压缩意味着文件通过重命名代码、最小化空白或以其他方式减少多余代码,尽可能减小文件大小,从而使需要部署的 JavaScript 量尽可能小。

关于部署过程的思考

实际的部署过程要复杂一些。有无数种部署方法,但鉴于我们是在一个代理后端服务器上构建的应用程序,其中一种方法是我们将项目集成到一个现有的后端服务器中,使用某种东西来服务我们的前端代码,例如 Rails/node/Phoenix 服务器。这允许我们运行这段代码作为我们的前端,并在幕后有一个后端服务器支持这个应用程序。没有那个,我们的应用程序将无法工作;如你所回忆,这是一个由Todo后端支持的应用程序。

退出我们的项目

我们可用的另一个选项是能够在 Create React App 中 eject 我们的项目。Ejecting 一个应用意味着它移除了 Create React App CLI 的所有脚手架和限制,以及你可能从这个操作中预期到的所有好处和注意事项。首先,我们获得了更大的能力和控制权,可以按需调整事物,这是非常好的;但这也让你进入了一个需要理解你的 Babel 配置、Webpack 配置以及你之前能够忽略的每一个幕后配置选项的世界。

eject 是一把双刃剑;它是一个强大的工具,它让你能够超越 Create React App 带入你世界中的规则。然而,你现在将负责未来配置修改带来的任何头痛问题。

话虽如此,eject 在 Create React App 的世界中也是一个重要的命令。我见过许多项目最初是以 Create React App 构建的项目开始的,但随着应用变得足够复杂,它们发生了变化。在那个阶段,进行 eject 是一个好主意,这样你就可以根据需要相应地调整事物。再次强调,本书的一个主要重点是理解如何成为 Create React App 及其最新版本的专家;因此,理解和使用所有命令是这一过程的重要组成部分。

如何进行 eject

让我们先来谈谈如何使用 Create React App 进行 eject。这个过程本身其实很简单:你只需运行 npm run ejectyarn eject,然后过程就开始了。这也不是足够的信息来做出关于何时何地使用 Create React App 进行 eject 的明智决定,所以我们将实际探索运行该命令的结果。我们将首先将其移动到我们选择的源控制中的新分支(或者,如果你没有使用源控制,将目录复制到某个地方,这样你就可以在没有失去项目的风险下进行实验)。

这是一个永久性的操作。如果你不希望被这些更改所困扰,确保你已经复制了你的文件夹,或者以某种方式创建了分支,这样你就不需要在这一部分被锁定在 eject 中了!

我们得到以下输出:

$ yarn eject

yarn run v1.12.3
$ react-scripts eject
? Are you sure you want to eject? This action is permanent. (y/N)
If you answer yes, you'll see a ton of output:

Ejecting...

Copying files into /Users/brandon/Documents/dev/create-react-app-book/code/todoifier
Adding /config/env.js to the project
Adding /config/paths.js to the project
Adding /config/webpack.config.dev.js to the project
Adding /config/webpack.config.prod.js to the project
Adding /config/webpackDevServer.config.js to the project
Adding /config/jest/cssTransform.js to the project
Adding /config/jest/fileTransform.js to the project
Adding /scripts/build.js to the project
Adding /scripts/start.js to the project
Adding /scripts/test.js to the project

Updating the dependencies
Removing react-scripts from dependencies
Adding ...lots of packages... to dependencies

Updating the scripts
Replacing "react-scripts start" with "node scripts/start.js"
Replacing "react-scripts build" with "node scripts/build.js"
Replacing "react-scripts test" with "node scripts/test.js"

Configuring package.json
Adding Jest configuration
Adding Babel preset
Adding ESLint configuration

Running yarn...
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...

success Saved lockfile.
Ejected successfully!

Please consider sharing why you ejected in this survey:
http://goo.gl/forms/Bi6CZjk1EqsdelXk1

Done in 14.39s.

哇!当你 eject 时会发生很多事情!它大约创建了 13 个新的脚本,其中大多数是辅助/实用 JavaScript 文件或 Webpack、Babel 或 Jest 等事物的配置文件。这其中的许多是为了让你能够继续使用你的项目,就像它已经是一个主要的 Create React App 项目一样,尽管你刚刚进行了 eject。例如,我们的大多数命令应该工作得完全一样。例如,如果我运行 yarn test,我应该仍然得到一个完整的 Test Suite 运行并通过:

$ yarn test
yarn run v1.12.3
node scripts/test.js

PASS src/NewTodo/NewTodo.test.js
PASS src/TodoList/TodoList.test.js
PASS src/Todo/Todo.test.js
PASS src/App/App.test.js

Test Suites: 4 passed, 4 total
Tests: 21 passed, 21 total
Snapshots: 3 passed, 3 total
Time: 5.939s
Ran all test suites.

Watch Usage: Press w to show more.

同样,如果我运行 yarn start,我应该能够期待以与我之前相同的方式使用我的 React 项目:

$ yarn run v1.12.3
node scripts/start.js

我们的浏览器应该仍然会启动并自动打开到http://localhost:3000/。我们还可以继续运行我们自己的后端模拟服务器,请求会被适当代理!正如您所看到的,Create React App 团队已经尽其所能使eject过程尽可能无痛和无缝。我们甚至还可以使用yarn build来构建生产环境。

我们还可以看到为我们创建的配置文件,基于 Create React App 如何构建其项目。例如,我们可以看到 Webpack 配置在这些文件中:

Adding /config/webpack.config.dev.js to the project
Adding /config/webpack.config.prod.js to the project

如果您想以某种自定义方式调整您的config,这就是您需要操作的地方,您可以根据他们在这里填充的极其详尽的文件来构建您的配置。

您也可以通过查看scripts文件夹中的scripts/test.js以及位于config/目录下的 Jest 特定配置文件,来了解他们是如何无缝设置 Jest 的。

使用 eject 的缺点

记住,这并非免费提供,也没有立即将项目 eject 的理由。首先,当 Create React App 团队更新 Create React App 的脚本时,您将不会获得任何潜在的时间节省更改或生产力提升器。如果出现问题,如果某些东西工作不正常,或者在其他情况下,您实际上将无处寻求支持。

eject命令就像购买现成的东西。它可能不再有任何附加条件,但它也没有任何支持。买家需谨慎!

摘要

就这些了!在这个阶段,您应该已经对 Create React App 2 的变更和好处有了稳固的理解。我们已经探索了新旧功能,以确保我们能够构建尽可能最新、最现代的 JavaScript 实现!我们正在使用我们在可能构建的大多数前端项目中已经会使用的库和技术。

我们还深入研究了如何使用 Create React App 设置健康的软件开发生命周期,使我们的应用程序在发生更改时运行顺畅且具有弹性。我们的应用程序经过良好的彻底测试,并且不易随机崩溃。这一切都是我们在没有任何配置的情况下完成的;在我看来,这是在 Create React App 中与前端开发项目合作的最大优势之一!

我们还把许多其他非特定的 Create React App 解决方案整合到了我们的代码中,无论是通过 CSS 模块或 SASS 提供更好的 CSS 支持,还是通过服务库提供额外的库和巧妙的抽象。我们努力以智能的方式编写项目代码,以便于轻松重用和扩展,同时不会使我们的项目变得晦涩或让其他开发者难以贡献。我们还代理了一个后端 API,这样我们就可以与后端开发者合作,向他们展示前端使用的接口和数据语言,减少多个开发团队和理念之间的摩擦和来回沟通!

最后,你已经看到了当我们超越 Create React App 的安全边界,将其应用推出到标准的 Webpack 配置时,我们的选项会变成什么样子!我们的应用依然存在,并且我们可以配置新的和令人兴奋的项目功能,而不会束缚我们自己或我们的开发团队!

我希望你已经学到了很多关于如何真正开始使用 Create React App(以及通过代理,React 本身)的知识。非常感谢你陪我走过了这段旅程,我迫不及待地想看看你在那里能创造出什么!

posted @ 2025-09-08 13:02  绝不原创的飞龙  阅读(17)  评论(0)    收藏  举报