七、为浏览器编写代码

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

在本章中,我们还将介绍 React 中的事件是如何工作的,以及该库如何实现一些高级技术,为我们提供跨不同浏览器的一致界面。我们将看看 React 团队为使事件系统非常高效而实施的一些有趣的解决方案。

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

参考文献之后,我们将看看如何使用 React 附加组件和第三方库(如react-motion)轻松实现动画。最后,我们将学习在 React 中使用可缩放矢量图形SVG)是多么容易,以及如何为我们的应用程序创建动态可配置的图标。

在本章中,我们将介绍以下主题:

  • 使用不同的技术创建带有 React 的表单
  • 侦听 DOM 事件并实现自定义处理程序
  • 使用 REF 在 DOM 节点上执行命令式操作的一种方法
  • 创建跨不同浏览器工作的简单动画
  • SVG 的生成方法

技术要求

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

  • Node.js 12+
  • Visual Studio 代码

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

理解和实施表格

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

非受控部件

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

代码非常简单:

import { useState, ChangeEvent, MouseEvent } from 'react' const Uncontrolled = () => {
  const [value, setValue] = useState('')

  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)
}

这个处理器非常简单;我们只需记录当前存储在状态中的值。我们还希望克服提交表单时浏览器的默认行为,以执行自定义操作。这似乎是合理的,而且在单个字段中效果非常好。现在的问题是,如果我们有多个字段呢?假设我们有几十个不同的领域?

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

让我们用名字和姓氏字段创建一个新表单。我们可以重用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}`)
}

最后,我们呈现以下形式:

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 组件,它通过使用组件状态来控制表单中输入元素的值。

在这里,我们将看到如何用一些值预先填充表单字段,这些值可以从服务器接收,也可以作为来自父级的道具。为了充分理解这个概念,我们将从一个非常简单的无状态函数组件开始,并逐步改进它。

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

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}`)
}

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

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>

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

This technique is described in a collection of patterns by Michael Chan: http://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> 
) 

从这一点开始,每当我们需要为同一个组件创建一个新的事件处理程序,而不是创建一个新的方法并绑定它,我们可以只向开关添加一个新的案例。

关于 React 中的事件,需要了解的几个更有趣的事情是,合成事件被重用,并且有一个单个全局处理程序。第一个概念意味着我们不能存储合成事件并在以后重用它,因为它在操作之后立即变为 null。这种技术在性能方面是非常好的,但是如果出于某种原因希望将事件存储在组件的状态中,则可能会出现问题。为了解决这个问题,React 为我们提供了一个关于合成事件的persist方法,我们可以调用该方法使事件持久化,以便我们可以存储它并在以后检索它。

第二个非常有趣的实现细节是关于性能的,它与 React 将事件处理程序附加到 DOM 的方式有关。

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

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

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

探索参考文献

人们喜欢做出反应的原因之一是它是陈述性的。声明性意味着您只需在任何时间点描述希望在屏幕上显示的内容,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()
} 

如您所见,我们正在引用inputRefcurrent属性并对其调用focus方法。

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

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

这就是逻辑的核心。我们创建了一个表单,表单中有一个输入元素,并在其ref属性上定义了一个函数。

我们定义的回调在装入组件时调用,元素参数表示输入的 DOM 实例。重要的是要知道,当卸载组件时,会使用null参数调用相同的回调以释放内存。

我们在回调中所做的是存储元素的引用,以便将来能够使用它(例如,当触发handleClick方法时)。然后,我们有了按钮及其事件处理程序。在浏览器中运行上述代码将显示带有字段和按钮的表单,单击按钮将按预期聚焦输入字段。

As we mentioned previously, in general, we should try to avoid using refs because they force the code to be more imperative, and they become harder to read and maintain.

实现动画

当我们考虑 UI 和浏览器时,我们当然也必须考虑动画。动画用户界面对用户来说更令人愉快,并且它们是向用户展示已经发生或即将发生的事情的一个非常重要的工具。

本节的目的不是详尽地介绍如何创建动画和漂亮的 UI;这里的目标是为您提供一些基本信息,介绍我们可以为 React 组件设置动画的常见解决方案。

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

让我们看看如何使用 React 插件对文本应用简单的淡入效果,然后我们将使用react-motion执行相同的操作,这是一个第三方库,使创建复杂动画更加容易。

开始构建动画组件需要做的第一件事是安装附加组件:

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

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

import CSSTransitionGroup from 'react-addons-css-transition-group'

然后,我们只包装要应用动画的组件:

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

