React18-设计模式和最佳实践-一-

68 阅读1小时+

React18 设计模式和最佳实践(一)

原文:zh.annas-archive.org/md5/513d5bc62ca582f3c794eb80a8a48f5b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

React是一个革命性的开源 JavaScript 库,通过构建由称为组件的小型、隔离的块组成的复杂用户界面,为 Web 应用程序注入活力。本书作为一份路线图,引导您领略 React 的奇妙之处,通过引入一个高效的工作流程来提高您的生产力,同时不牺牲质量。

我们的旅程从深入 React 的核心开始,全面了解其内部机制和架构。在这个坚实的基础之上,我们将引导您编写干净且易于维护的代码,将复杂的概念分解成易于消化和管理的小块。

在我们继续旅程的过程中,我们将揭示构建组件的艺术,这些组件不仅仅是单一实体,而是可以在整个应用程序中重复使用的部件。我们将阐明构建应用程序结构的方法,使它们更加有序和易于管理。随着我们为您提供有效实现这一目标的策略和技术,创建功能表单的看似艰巨的任务将变得轻而易举。

随着我们进一步攀登,我们将沉浸在 React 组件的样式化中。您将学习如何通过美学吸引力使您的应用程序生动起来,同时确保它们保持快速和响应。此外,您还将发现提高应用程序性能的秘密,对您的组件进行微调以实现速度和效率。

在我们旅程的最后阶段,我们将深入研究有效的测试方法,提高您应用程序的质量和可靠性。您还将获得对贡献 React 及其繁荣生态系统的洞察,加入那些不断推动其发展的开发者行列。

到本书结束时,试错过程、开发障碍和猜测将成为过去。您将掌握 React 的力量,拥有构建和部署真实世界 React Web 应用程序所需的知识和技能,自信且优雅。

这本书面向谁

这本书是为那些希望提高他们对 React 的理解并将其应用于实际应用程序开发的网络开发者而写的。假设您具有中级 React 和 JavaScript 经验。

本书涵盖的内容

第一章React 的初学者之旅中,我们通过学习编写声明式代码和区分我们的组件与 React 的元素来开始理解 React 的旅程。我们还讨论了为什么我们在 React 中将逻辑和模板结合起来,这个决定最初颇具争议,但最终是有益的。在 JavaScript 快速发展的世界中,我们建议采取小而可控的步骤以避免疲劳。我们通过介绍新的create-vite工具来结束,为您在 React 中进行动手编码体验做好准备。

第二章介绍 TypeScript中,我们将学习 TypeScript 的基础知识,包括创建简单的类型、接口、使用枚举、命名空间和模板字符串。我们还将了解如何设置我们的第一个 TypeScript 配置文件(tsconfig.json),并将其分为两部分 - 一个公共部分和一个特定部分,这在与 MonoRepos 一起工作时特别有用。在本章之后,你将准备好深入使用 JSX/TSX 代码,并在下一章探索使你的代码更好的方法。准备好使用 TypeScript 使你的 React 应用易于使用和维护。

第三章清理你的代码中,我们将了解 JSX,包括它的编写方式和它能做什么。我们还将设置 Prettier 和 ESLint 以保持我们的代码整洁并防止错误。此外,我们将学习函数式编程,它使我们的代码更容易管理和测试。在整理好我们的代码后,我们将准备好更深入地学习 React,并在下一章学习如何制作我们可以重复使用的组件。通过养成良好的习惯,我们可以构建简单易管理、可扩展和可检查的 React 应用。

第四章探索流行的组合模式中,我们将学习如何使用“props”使我们的可重用组件更好地协同工作。使用 props 有助于保持我们的组件独立和定义明确。我们将探讨两种常见的组件组织方式:容器模式和展示模式,它们将组件的规则和外观分开。我们还将了解高阶组件HOCs)用于处理上下文,而不会使我们的组件过于依赖,以及函数作为子组件的模式,用于动态创建组件。

第五章为浏览器编写代码中,我们将了解 React 如何在 Web 浏览器中用于创建表单、处理事件和动画 SVG。我们将学习useRef钩子,它是一种轻松访问 DOM 节点的方法。

使用 React 简单直接的方法,管理复杂的 Web 应用变得更容易。此外,如果需要,React 允许我们直接访问 DOM 节点,这使得我们可以轻松地将 React 与其他库一起使用。

第六章让你的组件看起来很漂亮中,我们将深入了解 React 中的样式。我们首先将探讨 CSS 在大项目中工作的问题,以 Meta 的经验为例。我们将学习如何在我们的 React 组件内部直接编写样式,这使我们的代码整洁且易于阅读。但我们也将了解这种方法的优势,并探索另一种样式方法,称为 CSS 模块,它允许我们在单独的文件中编写 CSS,但将样式限制在单个组件中。最后,我们将了解styled-components,这是一个流行的 React 样式库。到本章结束时,你将拥有许多使你的 React 应用看起来很棒的工具。

第七章应避免的反模式中,我们将讨论四种使用组件的方法,这些方法可能会减慢或破坏我们的 Web 应用。对于每个问题,我们将使用示例来展示出了什么问题以及如何修复它。我们将了解为什么使用属性设置状态可能会在状态和属性之间引起问题。我们还将看到如何使用错误的“key”属性破坏 React 更新组件的方式。最后,我们将了解为什么将非标准属性扩展到 DOM 元素是一个坏主意。理解这些问题将帮助我们更有效地使用 React 并避免常见错误。

第八章React Hooks中,我们将愉快地学习关于新 React Hooks 的知识。我们将了解它们是如何工作的,如何使用它们来获取数据,以及如何将类组件转换为 Hooks 组件。我们还将了解副作用以及memouseMemouseCallback之间的区别。最后,我们将看到useReducer Hook 是如何工作的,以及它与react-redux的不同之处。所有这些都将帮助我们使我们的 React 组件更快、更好。

第九章React Router中,我们将学习关于 React Router 的知识,这是我们与 React 一起使用来在单页应用中切换页面的工具。React 本身不这样做,所以我们使用 React Router。我们将了解如何使用它来使我们的应用响应不同的 URL 并管理导航。到本章结束时,你将了解 React Router 是如何工作的,以及如何在你的项目中使用它。我们将学习react-routerreact-router-domreact-router-native包之间的区别,如何设置 React Router,如何添加<Routes>组件,以及如何向路由添加参数。

第十章React 18 新特性中,我们将探索新的和改进的 React 18。它拥有众多特性,使得构建酷炫、交互式的 APP 变得更加容易。

通过自动状态更新分组、并发渲染、用于获取数据的 Suspense、更好的错误处理和新的组件类型,你可以创建引人入胜且快速的 APP。如果你使用 React,考虑升级到 React 18 是个不错的选择。我们还将探讨 Node 18 和 19 的一些重大新特性,这些特性可以使我们的 Web 项目更加出色。

第十一章数据管理中,我们将学习关于 React Context API 以及如何使用 React Suspense 与 SWR 的知识。我们将了解 Context API 的基础知识,包括创建和使用上下文以及useContext钩子如何使这一切变得更加简单。我们还将探讨 React Suspense 以及它是如何帮助我们更好地处理加载状态以提供更流畅的用户体验的。我们还将了解 SWR,它使得使用 React Suspense 获取和缓存数据变得更加容易。最后,我们将学习如何使用新的 Redux Toolkit。所有这些工具都将帮助我们构建更快、更用户友好的 React 应用。

第十二章服务器端渲染,我们将通过 React 完成对服务器端渲染SSR)的旅程。现在你将知道如何创建一个使用 SSR 的应用程序,以及为什么它对搜索引擎优化(SEO)、社交分享和提升性能等方面很有用。我们将学习如何在服务器上加载数据并将其放入 HTML 模板中,以便在浏览器启动时客户端应用程序准备好。最后,我们将看到像 Next.js 这样的工具如何通过减少额外代码和隐藏一些复杂部分来简化在 React 中设置 SSR。

第十三章通过真实项目理解 GraphQL,我们将学习 GraphQL,这是一个帮助我们更高效地与 API 和数据工作的酷炫工具。与常规的 REST API 不同,GraphQL 允许我们请求我们需要的精确内容,而不需要更多。我们将用它来为真实项目制作一个简单的登录和用户注册系统。我们将学习如何安装 PostgreSQL,使用.env文件设置环境变量,设置 Apollo Server,执行 GraphQL 查询和突变,与解析器一起工作,创建 Sequelize 模型,使用 JWT,在 GraphQL Playground 中玩耍,并进行身份验证。到那时,你将知道如何在你的项目中使用 GraphQL。

第十四章MonoRepo 架构,我们将讨论一个称为“MonoRepo”的概念。通常,当我们构建应用程序时,我们有一个应用程序,一个 git 仓库和一个构建输出。但许多组织使用单个仓库来存储所有应用程序、组件和库,以简化开发。这就是我们所说的单仓库。这就像把所有的代码放在一个大篮子里,而不是许多小篮子里。这使得保持一切更新变得更容易,并且可以节省时间。我们还将讨论 MonoRepo 如何使代码重构、提高团队合作和加快更新包依赖项的过程变得更容易,而无需每次更新时发布新版本。

第十五章提高应用程序的性能,我们将探讨使你的应用程序运行得更顺畅、更快的技巧,以提供更好的用户体验。我们将深入了解 React 如何更新应用程序的显示,以及如何使用键来提高此过程的效率。我们将发现结构良好、任务导向的组件在提升应用程序性能方面的重要性。我们将讨论不可变性的概念及其在帮助React.memoshallowCompare有效工作中的重要性。接近尾声时,我们将介绍各种工具和库,这些工具和库可以进一步加快你的应用程序。本章旨在为你提供宝贵的知识,以增强你应用程序的速度和性能。

在第十六章,测试和调试中,我们将学习有关测试的所有内容。你会发现为什么测试很重要,并探索检查我们的 React 组件是否按预期工作的不同工具和技术。我们将使用 React Testing Library 和 Jest 等库来编写和运行测试,甚至了解如何测试应用中的复杂部分,如高阶组件或字段众多的表单。此外,我们还将学习如何使用 React DevTools 和 Redux DevTools 等工具来帮助我们开发更好的应用。到本章结束时,你将牢固掌握如何通过有效的测试保持应用的良好运行状态。

在第十七章,部署到生产环境中,我们将把您构建的 React 应用分享给全世界!我们将使用一个名为 DigitalOcean 的云服务来完成这项工作。您将学习如何使用 Node.js 和 nginx 在服务器上运行您的应用,我们将使用 DigitalOcean 的 Ubuntu 服务器来完成这项工作。我们将向您介绍如何设置 DigitalOcean Droplet、配置它并将其链接到您的域名。我们还将向您介绍 CircleCI,这是一个帮助您自动确保应用始终为用户准备就绪的工具,无论您做了多少更改。到本章结束时,您的应用将可以在互联网上供所有人查看!

要充分利用本书

要掌握 React,你需要具备 JavaScript 和 Node.js 的基础知识。本书主要面向网页开发者,在撰写本书时,对读者的以下假设:

  • 读者知道如何安装 Node.js 的最新版本。

  • 读者是一位中级开发者,能够理解 JavaScript ES6 语法。

  • 读者对 CLI 工具和 Node.js 语法有一些经验。

下载示例代码文件

本书代码包托管在 GitHub 上,网址为 github.com/PacktPublishing/React-18-Design-Patterns-and-Best-Practices-Fourth-Edition/

我们还从丰富的图书和视频目录中提供了其他代码包,可在 github.com/PacktPublishing/ 上找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/o1WtB

使用的约定

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

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。例如:“在您创建此 util 之后,您需要在 packages/utils/src/index.ts 创建 index.ts 文件。”

代码块设置为以下格式:

{
"name": "api",
"version": "1.0.0",
"main": "index.js",
"author": "",
"license": "ISC"
} 

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

cd packages/api
npm init -y 

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。例如:“我们需要创建的第一个包,以便能够编译其他包,称为devtools。”

警告或重要说明看起来像这样。

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

联系我们

我们始终欢迎读者的反馈。

一般反馈:请通过feedback@packtpub.com发送电子邮件,并在邮件主题中提及书籍标题。如果您对此书的任何方面有疑问,请通过questions@packtpub.com发送电子邮件给我们。

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在此书中发现错误,我们将不胜感激,如果您能向我们报告,请访问www.packtpub.com/submit-errata,点击提交勘误,并填写表格。

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

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

分享您的想法

一旦您阅读了React 18 设计模式和最佳实践,第四版,我们很乐意听到您的想法!请点击此处直接转到此书的 Amazon 评论页面并分享您的反馈。

您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。

下载此书的免费 PDF 副本

感谢您购买此书!

你喜欢在路上阅读,但无法携带你的印刷书籍到处走?你的电子书购买是否与你的选择设备不兼容?

不要担心,现在每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠远不止这些,您还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。

按照以下简单步骤获取好处:

  1. 扫描下面的二维码或访问以下链接

packt.link/free-ebook/978-1-80323-310-9

  1. 提交您的购买证明

  2. 就这些!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件。

第一章:React 入门第一步

亲爱的读者们!

本书假设您已经了解 React 是什么以及它能为您解决什么问题。您可能已经使用 React 编写了一个小型/中型应用程序,并希望提高您的技能并解答所有疑问。您应该知道 React 由 Meta 的开发者和 JavaScript 社区内的数百名贡献者维护。React 是创建 UIs 最受欢迎的库之一,它因其与文档对象模型DOM)智能交互而闻名,速度快。它包含 JSX,这是一种在 JavaScript 中编写标记的新语法,这要求您改变对关注点分离的看法。它有许多酷炫的功能,例如服务器端渲染,这使您能够编写通用应用程序。

在本章中,我们将探讨一些基本概念,这些概念对于有效地使用 React 至关重要,但对于初学者来说也足够简单,可以自行理解:

  • 命令式编程和声明式编程之间的区别

  • React 组件及其实例,以及 React 如何使用元素来控制 UI 流程

  • React 如何改变我们构建 Web 应用程序的方式,强制执行不同的关注点分离新概念,以及其不受欢迎的设计选择背后的原因

  • 为什么人们会感到 JavaScript 疲劳,以及您如何避免在接近 React 生态系统时开发者最常犯的错误

技术要求

要跟随本书,您需要有一些使用终端运行 Unix 命令的经验。此外,您需要安装Node.js。您有两个选择:第一个是从官方网站直接下载 Node.js(nodejs.org),第二个选项(推荐)是从github.com/nvm-sh/nvm安装Node 版本管理器NVM)。

如果您决定使用 NVM,您可以安装任何版本的 Node.js,并通过nvm install命令切换版本:

  • node是最新版本的别名:

    nvm install node 
    
  • 您还可以安装 Node.js 的全局版本(nvm将本地安装最新版本的 Node.js 到用户的计算机上):

    nvm install 19
    nvm install 18
    nvm install 17
    nvm install 16
    nvm install 15 
    
  • 或者,您可以安装一个非常具体的版本:

    nvm install 12.14.3 
    
  • 安装了不同版本后,您可以通过使用nvm use命令在它们之间切换:

    nvm use node # for latest version
    nvm use 16 # for the latest version of node 16.X.X
    nvm use 12.14.3 # Specific version 
    
  • 最后,您可以通过运行以下命令指定默认的 Node.js 版本:

    nvm alias default node
    nvm alias default 16
    nvm alias default 12.14.3 
    

