Skip to content

Latest commit

 

History

History
472 lines (310 loc) · 20.5 KB

File metadata and controls

472 lines (310 loc) · 20.5 KB

四、探索流行的组合模式

现在,是学习如何使组件有效地相互通信的时候了。React 功能强大,因为它允许您构建包含小型、可测试和可维护组件的复杂应用程序。应用这个范例,您可以控制应用程序的每一个部分。

在本章中,我们将介绍一些最流行的合成模式和工具。

我们将讨论以下主题:

  • 组件如何使用道具和子组件相互通信
  • 容器和表示模式,以及它们如何使我们的代码更易于维护
  • 什么是高阶组件HOCs),以及由于它们,我们如何以更好的方式构建我们的应用程序
  • 子组件模式的功能是什么,它的好处是什么

技术要求

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

  • Node.js 12+
  • Visual Studio 代码

您可以在本书的 GitHub 存储库中找到本章的代码 https://github.com/PacktPublishing/React-17-Design-Patterns-and-Best-Practices-Third-Edition/tree/main/Chapter04

通信组件

重用函数是我们作为开发人员的目标之一,在上一章中,我们了解了 React 如何使创建可重用组件变得容易。可重用组件可以跨应用程序的多个域共享,以避免重复。

具有干净界面的小组件可以组合在一起,以创建功能强大且可维护的复杂应用程序。

组成 React 组件非常简单;您只需在渲染中包含它们:

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

例如,您可以创建一个Profile组件,只需组合一个Picture组件来显示配置文件图像和一个UserName组件来显示用户的名称和屏幕名称。

通过这种方式,您可以非常快速地生成用户界面的新部分,只需编写几行代码。无论何时组合组件(如上例所示),都可以使用道具在组件之间共享数据。道具是父组件将其数据沿树向下传递给每个需要它的组件(或部分组件)的方式。

当一个组件将一些道具传递给另一个组件时,它被称为所有者,而不考虑它们之间的父子关系。例如,在前面的代码片段中,Profile不是Picture的直接父项(div标记是),但Profile拥有Picture,因为它将道具传递给它。

在下一节中,您将了解children道具以及如何正确使用它。

使用儿童道具

有一个特殊的道具可以从所有者传递到渲染中定义的组件-children

在 React 文档中,它被描述为不透明,因为它是一个属性,不会告诉您它包含的值的任何信息。父组件渲染中定义的子组件通常接收作为组件本身属性在 JSX 中传递的道具,或作为_jsx函数的第二个参数传递的道具。组件也可以通过内部嵌套的组件来定义,它们可以使用children属性访问这些子组件。

考虑到我们有一个拥有 TytT1 属性的 Hyt T0}属性,它表示按钮的文本:

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组件中的任何元素都将被呈现为button元素的子元素,并以btn作为类名。

例如,如果我们想要呈现按钮内部的图像和包装成span标记的一些文本,我们可以这样做:

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

前面的代码段在浏览器中呈现如下:

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

这是一种非常方便的方式,允许组件接受任何children元素,并将这些元素封装在预定义的父元素中。

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

我们可以传递一个孩子,如下代码所示:

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

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

探索容器和呈现模式

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

React 组件通常包含逻辑和*表示的混合。*从逻辑上讲,我们指的是与 UI 无关的任何东西,例如 API 调用、数据操作和事件处理程序。演示文稿是render中的一部分,我们在其中创建要在 UI 上显示的元素。

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

在逻辑和表示之间创建定义良好的边界不仅使组件更加可重用,而且还提供了许多其他好处,您将在本节中了解这些好处。同样,学习新概念的最佳方法之一是通过查看实际示例,因此让我们深入研究一些代码。

假设我们有一个组件,它使用地理定位 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钩子向 API 发出请求:

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

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

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

最后,我们展示了latitudelongitude值:

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

需要注意的是,在第一个render期间,latitudelongitudenull,因为我们在安装组件时向浏览器询问坐标。在实际组件中,您可能希望在返回数据之前显示微调器。要做到这一点,您可以使用我们在第 2 章“清理代码”中看到的一种条件技术。