正如你所看到的,有一些道具需要解释。

首先,我们宣布transitionName道具。ReactCSSTransitionGroup将一个名为该属性的类应用于子元素,这样我们就可以使用 CSS 转换来创建动画。

对于单个类,我们无法轻松创建正确的动画,这就是过渡组根据动画状态应用多个类的原因。在本例中,使用transitionAppear道具,我们告诉组件,当孩子们出现在屏幕上时,我们希望为他们设置动画。

因此,库所做的是在渲染组件后立即将fade-appear类(其中fadetransitionName属性的值)应用于组件。下一步,应用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; 
}

我们正在使用ease-in函数将500ms中的不透明度从0.01转换为1。这很容易,但我们可以创建更复杂的动画,也可以为组件的不同状态设置动画。例如,*-enter*-enter-active类在作为转换组的子元素添加新元素时应用。类似的情况也适用于删除元素。

在下一节中,我们将查看在 React:react-motion中创建动画的最流行的库,该库由程楼维护。它提供了一个非常干净和易于使用的 API,为我们创建任何动画提供了一个非常强大的工具。

反作用运动

React Motion是 React 应用程序的动画库,可轻松创建和实现逼真的动画。一旦动画的复杂性增长,或者当我们需要依赖于其他动画的动画,或者当我们需要将一些基于物理的行为应用到我们的组件(这有点更先进)时,我们就会意识到过渡组对我们的帮助不够,因此我们可以考虑使用第三方库。

要使用它,我们首先必须安装它:

npm install --save react-motion @types/react-motion

一旦安装成功,我们需要导入Motion组件和spring功能。Motion是我们将用于包装要设置动画的元素的组件,而函数是一个实用程序,可以将值从初始状态插值到最终状态:

import { Motion, spring } from 'react-motion'

让我们看看代码:

const Transition = () => ( 
  <Motion 
    defaultStyle={{ opacity: 0.01 }} 
    style={{ opacity: spring(1) }} 
  > 
    {interpolatingStyle => ( 
      <h1 style={interpolatingStyle}>Hello React</h1> 
    )} 
  </Motion> 
)

这里有很多有趣的东西。首先,您可能已经注意到,该组件将函数用作子模式(请参见第 4 章探索流行的组合模式),这是一种非常强大的技术,可以定义在运行时接收值的子模式。

然后我们可以看到,Motion组件有两个属性:第一个是defaultStyle,它表示初始的style属性。再次,我们将不透明度设置为0.0.1以隐藏元素并开始淡入淡出。

style属性代表最终样式,但我们不直接设置值;相反,我们使用spring函数,以便将值从初始状态插值到最终状态。

spring函数的每次迭代中,子函数接收给定时间点的插值样式,只需将接收到的对象应用于组件的style属性,我们就可以看到不透明度的转换。

这个库可以做一些更酷的事情,但是首先要学习的是基本概念,这个例子应该能够阐明它们。

比较过渡小组和react-motion的两种不同方法也很有趣,以便能够为您正在进行的项目选择正确的方法。

最后,在下一节中,我们将看到如何在 React 中使用 SVG。

探索 SVG

最后但并非最不重要的是,我们可以在浏览器中应用的绘制图标和图形的最有趣的技术之一是可缩放矢量图形SVG

SVG 之所以伟大,是因为它是一种描述向量的声明性方式,并且完全符合 React 的目的。我们曾经使用图标字体来创建图标,但它们有众所周知的问题,首先是它们无法访问。用 CSS 定位图标字体也很困难,而且它们在所有浏览器中并不总是很漂亮。这些就是我们在 web 应用程序中更喜欢 SVG 的原因。

从 React 的角度来看,如果我们从render方法输出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" /> 
)

在这里,颜色是默认设置的,不能更改,而其他道具将透明地传递到原始圆。

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

我们可以应用这项技术,创建不同的圆变体,如SmallCircleRightCircle,以及构建 UI 所需的所有其他内容。

总结

在本章中,我们研究了在使用 React 以浏览器为目标时可以做的不同事情,从表单创建到事件,从动画到 SVG。此外,我们还学习了如何使用新的useRef挂钩。React 为我们提供了一种声明性的方式来管理创建 web 应用程序时需要处理的所有方面。

如果我们需要它,React 允许我们访问实际的 DOM 节点,这意味着我们可以使用它们执行命令式操作,如果我们需要将 React 与现有命令库集成,这将非常有用。

下一章将介绍 CSS 和内联样式,并将阐明用 JavaScript 编写 CSS 意味着什么。