简而言之,以下是完成本章所需的条件列表:

你可以在书的 GitHub 仓库中找到代码:github.com/PacktPublishing/React-18-Design-Patterns-and-Best-Practices-Fourth-Edition

区分声明式和命令式编程

当阅读 React 文档或关于 React 的博客文章时,你无疑会遇到声明式这个术语。React 之所以如此强大,其中一个原因就是它强制执行声明式编程范式。

因此,要精通 React,理解声明式编程的含义以及命令式和声明式编程之间的主要区别是至关重要的。最简单的方法是将命令式编程视为描述事物如何工作的方式,将声明式编程视为描述你想要实现的方式。

在命令式世界中,进入酒吧喝啤酒是一个现实生活中的例子,你通常会向酒吧服务员下达以下指示:

  1. 找一个杯子并从架子上取下来。

  2. 把杯子放在水龙头下。

  3. 把手拉到杯满为止。

  4. 把杯子递给我。

在声明式世界中,你只需说,“请给我一杯啤酒,好吗?”

声明式方法假设酒吧服务员已经知道如何服务啤酒,这是声明式编程工作方式的一个重要方面。

让我们来看一个 JavaScript 的例子。在这里,我们将编写一个简单的函数,给定一个小写字符串数组,返回一个包含相同字符串的大写数组:

toUpperCase(['foo', 'bar']) // ['FOO', 'BAR'] 

解决这个问题的命令式函数可以这样实现:

const toUpperCase = input => { 
  const output = []

  for (let i = 0; i < input.length; i++) { 
    output.push(input[i].toUpperCase())
  } 

  return output
} 

首先,创建一个空数组来存放结果。然后,函数遍历输入数组的所有元素,将大写值推入空数组。最后,返回输出数组。

声明式解决方案如下:

const toUpperCase = input => input.map(value => value.toUpperCase()) 

输入数组的项被传递给一个map函数,该函数返回一个包含大写值的新数组。有一些值得注意的显著差异:前一个例子不太优雅,需要更多的努力才能理解。后者更简洁,更容易阅读,这在大型代码库中,维护性至关重要。

另一个值得提到的方面是,在声明式示例中,不需要使用变量或在执行过程中更新它们的值。声明式编程倾向于避免创建和修改状态。

作为最后的例子,让我们看看 React 声明式意味着什么。我们将尝试解决的问题是在 Web 开发中常见的任务:创建一个切换按钮。

想象一个简单的 UI 组件,比如切换按钮。当你点击它时,如果它之前是灰色(关闭),它会变成绿色(开启);如果它之前是绿色(开启),它会变成灰色(关闭)。

做这件事的命令式方法如下:

const toggleButton = document.querySelector('#toggle')
toogleButton.addEventListener('click', () => {
  if (toggleButton.classList.contains('on')) {
    toggleButton.classList.remove('on')
    toggleButton.classList.add('off')
  } else {
    toggleButton.classList.remove('off')
    toggleButton.classList.add('on')
  }
}) 

它是命令式的,因为需要所有这些指令来更改类。相比之下,使用 React 的声明式方法如下:

// To turn on the Toggle
<Toggle on />
// To turn off the toggle
<Toggle /> 

在声明式编程中,开发者只需描述他们想要实现的内容,无需列出所有使它工作的步骤。React 提供声明式方法使得它易于使用,因此生成的代码简单,这通常会导致更少的错误和更高的可维护性。

在下一节中,你将学习 React 元素的工作原理,并了解更多关于如何在 React 组件上传递 props 的上下文。

React 元素的工作原理

在这本书中,我们假设你已经熟悉组件及其实例,但如果你想要有效地使用 React,你应该了解另一个对象——元素。元素是轻量级的不可变描述,用于表示应该渲染的内容,而组件则是更复杂的具有状态的对象,负责生成元素。

每当你调用createClassextend Component声明一个无状态函数时,你都在创建一个组件。React 在运行时管理你组件的所有实例,在给定的时间点内存中可以存在同一组件的多个实例。

如前所述,React 遵循声明式范式,无需告诉它如何与 DOM 交互;你只需声明你希望在屏幕上看到的内容,React 就会为你完成工作。使这个过程更具表达性和可读性的一个工具是 JSX,它允许你直接在 JavaScript 代码中编写类似 HTML 的语法。JSX 不是必需的,但在 React 社区中广泛使用。

为了控制 UI 流程,React 使用一种称为元素的特殊类型的对象。这些元素是通过React.createElement()函数创建的,或者更常见的是,通过 JSX 语法。元素只包含严格需要表示界面的信息。

下面是一个使用 JSX 创建的元素的示例:

 <Title color="red">
    <h1>Hello, H1!</h1>
  </Title> 

此 JSX 代码被转换成如下 JavaScript 对象:

 {
    type: Title,
    props: {
      color: 'red',
      children: {
        type: 'h1',
        props: {
          children: 'Hello, H1!'
        }
      }
    }
  } 

元素的类型至关重要,因为它告诉 React 如何处理它。如果类型是字符串,则元素表示一个 DOM 节点;如果是函数,则元素表示一个组件。

你可以嵌套 DOM 元素和组件来创建渲染树,表示应用程序用户界面的结构。通过以分层的方式组织你的元素和组件,你可以创建复杂和动态的 UI。

React 使用一种称为虚拟 DOM 的技术,它是实际 DOM 的内存表示。它比较当前树和新树,以最小化实际 DOM 更新的数量。这个过程称为协调,并由 React DOM 和 React Native 用于为其各自的平台创建 UI。

当一个元素的类型是函数时,React 会调用该函数,并将元素的属性传递给它以获取底层元素。它会递归地重复此过程,直到构建出可以在屏幕上渲染的 DOM 节点树。

总结来说,元素在 React 的声明式范式中的作用至关重要,它允许你创建复杂用户界面,而无需手动管理 DOM 元素的创建和销毁。

通过理解元素和组件如何协同工作,以及 React 如何使用虚拟 DOM 和协调高效地更新 UI,你将能够构建动态且高效的 Web 应用程序。

重新学习一切

当第一次使用 React 时,以开放的心态去接近它是至关重要的。这是因为 React 代表了一种新的设计 Web 和移动应用程序的方式,打破了许多传统的最佳实践。

在过去的二十年里,我们已经了解到关注点分离是至关重要的,这通常涉及到将逻辑与模板分离。我们的目标是把 JavaScript 和 HTML 写在不同的文件中,为此已经创建了各种模板解决方案来帮助开发者实现这一目标。

然而,这种方法的缺点是它往往造成了一种分离的错觉。实际上,JavaScript 和 HTML 无论在哪里都是紧密耦合的。为了说明这一点,让我们考虑一个示例模板:

{{#items}} 
  {{#first}} 
    <li><strong>{{name}}</strong></li> 
  {{/first}} 
  {{#link}} 
    <li><a href="{{url}}">{{name}}</a></li> 
  {{/link}} 
{{/items}} 

前面的代码片段来自 Mustache 网站,这是最受欢迎的模板系统之一。

第一行告诉 Mustache 遍历一个项目集合。在循环内部,有一些条件逻辑来检查 #first#link 属性是否存在,并根据它们的值渲染不同的 HTML 片段。变量被括在花括号中。

如果你的应用程序只需要显示一些变量,模板库可能是一个不错的解决方案,但当涉及到开始处理复杂的数据结构时,情况就改变了。模板系统和它们的 领域特定语言DSL) 提供了一组功能,并试图提供与真实编程语言相同的功能,而不达到相同的完整性水平。正如示例所示,模板高度依赖于从逻辑层接收的模型来显示信息。

另一方面,JavaScript 与模板渲染的 DOM 元素交互,以更新 UI,即使它们是从不同的文件加载的。同样的问题也适用于样式 – 它们在不同的文件中定义,但在模板中被引用,CSS 选择器遵循标记的结构,因此几乎不可能在不破坏另一个的情况下更改其中一个,这就是 耦合 的定义。这就是为什么经典的关注点分离最终变成了更多技术的分离,这当然不是一件坏事,但它并没有解决任何真正的问题。

React 试图通过将模板放在它们应该的位置——逻辑旁边——向前迈出一大步。它这样做的原因是 React 建议你通过组合称为组件的小块来组织你的应用程序。框架不应该告诉你如何分离关注点,因为每个应用程序都有自己的,只有开发者应该决定如何限制它们应用程序的边界。

基于组件的方法彻底改变了我们编写 Web 应用程序的方式,这就是为什么经典的概念——关注点分离——正逐渐被一个更加现代的结构所取代。React 强加的范式并不新鲜,它也不是由其创造者发明的,但 React 为使这个概念主流化做出了贡献,最重要的是,它以更容易被不同水平的专业开发者理解的方式普及了这个概念。

渲染 React 组件看起来像这样:

return ( 
  <button style={{ color: 'red' }} onClick={handleClick}> 
    Click me! 
  </button> 
) 

我们都同意,一开始这看起来有点奇怪,但这仅仅是因为我们还不习惯那种语法。一旦我们学会了它,并意识到它的强大;我们就理解了它的潜力。使用 JavaScript 进行逻辑和模板化不仅帮助我们更好地分离关注点,而且还赋予我们更多的能力和表现力,这是我们构建复杂 UI 所需要的东西。

因此,即使混合 JavaScript 和 HTML 的想法一开始听起来很奇怪,但给 React 五分钟时间是至关重要的。开始使用新技术最好的方式是在一个小型项目上尝试,看看效果如何。一般来说,正确的做法总是准备好放弃一切,并改变你的思维方式,如果长期利益值得的话。

另有一个概念相当有争议,难以接受,那就是 React 背后的工程师试图推广给社区的:将样式逻辑也移入组件内部。最终目标是封装创建我们组件所使用的每一种技术,并根据它们的领域和功能来分离关注点。以下是一个从 React 文档中摘取的样式对象的例子:

const divStyle = { 
  color: 'white', 
  backgroundImage: `url(${imgUrl})`, 
  WebkitTransition: 'all', // note the capital 'W' here 
  msTransition: 'all' // 'ms' is the only lowercase vendor prefix 
}

ReactDOM.render(<div style={divStyle}>Hello World!</div>, mountNode) 

这套解决方案,其中开发者使用 JavaScript 编写他们的样式,被称为**#CSSinJS**,我们将在第六章让你的组件看起来更美观中详细讨论。

在下一节中,我们将看到如何避免由运行 React 应用程序所需的大量配置引起的 JavaScript 疲劳(主要是 webpack)。

理解 JavaScript 疲劳

有一种普遍的观点认为 React 由一套庞大的技术和工具组成,如果你想使用它,你就被迫处理包管理器、转译器、模块打包器和无穷无尽的库。这种想法已经非常普遍,并且被广泛传播,以至于它已经被明确定义并命名为JavaScript 疲劳

关于 React 的误解

理解 JavaScript 疲劳的原因并不难。React 生态系统中的所有存储库和库都是使用闪亮的新技术、JavaScript 的最新版本和最先进的技术和范式构建的。此外,GitHub 上有大量的 React 模板代码,每个都有数十个依赖项,为任何问题提供解决方案。

然而,重要的是要理解 React 是一个非常小的库,它可以在任何页面(甚至 JSFiddle 内)中使用,就像人们以前使用 jQuery 或 Backbone 一样,只需在关闭 body 元素之前在页面上包含脚本即可。

无疲劳地开始使用 React

React 被拆分为两个包:

  • react:实现了库的核心功能

  • react-dom:包含所有与浏览器相关的功能

原因在于核心包用于支持不同的目标,例如 React DOM 在浏览器上和在移动设备上的 React Native。在单个 HTML 页面内运行 React 应用程序不需要任何包管理器或复杂的操作。

这里是开始使用 React 需要包含在 HTML 中的 URL:

  • unpkg.com/react@18.2.0/umd/react.production.min.js

  • unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js

对于简单的 UI,我们只需使用 createElement (自 React 17 起称为 _jsx)即可,只有在开始构建更复杂的东西时,我们才需要包含一个转译器来启用 JSX 并将其转换为 JavaScript。随着应用的成长,我们可能需要路由器、API 端点和外部依赖项。

JavaScript 生态系统的优势

尽管 JavaScript 生态系统发展迅速且不断变化,但它提供了几个优势。社区在推动创新和快速演变中发挥着重要作用。一旦宣布或起草了规范,社区中就会有人将其实现为转译器插件或 polyfill,让其他人可以在浏览器供应商达成一致并开始支持它之前进行实验。

这使得 JavaScript 和浏览器与其他语言或平台相比成为一个独特的环境。缺点是变化很快,但这只是找到一个正确平衡的问题,即在押注新技术和保持安全之间找到平衡。

再见 Create-React-App,欢迎 Vite!

最近,React 团队决定从他们的官方文档中移除 create-react-app,这表明它不再是设置新 React 项目的默认方法。相反,React 现在推荐使用像 Next.js、Remix 或 Gatsby 这样的框架,以获得更全面的解决方案。然而,如果你需要更简单的替代方案,可以选择 Vite 或 Parcel 这样的构建工具。

Vite 作为解决方案

Vite 是由 Vue.js 的创造者 Evan You 创建的一个构建工具和开发服务器。它利用现代浏览器中本地的 ES 模块功能,以实现快速开发和高效的生成构建。

要使用 Vite 与 React,首先,使用以下命令全局安装 Vite:

 npm install -g create-vite 

然后,使用 React TypeScript 模板创建一个新的 Vite 项目:

 create-vite my-react-app --template react-ts 

最后,进入新创建的项目文件夹并启动开发服务器:

 cd my-react-app
  npm install
  npm run dev 

您应该看到项目默认在端口 5173 上运行。

图片

图 1.1:Vite 默认应用

如果您想将端口改为 3000,您可以像这样修改 vite.config.ts 文件:

 import { defineConfig } from 'vite'
  import react from '@vitejs/plugin-react'
  // https://vitejs.dev/config/
  export default defineConfig({
    plugins: [react()],
    server: {
      port: 3000
    }
  }) 

使用 Vite,您可以用最少的依赖来搭建和运行一个 React 应用程序,同时仍然可以访问构建完整的 React 应用程序所需的所有高级技术功能。

摘要

在本章中,我们学习了对于理解本书的其余部分非常重要的基本概念,这些概念对于日常使用 React 至关重要。我们现在知道如何编写声明式代码,并且对创建的组件与 React 用于在屏幕上显示其实例的元素之间的区别有了清晰的理解。

我们了解了将逻辑和模板放在一起选择的原因,以及为什么这个不受欢迎的决定对 React 来说是一个巨大的胜利。我们探讨了在 JavaScript 生态系统中为什么会普遍感到疲劳的原因,但我们也已经看到了如何通过遵循迭代方法来避免这些问题。

最后,我们了解了新的 create-vite CLI 是什么,我们现在可以开始编写一些真正的代码了。

在下一章中,您将学习 TypeScript 以及如何在项目中使用它。

加入我们的 Discord 社区

加入我们的 Discord 空间,与作者和其他读者进行讨论:

packt.link/React18DesignPatterns4e

图片

第二章:介绍 TypeScript

本章假设你已经有 JavaScript 的经验,并且对通过学习 TypeScript 来提高代码质量感兴趣。TypeScript 是一个类型化的 JavaScript 超集,它可以编译成 JavaScript。换句话说,TypeScript 实质上是带有一些额外功能的 JavaScript。

由微软 C# 的创造者 Anders Hejlsberg 设计,TypeScript 是一种开源语言,它增强了 JavaScript 的功能。通过引入静态类型和其他高级功能,TypeScript 帮助开发者编写更可靠和可维护的代码。

在本章中,我们将探讨 TypeScript 的特性和如何将现有的 JavaScript 代码转换为 TypeScript。到本章结束时,你将深入理解 TypeScript 的优势以及如何利用它们来创建更健壮和可扩展的应用程序。

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

  • TypeScript 的特性

  • 将 JavaScript 代码转换为 TypeScript

  • 类型

  • 接口

  • 扩展接口和类型

  • 实现接口和类型

  • 合并接口

  • 枚举

  • 命名空间

  • 模板字面量类型

  • TypeScript 配置文件

技术要求

为了完成本章的内容,你需要以下工具:

  • Node.js 19+

  • Visual Studio Code

TypeScript 的特性

TypeScript 是一种由微软开发和维护的流行开源编程语言,正迅速在全球开发者中流行起来。它被引入作为一种 JavaScript 的超集,旨在简化更大规模的应用程序的开发,同时提高代码质量和可维护性。TypeScript 利用静态类型并编译成干净的、简单的 JavaScript 代码,确保与现有的 JavaScript 环境兼容。

这门强大的语言带来了一系列强大的特性,使其与众不同,成为许多程序员的优选。值得注意的是,TypeScript 将强类型注入到 JavaScript 中,提供了更好的错误检查并减少了运行时错误。此外,它完全支持面向对象编程,具有类、接口和继承等高级功能。

由于任何有效的 JavaScript 代码也是 TypeScript,因此从 JavaScript 过渡到 TypeScript 可以逐步进行,开发者可以逐步将类型引入到他们的代码库中。这使得 TypeScript 成为小型和大型项目都适用的灵活、可扩展的解决方案。

在本节中,我们将总结 TypeScript 的基本特性,这些特性是你应该利用的:

  • TypeScript 是 JavaScript: TypeScript 是 JavaScript 的超集,这意味着你写的任何 JavaScript 代码都将与 TypeScript 兼容。如果你已经知道如何使用 JavaScript,那么你已经拥有了使用 TypeScript 所需的所有知识。你只需要学习如何给你的代码添加类型。所有的 TypeScript 代码最终都会被转换成 JavaScript。

  • JavaScript 是 TypeScript:这仅仅意味着你可以将任何有效的.js文件重命名为.ts扩展名,并且它将工作。

  • 错误检查:TypeScript 编译代码并检查错误,这有助于在运行代码之前识别问题。

  • 强类型:默认情况下,JavaScript 不是强类型。使用 TypeScript,你可以为所有变量和函数添加类型,甚至可以指定返回值类型。

  • 支持面向对象编程:TypeScript 支持类、接口、继承等高级概念。这有助于更好地组织代码并提高其可维护性。

在讨论了 TypeScript 的关键特性之后,让我们深入探讨将 JavaScript 代码转换为 TypeScript 的实际演示。

将 JavaScript 代码转换为 TypeScript

在本节中,我们将看到如何将一些 JavaScript 代码转换为 TypeScript。

假设我们必须检查一个单词是否是回文。这个算法的 JavaScript 代码如下:

function isPalindrome(word) {
  const lowerCaseWord = word.toLowerCase()
  const reversedWord = lowerCaseWord.split('').reverse().join('')
  return lowerCaseWord === reversedWord
} 

你可以将这个文件命名为palindrome.ts

如你所见,我们接收一个string变量(word),并返回一个boolean值。那么,这如何翻译成 TypeScript?

function isPalindrome(word: string): boolean {
  const lowerCaseWord = word.toLowerCase()
  const reversedWord = lowerCaseWord.split('').reverse().join('')
  return lowerCaseWord === reversedWord
} 

你可能正在想,“太好了,我已经将string类型指定为word,并将函数返回值指定为boolean类型,但现在怎么办?”

如果你尝试用与string不同的值运行函数,你会得到一个 TypeScript 错误:

 console.log(isPalindrome('Level')) // true
    console.log(isPalindrome('Anna')) // true
    console.log(isPalindrome('Carlos')) // false
    console.log(isPalindrome(101)) // TS Error
    console.log(isPalindrome(true)) // TS Error
    console.log(isPalindrome(false)) // TS Error 

所以,如果你尝试将一个数字传递给函数,你会得到以下错误:

图形用户界面,文本,应用程序  自动生成的描述

图 2.1:类型 number 不能赋值给类型为 string 的参数

这就是为什么 TypeScript 非常有用,因为它会强制你更严格、更明确地对待你的代码。

类型

在最后一个例子中,我们看到了如何为我们的函数参数和返回值指定一些原始类型,但你可能想知道如何更详细地描述一个对象或数组。类型可以帮助我们更好地描述我们的对象或数组。例如,假设你想描述一个User类型以将信息保存到数据库中:

type User = {
  username: string
  email: string
  name: string
  age: number
  website: string
  active: boolean
}
const user: User = {
  username: 'czantany',
  email: 'carlos@milkzoft.com',
  name: 'Carlos Santana',
  age: 33,
  website: 'http://www.js.education',
  active: true
}
// Let's suppose you will insert this data using Sequelize...
models.User.create({ ...user }} 

如果我们忘记添加一个节点或在其中放入一个无效值,我们会得到以下错误:

文本  自动生成的描述

图 2.2:类型 User 中缺少年龄,但需要

如果你需要可选节点,你可以在节点的年龄旁边始终放置一个?,如下面的代码块所示:

type User = {
  username: string
  email: string
  name: string
  age?: number
  website: string
  active: boolean
} 

你可以随意命名type,但遵循一个良好的实践是添加前缀T。例如,User类型将变为TUser。这样,你可以快速识别它是type,而不会混淆地认为它是一个类或 React 组件。

接口

接口与类型非常相似,有时开发者不知道它们之间的区别。接口可以用来描述对象的形状或函数签名,就像类型一样,但语法不同:

interface User {
  username: string
  email: string
  name: string
  age?: number
  website: string
  active: boolean
} 

您可以随意命名接口,但遵循一个良好的实践是添加前缀 I。例如,User 接口将变为 IUser。这样,您可以快速识别它是一个接口,并且不会混淆地认为它是一个类或 React 组件。

接口也可以扩展、实现和合并。

扩展接口和类型

接口或类型也可以扩展,但同样,语法将有所不同,如下面的代码块所示:

// Extending an interface
interface IWork {
  company: string
  position: string
}
interface IPerson extends IWork {
  name: string
  age: number
}
// Extending a type
type TWork = {
  company: string
  position: string
}
type TPerson = TWork & {
  name: string
  age: number
}
// Extending an interface into a type
interface IWork {
  company: string
  position: string
}
type TPerson = IWork & {
  name: string
  age: number
} 

如您所见,通过使用 & 字符,您可以扩展一个类型,而使用 extends 关键字扩展接口。

理解接口和类型的扩展为我们深入了解它们的实现铺平了道路。让我们过渡到展示 TypeScript 中的类如何实现这些接口和类型,同时考虑到处理联合类型时的固有约束。

实现接口和类型

一个类可以以完全相同的方式实现接口或类型别名。但它不能实现(或扩展)一个命名 联合类型类型别名。例如:

// Implementing an interface
interface IWork {
  company: string
  position: string
}
class Person implements IWork {
  name: 'Carlos'
  age: 35
}
// Implementing a type
type TWork = {
  company: string
  position: string
}
class Person2 implements TWork {
  name: 'Cristina'
  age: 34
}
// You can't implement a union type
type TWork2 = {   company: string;   position: string } | {   name: string;   age: number }
class Person3 implements TWork2 {
  company: 'Google'
  position: 'Senior Software Engineer'
} 

如果您编写前面的代码,您的编辑器将出现以下错误:

计算机屏幕截图,描述自动生成,中等置信度

图 2.3:一个类只能实现具有静态已知成员的对象类型或对象类型的交集

如您所见,您无法实现联合类型。

合并接口

与类型不同,接口可以定义多次,并且将被视为单个接口(所有声明将合并),如下面的代码块所示:

interface IUser {
  username: string
  email: string
  name: string
  age?: number
  website: string
  active: boolean
}
interface IUser {
  country: string
}
const user: IUser = {
  username: 'czantany',
  email: 'carlos@milkzoft.com',
  name: 'Carlos Santana',
  country: 'Mexico',
  age: 35,
  website: 'http://www.js.education',
  active: true
} 

这在需要在不同场景下通过重新定义相同的接口来扩展你的接口时非常有用。

枚举

枚举是 TypeScript 具有的少数几个不是 JavaScript 类型 级扩展的特性之一。枚举允许开发者定义一组 命名常量。使用枚举可以使文档化意图或创建一组不同情况变得更容易。

枚举可以存储数字或字符串值,通常用于提供预定义值。我个人喜欢在主题系统中使用它们来定义一组颜色,如下所示:

表格描述自动生成

图 2.4:用于颜色调板的枚举

接下来,让我们探索 TypeScript 的另一个有用特性,即命名空间。

命名空间

您可能在其他编程语言中听说过 命名空间,例如 Java 或 C++。在 JavaScript 中,命名空间只是全局作用域中的命名对象。它们充当一个区域,在该区域中,变量、函数、接口或类在局部作用域中被组织并分组在一起,以避免全局作用域中组件之间的命名冲突。

虽然模块也用于代码组织,但对于简单用例,命名空间实现起来更为直接。然而,模块提供了命名空间不提供的额外好处,例如代码隔离、捆绑支持、重新导出组件以及重命名组件。

在我的个人项目中,我发现当使用 styled-components 时,命名空间对于分组样式很有用,例如:

import styled from 'styled-components'
export namespace CSS {
  export const InputWrapper = styled.div`
    padding: 10px;
    margin: 0;
    background: white;
    width: 250px;
  `
  export const InputBase = styled.input`
    width: 100%;
    background: transparent;
    border: none;
    font-size: 14px;
  `
} 

然后当我需要使用它时,我会这样使用它:

import React, { ComponentPropsWithoutRef, FC } from 'react'
import { CSS } from './Input.styled'
export interface Props extends ComponentPropsWithoutRef<'input'> {
  error?: boolean
}
const Input: FC<Props> = ({
  type = 'text',
  error = false,
  value = '',
  disabled = false,
  ...restProps
}) => (
    <CSS.InputWrapper style={error ? { border: '1px solid red' } : {}}>
      <CSS.InputBase type={type} value={value} disabled={disabled} {...restProps} />
    </CSS.InputWrapper>
  ) 

这非常有用,因为我不需要担心导出多个样式组件。我只需导出 CSS 命名空间,就可以使用该命名空间内部定义的所有样式组件。

模板字符串

在 TypeScript 中,模板字符串基于字符串字面量类型,可以使用联合扩展成多个字符串。这些类型对于定义一个主题名称很有用,例如:

 type Theme = 'light' | 'dark' 

Theme 是一个联合类型,只能分配两种字符串字面量类型之一:'light''dark'。这提供了类型安全并防止了由于传递无效值作为主题名称而导致的运行时错误。

使用这种方法,您可以定义一个变量、参数或参数的可能值集合,并确保在编译时只使用有效的值。这使得您的代码更加可靠且易于维护。

TypeScript 配置文件

目录中存在 tsconfig.json 文件表示该目录是 TypeScript 项目的根目录。tsconfig.json 文件指定了项目所需的根文件和编译器选项。

您可以在官方 TypeScript 网站上检查所有编译器选项:www.typescriptlang.org/tsconfig

这是我通常在我的项目中使用的 tsconfig.json 文件。我总是将它们分为两个文件:tsconfig.common.json 文件将包含所有共享的编译器选项,而 tsconfig.json 文件将扩展 tsconfig.common.json 文件并添加一些特定于该项目的选项。这在您与 MonoRepos 一起工作时非常有用。

我的 tsconfig.common.json 文件看起来像这样:

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "alwaysStrict": true,
    "declaration": true,
    "declarationMap": true,
    "downlevelIteration": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "jsx": "react-jsx",
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "module": "commonjs",
    "moduleResolution": "node",
    "noEmit": false,
    "noFallthroughCasesInSwitch": false,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "outDir": "dist",
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "strictFunctionTypes": true,
    "strictNullChecks": true,
    "suppressImplicitAnyIndexErrors": false,
    "target": "ESNext"
  },
  "exclude": ["node_modules", "dist", "coverage", ".vscode", "**/__tests__/*"]
} 

我的 tsconfig.json 文件看起来像这样:

{
  "extends": "./tsconfig.common.json",
  "compilerOptions": {
    "baseUrl": "./packages",
    "paths": {
      "@web-creator/*": ["*/src"]
    }
  }
} 

第十四章 中,我将解释如何创建 MonoRepos 架构。

摘要

在本章中,我们介绍了 TypeScript 的基础知识,包括创建基本类型和接口、扩展它们,以及使用枚举、命名空间和模板字符串。我们还探讨了设置第一个 TypeScript 配置文件(tsconfig.json)并将其分为两部分——一部分用于共享,另一部分用于扩展 tsconfig.common.json。这种方法在处理 MonoRepos 时尤其有用。

在下一章中,我们将深入探讨使用 JSX/TSX 代码,并探索可以应用于改进代码风格的多种配置。你将学习如何利用 TypeScript 的力量来创建高效且易于维护的 React 应用程序。

第三章:清理您的代码

本章假设您已经具备 JSXJavaScript XML)的相关经验,并且有兴趣提高您使用它的技能以有效利用它。为了在使用 JSX/TSX 时不出现任何问题或意外行为,了解其底层工作原理及其作为构建 UI 的有用工具的原因至关重要。

我们的目标是编写干净的 JSX/TSX 代码,维护它,并对其内部工作原理有良好的理解,包括它如何转换为 JavaScript 以及它提供的功能。

通过理解 JSX/TSX 的复杂性,您可以充分利用其全部潜力来构建高效且可扩展的 UI。我们将探讨各种技巧和技术,帮助您编写更好的代码并避免常见的错误。到本章结束时,您将牢固掌握 JSX/TSX 的工作原理以及如何在您的 React 应用程序中有效地使用它。

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

  • JSX 是什么,为什么我们应该使用它?

  • Babel 是什么,以及我们如何使用它来编写现代 JavaScript 代码?

  • JSX 的主要特性和 HTML 与 JSX 之间的区别。

  • 编写优雅且可维护的 JSX 的最佳实践。

  • 如何通过 linting(特别是 ESLint)使我们的 JavaScript 代码在应用程序和团队之间保持一致性。

  • 函数式编程的基础以及遵循函数式范式将如何帮助我们编写更好的 React 组件。

技术要求

要完成本章,您需要以下内容:

  • Node.js 19+

  • Visual Studio Code

使用 JSX

第一章 中,我们看到了 React 如何改变关注点分离的概念,将边界移动到组件内部。我们还学习了 React 如何使用组件返回的元素在屏幕上显示 UI。

现在我们来看看我们如何在组件内部声明元素。

React 提供了两种定义元素的方法。第一种是通过使用 JavaScript 函数,第二种是通过使用 JSX,这是一种可选的类似 XML 的语法。以下是新官方 React.js 文档的截图(react.dev):

图形用户界面,网站描述自动生成

图 3.1:React.js 的新官方文档网站

首先,JSX 是人们未能接近 React 的主要原因之一,因为第一次看到主页上的示例,看到 JavaScript 与 HTML 混合在一起,对我们大多数人来说可能看起来很奇怪。

一旦我们习惯了它,我们就会意识到它非常方便,正是因为它与 HTML 类似,对已经在网络上创建过 UI 的人来说看起来非常熟悉。开闭标签使得表示嵌套元素树变得更加容易,这在使用纯 JavaScript 时将是难以阅读和难以维护的。

让我们更详细地了解以下子节中的 JSX。

Babel

Babel 是一个流行的 JavaScript 编译器,在 React 社区中得到广泛使用。它允许开发者使用最新的语言特性编写代码,如 JSX 和 ES6,这些特性可能尚未在所有浏览器中得到支持。通过将代码转换成更广泛支持的 ES5,Babel 确保你的应用程序在不同浏览器上平稳运行。

要使用 Babel,你首先需要安装必要的包。在旧版本(Babel 6.x)中,你会安装 babel-cli 包,它包含了 babel-nodebabel-core。然而,在较新版本中,这些包已经被分离成单独的模块:@babel/core@babel/cli@babel/node 等等。

要安装 Babel,请按照以下步骤操作:

  1. 在全局范围内安装所需的包(尽管通常更喜欢本地安装):

    npm install -g @babel/core @babel/node 
    
  2. 要使用 Babel 编译 JavaScript 文件,请运行:

    babel source.js -o output.js 
    
  3. Babel 可以高度配置,你可以使用预设来自定义它。要安装最常用的预设,请运行:

    npm install -g @babel/preset-env @babel/preset-react 
    
  4. 在你的项目根目录中创建一个 .babelrc 配置文件,并添加以下内容以告诉 Babel 使用已安装的预设:

    {
      "presets": [
        "@babel/preset-env",
        "@babel/preset-react"
      ]
    } 
    

现在,你可以在源文件中编写 ES6 和 JSX,Babel 会将它们转换成浏览器兼容的 ES5 JavaScript 代码。

创建第一个元素

现在我们支持 JSX 的环境,让我们探索一个基本示例:创建一个 div 元素。使用 _jsx 函数,我们可以编写:

_jsx('div', {}) 

然而,使用 JSX,我们可以简单地编写:

<div /> 

这看起来与常规 HTML 类似,但关键的区别在于我们正在 .js 文件中编写标记。请注意,JSX 只是一种语法糖,在浏览器中执行之前会被转换成 JavaScript。

当我们运行 Babel 时,我们的 <div /> 元素被转换成 _jsx('div', {})。在构建模板时请记住这一点。

React 17 开始,React.createElement('div') 已被弃用,库现在内部使用 react/jsx-runtime 来渲染 JSX。这意味着你不再需要导入 React 对象来编写 JSX 代码。相反,你可以像上一个示例中那样直接编写 JSX。

DOM 元素和 React 组件

JSX 允许我们创建 HTML 元素和 React 组件,唯一的区别在于它们是否以大写字母开头。例如,要渲染 HTML 按钮,我们使用 <button />,而要渲染 Button 组件,我们使用 <Button />

第一个按钮被转换成以下形式:

_jsx('button', {}) 

第二个被转换成以下形式:

_jsx(Button, {}) 

关键区别在于第一次调用时,我们传递 DOM 元素的类型作为字符串,而在第二次调用中,我们传递组件本身。因此,组件必须在作用域中存在才能正常工作。

JSX 还支持自闭合标签,这对于保持代码简洁和避免不必要的标签重复很有用。

属性

当你的 DOM 元素或 React 组件有 props 时,JSX 非常方便。使用 XML 在元素上设置属性相当简单:

<img src="img/logo.png" alt="Cabañas San Pancho" /> 

在 JavaScript 中,这将是以下内容:

_jsx("img", { 
  src: "https://www.ranchosanpancho.com/images/logo.png", 
  alt: "Cabañas San Pancho" 
}) 

这要难读得多,即使只有几个属性,没有一点推理也难以阅读。

子元素

JSX 允许你定义子元素来描述元素的树形结构并组合复杂的 UI。一个基本的例子是在其中包含文本的链接,如下所示:

<a href="https://ranchosanpancho.com">Click me!</a> 

这将被转换成以下内容:

_jsx( 
  "a", 
  { href: "https://ranchosanpancho.com" }, 
  "Click me!" 
) 

我们的链接可以包含在一个 div 元素中,以满足某些布局要求,实现这一点的 JSX 片段如下所示:

<div> 
  <a href="https://ranchosanpancho.com">Click me!</a> 
</div> 

JavaScript 的等效代码如下:

_jsx( 
  "div", 
  null, 
  _jsx( 
    "a", 
    { href: "https://ranchosanpancho.com" }, 
    "Click me!" 
  ) 
) 

现在应该很清楚 JSX 的 XML-like 语法是如何使一切更加可读和可维护的,但了解与我们的 JSX 并行的 JavaScript 如何控制元素的创建始终很重要。好处是,我们不仅限于将元素作为元素的子元素,我们还可以使用 JavaScript 表达式,例如函数或变量。

要做到这一点,我们必须将表达式包裹在花括号内:

<div> 
  Hello, {variable}. 
  I'm a {() => console.log('Function')}. 
</div> 

同样适用于非字符串属性,如下所示:

<a href={someFunction()}>Click me!</a> 

如你所见,任何变量或函数都应该被包裹在花括号内。

与 HTML 的区别

到目前为止,我们已经探讨了 JSX 和 HTML 之间的相似之处。现在让我们看看它们之间的一些细微差别以及它们存在的原因。

属性

我们必须始终记住 JSX 不是一个标准语言,并且它会被转换成 JavaScript。因此,一些属性不能使用。

例如,我们不得不使用 className 而不是 class,以及使用 htmlFor 而不是 for,如下所示:

<label className="awesome-label" htmlFor="name" /> 

原因是 classfor 是 JavaScript 的保留字。

样式

一个相当显著的区别是 style 属性的工作方式。我们将在 第六章让你的组件看起来更美观 中更详细地探讨如何使用它,但现在我们将关注它的工作方式。

style 属性不接受 CSS 字符串,就像 HTML 那样,而是期望一个 JavaScript 对象,其中样式名称是 camelCased

<div style={{ backgroundColor: 'red' }} /> 

如你所见,你可以向 style prop 传递一个对象,这意味着如果你愿意,你的样式甚至可以放在一个单独的变量中:

const styles = {
  backgroundColor: 'red'
} 
<div style={styles} /> 

这是更好地控制你的内联样式的方法。

值得注意的是,与 HTML 相比的一个重要区别是,由于 JSX 元素被转换成 JavaScript 函数,而在 JavaScript 中你不能返回两个函数,所以当你有多个同一级别的元素时,你被迫将它们包裹在一个父元素中。

让我们看看一个简单的例子:

<div />
<div /> 

这会给我们以下错误:

Adjacent JSX elements must be wrapped in an enclosing tag. 

另一方面,以下是可以工作的:

<div> 
  <div /> 
  <div /> 
</div> 

以前,React 强制你返回一个被 <div> 元素或其他标签包裹的元素;自 React 16.2.0 以来,可以直接返回一个数组,如下所示:

return [
  <li key="1">First item</li>, 
  <li key="2">Second item</li>, 
  <li key="3">Third item</li>
] 

或者,你甚至可以直接返回一个字符串,如下面的代码块所示:

return 'Hello World!' 

此外,React 现在有一个名为Fragment的新特性,它也可以作为一个特殊的元素包装器。它可以指定为React.Fragment

import { Fragment } from 'react'
return ( 
  <Fragment>
    <h1>An h1 heading</h1> 
    Some text here. 
    <h2>An h2 heading</h2> 
    More text here.
    Even more text here.
  </Fragment>
) 

或者你可以使用空标签(<></>):

return ( 
  <>
    <ComponentA />
    <ComponentB />
    <ComponentC />
  </>
) 

Fragment不会在 DOM 上渲染任何可见的内容;它只是一个用于包裹你的 React 元素或组件的辅助标签。

空格

在开始时可能会有一点小麻烦,再次强调,这涉及到我们应该始终记住 JSX 不是 HTML,即使它有类似 XML 的语法。JSX 处理文本和元素之间的空格的方式与 HTML 不同,这种方式可能不符合直观。

考虑以下代码片段:

<div> 
  <span>My</span> 
  name is 
  <span>Carlos</span> 
</div> 

在一个解析 HTML 的浏览器中,这段代码会显示My name is Carlos,这正是我们预期的结果。

在 JSX 中,相同的代码会被渲染为MynameisCarlos,这是因为三个嵌套的行被转换成了div元素的单独子元素,没有考虑到空格。一个常见的解决方案是在元素之间显式地放置一个空格,如下所示:

<div> 
  <span>My</span> 
  {' '}
  name is
  {' '} 
  <span>Carlos</span> 
</div> 

如你可能已经注意到的,我们正在使用一个包裹在 JavaScript 表达式中的空字符串来强制编译器在元素之间应用空格。

布尔属性

在真正开始之前,还有一些关于你在 JSX 中定义布尔属性的方式值得提及。

如果你设置了一个没有值的属性,JSX 会假设它的值是true,这与 HTML 中的disabled属性的行为相同,例如。

这意味着如果我们想将属性设置为false,我们必须明确地声明为false

<button disabled /> 
_jsx("button", { disabled: true }) 

下面是布尔属性的另一个例子:

<button disabled={false} /> 
_jsx("button", { disabled: false }) 

在开始时这可能会让人困惑,因为我们可能会认为省略属性意味着false,但事实并非如此。在使用 React 时,我们应该始终明确以避免混淆。

展开属性

一个重要的特性是展开属性操作符(...),它来自 ECMAScript“提案”中的 rest/spread 属性,并且在我们想要将 JavaScript 对象的全部属性传递给元素时非常方便。

一种常见的做法可以减少错误,那就是不要通过引用传递整个 JavaScript 对象给子组件,而是使用它们的原始值,这些值可以轻松验证,使组件更加健壮和防错。

让我们看看它是如何工作的:

const attrs = { 
  id: 'myId',
  className: 'myClass'
}
return <div {...attrs} /> 

之前的代码会被转换成以下形式:

var attrs = { 
  id: 'myId',
  className: 'myClass'
} 
return _jsx('div', attrs) 

模板字面量

模板字面量是允许嵌入表达式、多行字符串和字符串插值的字符串字面量。它们由反引号(``)字符包围,而不是单引号或双引号。

模板字面量最有用的特性之一是能够使用美元符号和花括号(${expression})包含占位符。这允许我们轻松地将变量或复杂的表达式插入到字符串模板中。以下是一个例子:

const name = 'Carlos'
const age = 35
const message = `Hello, my name is ${name} and I am ${age} years old.`
console.log(message) 

这将输出以下内容:

Hello, my name is Carlos and I am 35 years old. 

除了字符串插值外,模板字符串还支持多行字符串,这使得在不使用加号(+)运算符连接多个字符串的情况下编写和阅读复杂的字符串变得更加容易。

常见模式

既然我们已经了解了 JSX 的工作原理并能熟练掌握它,我们就准备好按照一些有用的约定和技术来正确地使用它了。

多行

让我们从一个非常简单的例子开始。正如之前所述,我们应该优先选择 JSX 而不是 React 的 _jsx 函数的主要原因之一是因为其类似 XML 的语法,以及平衡的开启和关闭标签非常适合表示节点树。

因此,我们应该尝试以正确的方式使用它并最大限度地发挥其作用。以下是一个例子;每当有嵌套元素时,我们总是应该使用多行:

<div> 
  <Header /> 
  <div> 
    <Main content={...} /> 
  </div> 
</div> 

这比以下情况更可取:

<div><Header /><div><Main content={...} /></div></div> 

例外情况是如果子元素不是文本或变量等元素。在这种情况下,保持在同一行上并避免向标记中添加噪音是有意义的,如下所示:

<div> 
  <Alert>{message}</Alert> 
  <Button>Close</Button> 
</div> 

总是记得在多行编写元素时将它们包裹在括号内。JSX 总是被替换为函数,而写在新的行上的函数可能会因为自动分号插入而导致意外结果。例如,假设你从渲染方法中返回 JSX,这是你在 React 中创建 UI 的方式。

以下示例工作正常,因为 div 元素与 return 在同一行上:

return <div /> 

然而,以下是不正确的:

return 
  <div /> 

原因是,这样你会有以下情况:

return
_jsx("div", null) 

这就是为什么你必须将语句包裹在括号中的原因,如下所示:

return ( 
  <div /> 
) 

多属性

在编写 JSX 时,一个常见的问题是一个元素具有多个属性。一个解决方案是将所有属性都写在同一行上,但这会导致代码行非常长,而我们不希望代码中出现这种情况(参见下一节了解如何强制执行编码风格指南)。

一个常见的解决方案是将每个属性都写在新的行上,缩进一级,然后将关闭括号与开启标签对齐:

<button 
  foo="bar" 
  veryLongPropertyName="baz" 
  onSomething={this.handleSomething} 
/> 

条件语句

当我们开始处理 条件语句 时,事情会变得更有趣,例如,如果我们只想在满足某些条件时渲染某些组件。我们可以在条件中使用 JavaScript 是一个很大的优点,但在 JSX 中有许多表达条件的方式,理解每种方式的优点和问题对于编写既可读又可维护的代码非常重要。

假设我们只想在用户当前登录到我们的应用程序时显示注销按钮。

一个简单的示例片段如下:

let button

if (isLoggedIn) { 
  button = <LogoutButton />
} 

return <div>{button}</div> 

这确实可行,但可读性并不高,尤其是当有多个组件和多个条件时。

在 JSX 中,我们可以使用内联条件:

<div> 
  {isLoggedIn && <LoginButton />} 
</div> 

这之所以有效,是因为如果条件是false,则不会渲染任何内容,但如果条件是true,则LoginButtoncreateElement函数会被调用,并将元素返回以组成结果树。

如果条件有备选方案(经典的if...else语句),并且我们想要,例如,当用户登录时显示注销按钮,否则显示登录按钮,我们可以如下使用 JavaScript 的if...else语句:

let button
if (isLoggedIn) { 
  button = <LogoutButton />
} else { 
  button = <LoginButton />
} 

return <div>{button}</div> 

或者,更好的是,我们可以使用一个使代码更紧凑的三元条件:

<div> 
  {isLoggedIn ? <LogoutButton /> : <LoginButton />} 
</div> 

你可以在流行的仓库中找到使用的三元条件,例如 Redux 现实世界的例子(github.com/reactjs/redux/blob/master/examples/real-world/src/components/List.js#L28),其中三元条件用于在组件正在获取数据时显示Loading标签,或者根据isFetching变量的值在按钮内部显示Load More

<button [...]> 
  {isFetching ? 'Loading...' : 'Load More'} 
</button> 

现在我们来看当事情变得更加复杂时最好的解决方案,例如,我们必须检查多个变量以确定是否渲染组件:

<div>
  {dataIsReady && (isAdmin || userHasPermissions) && 
    <SecretData />
  }
</div> 

在这种情况下,很明显使用内联条件是一个好的解决方案,但可读性受到了严重影响。相反,我们可以在组件内部创建一个辅助函数并在 JSX 中使用它来验证条件:

const MyComponent = ({ dataIsReady, isAdmin, userHasPermissions }) => {
  const canShowSecretData = () => { 
    return dataIsReady && (isAdmin || userHasPermissions)
  } 

  return (
    <div> 
      {canShowSecretData() && <SecretData />} 
    </div>
  )
} 

如你所见,这个更改使代码更易于阅读,条件更明确。如果你在 6 个月后查看这段代码,仅通过阅读函数名你仍然可以找到它很清晰。

这同样适用于计算属性。假设你有两个单独的属性用于货币和价值。你可以在render内部创建一个价格字符串,而不是这样做,你可以创建一个函数:

const MyComponent = ({ currency, value }) => {
     const getPrice = () => { 
    return `${currency}${value}`
  }

  return <div>{getPrice()}</div>
} 

这更好,因为它被隔离了,如果你想要测试它是否包含逻辑,你可以轻松地进行测试。

回到条件语句,我们可以创建一个自定义组件并命名为RenderIf来条件性地渲染我们的组件:

import React, { FC, ReactElement } from 'react'
interface Props {
  children: ReactElement | string
  isTrue?: Boolean
  isFalse?: Boolean
}
const RenderIf: FC<Props> = ({ children, isTrue, isFalse }) => {
  if (isTrue === true) {
    return <>{children}</>
  }
  if (isFalse === false) {
    return <>{children}</>
  }
  return null
}
export default RenderIf 

我们可以很容易地在我们的项目中使用它,如下所示:

import RenderIf from './RenderIf'
const MyComponent = ({ dataIsReady, isAdmin, userHasPermissions }) => {  
  return (
    <div> 
      <RenderIf isTrue={dataIsReady && (isAdmin || userHasPermissions)}>
        <SecretData />
      </RenderIf> 
    </div>
  )
} 

循环

在 UI 开发中,显示项目列表是一个非常常见的操作。当涉及到显示列表时,使用 JavaScript 作为模板语言是一个非常不错的选择。

如果我们在 JSX 模板中编写一个返回数组的函数,数组中的每个元素都会被编译成一个元素。

正如我们之前看到的,我们可以在大括号内使用任何 JavaScript 表达式,并且给定一个对象数组,生成元素数组最常见的方式是使用map

让我们深入一个现实世界的例子。假设你有一个用户列表,每个用户都有一个附加的name属性。

要创建一个无序列表来显示用户,你可以这样做:

<ul> 
  {users.map(user => <li>{user.name}</li>)} 
</ul> 

这个片段既简单又强大,因为 HTML 和 JavaScript 的力量在这里汇聚。

子渲染

值得强调的是,我们总是希望保持我们的组件非常小,我们的渲染方法非常干净和简单。

然而,这并不是一个容易实现的目标,尤其是在您迭代创建应用程序时,在第一次迭代中,您不确定如何将组件拆分成更小的部分。那么,当渲染方法变得太大难以维护时,我们应该怎么做?一个解决方案是以一种让我们能够保持所有逻辑在同一个组件中的方式将其拆分成更小的函数。

让我们来看一个例子:

const renderUserMenu = () => { 
  // JSX for user menu 
} 

const renderAdminMenu = () => { 
  // JSX for admin menu 
} 

return ( 
  <div> 
    <h1>Welcome back!</h1> 
    {userExists && renderUserMenu()} 
    {userIsAdmin && renderAdminMenu()} 
  </div> 
) 

这并不总是被认为是最佳实践,因为这看起来更明显地应该将组件拆分成更小的部分。然而,有时保持渲染方法更干净是有帮助的。例如,在 Redux 的实际应用示例中,使用子渲染方法来渲染加载更多按钮。

现在我们已经是 JSX 的高级用户了,是时候继续前进,看看如何在我们的代码中遵循样式指南,以使其保持一致。

代码样式

在本节中,您将学习如何通过验证代码风格来实施EditorConfigESLint,从而提高您的代码质量。在您的团队中拥有标准的代码风格并避免使用不同的代码风格非常重要。

EditorConfig

EditorConfig帮助开发者在不同 IDE 之间保持一致的编码风格。

EditorConfig 被许多编辑器支持。您可以在官方网站上检查您的编辑器是否受支持,www.editorconfig.org

您需要在您的root目录中创建一个名为.editorconfig的文件——我使用的配置如下:

root = true
[*]
indent_style = space 
indent_size = 2
end_of_line = lf
charset = utf-8 
trim_trailing_whitespace = true 
insert_final_newline = true
[*.html] 
indent_size = 4
[*.css] 
indent_size = 4
[*.md]
trim_trailing_whitespace = false 

您可以使用[*]影响所有文件,以及使用[.extension]影响特定文件。

Prettier

Prettier是一个有偏见的代码格式化工具,支持多种语言,可以与大多数编辑器集成。这个插件非常有用,因为您可以在保存时格式化代码,您不需要在代码审查中讨论代码风格,这将为您节省大量时间和精力。

如果您使用 Visual Studio Code,您必须首先安装 Prettier 扩展:

文本,网站描述自动生成

图 3.2:Prettier – 代码格式化工具

然后,如果您想在保存文件时配置格式化选项,您需要前往设置,搜索在保存时格式化,并确认该选项:

文本描述自动生成

图 3.3:配置保存文件时的格式化选项

这将影响您所有的项目,因为它是一个全局设置。如果您只想在特定项目中应用此选项,您必须在您的项目内部创建一个.vscode文件夹和一个settings.json文件,其中包含以下代码:

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true
} 

然后,您可以在您的.prettierrc文件中配置您想要的选项——这是我通常使用的配置:

{
  "arrowParens": "avoid",
  "bracketSpacing": true,
  "jsxSingleQuote": false,
  "printWidth": 100,
  "quoteProps": "as-needed",
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "none",
  "useTabs": false
} 

这将帮助您或您的团队标准化代码风格。

ESLint

编写高质量的代码始终是我们的目标,但错误仍然可能发生,花费数小时追踪由简单的打字错误引起的错误可能会非常令人沮丧。幸运的是,有一些工具可以帮助我们在键入时就捕获这些错误,从而避免简单的语法错误。

如果你来自像 C#这样的静态类型语言,你可能已经习惯了在 IDE 中得到警告。在 JavaScript 世界中,用于代码检查的流行工具是 ESLint。ESLint 是一个于 2013 年发布的开源项目,它高度可配置和可扩展。

在快速发展的 JavaScript 生态系统中,库和技术经常变化,因此拥有一个可以轻松扩展插件和规则的工具至关重要,这些规则可以根据需要启用或禁用。此外,由于像 Babel 这样的转换器和尚未成为标准 JavaScript 版本一部分的实验性语言功能,我们需要能够告诉我们的代码检查器我们在源文件中遵循哪些规则。代码检查器不仅帮助我们更快地捕获错误,而且还强制执行常见的编码风格指南,这对于大型团队尤为重要,因为一致性是关键。

在接下来的章节中,我们将更详细地探讨 ESLint 以及它是如何帮助我们编写更好、更一致的代码的。

安装

首先,我们必须安装 ESLint 和一些插件,如下所示:

npm install -g eslint eslint-config-airbnb eslint-config-prettier eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-react 

一旦安装了可执行文件,我们就可以使用以下命令运行它:

eslint source.ts 

输出将告诉我们文件中是否有错误。

当我们第一次安装和运行它时,我们没有看到任何错误,因为它完全可配置,并且它没有自带任何默认规则。

配置

让我们开始配置 ESLint。它可以通过位于项目根目录的.eslintrc文件进行配置。为了添加一些规则,让我们创建一个.eslintrc文件,配置为 TypeScript,并添加一条基本规则:

{
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint", "prettier"],
  "extends": [
    "airbnb",
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended"
  ],
  "settings": {
    "import/extensions": [".js", ".jsx", ".ts", ".tsx"],
    "import/parsers": {
      "@typescript-eslint/parser": [".ts", ".tsx"]
    },
    "import/resolver": {
      "node": {
        "extensions": [".js", ".jsx", ".ts", ".tsx"]
      }
    }
  },
  "rules": {
    "semi": [2, "never"]
  }
} 

这个配置文件需要一些解释:“semi”是规则的名称,而[2, "never"]是值。第一次看到它时可能不太直观。

ESLint 规则有三个级别,用于确定问题的严重性:

  1. off(或0):规则被禁用。

  2. warn(或1):规则是一个警告。

  3. error(或2):规则会抛出一个错误。

我们使用值2是因为我们希望 ESLint 在代码不遵循规则时每次都抛出错误。第二个参数告诉 ESLint 我们希望分号永远不被使用(相反是总是)。ESLint 及其插件都有很好的文档,对于任何单个规则,你都可以找到规则的描述和一些示例,说明它在何时通过以及在何时失败。

现在创建一个index.ts文件,内容如下:

const foo = 'bar'; 

如果我们运行eslint index.js,我们会得到以下结果:

Extra semicolon (semi) 

这很好;我们设置了代码检查器,它正在帮助我们遵循第一条规则。

这里有一些我更喜欢关闭或更改的规则:

"rules": {
    "semi": [2, "never"],
    "@typescript-eslint/class-name-casing": "off",
    "@typescript-eslint/interface-name-prefix": "off",
    "@typescript-eslint/member-delimiter-style": "off",
    "@typescript-eslint/no-var-requires": "off",
    "@typescript-eslint/ban-ts-ignore": "off",
    "@typescript-eslint/no-use-before-define": "off",
    "@typescript-eslint/ban-ts-comment": "off",
    "@typescript-eslint/explicit-module-boundary-types": "off",
    "no-restricted-syntax": "off",
    "no-use-before-define": "off",
    "import/extensions": "off",
    "import/prefer-default-export": "off",
    "max-len": [
      "error",
      {
        "code": 100,
        "tabWidth": 2
      }
    ],
    "no-param-reassign": "off",
    "no-underscore-dangle": "off",
    "react/jsx-filename-extension": [
      1,
      {
        "extensions": [".tsx"]
      }
    ],
    "import/no-unresolved": "off",
    "consistent-return": "off",
    "jsx-a11y/anchor-is-valid": "off",
    "sx-a11y/click-events-have-key-events": "off",
    "jsx-a11y/no-noninteractive-element-interactions": "off",
    "jsx-a11y/click-events-have-key-events": "off",
    "jsx-a11y/no-static-element-interactions": "off",
    "react/jsx-props-no-spreading": "off",
    "jsx-a11y/label-has-associated-control": "off",
    "react/jsx-one-expression-per-line": "off",
    "no-prototype-builtins": "off",
    "no-nested-ternary": "off",
    "prettier/prettier": [
      "error",
      {
        "endOfLine": "auto"
      }
    ]
  } 

Git 钩子

为了避免在我们的仓库中有未检查的代码,我们可以通过 Git 钩子在我们流程的某个点上添加 ESLint。例如,我们可以使用husky在名为pre-commit的 Git 钩子中运行我们的代码检查器,并且运行名为pre-push的钩子来运行我们的单元测试也是很有用的。

要安装husky,你需要运行以下命令:

npm install --save-dev husky 

然后,在我们的package.json文件中,我们可以添加这个节点来配置我们想要在 Git 钩子中运行的任务。

编辑package.json > prepare脚本并运行一次:

 npm pkg set scripts.prepare="husky install"
  npm run prepare 

添加一个钩子:

 npx husky add .husky/pre-commit "npm run lint"
  git add .husky/pre-commit 

提交一个版本:

 git commit -m "Keep calm and commit"
  # `npm run lint` will run every time you commit 

ESLint 命令有一个特殊的选项(标志)叫做--fix – 使用这个选项,ESLint 将尝试自动修复我们所有的代码检查错误(不是所有的)。注意这个选项,因为有时它可能会稍微影响我们的代码风格。另一个有用的标志是--ext,用于指定我们想要验证的文件扩展名 – 在这种情况下,只验证.tsx.ts文件。

在下一节中,你将了解函数式编程FP)是如何工作的,以及诸如首类对象、纯净性、不可变性、柯里化和组合等主题。

函数式编程

除了遵循最佳实践和使用代码检查器来捕获错误和强制一致性之外,另一种清理我们代码的方法是采用FP风格。

正如我们在第一章React 入门中讨论的那样,React 的声明式编程方法使我们的代码更易读。FP 也是一种声明式范式,其中避免了副作用,并且数据被视为不可变,这使得代码更容易维护和推理。

尽管我们不会在本节深入探讨 FP,但我们会介绍一些在 React 中常用且你应该了解的概念。

FP 原则,如不可变性、纯净函数和高阶函数,可以帮助我们编写更易于维护和测试的代码。通过将我们的数据视为不可变,我们可以避免副作用,并使我们的应用程序流程更容易推理。总是对相同的输入返回相同输出的纯净函数帮助我们避免意外的副作用,并使我们的代码更容易测试。接受函数作为参数并/或返回函数作为输出的高阶函数可以帮助我们创建更模块化和可重用的代码。

通过采用 FP 风格,我们可以编写更多声明式代码和更少的命令式代码,使我们的组件更容易阅读和推理。

首类函数

JavaScript 有首类函数,因为它们被当作任何其他变量一样对待,这意味着你可以将一个函数作为参数传递给其他函数,或者它可以被另一个函数返回并赋值给一个变量。

这允许我们引入高阶函数HoFs)的概念。HoFs 是接受一个函数作为参数(可选地还有其他参数),并返回一个函数的函数。返回的函数通常具有一些特殊的行为。