现在,这个组件没有任何问题,并且工作正常。将其与请求和加载位置的部分分离,以便更快地对其进行迭代,不是很好吗?

我们将使用容器和表示模式来隔离表示部分。在这种模式中,每个组件被分成两个较小的组件,每个组件都有明确的职责。容器知道关于组件逻辑的一切,并且是调用 API 的地方。它还处理数据操作和事件处理。

presentational 组件是定义 UI 的地方,它以道具的形式从容器接收数据。由于表示组件通常是无逻辑的,因此我们可以将其创建为一个功能性的、无状态的组件。

没有规则规定表示组件不能有状态(例如,它可以在其中保留 UI 状态)。在本例中,我们需要一个组件来显示纬度和经度,因此我们将使用一个简单的函数。

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

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

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

这条规则并不严格,但 React 社区广泛使用的最佳做法是在Container组件名称的末尾追加Container,并将原始名称赋予表示名称。

我们还需要更改render的实现,并删除其所有 UI 部分,如下所示:

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

正如您在前面的代码片段中所看到的,我们没有在容器的return中创建 HTML 元素,而是使用表示元素(我们将在下一步创建),并将状态传递给它。状态为latitudelongitude,默认为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 的一种非常优雅的方式。它们是纯函数,给定一个state,返回它的元素。在本例中,我们的函数从所有者处接收latitudelongitude,并返回标记结构以显示它。

如果您第一次在浏览器中运行零部件,浏览器将需要您的许可才能了解您的位置:

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

按照容器和表示模式,我们创建了一个哑的可重用组件,我们可以将其放入样式指南中,以便向其传递假坐标。如果在应用程序的某些其他部分中,我们需要显示相同的数据结构,则不必创建新组件;我们只需将其包装到一个新的容器中,例如,可以从不同的端点加载纬度和经度。

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

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

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

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

在我们的示例中,我们从单个组件开始,我们意识到可以将 API 调用与标记分离。决定将什么放入容器中以及将什么放入演示文稿并不总是简单的;以下几点应该有助于您做出决定:

以下是容器组件的特征:

  • 他们更关心行为。
  • 他们呈现他们的表现成分。
  • 他们进行 API 调用和操作数据。
  • 它们定义事件处理程序。

以下是表象成分的特征:

  • 他们更关注视觉表现。
  • 它们呈现 HTML 标记(或其他组件)。
  • 他们以道具的形式从父母那里接收数据。
  • 它们通常作为无状态功能组件编写。

如您所见,这些模式构成了一个非常强大的工具,可以帮助您更快地开发 web 应用程序。让我们在下一节中看看 HOC 是什么。

理解 HOC

第二章函数编程部分中,我们提到了higher order functionHOFs的概念,这些函数在给定一个函数时,会通过一些额外的行为对其进行增强,并返回一个新的行为。当我们将 HOF 的思想应用于组件时,我们称这些高阶组件(或HOCs为简洁起见)。

首先,让我们看看HoC是什么样子:

const HoC = Component => EnhancedComponent

HOC 是将组件作为输入并返回增强组件作为输出的函数。

让我们从一个非常简单的示例开始,了解增强组件的外观。

假设,无论出于何种原因,您需要将相同的className属性附加到每个组件。您可以通过向每个render方法添加className属性来更改所有render方法,也可以编写一个 HOC,如以下所示:

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

In the React community, it is very common to have the prefix with for HOCs.

前面的代码最初可能有点难以理解;让我们一起经历吧。

我们声明一个withClassName函数,它接受一个Component并返回另一个函数。返回的函数是一个功能组件,它接收一些道具并呈现原始组件。收集到的道具被分散开来,一个带有"my-class"值的className属性被传递给功能组件。

HOC 通常在组件上传播收到的道具的原因是,它们往往是透明的,并且只添加新的行为。

这很简单,也不是很有用,但它应该让您更好地理解 HOC 是什么以及它们看起来是什么样子。现在让我们看看如何在组件中使用withClassNameHOC。