让我们来看一个例子:

const add = (x, y) => x + y
const log = fn => (...args) => { 
  return fn(...args)
}
const logAdd = log(add) 

这里,一个函数正在添加两个数字,它增强了一个记录所有参数并执行原始函数的函数。

这个概念非常重要,因为,在 React 世界中,一个常见的模式是使用高阶组件将我们的组件视为函数,并用共同的行为来增强它们。我们将在第四章探索流行的组合模式中看到 HOC 和其他模式。

纯度

函数式编程的一个重要方面是编写纯函数。你将在 React 生态系统中经常遇到这个概念,特别是如果你查看像 Redux 这样的库。

函数要成为纯函数意味着什么?

一个函数是纯的,当且仅当没有副作用,这意味着函数不会改变任何不是函数本身局部的东西。

例如,一个改变应用程序状态的函数,或修改在更高作用域中定义的变量,或者一个接触外部实体(如文档对象模型DOM))的函数,被认为是非纯函数。非纯函数更难调试,而且大多数时候不可能多次应用并期望得到相同的结果。

例如,以下函数是纯函数:

const add = (x, y) => x + y 

它可以运行多次,总是得到相同的结果,因为没有任何东西被存储在任何地方,也没有任何东西被修改。

以下函数不是纯函数:

let x = 0
const add = y => (x = x + y) 

运行add(1)两次,我们得到两个不同的结果。第一次我们得到1,但第二次我们得到2,即使我们用相同的参数调用相同的函数。我们得到这种行为的原因是全局状态在每次执行后都会被修改。

不可变性

我们已经看到了如何编写不改变状态的纯函数,但如果我们需要改变变量的值怎么办?在函数式编程(FP)中,一个函数不是改变变量的值,而是创建一个新的变量并返回它,这个变量具有新的值。

这种处理数据的方式被称为不可变性

一个不可变值是一个不能被改变的值。

让我们来看一个例子:

const add3 = arr => arr.push(3)
const myArr = [1, 2]
add3(myArr); // [1, 2, 3]
add3(myArr); // [1, 2, 3, 3] 

前面的函数不遵循不可变性,因为它改变了给定数组的价值。再次强调,如果我们两次调用相同的函数,我们会得到不同的结果。

我们可以使用concat将前面的函数改为不可变,它返回一个新数组而不修改给定的数组:

const add3 = arr => arr.concat(3)
const myArr = [1, 2]
const result1 = add3(myArr) // [1, 2, 3]
const result2 = add3(myArr) // [1, 2, 3] 

我们运行函数两次后,myArr仍然保持其原始值。

柯里化

函数式编程中的一个常见技术是柯里化。柯里化是将接受多个参数的函数转换为一次接受一个参数并返回另一个函数的过程。让我们通过一个例子来澄清这个概念。