首先,我们创建一个无状态功能组件,该组件接收类名并将其应用于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 => {
  return <Component {...props} />
}

您可能已经发现了 HOC 命名方式的模式。通常的做法是在 HOC 前面加前缀,HOC 使用with模式为它们增强的组件提供一些信息。

现在需要定义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 () => { // <<< This emulates the componentWillUnmount
    window.removeEventListener('resize', handleResize)
  }
}, []) // <<< This emulates the componentDidMount

最后,原始组件以以下方式呈现:

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

正如您在这里可能注意到的,我们正在传播我们之前看到的道具,但我们也正在通过innerWidth状态。

我们将innerWidth值存储为状态以实现原始行为,但不会污染组件的状态;我们用道具代替。

使用道具始终是加强可重用性的一个很好的解决方案。

现在,使用 HOC 并获取innerWidth值非常简单。

The new React Hooks can easily replace an HOC by creating custom Hooks.

我们创建了一个功能组件,该组件期望将innerWidth作为属性:

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

我们将其加强如下:

const MyComponentWithInnerWidth = withInnerWidth(MyComponent)

首先,我们不污染任何状态,也不要求组件实现任何功能。这意味着组件和 HOC 是不耦合的,它们都可以在整个应用程序中重用。

同样,使用道具而不是状态可以使我们的组件变得哑巴,这样我们就可以在样式指南中使用它,忽略任何复杂的逻辑,只传递道具。

在这种特殊情况下,我们可以为我们支持的每个不同的innerWidth尺寸创建一个组件。

考虑下面的例子:

<MyComponent innerWidth={320} /> 

或考虑以下事项:

<MyComponent innerWidth={960} /> 

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

理解功能希尔德

有一种模式正在 React 社区内获得共识,称为FunctionAsChild。它在流行的react-motion库中被广泛使用,我们将在第 7 章,为浏览器编写代码中看到。

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

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

正如您所看到的,FunctionAsChild是一个组件,它有一个children属性定义为一个函数,它没有被用作 JSX 表达式,而是被调用。

上述组件可按以下方式使用:

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

它看起来很简单:children 函数是在父函数的render方法中启动的,它返回包装在div标记中的Hello, World!文本,并显示在屏幕上。

让我们深入研究一个更有意义的示例,其中父组件将一些参数传递给children函数。

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

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

上述组件可按以下方式使用:

<Name> 
  {name => <div>Hello, {name}!</div>} 
</Name>

代码段再次呈现Hello, World!,但这次名称已由父级传递。应该很清楚这种模式是如何工作的,所以让我们看看这种方法的优点。

第一个好处是,我们可以包装组件,在运行时传递变量,而不是像对待 HOC 那样传递固定属性。

一个很好的例子是一个Fetch组件,它从 API 端点加载一些数据并将其返回给children函数:

<Fetch url="..."> 
  {data => <List data={data} />} 
</Fetch> 

其次,使用这种方法组合组件不会强制children使用一些预定义的道具名称。因为函数接收变量,所以它们的名称可以由使用组件的开发人员决定。这使得FunctionAsChild解决方案更加灵活。

最后但并非最不重要的一点是,包装器是高度可重用的,因为它不会对它接收到的children做出任何假设,它只需要一个函数。因此,相同的FunctionAsChild组件可用于应用程序的不同部分,服务于各种children组件。

总结

在本章中,我们学习了如何组合可重用组件并使它们有效地通信。道具是一种将组件彼此分离并创建干净且定义良好的接口的方法。

然后,我们研究了 React 中一些最有趣的组成模式。第一种是所谓的容器,另一种是呈现模式。这些模式帮助我们将逻辑与表示分离,并创建具有单一职责的更专业的组件。

多亏了 HOCs,我们学会了如何处理上下文,而无需将组件与上下文耦合。最后,我们看到了如何通过遵循FunctionAsChild模式动态组合组件。

在下一章中,我们将学习 GraphQL 以及如何创建 JWT 令牌、执行登录和使用 Sequelize 创建模型。