让我们从之前见过的add函数开始,将其转换为一个柯里化函数。

假设我们有以下代码:

const add = (x, y) => x + y 

我们可以定义函数如下:

const add = x => y => x + y 

我们以以下方式使用它:

const add1 = add(1)
add1(2); // 3
add1(3); // 4 

这是一种相当方便的编写函数的方法,因为第一个值在应用第一个参数后存储,我们可以多次重用第二个函数。

组合

最后,一个在函数式编程中非常重要且可以应用于 React 的概念是组合。函数(和组件)可以组合在一起,产生具有更高级功能和属性的新函数。

考虑以下函数:

const add = (x, y) => x + y
const square = x => x * x 

这些函数可以组合在一起,创建一个新的函数,该函数先加两个数字,然后将结果加倍:

const addAndSquare = (x, y) => square(add(x, y)) 

按照这种范式,我们最终得到的是小型、简单、可测试的纯函数,它们可以组合在一起。

摘要

在本章中,我们介绍了 JSX 的基础知识,包括其语法和功能。我们还学习了如何配置 Prettier 和 ESLint 以保持代码库的一致性,并在代码早期阶段捕获错误。此外,我们还探讨了函数式编程的一些基本概念,这些概念可以帮助我们编写更易于维护和测试的代码。

现在我们已经将代码整理得干净、有序,我们准备深入探索 React,并在下一章学习如何编写真正可重用的组件。通过遵循最佳实践和养成良好的编码习惯,我们可以创建易于维护、扩展和测试的 React 应用程序。

加入我们的 Discord 社区

加入我们的社区 Discord 空间,与作者和其他读者进行讨论:

packt.link/React18DesignPatterns4e

二维码

第四章:探索流行的组合模式

在本章中,我们将学习如何使组件之间有效地进行通信,这是使用小型、可测试和可维护的组件构建复杂 React 应用程序的关键部分。通过掌握 React 中流行的组合模式和工具,您将能够控制应用程序的每一个部分,并构建可扩展的软件。

让我们深入探讨如何利用这些模式和工具构建更好的 React 应用程序。我们将涵盖以下主题:

  • 组件如何使用 props 和 children 进行通信

  • 容器和表现模式以及它们如何使我们的代码更易于维护

  • 高阶组件HOCs)是什么以及如何利用它们以更好的方式构建我们的应用程序

  • 子组件模式的功能及其好处

技术要求

要完成本章,您需要以下内容:

  • Node.js 19+

  • Visual Studio Code

您可以在本书的 GitHub 仓库中找到本章的代码,网址为github.com/PacktPublishing/React-18-Design-Patterns-and-Best-Practices-Fourth-Edition/tree/main/Chapter04

通信组件

使用 React 构建应用程序的关键好处之一是编写 React 组件。通过创建具有清晰界面的小型、可重用组件,您可以轻松地将它们组合在一起,以创建既强大又易于维护的复杂应用程序。

具有清晰界面的小型组件可以组合在一起,以创建既强大又易于维护的复杂应用程序。

组合 React 组件很简单;您只需在 render 中包含它们:

const Profile = ({ user }) => ( 
  <>
    <Picture profileImageUrl={user.profileImageUrl} /> 
    <UserName name={user.name} screenName={user.screenName} /> 
  </> 
) 

例如,您可以通过简单地组合一个Picture组件来显示用户头像,以及一个UserName组件来显示用户名和屏幕名,来创建一个Profile组件。

以这种方式,您可以非常快速地生成新的用户界面部分,只需编写几行代码。每当您组合组件时,就像前面的例子一样,您可以使用 props 在它们之间共享数据。Props 是父组件将数据传递到树中每个需要它(或其部分)的组件的方式。

Profile is not the direct parent of Picture (the div tag is), but Profile owns Picture because it passes down the props to it.

在下一节中,您将了解children属性及其正确使用方法。

使用 children 属性

有一个特殊的属性可以从所有者传递到其内部定义的组件——children

在 React 文档中,它被描述为不透明,因为它是一个不告诉你其包含值的属性。通常,在父组件的渲染中定义的子组件会接收到作为组件自身属性传递的 props,或者在 JSX 中的_jsx函数的第二个参数中传递。组件也可以在它们内部定义嵌套组件,并且它们可以使用children属性访问这些子组件。

考虑我们有一个具有文本属性的Button组件,该属性表示按钮的文本:

const Button = ({ text }) => <button className="btn">{text}</button> 

该组件可以使用以下方式:

<Button text="Click me!" /> 

这将渲染以下代码:

<button class="btn">Click me!</button> 

现在,假设我们想在应用程序的多个部分使用具有相同类名的相同按钮,并且我们还想能够显示比简单的字符串更多的内容。我们的 UI 由带文本的按钮、带文本和图标的按钮以及带文本和标签的按钮组成。

在大多数情况下,一个好的解决方案是向Button添加多个参数或创建不同版本的Button,每个版本都有其单一的专业化,例如IconButton

然而,我们应该意识到Button可能只是一个包装器,我们能够在其中渲染任何元素并使用children属性:

const Button = ({ children }) => <button className="btn">{children}</button> 

通过传递children属性,我们不仅限于简单的单个文本属性,我们还可以将任何元素传递给Button,并且它将替换children属性进行渲染。

在这种情况下,我们包裹在Button组件内的任何元素都将被渲染为具有btn类名的按钮元素的子元素。

例如,如果我们想在按钮内渲染一个图像,并在span标签内包裹一些文本,我们可以这样做:

<Button> 
    <img src="img/..." alt="..." /> 
    <span>Click me!</span> 
</Button> 

前面的代码片段在浏览器中的渲染方式如下:

<button class="btn"> 
    <img src="img/..." alt="..." /> 
    <span>Click me!</span> 
</button> 

这是一种相当方便的方式,允许组件接受任何子元素并将这些元素包裹在预定义的父元素内。

现在,我们可以在Button组件内传递图像、标签,甚至其他 React 组件,并且它们将被渲染为其子元素。正如您在前面的示例中看到的,我们将children属性定义为数组,这意味着我们可以传递任意数量的元素作为组件的子元素。

我们可以传递单个子元素,如下面的代码所示:

<Button>
    <span>Click me!</span> 
</Button> 

现在,让我们在下一节中探索容器和展示模式。

探索容器和展示模式

在上一章中,我们看到了如何逐步将耦合组件转换为可重用组件。现在我们将看到如何将类似的模式应用到我们的组件中,使它们更清晰、更易于维护。

React 组件通常包含逻辑和展示的混合。通过逻辑,我们指的是与 UI 无关的任何内容,例如 API 调用、数据处理和事件处理程序。展示是渲染的部分,我们在其中创建要在 UI 上显示的元素。

在 React 中,有一些简单而强大的模式,称为containerpresentational,我们可以在创建组件时应用这些模式,帮助我们分离这两个关注点。

在逻辑和表示之间创建明确的边界不仅使组件更可重用,还提供了许多其他好处,你将在本节中了解到。再次强调,学习新概念的最佳方法之一是通过查看实际示例,所以让我们深入一些代码。

假设我们有一个组件,它使用地理位置 API 来获取用户的当前位置,并在浏览器页面上显示纬度和经度。

首先,我们在components文件夹中创建一个Geolocation.tsx文件,并使用函数组件定义Geolocation组件:

import { useState, useEffect } from 'react'
const Geolocation = () => {}
export default Geolocation 

然后我们定义我们的状态:

const [latitude, setLatitude] = useState<number | null>(null)
const [longitude, setLongitude] = useState<number | null>(null) 

现在,我们可以使用useEffect Hook 来触发对 API 的请求:

useEffect(() => { 
    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition(handleSuccess)
        } 
}, [navigator]) 

当浏览器返回数据时,我们使用以下函数将结果存储在状态中(在useEffect Hook 之前放置此函数):

const handleSuccess = ({ 
    coords: { latitude, longitude } 
  }: { coords: { latitude: number; longitude: number }}) => { 
    setLatitude(latitude)
    setLongitude(longitude)
} 

最后,我们显示纬度和经度值:

return ( 
    <div>
      <h1>Geolocation:</h1>
      <div>Latitude: {latitude}</div> 
      <div>Longitude: {longitude}</div> 
    </div> 
) 

需要注意的是,在第一次渲染时,纬度和经度是空的,因为我们是在组件挂载时请求浏览器的坐标。在现实世界的组件中,你可能想在数据返回之前显示一个加载指示器。为此,你可以使用我们在第三章清理你的代码中看到的一种条件技术。

现在,这个组件没有任何问题,并且按预期工作。如果能够将其与请求和加载位置的部分分开,以便更快地迭代它,那岂不是很好?

我们将使用容器和表示模式来隔离表示部分。在这个模式中,每个组件都被分成两个更小的组件,每个组件都有其明确的职责。容器了解组件的所有逻辑,是调用 API 的地方。它还处理数据处理和事件处理。

表示组件是定义 UI 的地方,它从容器接收以 props 形式的数据。由于表示组件通常没有逻辑,我们可以将其创建为一个无状态的函数组件。

没有规则规定表示组件不能有状态(例如,它可以在内部保持 UI 状态)。在这种情况下,我们需要一个组件来显示纬度和经度,所以我们将使用一个简单的函数。

首先,我们应该将我们的Geolocation组件重命名为GeolocationContainer

const GeolocationContainer = () => {...} 

我们还将更改文件名,从Geolocation.tsx更改为GeolocationContainer.tsx

这个规则并不是强制性的,但在 React 社区中广泛使用的一种最佳实践是在Container组件名称的末尾附加Container,并将原始名称给表示组件。

我们还必须更改render的实现,并从中删除所有 UI 部分,如下所示:

return <Geolocation latitude={latitude} longitude={longitude} /> 

如前文片段所示,我们不是在容器的返回值内部创建 HTML 元素,而是使用当前的表现性组件(我们将在下一节创建),并将状态传递给它。这些状态是纬度和经度,默认为 null,当浏览器触发回调时,它们包含用户的实际位置。

让我们创建一个新文件,命名为Geolocation.tsx,在其中定义如下功能组件:

import { FC } from 'react'
type Props = {
  latitude: number
  longitude: number
}
const Geolocation: FC<Props> = ({ latitude, longitude }) => (
  <div>
    <h1>Geolocation:</h1>
    <div>Latitude: {latitude}</div>
    <div>Longitude: {longitude}</div>
  </div>
)
export default Geolocation 

功能组件是定义 UI 的一种极其优雅的方式。它们是纯函数,给定一个状态,就返回它的元素。在这种情况下,我们的函数从所有者那里接收纬度和经度,并返回用于显示的标记结构。

第一次在浏览器中运行组件时,浏览器将要求您授权以允许它知道您的位置。

图形用户界面,文本,应用程序  自动生成的描述

图 4.1:浏览器将要求您的权限以访问您的位置

在您允许浏览器知道您的位置后,您将看到如下内容:

图形用户界面,文本,应用程序  自动生成的描述

图 4.2:显示纬度和经度

遵循容器和表现性模式,我们创建了一个“哑”或表现性组件,它是可重用的,并且可以轻松地集成到我们的组件中。这使得我们可以方便地传递模拟坐标进行测试或演示。如果应用程序的其他地方需要类似的数据结构,它消除了从头开始构建新组件的需要。相反,我们可以在新容器中封装这个现有的组件。例如,这个容器可以设计为从单独的端点检索纬度和经度信息。

同时,我们团队中的其他开发者可以通过添加一些错误处理逻辑来改进使用地理位置的容器,而不会影响其展示。他们甚至可以构建一个临时的表现性组件,仅用于显示和调试数据,然后在准备就绪时替换为真实的表现性组件。

能够在同一个组件上并行工作对团队来说是一个巨大的胜利,尤其是对于那些构建界面是一个迭代过程的公司。

这种模式简单但非常强大,当应用于大型应用程序时,它可以在开发速度和项目的可维护性方面产生差异。另一方面,如果没有真正的原因就应用这种模式,可能会带来相反的问题,使代码库变得不那么有用,因为它涉及到创建更多文件和组件。

因此,当我们决定一个组件必须按照容器和展示模式进行重构时,我们应该仔细思考。一般来说,正确的路径是从一个单一组件开始,只有在逻辑和展示变得过于耦合时才将其拆分。

在我们的例子中,我们从一个单一组件开始,并意识到我们可以将 API 调用与标记分离。决定将什么放入容器以及什么进入展示并不总是直接的;以下要点应有助于你做出这个决定:

以下是一些容器组件的特点:

  • 它们更关注行为。

  • 它们渲染它们的展示组件。

  • 它们执行 API 调用并操作数据。

  • 它们定义事件处理器。

以下是一些展示组件的特点:

  • 它们更关注视觉表示。

  • 它们渲染 HTML 标记(或其他组件)。

  • 它们以 props 的形式从父组件接收数据。

  • 它们通常被写成无状态的函数组件。

如你所见,这些模式形成了一个非常强大的工具,将帮助你更快地开发你的网络应用程序。让我们看看下一节中 HOCs 是什么。

理解 HOCs

在第三章 清理你的代码函数式编程 部分,我们介绍了 高阶函数HOFs)的概念。HOFs 是接受另一个函数作为参数、增强其行为并返回一个新函数的函数。将 HOFs 的想法应用于组件会导致 高阶组件HOCs)的产生。

一个 HOC 看起来是这样的:

const HoC = Component => EnhancedComponent 

HOCs 是接受一个组件作为输入并返回一个增强组件作为输出的函数。让我们从一个简单的例子开始,了解增强组件是什么样的。

假设你需要将相同的 className 属性附加到每个组件上。你可以手动将 className 属性添加到每个渲染方法中,或者你可以编写一个像这样的 HOC:

const withClassName = Component => props => (
  <Component {...props} className="my-class" />
) 

在 React 社区中,使用 with 前缀为 HOCs(高阶组件)是常见的。

上述代码一开始可能让人感到困惑,所以让我们来分解一下。我们声明了一个 withClassName 函数,它接受一个 Component 并返回另一个函数。返回的函数是一个函数组件,它接收一些属性并渲染原始组件。收集到的属性被展开,并传递一个值为 "my-class"className 属性给函数组件。

HOCs 通常会传播它们接收的属性,因为它们旨在保持透明并仅添加新行为。

虽然这个例子很简单且并不特别有用,但它应该能让你更好地理解 HOCs 是什么以及它们看起来像什么。现在,让我们看看我们如何在组件中使用 withClassName HOC。

首先,创建一个接收 className 并将其应用于 div 标签的无状态函数组件:

const MyComponent = ({ className }) => <div className={className} /> 

不是直接使用组件,而是将其传递给一个 HOC,如下所示:

const MyComponentWithClassName = withClassName(MyComponent) 

将我们的组件包裹在withClassName函数中确保它们接收className属性。

现在,让我们创建一个更令人兴奋的 HOC 来检测innerWidth。首先,创建一个接收Component的函数:

 import { useEffect, useState } from 'react'
  const withInnerWidth = Component => props => <Component {...props} /> 

使用with模式为增强组件提供信息的 HOCs 通常会有前缀。

接下来,定义innerWidth状态和handleResize函数:

const withInnerWidth = Component => props => {
  const [innerWidth, setInnerWidth] = useState(window.innerWidth)
  const handleResize = () => {
    setInnerWidth(window.innerWidth)
  }
  return <Component {...props} />
} 

然后,添加效果:

useEffect(() => {
  window.addEventListener('resize', handleResize)
  return () => {
    window.removeEventListener('resize', handleResize)
  }
}, []) 

最后,按照如下方式渲染原始组件:

return <Component {...props} innerWidth={innerWidth} /> 

在这里,我们像之前一样展开属性,但同时也传递了innerWidth状态。

我们将innerWidth值存储为状态,以在不污染组件状态的情况下实现原始行为。相反,我们使用属性。使用属性是提高可重用性的绝佳方式。

现在,使用 HOC 和获取innerWidth值是直接的。新的 React Hooks 可以通过创建自定义 Hooks 轻松地替换 HOC。创建一个期望innerWidth作为属性的函数组件:

const MyComponent = ({ innerWidth }) => {
  console.log('window.innerWidth', innerWidth)
  // ...
} 

如下增强它:

const MyComponentWithInnerWidth = withInnerWidth(MyComponent) 

通过使用 HOCs,我们避免了任何状态的污染,并且不需要组件实现任何函数。这意味着组件和 HOC 不是耦合的,并且可以在整个应用程序中重用。

使用属性而不是状态,我们可以创建一个“哑”组件,可以在我们的风格指南中使用,忽略复杂的逻辑,只需传递属性。

在这个特定的情况下,我们可以为支持的每个不同的innerWidth大小创建一个组件。考虑以下示例:

<MyComponent innerWidth={320} /> 

或者如下:

<MyComponent innerWidth={960} /> 

如您所见,通过使用高阶组件(HOCs),我们可以传递一个组件,然后返回一个具有额外功能的新组件。一些常见的 HOCs 包括来自 Redux 的connect和来自 Relay 的createFragmentContainer

理解函数作为子组件

FunctionAsChild模式在 React 社区中得到了共识。它在像react-motion这样的流行库中广泛使用,我们将在第五章为浏览器编写代码中探讨。

主要概念是,我们不是将子组件作为组件传递,而是定义一个可以接收来自父组件参数的函数。让我们看看它是什么样子:

const FunctionAsChild = ({ children }) => children() 

如您所见,FunctionAsChild是一个具有子属性定义为函数的组件。它不是用作 JSX 表达式,而是被调用。

之前提到的组件可以这样使用:

<FunctionAsChild>
  {() => <div>Hello, World!</div>}
</FunctionAsChild> 

这个例子相当简单:子函数在父组件的渲染方法中执行,返回包裹在div标签中的Hello, World!文本,该文本在屏幕上显示。

现在,让我们探索一个更有意义的例子,其中父组件将一些参数传递给子函数。

创建一个Name组件,它期望一个函数作为子组件,并将'World'字符串传递给它:

const Name = ({ children }) => children('World') 

之前提到的组件可以这样使用:

<Name>
  {name => <div>Hello, {name}!</div>}
</Name> 
Hello, World! again, but this time the name has been passed by the parent. It should now be clear how this pattern works. Let’s look at the advantages of this approach:
  • 主要优势是能够封装组件,动态传递变量,而不是使用静态属性,这是高阶组件中常见的做法。一个很好的例子是Fetch组件,它被设计用来从特定的 API 端点检索数据,并将其随后返回给其子函数:

    <Fetch url="...">
      {data => <List data={data} />}
    </Fetch> 
    
  • 其次,使用这种方法组合组件不会强迫子组件使用预定义的 prop 名称。由于函数接收变量,使用该组件的开发者可以决定它们的名称。这种灵活性使得“函数作为子组件”解决方案更加灵活。

  • 最后,包装器非常可重用,因为它不对接收到的子组件做出任何假设——它只期望一个函数。因此,同一个FunctionAsChild组件可以在应用程序的不同部分使用,以服务于各种子组件。

通过采用“函数作为子组件”模式,你可以在 React 应用程序中创建更灵活、多功能和可重用的组件。

摘要

在本章中,我们学习了如何有效地使用 props 来组合和通信我们的可重用组件。通过使用 props,我们可以创建定义良好的接口,并使我们的组件彼此解耦。

我们还探索了两种流行的组合模式:容器模式和展示模式,这些模式帮助我们分离逻辑和展示,以便为更专业和专注的组件提供支持。此外,我们还发现了高阶组件HOCs)作为处理上下文的一种方式,它允许我们不必将组件紧密耦合到上下文中,以及用于动态组合组件的“函数作为子组件”模式。

在下一章中,我们将深入探讨受控组件与不受控组件、refs、处理事件和 React 中的动画。

第五章:为浏览器编写代码

当我们与 React 和浏览器一起工作时,我们可以执行一些特定的操作。例如,我们可以要求我们的用户通过表单输入一些信息。在本章中,我们将探讨如何应用不同的技术来处理表单。我们可以实现非受控组件,让字段保持其内部状态,或者我们可以使用受控组件,其中我们可以完全控制字段的状态。

在本章中,我们还将探讨 React 中的事件是如何工作的,以及库如何实现一些高级技术,以在不同浏览器之间提供一致的接口。我们将探讨 React 团队实施的一些有趣的解决方案,以使事件系统非常高效。

事件发生后,我们将跳转到refs来查看我们如何在 React 组件中访问底层DOM节点。这代表了一个强大的功能,但应该谨慎使用,因为它打破了使 React 易于工作的某些约定。

在 refs 之后,我们将探讨如何使用 React 插件轻松实现动画。最后,我们将学习如何在 React 中与可缩放矢量图形SVG)一起工作有多容易,以及我们如何为我们的应用程序创建动态可配置的图标。

本章我们将讨论以下主题:

  • 使用不同的技术用 React 创建表单

  • 监听 DOM 事件并实现自定义处理程序

  • 使用 refs 在 DOM 节点上执行命令式操作的方法

  • 创建在不同浏览器上都能工作的简单动画

  • React 生成 SVG 的方式

技术要求

要完成本章,你需要以下内容:

  • Node.js 19+

  • Visual Studio Code

你可以在本书的 GitHub 仓库中找到本章的代码:github.com/PacktPublishing/React-18-Design-Patterns-and-Best-Practices-Fourth-Edition/tree/main/Chapter05

理解和实现表单

在本节中,我们将学习如何使用 React 实现表单。一旦我们开始用 React 构建真实的应用程序,我们就需要与用户交互。如果我们想在浏览器中从我们的用户那里获取信息,表单是最常见的解决方案。由于库的工作方式和其声明性特性,使用 React 处理输入字段和其他表单元素并不简单,但一旦我们理解了其逻辑,它就会变得清晰。在下一节中,我们将学习如何使用非受控和受控组件。

非受控组件

非受控组件类似于常规 HTML 表单输入,对于这些输入,你将无法自行管理其值,而是 DOM 将负责处理该值,你可以通过使用 React ref 来获取这个值。让我们从一个基本示例开始——显示一个带有输入字段和提交按钮的表单。

代码相当简单:

import { FC, useState, ChangeEvent, MouseEvent } from 'react'
const Uncontrolled: FC = () => {
  const [value, setValue] = useState<string>('')
  return (
    <form>
        <input type="text" />
        <button>Submit</button>
    </form>
  )
}
export default Uncontrolled 

如果我们在浏览器中运行前面的代码片段,我们会看到我们预期的结果——一个可以写入内容的输入框和一个可点击的按钮。这是一个未受控组件的例子,我们并没有设置输入框的值,而是让组件管理其自身的内部状态。

很可能,我们想在点击 Submit 按钮时对元素的值做些处理。例如,我们可能想将数据发送到 API 端点。

我们可以通过添加一个 onChange 监听器轻松做到这一点(我们将在本章后面更多地讨论事件监听器)。让我们看看添加监听器意味着什么。

我们需要创建 handleChange 函数:

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value)
} 

事件监听器接收一个事件对象,其中目标表示生成事件的字段,我们对其值感兴趣。我们首先只是记录它,因为重要的是要从小步骤开始,但很快我们会将值存储到状态中。

最后,我们渲染表单:

return (
  <form>
    <input type="text" onChange={handleChange} />
    <button>Submit</button>
  </form>
) 

如果我们在浏览器中渲染组件并将单词 React 输入到表单字段中,我们将在控制台看到如下内容:

R
Re
Rea
Reac
React 

handleChange 监听器会在输入框的值每次改变时被触发。因此,我们的函数会在每个输入字符时被调用一次。下一步是存储用户输入的值,并在用户点击 Submit 按钮时使其可用。

我们只需更改处理程序的实施方式,将其存储在状态中而不是记录它,如下所示:

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
  setValue(e.target.value)
} 

当表单提交时得到通知与监听输入字段的变化事件非常相似;它们都是浏览器在发生某些事情时调用的事件。

让我们定义 handleSubmit 函数,其中我们只是记录值。在现实世界的场景中,你可以将数据发送到 API 端点或传递给另一个组件:

const handleSubmit = (e: MouseEvent<HTMLButtonElement>) => {
  e.preventDefault()

  console.log(value) // Here we are logging the value state
} 

这个处理程序相当简单;我们只是记录当前存储在状态中的值。我们还想克服表单提交时浏览器默认行为,执行自定义操作。这似乎是合理的,并且对于单个字段来说效果很好。现在的问题是,如果我们有多个字段怎么办?假设我们有数十个不同的字段?

让我们从基本示例开始,手动创建每个字段和处理程序,并看看我们如何通过应用不同的优化级别来改进它。

让我们创建一个新的表单,包含姓氏和名字字段。我们可以重用 Uncontrolled 组件并添加一些新的状态:

const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('') 

我们在状态中初始化两个字段,并为每个字段定义一个事件处理程序。正如你可能已经注意到的,当有很多字段时,这并不很好地扩展,但在转向更灵活的解决方案之前,清楚地理解问题是重要的。

现在,我们实现新的处理程序:

const handleChangeFirstName = ({ target: { value } }) => {
  setFirstName(value)
}

const handleChangeLastName = ({ target: { value } }) => {
  setLastName(value)
} 

我们还必须稍微更改提交处理程序,以便在点击时显示姓名的首尾:

const handleSubmit = (e: MouseEvent<HTMLButtonElement>) => {
  e.preventDefault()

  console.log(`${firstName} ${lastName}`) // Logging the firstName and lastName states
} 

最后,我们渲染表单:

return (
  <form onSubmit={handleSubmit}>
    <input type="text" onChange={handleChangeFirstName} />
    <input type="text" onChange={handleChangeLastName} />
    <button>Submit</button>
  </form>
) 

我们已经准备就绪:如果我们将在浏览器中运行前面的组件,我们将看到两个字段,如果我们将在第一个字段中输入Carlos,在第二个字段中输入Santana,当表单提交时,我们将在浏览器控制台中看到完整的姓名显示。

再次强调,这种方法效果良好,我们可以通过这种方式做一些有趣的事情,但它无法处理复杂场景,除非我们编写大量的模板代码。

让我们看看如何对其进行一点优化。我们的目标是使用单个更改处理程序,这样我们就可以添加任意数量的字段,而无需创建新的监听器。

让我们回到组件,并更改我们的状态:

const [values, setValues] = useState({ firstName: '', lastName: '' }) 

我们可能仍然想要初始化值,在本节的后面部分,我们将探讨如何为表单提供预填充值。

现在,有趣的部分在于我们可以如何修改onChange处理程序实现,使其在不同的字段中工作:

const handleChange = ({ target: { name, value } }) => {   
  setValues({
    ...values,
    [name]: value
  })
} 

如我们之前所见,我们接收的事件的target属性代表了触发事件的输入字段,因此我们可以使用字段的名称及其值作为变量。

然后,我们必须为每个字段设置名称:

return (
  <form onSubmit={handleSubmit}>
    <input
      type="text"
      name="firstName"
      onChange={handleChange}
    />
    <input
      type="text"
      name="lastName"
      onChange={handleChange}
    />
    <button>Submit</button>
  </form>
) 

就这样!我们现在可以添加任意多的字段,而无需创建额外的处理程序。

受控组件

受控组件是 React 组件,它通过使用组件状态来控制表单中输入元素的值。

在这里,我们将探讨如何使用一些值预先填充表单字段,这些值可能来自服务器或作为父组件的 props。为了完全理解这个概念,我们将从一个非常简单的无状态函数组件开始,并逐步改进它。

第一个示例显示了输入字段内的预定义值:

const Controlled = () => (
  <form>
    <input type="text" value="Hello React" />
    <button>Submit</button>
  </form>
) 

如果我们在浏览器中运行此组件,我们会发现它按预期显示了默认值,但它不允许我们更改值或在其内部输入其他内容。

原因在于,在 React 中,我们声明了希望在屏幕上显示的内容,设置固定值属性总会导致渲染该值,无论采取其他什么操作。这在实际应用中可能不是我们希望的行为。

如果我们打开控制台,我们会得到以下错误消息。React 本身告诉我们我们做错了什么:

You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. 

现在,如果我们只想让输入字段有一个默认值,并且我们想要能够通过输入来更改它,我们可以使用defaultValue属性:

import { useState } from 'react'
const Controlled = () => {
  return (
    <form>
      <input type="text" defaultValue="Hello React" />
      <button>Submit</button>
    </form>
  )
}
export default Controlled 

这样,当字段被渲染时,它将显示Hello React,然后用户可以在其中输入任何内容并更改其值。现在让我们添加一些状态:

const [values, setValues] = useState({ firstName: 'Carlos', lastName: 'Santana' }) 

处理程序与之前相同:

const handleChange = ({ target: { name, value } }) => {
  setValues({
    [name]: value
  })
}

const handleSubmit = (e) => {
  e.preventDefault()

  console.log(`${values.firstName} ${values.lastName}`)
} 

实际上,我们将使用输入字段的值属性来设置它们的初始值以及更新后的值:

return (
  <form onSubmit={handleSubmit}>
    <input
        type="text"
        name="firstName"
        value={values.firstName}
        onChange={handleChange}
    />
    <input
        type="text"
        name="lastName"
        value={values.lastName}
        onChange={handleChange}
    />
    <button>Submit</button>
  </form>
) 

当表单第一次渲染时,React 使用状态中的初始值作为输入字段的值。当用户在字段中输入某些内容时,handleChange函数被调用,并将字段的新值存储在状态中。

当状态发生变化时,React 会重新渲染组件并再次使用它来反映输入字段的当前值。我们现在完全控制字段的值,我们称这种模式为受控组件

在下一节中,我们将处理事件,这是 React 处理来自表单的数据的基本部分。

处理事件

事件在不同的浏览器中工作方式略有不同。React 试图抽象事件的工作方式,并为开发者提供一个一致的接口来处理。这是 React 的一个伟大特性,因为我们可以忘记我们正在针对的浏览器,并编写事件处理程序和函数,它们是供应商无关的

为了提供这个功能,React 引入了合成事件的概念。合成事件是一个对象,它包装了浏览器提供的原始事件对象,并且无论在哪里创建,都具有相同的属性。

要将事件监听器附加到节点并在事件触发时获取事件对象,我们可以使用一个简单的约定,它回忆了事件附加到 DOM 节点的方式。实际上,我们可以使用单词 on 加上camelCased事件名称(例如,onKeyDown)来定义在事件发生时被触发的回调。一个流行的约定是将事件处理程序函数命名为事件名称,并使用handle作为前缀(例如,handleKeyDown)。

我们在之前的示例中已经看到了这种模式的实际应用,当时我们正在监听表单字段的onChange事件。让我们再次回顾一个基本的事件监听器示例,看看我们如何以更优雅的方式在同一个组件内部组织多个事件。我们将实现一个简单的按钮,并且像往常一样,首先创建一个组件:

const Button = () => {
}
export default Button 

然后我们定义事件处理程序:

const handleClick = (syntheticEvent) => {
      console.log(syntheticEvent instanceof MouseEvent)
      console.log(syntheticEvent.nativeEvent instanceof MouseEvent)
    } 

正如您在这里所看到的,我们正在做一件非常简单的事情:我们只是检查从 React 接收的事件对象的类型以及附加到其上的原生事件类型。我们期望第一个返回 false,第二个返回 true。

您永远不需要访问原始的本地事件,但了解您可以在需要时这样做是好的。最后,我们使用onClick属性定义按钮,并将事件监听器附加到它:

return (
  <button onClick={handleClick}>Click me!</button>
) 

现在,假设我们想要将第二个处理程序附加到按钮,以便监听双击事件。一个解决方案是创建一个新的单独的处理程序,并使用onDoubleClick属性将其附加到按钮,如下所示:

<button
  onClick={handleClick}
  onDoubleClick={handleDoubleClick}
>
  Click me!
</button> 

请记住,我们总是力求编写更少的样板代码并避免代码重复。因此,一个常见的做法是为每个组件编写一个单个事件处理程序,它可以根据事件类型触发不同的操作。

这种技术由 Michael Chan 在一系列模式中描述:

reactpatterns.com/#event-switch

让我们实现通用事件处理器:

const handleEvent = (event) => {
  switch (event.type) {
    case 'click':
        console.log('clicked')
      break

    case 'dblclick':
        console.log('double clicked')
      break

    default:
        console.log('unhandled', event.type)
  }
} 

通用事件处理器接收事件对象并根据事件类型切换以触发正确的动作。如果我们想在每个事件上调用一个函数(例如,分析)或者某些事件共享相同的逻辑,这特别有用。

最后,我们将新的事件监听器附加到onClickonDoubleClick属性:

return (
  <button
    onClick={handleEvent}
    onDoubleClick={handleEvent}
  >
    Click me!
  </button>
) 

从这一点开始,每当我们需要为同一组件创建一个新的事件处理器时,我们只需向 switch 添加一个新的情况,而不是创建一个新的方法和绑定它。

关于 React 中事件的一些有趣的事情是,合成事件会被重用,并且存在一个全局处理器。第一个概念意味着我们不能存储一个合成事件并在之后重用它,因为事件在动作之后立即变为 null。这种技术在性能方面非常好,但如果出于某种原因我们想在组件的状态中存储事件,可能会遇到问题。为了解决这个问题,React 为我们提供了一个在合成事件上的持久化方法,我们可以调用它来使事件持久化,以便我们可以存储和稍后检索它。

第二个非常有趣的具体实现细节又是关于性能的,这涉及到 React 将事件处理器附加到 DOM 的方式。

每当我们使用 on 属性时,我们都在向 React 描述我们想要实现的行为,但库不会将实际的事件处理器附加到底层的 DOM 节点。

它所做的相反,是将单个事件处理器附加到根元素,该处理器监听所有事件,这要归功于事件冒泡。当浏览器触发我们感兴趣的事件时,React 代表它调用特定组件上的处理器。这种技术称为事件委托,用于内存和速度优化。

在下一节中,我们将探索 React refs 并了解如何利用它们。

探索 refs

人们喜欢 React 的一个原因是因为它是声明式的。声明式意味着你只需描述在任何时刻你想在屏幕上显示的内容,React 会负责与浏览器的通信。这个特性使得 React 在推理上非常容易,同时也很强大。

然而,可能有一些情况下你需要访问底层的 DOM 节点来执行一些命令式操作。这应该被避免,因为在大多数情况下,有更符合 React 的解决方案来实现相同的结果,但了解我们有这个选项并且知道它是如何工作的,以便我们可以做出正确的决定。

假设我们想要创建一个简单的表单,其中包含一个输入元素和一个按钮,并且我们希望它以这样的方式运行:当按钮被点击时,输入字段会获得焦点。我们想要做的是在浏览器窗口中调用输入节点(即输入的实际 DOM 实例)的 focus 方法。

让我们创建一个名为Focus的组件;你需要导入useRef并创建一个inputRef常量:

import { useRef } from 'react'
const Focus = () => {
  const inputRef = useRef(null)
}
export default Focus 

然后,我们实现handleClick方法:

const handleClick = () => {
  inputRef.current.focus()
} 

如你所见,我们正在引用inputRef的当前属性,并在其上调用 focus 方法。

要了解其来源,你只需检查render的实现:

return (
  <>
    <input
      type="text"
      ref={inputRef}
    />
    <button onClick={handleClick}>Set Focus</button>
  </>
) 

接下来是逻辑的核心。我们在表单内部创建一个带有输入元素的表单,并在其ref属性上定义一个函数。

当组件挂载时,我们定义的回调会被调用,其中的 element 参数代表输入的 DOM 实例。重要的是要知道,当组件卸载时,相同的回调会被带有 null 参数调用,以释放内存。

在回调中我们所做的是存储元素的引用,以便将来使用(例如,当handleClick方法被触发时)。然后,我们有带有其事件处理器的按钮。在浏览器中运行前面的代码将显示带有字段和按钮的表单,点击按钮将使输入字段获得焦点,正如预期的那样。

正如我们之前提到的,通常情况下,我们应该尽量避免使用 refs,因为它们会使代码变得更加命令式,并且它们变得难以阅读和维护。

理解forwardRef

React.forwardRef是一个有用的特性,允许你从父组件向下传递一个 ref(简称“引用”)到子组件。本文将提供一个关于React.forwardRef的基本介绍,并提供一个简单的示例来帮助你理解其实际用法。

React 中的 refs 是一种机制,用于访问和与组件渲染的 DOM 元素交互。它们提供了一种修改 DOM 或直接访问 DOM 属性的方法。

React.forwardRef是一个高阶组件,允许你将 ref 传递给子组件。当你需要从父组件访问子组件的 DOM 元素或实例时,这非常有用。

要创建一个可以接受转发 ref 的组件,你将使用React.forwardRef函数,它接受一个作为参数的渲染函数。这个渲染函数接收两个参数:组件的props和转发的 ref。

import React from 'react'
const TextInputWithRef = React.forwardRef((props, ref) => {
  return <input ref={ref} type="text" {...props} />
})
export default TextInputWithRef 

要使用forwardRef组件,你需要使用useRef()钩子创建一个 ref,并将其分配给forwardRef组件。

import React, { useRef } from 'react'
import TextInputWithRef from './TextInputWithRef'
function App() {
  const inputRef = useRef()
  const handleClick = () => {
    inputRef.current.focus()
  }
  return (
    <div>
      <TextInputWithRef ref={inputRef} />
      <button onClick={handleClick}>Focus on input</button>
    </div>
  )
}
export default App 

在这个例子中,我们创建了一个TextInputWithRef组件,它接受一个传递的 ref。在App组件中,我们使用useRef()钩子创建一个 ref,然后将其传递给TextInputWithRef组件。当点击“聚焦到输入框”按钮时,会调用handleClick函数,该函数聚焦到输入元素。

React.forwardRef是一个强大的功能,允许您将 ref 从父组件传递到子组件,从而提供对子组件行为的更大控制。

通过理解refsforwardRef的基本知识,并检查一个简单的示例,你可以在你的 React 应用程序中有效地利用这个功能。

在探讨了利用React.forwardRef以实现对组件的更高级控制之后,我们现在可以将注意力转向另一个增强 React 应用程序用户体验的关键方面:实现动画。

实现动画

当我们思考 UI 和浏览器时,我们肯定也会想到动画。动画化的 UI 对用户来说更愉快,它们是向用户展示已经发生或即将发生的事情的重要工具。

本节的目的不是提供一个创建动画和美观 UI 的详尽指南;这里的目的是提供一些关于我们可以实施以动画化我们的 React 组件的常见解决方案的基本信息。

对于像 React 这样的 UI 库,为开发者提供一个简单的方式来创建和管理动画至关重要。React 附带了一个名为react-transition-group的附加组件,它是一个帮助我们以声明式方式构建动画的组件。再次强调,能够以声明式方式执行操作非常强大,它使得代码更容易推理和与团队共享。

要开始构建动画组件,我们首先需要安装附加组件:

npm install --save react-transition-group @types/react-transition-group 

完成这些后,我们可以导入组件:

import { TransitionGroup} from 'react-transition-group' 

然后,我们只需将动画应用到我们想要应用的组件上:

const Transition = () => (
  <TransitionGroup
    transitionName="fade"
    transitionAppear
    transitionAppearTimeout={500}
  >
    <h1>Hello React</h1>
  </TransitionGroup>
) 

如您所见,有一些属性需要解释。首先,我们声明了transitionName属性。ReactTransitionGroup将具有该属性名称的类应用到子元素上,这样我们就可以使用 CSS 过渡来创建动画。

使用单个类,我们无法轻松地创建合适的动画,这就是为什么过渡组根据动画的状态应用多个类。在这种情况下,使用transitionAppear属性,我们告诉组件当子元素出现在屏幕上时,我们想要对它们进行动画处理。

因此,库所做的是在组件渲染后立即将其fade-appear类(其中fadetransitionName属性的值)应用到组件上。在下一次 tick 时,应用fade-appear-active类,这样我们就可以使用 CSS 从初始状态到新状态触发动画。

我们还必须设置transitionAppearTimeout属性来告诉 React 动画的长度,这样它就不会在动画完成之前从 DOM 中删除元素。

使元素淡入的 CSS 如下。

首先,我们在初始状态中定义元素的透明度:

.fade-appear {
  opacity: 0.01;
} 

然后,我们使用第二个类定义我们的过渡,它一旦应用到元素上就开始:

.fade-appear.fade-appear-active {
  opacity: 1;
  transition: opacity .5s ease-in;
} 

我们正在使用缓动函数在 500ms 内将透明度从 0.01 过渡到 1。这很简单,但我们可以创建更复杂的动画,我们还可以动画化组件的不同状态。例如,当新的元素作为过渡组的子元素被添加时,会应用*-enter*-enter-active类。类似的事情也适用于删除元素。

在深入研究动画的动态世界并了解它们如何极大地增强我们的 React 组件之后,现在让我们将注意力转向网络设计的另一个迷人方面:可伸缩矢量图形(SVG)的探索。

探索 SVG

最后,我们可以在浏览器中应用的一种最有趣的技巧是使用 SVG 来绘制图标和图表。

SVG 很棒,因为它是一种描述矢量的声明性方式,它与 React 的目的完美契合。我们过去使用图标字体来创建图标,但它们有众所周知的问题,首先是它们不可访问。使用 CSS 定位图标字体也很困难,并且它们并不总是在所有浏览器中看起来都很漂亮。这就是我们应该在我们的 Web 应用程序中优先考虑 SVG 的原因。

从 React 的角度来看,无论我们从渲染方法中输出 div 还是 SVG 元素,这都没有任何区别,这也是它如此强大的原因。我们也倾向于选择 SVG,因为我们可以使用 CSS 和 JavaScript 轻松地在运行时修改它们,这使得它们成为 React 函数式方法的绝佳候选者。

因此,如果我们把我们的组件视为其属性的函数,我们就可以很容易地想象出如何创建自包含的 SVG 图标,我们可以通过向它们传递不同的属性来操作它们。在 React web 应用中创建 SVG 的一个常见方法是将我们的矢量包装在一个 React 组件中,并使用属性来定义它们的动态值。

让我们看看一个简单的例子,我们画一个蓝色圆圈,从而创建一个包装 SVG 元素的 React 组件:

const Circle = ({ x, y, radius, fill }) => (
  <svg>
    <circle cx={x} cy={y} r={radius} fill={fill} />
  </svg>
) 

如您所见,我们可以轻松使用无状态的函数式组件来包装 SVG 标记,并且它接受 SVG 相同的属性。

以下是一个示例用法:

<Circle x={20} y={20} radius={20} fill="blue" /> 

我们显然可以使用 React 的全部功能并设置一些默认参数,这样,如果圆图标没有属性被渲染,我们仍然会显示一些内容。

例如,我们可以定义默认颜色:

const Circle = ({ x, y, radius, fill = 'red' }) => (...) 

当我们构建 UI 时,这非常强大,尤其是在一个团队中,我们共享我们的图标集,并希望在它中设置一些默认值,但我们也希望让其他团队决定他们的设置,而无需重新创建相同的 SVG 形状。

然而,在某些情况下,我们更喜欢更加严格,并固定一些值以保持一致性。使用 React,这是一个超级简单的任务。

例如,我们可以将基本圆圈组件包裹在RedCircle中,如下所示:

const RedCircle = ({ x, y, radius }) => (
  <Circle x={x} y={y} radius={radius} fill="red" />
) 

在这里,颜色默认设置,并且不能更改,而其他属性则透明地传递给原始圆。

以下截图显示了由 React 使用 SVG 生成的两个圆圈,蓝色和红色:

图标描述自动生成

图 5.1:两个圆圈,蓝色和红色 SVG

我们可以应用这种技术并创建圆的不同变体,例如SmallCircleRightCircle,以及构建我们的 UI 所需的一切。

摘要

在本章中,我们探讨了 React 在针对浏览器时的不同功能,从创建表单和处理事件到动画 SVG。我们还学习了新的useRef钩子,它提供了一种简单的方式来访问 DOM 节点。React 的声明式方法简化了复杂 Web 应用程序的管理。此外,React 提供了一种访问 DOM 节点的方式,如果需要,可以进行命令式操作,这使得将 React 与现有库集成变得更加容易。

在下一章中,我们将深入研究 CSS 和内联样式,并探讨在 JavaScript 中编写 CSS 的概念。