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

79 阅读43分钟

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

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第十五章:提高应用程序的性能

网络应用程序的有效性能对于提供良好的用户体验和提高转化率至关重要。React 库实现了不同的技术来快速渲染我们的组件,并尽可能少地接触文档对象模型DOM)。对 DOM 的应用通常代价高昂,因此最小化操作数量至关重要。

然而,有一些场景 React 无法优化过程,这就需要开发者实现特定的解决方案来使应用程序运行顺畅。

在本章中,我们将学习 React 的基本概念,并学习如何使用一些 API 来帮助库找到更新 DOM 的最佳路径,而不会降低用户体验。我们还将看到一些可能损害我们的应用程序并使其变慢的常见错误。

我们应该避免仅仅为了优化而优化我们的组件,并且只有在需要时才应用以下章节中将要看到的技巧。

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

  • 如何实现协调以及我们如何可以使用键来帮助 React 做得更好

  • 常见的优化技术和常见的性能相关错误

  • 有用的工具和库,使我们的应用程序运行更快

  • 使用不可变数据意味着什么以及如何实现

技术要求

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

  • Node.js 19+

  • Visual Studio Code

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

如何实现协调

大多数情况下,React 默认情况下就足够快,你不需要做任何事情来提高你应用程序的性能。React 利用不同的技术来优化屏幕上组件的渲染。

当 React 必须显示一个组件时,它会调用其render方法以及其子组件的递归render方法。组件的render方法返回一个 React 元素的树,React 使用这个树来决定必须执行哪些 DOM 操作以更新 UI。

每当组件状态发生变化时,React 会在节点上再次调用render方法,并将结果与之前的 React 元素树进行比较。库足够智能,可以找出应用预期更改所需的最小操作集。这个过程被称为协调,并且由 React 透明地管理。正因为如此,我们可以轻松地以声明性方式描述我们的组件在特定时间点应该如何看起来,并让库完成其余的工作。

React 试图在 DOM 上应用尽可能少的操作,因为接触 DOM 是一个昂贵的操作。

然而,比较两个元素树也不是免费的,React 做出了两个假设来减少其复杂性:

  • 如果两个元素具有不同的类型,它们将渲染不同的树。

  • 开发者可以使用键来标记在不同渲染调用中保持稳定的子元素。

从开发者的角度来看,第二点很有趣,因为它给我们提供了一个工具,可以帮助 React 更快地渲染我们的视图。

默认情况下,当返回到 DOM 节点的子元素时,React 会同时迭代两个子元素列表,并且每当有差异时,它就会创建一个突变。

让我们看看一些例子。在将以下两个树转换为添加到子元素末尾的元素时,将工作得很好:

<ul>
  <li>Carlos</li>
  <li>Javier</li>
</ul>
<ul>
  <li>Carlos</li>
  <li>Javier</li>
  <li>Emmanuel</li>
</ul> 

两个<li>Carlos</li>树通过 React 与两个<li>Javier</li>树匹配,然后它会插入<li>Emmanuel</li>树。

如果天真地实现,在开始处插入一个元素会产生较差的性能。如果我们看例子,当在这两个树之间转换时,它表现得非常糟糕:

<ul>
  <li>Carlos</li>
  <li>Javier</li>
</ul>
<ul>
  <li>Emmanuel</li>
  <li>Carlos</li>
  <li>Javier</li>
</ul> 

每个子元素都将被 React 突变,而不是它意识到它可以保持子树<li>Carlos</li><li>Javier</li>的完整性。这可能是一个问题。当然,这个问题可以解决,解决方法是使用 React 支持的key属性。让我们看看下一个。

使用键

子元素具有键,这些键被 React 用于匹配后续树和原始树之间的子元素。通过在我们的前一个示例中添加键,可以使树转换更高效:

<ul>
  <li key="2018">Carlos</li>
  <li key="2019">Javier</li>
</ul>
<ul>
  <li key="2017">Emmanuel</li>
  <li key="2018">Carlos</li>
  <li key="2019">Javier</li>
</ul> 

React 现在知道2017键是新的,而20182019键只是移动了。

找到一个键并不难。你将要显示的元素可能已经有一个唯一的 ID。因此,键可以直接来自你的数据:

<li key={element.id}>{element.title}</li> 

你可以给你的模型添加一个新的 ID,或者键可以由内容的一些部分生成。键必须在它的兄弟元素中是唯一的;它不需要在全局范围内是唯一的。可以将数组中的项目索引作为键传递,但现在这被认为是一种坏做法。然而,如果项目从未被记录,这可以很好地工作。重新排序将严重影响性能。

如果你使用map函数渲染多个项目,并且没有指定key属性,你会得到这个消息:警告:数组或迭代器中的每个子元素都应该有一个唯一的 key 属性

让我们在下一节学习一些优化技术。

优化技术

重要的是要注意,在这本书的所有示例中,我们使用的是要么是用create-react-app创建的,要么是从零开始创建的,但总是使用 React 的开发版本。

使用 React 的开发版本对于编码和调试非常有用,因为它提供了修复各种问题的所有必要信息。然而,所有的检查和警告都有代价,我们希望在生产中避免这些代价。

因此,我们应该对应用程序做的第一个优化是构建包,将NODE_ENV环境变量设置为production。这在使用 webpack 时很容易,只需使用以下方式中的DefinePlugin即可:

new webpack.DefinePlugin({
  'process.env': {
   NODE_ENV: JSON.stringify('production')
  }
}) 

为了达到最佳性能,我们不仅希望创建带有production标志的包,还希望将我们的包拆分,一个用于我们的应用程序,另一个用于node_modules

要做到这一点,您需要使用 webpack 中的新optimization节点:

optimization: {
  splitChunks: {
   cacheGroups: {
    default: false,
    commons: {
     test: /node_modules/,
     name: 'vendor',
     chunks: 'all'
    }
   }
  }
} 

Webpack 有两种模式,开发生产。默认情况下,生产模式是启用的,这意味着当您使用生产模式编译包时,代码将被压缩和压缩;您可以使用以下代码块指定它:

{
  mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
} 

您的webpack.config.ts文件应如下所示:

module.exports = {
  entry: './index.ts',
  optimization: {
   splitChunks: {
    cacheGroups: {
     default: false,
     commons: {
      test: /node_modules/,
      name: 'vendor',
      chunks: 'all'
     }
    }
   }
  },
  plugins: [
   new webpack.DefinePlugin({
    'process.env': {
     NODE_ENV: JSON.stringify('production')
    }
   })
  ],
  mode: process.env.NODE_ENV === 'production' ? 'production' : 'development'
} 

使用此 webpack 配置,我们将获得非常优化的包;一个用于我们的供应商,另一个用于实际的应用程序。

工具和库

在下一节中,我们将介绍几种技术、工具和库,我们可以将这些应用到我们的代码库中以监控和改进性能。

不可变性

新的 React Hooks,如React.memo,对 props 使用浅比较方法,这意味着如果我们传递一个对象作为 props,并且我们更改其值之一,我们将不会得到预期的行为。

实际上,浅比较无法在属性上找到突变,并且组件永远不会重新渲染,除非对象本身发生变化。解决此问题的一种方法是通过使用不可变数据,这种数据一旦创建,就不能被更改。

例如,我们可以按以下模式设置状态:

const [state, setState] = useState({})
const obj = state.obj
obj.foo = 'bar'
setState({ obj }) 

即使对象的foo属性值已更改,对象的引用仍然是相同的,浅比较也不会识别出来。

我们可以做的另一件事是每次更改对象时创建一个新的实例,如下所示:

const obj = Object.assign({}, state.obj, { foo: 'bar' })
setState({ obj }) 

在这种情况下,我们得到一个新的对象,其foo属性设置为bar,浅比较将能够找到差异。使用 ES6 和 Babel,还有另一种以更优雅的方式表达相同概念的方法,即使用对象扩展运算符:

const obj = {
  ...state.obj,
  foo: 'bar'
} 
setState({ obj }) 

这种结构比之前的结构更简洁,并且会产生相同的结果,但截至写作时,它需要将代码转换为浏览器内执行。

React 提供了一些不可变性辅助工具,使处理不可变对象变得容易,还有一个名为immutable.js的流行库,它具有更强大的功能,但需要您学习新的 API。

Babel 插件

此外,还有一些有趣的Babel插件,我们可以安装并使用它们来提高我们 React 应用程序的性能。它们使应用程序运行更快,在构建时优化代码的部分。

第一个是我们可以选择使用的 React 常量元素转换器,它找到所有不依赖于 props 的静态元素,并将它们从 render(或函数组件)中提取出来,以避免不必要的 _jsx 调用。

使用 Babel 插件非常简单。我们首先使用 npm 安装它:

npm install --save-dev @babel/plugin-transform-react-constant-elements 

您需要创建 .babelrc 文件,并添加一个 plugins 键,其值为一个数组,包含我们想要激活的插件列表:

{
   "plugins": ["@babel/plugin-transform-react-constant-elements"]
} 

第二个 Babel 插件,我们可以选择使用来提高性能的是 React 内联元素转换,它将所有的 JSX 声明(或 _jsx 调用)替换为更优化的版本,以使执行更快。

使用以下命令安装插件:

npm install --save-dev @babel/plugin-transform-react-inline-elements 

接下来,您可以轻松地将插件添加到 .babelrc 文件中的插件数组中,如下所示:

{
  "plugins": ["@babel/plugin-transform-react-inline-elements"]
} 

这两个插件应该只在生产环境中使用,因为它们会使开发模式中的调试更加困难。到目前为止,我们已经学习了许多优化技术以及如何使用 webpack 配置一些插件。

摘要

我们的性能之旅已经结束,现在我们可以优化我们的应用程序,为用户提供更好的用户体验。

在本章中,我们学习了协调算法的工作原理以及 React 如何始终尝试采取最短路径来应用更改到 DOM。我们还可以通过使用键来帮助库优化其工作。一旦找到瓶颈,就可以应用本章中我们看到的技术之一来解决问题。

我们已经学习了如何通过重构和正确设计组件的结构来提供性能提升。我们的目标是拥有小型组件,它们以最佳方式完成单一任务。在章节末尾,我们讨论了不可变性,并看到了为什么不要修改数据对于使 React.memoshallowCompare 正常工作很重要。最后,我们回顾了不同的工具和库,它们可以使您的应用程序更快。

在下一章中,我们将探讨使用 Jest、React 测试库和 React DevTools 进行测试和调试。

加入我们的 Discord 社区

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

packt.link/React18DesignPatterns4e

二维码

第十六章:测试和调试

多亏了其组件,React 使得测试我们的应用变得容易。有许多不同的工具可供我们使用,我们可以用 React 创建测试。在本章中,我们将介绍最受欢迎的工具,以了解它们提供的优势。

Jest 是由 Meta 的 Christoph Nakazawa 和社区内的贡献者维护的一站式测试框架解决方案,旨在为您提供最佳的开发者体验。

到本章结束时,您将能够从头开始创建测试环境并为您的应用组件编写测试。

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

  • 为什么测试我们的应用很重要以及它们如何帮助开发者更快地工作

  • 如何设置 Jest 环境以使用 Enzyme 测试组件

  • React Testing Library 是什么以及为什么它是测试 React 应用的必备工具

  • 如何测试事件

  • 如何实现 Vitest

  • React DevTools 和一些错误处理技术

技术要求

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

  • Node.js 19+

  • Visual Studio Code

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

理解测试的优势

测试 Web UI 一直是一项困难的任务。从单元测试到端到端测试,接口依赖于浏览器、用户交互以及许多其他变量,这使得实施有效的测试策略变得困难。

如果您曾经尝试为 Web 编写端到端测试,您就会知道如何获得一致的结果是多么复杂,以及结果通常如何受到网络等不同因素的影响,导致出现假阴性。除此之外,用户界面经常更新以提高体验、最大化转化率或简单地添加新功能。

如果测试难以编写和维护,开发者就不太可能覆盖他们的应用。另一方面,测试很重要,因为它们使开发者对自己的代码更有信心,这体现在速度和质量上。如果一段代码经过良好的测试(并且测试编写得很好),开发者可以确信它能够正常工作并准备好发布。同样,多亏了测试,重构代码变得更加容易,因为测试保证了在重写过程中功能不会改变。

开发者往往专注于他们当前正在实施的功能,有时很难知道应用的其他部分是否受到了这些更改的影响。测试有助于避免回归,因为它们可以告诉我们新代码是否破坏了旧测试。对编写新功能的更大信心导致更快地发布。

测试应用程序的主要功能可以使代码库更加稳固,每当发现新的错误时,它都可以被重现、修复并通过测试来防止未来再次发生。

幸运的是,React(以及组件时代)使得测试用户界面变得简单高效。测试组件或组件树是一项不那么繁重的任务,因为应用程序的每个部分都有其职责和边界。如果组件以正确的方式构建,如果它们是纯的,并且旨在具有可组合性和可重用性,那么它们可以像简单的函数一样进行测试。

现代工具带给我们的另一个强大功能是使用 Node.js 和控制台运行测试的能力。为每个测试启动浏览器会使测试变慢且不可预测,降低开发者的体验;相反,使用控制台运行测试要快得多。

仅在控制台中测试组件有时在它们在真实浏览器中渲染时可能会出现意外的行为,但根据我的经验,这种情况很少见。当我们测试 React 组件时,我们想要确保它们能正常工作,并且给定不同的 props 集合,它们的输出始终是正确的。

我们还可能想要覆盖组件可能具有的所有各种状态。状态可能会通过点击按钮而改变,因此我们编写测试来检查所有事件处理器是否正在执行它们应该执行的操作。

当组件的所有功能都被覆盖,但我们还想做更多的时候,我们可以编写测试来验证组件在边缘情况下的行为。边缘情况是组件可以假设的状态,例如,当所有 props 都是 null 或存在错误时。一旦编写了测试,我们就可以相当有信心地认为组件的行为符合预期。

测试单个组件固然很好,但这并不能保证将多个单独测试过的组件组合在一起后仍然能正常工作。正如我们稍后将会看到的,使用 React,我们可以挂载一个组件树并测试它们之间的集成。

我们可以使用不同的技术来编写测试,其中最受欢迎的一种是测试驱动开发TDD)。应用 TDD 意味着先编写测试,然后再编写代码以通过测试。

遵循这种模式有助于我们编写更好的代码,因为我们被迫在实现功能之前更多地思考设计,这通常会导致更高的代码质量。

既然我们已经涵盖了所有这些内容,那就让我们挽起袖子开始为我们的 React 组件编写测试。我们还将了解一种称为测试驱动开发(test-driven development)的酷炫编程方式,并使用一个名为 Jest 的便捷工具来简化我们的 JavaScript 测试。准备好了吗?让我们深入其中,开始使用真实代码进行工作!

使用 Jest 进行无痛苦 JavaScript 测试

学习如何正确测试 React 组件最重要的方式是通过编写代码,这正是我们将在本节中要做的。

React 文档说明 Facebook 使用 Jest 来测试其组件。然而,React 并不强制你使用特定的测试框架,你可以使用你喜欢的任何一个而没有任何问题。为了看到 Jest 的实际应用,我们将从头创建一个项目,安装所有依赖项,并编写一个带有一些测试的组件。这将很有趣!

第一件事是要进入一个新的文件夹并运行以下命令:

npm init 

一旦创建了 package.json,我们就可以开始安装依赖项,第一个是 jest 包本身:

npm install --save-dev jest 

为了告诉 npm 我们想使用 jest 命令来运行测试,我们必须将以下脚本添加到 package.json 中:

"scripts": {
  "build": "webpack",
  "start": "node ./dist/server",
  "test": "jest",
  "test:coverage": "jest --coverage"
} 

为了使用 ES6 和 JSX 编写组件和测试,我们必须安装所有与 Babel 相关的包,以便 Jest 可以使用它们进行转译和理解代码。

第二组依赖项的安装方式如下:

npm install --save-dev @babel/core @babel/preset-env @babel/preset-react ts-jest 

如你所知,我们现在必须创建一个 .babelrc 文件,它被 Babel 用于知道我们希望在项目中使用的预设和插件。

.babelrc 文件看起来如下:

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

现在,是时候安装 React 和 ReactDOM,这是我们创建和渲染组件所需的:

npm install --save react react-dom 

设置已完成,我们可以运行 Jest 对 ES6 代码进行测试,并在 DOM 中渲染我们的组件,但还有一件事要做。

我们需要安装 jest-environment-jsdom@testing-library/jest-dom@testing-library/react

npm install @testing-library/jest-dom @testing-library/react jest-environment-jsdom 

在你安装了这些包之后,你必须创建 jest.config.js 文件:

module.exports = {
  preset: 'ts-jest',
  setupFilesAfterEnv: ['<rootDir>/setUpTests.ts'],
  testEnvironment: 'jsdom'
} 

然后,让我们创建 setUpTests.ts 文件:

import '@testing-library/jest-dom/extend-expect' 

现在,让我们想象我们有一个 Hello 组件 (src/components/Hello/index.tsx):

import React, { FC } from 'react'
type Props = {
  name?: string
}
function Hello({ name }: Props) { 
  return <h1 className="Hello">Hello {name || 'World'}</h1>
}
 Hello.defaultProps = {
  name: ''
}
export default Hello 

为了测试这个组件,我们需要创建一个具有相同名称但添加 .test(或 .spec)后缀的新文件。这将是我们的测试文件:

import React from 'react'
import { render, cleanup } from '@testing-library/react'
import Hello from './index'
 describe('Hello Component', () => {
  it('should render Hello World', () => {
   const wrapper = render(<Hello />)
   expect(wrapper.getByText('Hello World')).toBeInTheDocument()
  })
  it('should render the name prop', () => {
   const wrapper = render(<Hello name="Carlos" />)
   expect(wrapper.getByText('Hello Carlos')).toBeInTheDocument()
  })
  it('should has .Home classname', () => {
   const wrapper = render(<Hello />)
   expect(wrapper.container.firstChild).toHaveClass('Hello')
  })
  afterAll(cleanup)
}) 

然后,为了运行测试,你需要执行以下命令:

npm test 

你应该看到以下结果:

文本自动生成描述

图 16.1:npm 测试

PASS 标签表示所有测试都已成功通过;如果至少有一个测试失败,你将看到 FAIL 标签。让我们更改我们的一个测试使其失败:

it('should render the name prop', () => {
  const wrapper = render(<Hello name="Carlos" />)
  expect(wrapper.getByText('Hello World')).toBeInTheDocument()
}) 

这是结果:

文本自动生成描述

图 16.2:失败的测试

如你所见,FAIL 标签用 X 指定。此外,预期的和接收到的值提供了有用的信息,你可以看到预期的值和接收到的值。

如果你想要查看所有单元测试的覆盖率百分比,你可以执行以下命令:

npm run test:coverage 

结果如下:

图形用户界面,文本自动生成描述

图 16.3:通过测试

覆盖率还会生成结果的 HTML 版本;它创建一个名为 coverage 的目录,并在其中创建一个名为 Icov-report 的目录。如果你在浏览器中打开 index.xhtml 文件,你会看到以下 HTML 版本:

图形用户界面、应用程序、表格描述自动生成

图 16.4:Icov-report

现在你已经完成了第一次测试,并且知道如何收集覆盖率数据,让我们看看如何在下一节测试事件。

测试事件

事件在任何网络应用中都非常常见,我们同样需要测试它们,所以让我们学习如何测试事件。为此,让我们创建一个新的 ShowInformation 组件:

import { useState, ChangeEvent } from 'react'

 function ShowInformation() {
  const [state, setState] = useState({ name: '', age: 0, show: false })
  const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
   const { name, value } = e.target
   setState({
    ...state,
    [name]: value
   })
  }
  const handleShowInformation = () => {
   setState({
    ...state,
    show: true
   })
  }
  if (state.show) {
   return (
    <div className="ShowInformation">
    <h1>Personal Information</h1>
     <div className="personalInformation">
      <p><strong>Name:</strong> {state.name}</p>
      <p><strong>Age:</strong> {state.age}</p>
     </div>
    </div>
   )
  }

  return (
   <div className="ShowInformation">
    <h1>Personal Information</h1>
    <p><strong>Name:</strong></p>
    <p>
     <input name="name" type="text" value={state.name} onChange={handleOnChange} />
    </p>
    <p>
     <input name="age" type="number" value={state.age} onChange={handleOnChange} />
    </p>
    <p><button onClick={handleShowInformation}>Show Information</button></p>
   </div>
  )
}
export default ShowInformation 

现在,让我们在 src/components/ShowInformation/index.test.tsx 创建测试文件:

import { render, cleanup, fireEvent } from '@testing-library/react'
import ShowInformation from './index'
describe('Show Information Component', () => {
  let wrapper
  beforeEach(() => {
   wrapper = render(<ShowInformation />)
  })
  it ('should modify the name', () => {
   const nameInput = wrapper.container.querySelector('input[name="name"]') as HTMLInputElement
   const ageInput = wrapper.container.querySelector('input[name="age"]') as HTMLInputElement
   fireEvent.change(nameInput, { target: { value: 'Carlos' } })
   fireEvent.change(ageInput, { target: { value: 34 } })
   expect(nameInput.value).toBe('Carlos')
   expect(ageInput.value).toBe('34')
  })
  it ('should show the personal information when user clicks on the button', () => {
   const button = wrapper.container.querySelector('button')
   fireEvent.click(button)
   const showInformation = wrapper.container.querySelector('.personalInformation')
   expect(showInformation).toBeInTheDocument()
})
  afterAll(cleanup)
}) 

如果你运行测试并且一切正常,你应该会看到以下内容:

文本描述自动生成

图 16.5:通过测试

介绍 Vitest

Vitest 是一个基于 Vite 构建的单元测试框架,旨在追求速度和最小化配置。它可作为 Jest、Mocha 和 Chai 等各种测试工具的替代品。由于 Vitest 是建立在 Jest API 之上的,如果你已经知道如何使用 Jest,它的工作方式将非常相似。

在这个上下文中,我们将利用 Vite,这是一个旨在为现代网络项目提供快速和精简开发体验的构建工具。

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

npm install vite -g 

安装完成后,你需要使用 npm 命令创建你的第一个项目:

npm create vite@latest 

它会要求你输入项目名称。你可以使用 my-first-vite-project,然后选择你想要使用的框架(React),最后选择变体(TypeScript):

图 16.6:npm create vite@latest

接下来,你需要安装项目依赖并运行 npm run dev 命令。如果你这样做,你将在端口 5173 上看到类似以下内容:

图 16.7:Vite 应用

安装和配置 Vitest

一旦你的 Vite 应用运行起来,就是时候安装 Vitest 了。要做到这一点,你只需要在你的项目终端中运行这个命令:

 npm install -D vitest @test-library/react 

在安装了 Vitest 之后,你需要使用以下代码修改 vite.config.ts 文件:

/// <reference types="vitest" />
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
   environment: 'jsdom'
  }
}) 

如你所见,我们将使用 jsdom 环境,因此你也需要安装它:

npm install -D jsdom 

此外,Vitest 还提供了一个名为 Vitest UI 的插件,它使 Vitest 能够在浏览器中提供直观的用户界面来查看和交互测试。虽然这是一个可选插件,但我们将使用它。你可以通过执行以下命令来安装它:

npm install -D @vitest/ui 

为了测试你的代码,你需要使用 vitest --ui 命令将测试脚本添加到你的 package.json 文件中:

"scripts": {
  "dev": "vite",
  "build": "tsc && vite build",
  "preview": "vite preview",
  "test": "vitest --ui"
} 

我们将使用与 Jest 相同的 Hello 组件,尽管会有一些差异。你需要将此组件保存到 src/components/Hello/index.tsx

 import React, { FC } from 'react'
 type Props = {
  name?: string
 }
 const Hello: FC<Props> = ({ name }) => <h1 className="Hello">Hello {name || "World"}</h1>
 export default Hello 

然后,你需要在同一组件目录下创建一个名为 index.test.tsx 的测试文件:

 import { cleanup, render } from '@testing-library/react'
 import { afterAll, describe, expect, it } from 'vitest'
 import Hello from './index'
 describe("Hello Component", () => {
  it("should render Hello World", () => {
   const wrapper = render(<Hello />)
   expect(wrapper.getByText("Hello World")).toBeDefined()
  })

  it("should render the name prop", () => {
   const wrapper = render(<Hello name="Carlos" />)
   expect(wrapper.getByText("Hello Carlos")).toBeDefined()
  })

  it("should has .Home classname", () => {
   const wrapper = render(<Hello />)
   const firstChild = wrapper.container.firstChild as HTMLElement
   expect(firstChild?.classList.contains("Hello")).toBe(true)
  })

  afterAll(cleanup)
 }) 

如你所见,代码与 Jest 非常相似。然而,主要区别之一是我们现在正在导入我们将要使用的所有测试方法,例如 afterAlldescribeexpectit

如果您运行test命令,您应该在您的终端中看到类似以下的内容:

图片

图 16.8:npm 测试

如果您已经注意到,这是由我们之前安装的 Vitest UI 插件生成的链接。如果您点击该链接,您将看到以下内容:

图片

图 16.9:Vitest UI

目前,我们只有一个测试文件,但如果您添加更多,您将在左侧侧边栏上看到它们。现在,让我们点击我们当前的Hello测试:

图片

图 16.10 – 报告

您将能够看到正确通过的测试用例。然而,这个 UI 插件最有趣的优势之一是您甚至可以直接在浏览器中通过点击代码标签来修改测试代码:

图片

图 16.11:代码

让我们修改我们的代码,故意让一些测试失败。您可以将第一个测试改为"Hello Foo"而不是"Hello World",并确保保存(Cmd + S):

图片

图 16.12:失败的测试

如您所见,现在我们的第一次测试失败了,因为它无法找到"Hello Foo"文本。

启用全局变量

个人来说,我更喜欢在一个文件中导入所有必要的函数或变量。然而,我意识到在创建大量测试文件时,反复导入全局测试变量如describeitexpect等可能会变得繁琐和麻烦。

幸运的是,Vitest 提供了一个配置选项来启用globals,从而消除了每次都需要导入它们的需要。要启用此功能,您需要使用以下代码修改您的vite.config.ts文件:

/// <reference types="vitest" />
/// <reference types="vite/client" />
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
   environment: "jsdom",
   globals: true
  }
}) 

在进行前面提到的更改后,您还需要通过添加全局类型来更新您的tsconfig.json文件:

"compilerOptions": {
  "types": ["vitest/globals"]
} 

在遵循这些步骤之后,您现在将能够从您的测试文件中移除对globals的导入。如果您仍然遇到任何 TypeScript 错误,您可能需要重新启动您的 TypeScript 服务器或重新加载 VSCode 中的窗口。

在源代码中测试

Vitest 还提供了一种在源代码与实现一起运行测试的方法,类似于 Rust 的模块测试。

个人来说,我有一个老式的做法,我通常更喜欢为我的测试保留一个单独的测试文件。然而,在某些情况下,被测试的组件或函数非常小,创建一个新的测试文件可能看起来有些过度。

要启用此功能,您需要修改您的vite.config.ts文件并添加includeSource选项:

export default defineConfig({
  plugins: [react()],
  test: {
   environment: "jsdom",
   globals: true,
   includeSource: ["src/**/*.{ts,tsx}"]
  }
}) 

要解决 TypeScript 问题,您需要在您的tsconfig.json文件中添加vitest/importMeta类型进行另一个更改:

"compilerOptions": {
  "types": ["vitest/globals", "vitest/importMeta"]
} 

现在,让我们将我们的Hello组件测试文件移到同一个Hello组件内部。再次强调,这是可选的,只是为了演示这是可能的。最后,您可以选择使用哪种测试方法。

要实现这一点,我们需要在我们的 Hello 组件内部添加一个 if 语句来检查我们是否处于测试模式。我们可以用以下代码来完成这个任务:if (import.meta.vitest)。在这个块内部,我们将移动所有的测试用例,并且我们也将只在该块内部要求 React 测试库 方法。这样,我们的代码将类似于以下内容:

import React, { FC } from 'react'
  type Props = {
  name?: string;
}
const Hello: FC<Props> = ({ name }) => <h1 className="Hello">Hello {name || "World"}</h1>
export default Hello;
if (import.meta.vitest) {
  const { cleanup, render } = require('@testing-library/react')

  describe("Hello Component", () => {
   it("should render Hello World", () => {
    const wrapper = render(<Hello />)
    expect(wrapper.getByText("Hello World")).toBeDefined()
   })

   it("should render the name prop", () => {
    const wrapper = render(<Hello name="Carlos" />)
    expect(wrapper.getByText("Hello Carlos")).toBeDefined()
   })
   it("should has .Home classname", () => {
    const wrapper = render(<Hello />)
    const firstChild = wrapper.container.firstChild as HTMLElement
    expect(firstChild?.classList.contains("Hello")).toBe(true)
   })

   afterAll(cleanup)
  })
} 

现在,你可以删除你之前的文件(index.test.tsx)。如果你再次运行你的测试,它们应该会按预期工作。

不同之处在于现在你将能够看到整个代码(ComponentTest 用例):

图片

图 16.13:通过测试

这种方法可能会加快组件或函数的测试过程。然而,我个人仍然更喜欢在单独的测试文件中进行测试。尽管如此,你可以自由选择对你和你的项目最有效的方法。

在探索了源代码测试的概念之后,让我们继续了解如何有效地将 React DevTools 应用于我们的开发过程中,以优化我们应用程序的性能并确保其平稳运行。

使用 React DevTools

当在控制台测试不够时,并且我们想在浏览器内运行的应用程序中检查我们的应用程序时,我们可以使用 React DevTools。

你可以在以下网址将其作为 Chrome 扩展安装:chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgo fadopljbjfkapdkoienihi?hl=en

安装会在 Chrome DevTools 中添加一个名为 React 的标签页,你可以检查组件的渲染树并检查它们在特定时间点接收了哪些属性以及它们的状态。

可以读取属性和状态,并且可以实时更改它们以触发 UI 的更新并立即看到结果。这是一个必备的工具,在最新版本中,它有一个可以通过勾选跟踪 React 更新复选框来启用的新功能。

当这个功能被启用时,我们可以使用我们的应用程序,并查看当我们执行特定操作时哪些组件被更新。更新的组件会用彩色矩形突出显示,这使得发现可能的优化变得容易。

使用 Redux DevTools

如果你正在你的应用程序中使用 Redux,你可能想使用 Redux DevTools 来调试你的 Redux 流程。你可以在以下网址安装它:chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=es

此外,你还需要安装 redux-devtools-extension 包:

npm install --save-dev redux-devtools-extension 

一旦你安装了 React DevTools 和 Redux DevTools,你将需要配置它们。

如果你尝试直接使用 Redux DevTools,它将不会工作;这是因为我们需要将 composeWithDevTools 方法传递给 Redux 存储;这应该是 configureStore.ts 文件:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from '@reducers';

export default function configureStore({
  initialState,
  reducer
}) {
const middleware = [thunk];
return createStore(
   rootReducer,
   initialState,
   composeWithDevTools(applyMiddleware(...middleware))
  );
} 

这是测试我们 Redux 应用程序的最佳工具。

摘要

在本章中,你全面了解了测试的好处,以及可用于测试 React 组件的各种框架和工具。你学习了如何使用 React Testing Library 来实现和测试组件和事件,以及如何使用 Jest 覆盖率来优化你的测试过程。此外,你还探索了 React DevTools 和 Redux DevTools 等工具,以进一步增强你的开发体验。在测试复杂组件时,如高阶组件或具有多个嵌套字段的表单,记住常见的解决方案是很重要的,以确保你的测试能够准确反映应用程序的功能。

在下一章中,你将学习如何将你的应用程序部署到生产环境。

第十七章:部署到生产环境

现在你已经完成了你的第一个 React 应用程序,是时候学习如何将其部署到世界上了。为此,我们将使用名为DigitalOcean的云服务。

在本章中,你将学习如何使用 Node.js 和nginx在 DigitalOcean 的 Ubuntu 服务器上部署你的 React 应用程序。简而言之,我们将涵盖以下主题:

  • 创建 DigitalOcean Droplet 并配置它

  • 配置 nginx、PM2 和域名

  • 实施 CircleCI 进行持续集成

技术要求

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

  • Node.js 19+

  • Visual Studio Code

创建我们的第一个 DigitalOcean Droplet

我已经使用了 DigitalOcean 七年了,我可以这么说,它是我在尝试过的最好的云服务之一,不仅因为成本合理,而且因为它配置超级简单快捷,社区有大量的更新文档来修复大多数与服务器配置相关的问题。

到这一点,你需要投资一些钱来获取这项服务。我会向你展示最便宜的方法来做这件事,如果你将来想要增加 Droplet 的功率,你将能够增加容量而无需重新配置。

非常基础的 Droplet 的最低价格为每月 6.00 美元(每小时 0.009 美元)。

我们将使用 Ubuntu 20.04(但请随意使用最新版本,21.04);你需要了解一些基本的 Linux 命令才能配置你的 Droplet。如果你是使用 Linux 的初学者,不要担心——我会尽量用非常简单的方式展示每个步骤。

在 DigitalOcean 上注册

如果你没有 DigitalOcean 账户,你可以在cloud.digitalocean.com/registrations/new注册。

你可以用你的 Google 账户注册,或者手动注册。一旦你用 Google 注册,你将看到账单信息视图,如下所示:

图形用户界面,文本,应用程序,电子邮件自动生成描述

图 17.1:账单信息

你可以用信用卡或通过 PayPal 支付。一旦你配置了你的支付信息,DigitalOcean 将要求你提供一些关于你的项目的信息,以便它可以更快地配置你的 Droplet。

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

图 17.2:第一个应用程序

让我们继续创建我们的第一个 Droplet。

创建我们的第一个 Droplet

我们将从头开始创建一个新的 Droplet。按照以下步骤操作:

  1. 选择新 Droplet选项,如下面的截图所示:

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

图 17.3:新 Droplet

  1. 选择Ubuntu 20.04 (LTS) x64,如下所示:

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

图 17.4:选择镜像

  1. 然后,选择基础计划,如图所示:

表描述自动生成,置信度中等

图 17.5:选择一个计划

  1. 然后,你可以从支付计划选项中选择**$6/月**:

图 17.6:CPU 选项

  1. 选择一个区域。在这种情况下,我们将选择旧金山区域:

    图 17.7:选择区域

  2. 创建一个根密码,添加你的 Droplet 名称,然后点击创建 Droplet按钮,如下所示:

    图形用户界面、文本、应用程序、电子邮件描述自动生成

    图 17.8:身份验证

  3. 创建你的 Droplet 大约需要 30 秒。一旦创建完成,你将能够看到它:

图 17.9:我的第一个 Droplet

  1. 现在,在你的终端中,你可以使用以下命令访问 Droplet:

    ssh root@THE_DROPLET_IP 
    
  2. 第一次访问时,你会被要求输入指纹。你只需输入,然后它会要求你输入密码(你在创建 Droplet 时定义的密码)。

这是一种专门设计用于防止中间人攻击的安全功能。服务器的“指纹”充当一个独特的数字签名,该签名仅属于服务器本身。当你观察到与预期匹配的指纹时,你可以通过输入yes并按Enter键继续。随后,服务器将提示你输入密码。请提供你在创建 Droplet 时定义的密码,并按Enter键。请注意,出于安全原因,在输入密码时屏幕上不会显示任何字符。认证成功后,你将登录到你的服务器,准备开始执行命令。

文本描述自动生成

图 17.10:连接到 Droplet

现在我们已经准备好安装 Node.js,我们将在下一节中介绍。

安装 Node.js

现在你已经连接到你的 Droplet,让我们来配置它。首先,我们需要使用个人软件包存档安装 Node.js 的最新版本。本书撰写时 Node.js 的当前版本是 19.9.x。按照以下步骤安装 Node.js:

  1. 如果你阅读这个段落时 Node.js 有新版本,请更改setup_19.x命令中的版本:

    cd ~ 
    curl -sL https://deb.nodesource.com/setup_19.x -o nodesource_setup.sh 
    
  2. 一旦你获取到nodesource_setup.sh文件,运行以下命令:

    sudo bash nodesource_setup.sh 
    
  3. 然后,通过运行以下命令安装 Node:

    sudo apt install nodejs -y 
    
  4. 如果一切正常,请使用以下命令验证安装的 Node 和npm版本:

    node -v 
    v19.9.0 
    npm -v 
    9.6.3 
    

如果你需要 Node.js 的新版本,你可以随时升级它。

配置 Git 和 GitHub

我创建了一个特殊的仓库,帮助你将第一个 React 应用程序部署到生产环境(github.com/FoggDev/production)。

在你的 Droplet 上,你需要克隆这个 Git 仓库(或者如果你已经准备好了要部署的 React 应用程序,克隆你自己的仓库)。生产仓库是公开的,但通常你会使用私有仓库;在这种情况下,你需要将你的 Droplet 的 SSH 密钥添加到你的 GitHub 账户中。

要创建这个密钥,请按照以下步骤操作:

  1. 运行 ssh-keygen 命令,然后按三次 Enter 键,不输入任何密码短语:文本 自动生成的描述

    图 17.11:ssh-keygen

    如果你让你的终端闲置超过五分钟,你的 Droplet 连接可能会被关闭,你需要再次连接。

  2. 一旦你创建了你的 Droplet SSH 密钥,你可以通过运行以下命令来查看它:

    vi /root/.ssh/id_rsa.pub 
    

    你会看到类似以下的内容:

    文本 自动生成的描述

    图 17.12:ssh-rsa

  3. 复制你的 SSH 密钥,然后访问你的 GitHub 账户。转到 设置 | SSH 和 GPG 密钥github.com/settings/ssh/new)。然后,将你的密钥粘贴到文本区域,并为密钥添加标题:

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

图 17.13:向 GitHub 添加新的 SSH 密钥

  1. 一旦你点击 添加 SSH 密钥 按钮,你会看到你的 SSH 密钥,如下所示:

文本 自动生成的描述

图 17.14:SSH

  1. 现在,你可以使用以下命令克隆我们的仓库(或你的):

    git clone git@github.com:FoggDev/production.git 
    
  2. 当你第一次克隆时,你会收到一条消息,要求你允许 RSA 密钥指纹:

文本 自动生成的描述

图 17.15:克隆仓库

  1. 你必须输入 Yes 并按 Enter 键才能克隆它:

文本 自动生成的描述

图 17.16:已知主机

  1. 然后,你必须转到生产目录并安装 npm 包:

    cd production 
    npm install 
    
  2. 如果你想要测试应用程序,只需运行 start 脚本:

    npm start 
    
  3. 然后打开你的浏览器,转到你的 Droplet IP 并添加端口号。在我的例子中,它是 http://144.126.222.17:3000

包含标志的图片 自动生成的描述

图 17.17:开发模式下的项目运行

  1. 这将在开发模式下运行项目。如果你想以生产模式运行它,请使用以下命令:

    npm run start:production 
    

    你应该会看到 生产进程管理器PM2)正在运行,如下面的截图所示:

    文本 自动生成的描述

    图 17.18:PM2

  2. 如果你运行它并在你的 Chrome DevTools 中的 网络 选项卡下查看,你会看到正在加载的包:

计算机的截图 中度置信度自动生成的描述

图 17.19:网络选项卡

现在,我们的 React 应用程序已经在生产环境中运行,但在下一节中,让我们看看我们还能用 DigitalOcean 做些什么。

关闭我们的 Droplet

要关闭 Droplet,请按照以下步骤操作:

  1. 如果你想要关闭你的 Droplet,你可以转到 电源 部分,或者你可以使用 开启/关闭 开关:

文本  自动生成的描述

图 17.20:关闭 Droplet

  1. DigitalOcean 只在你开启 Droplet 时才会收费。如果你点击 开启 开关来关闭它,那么你会收到以下确认消息:

图形用户界面、文本、应用程序、电子邮件  自动生成的描述

图 17.21:关闭 Droplet

这样,你可以控制你的 Droplet,并在不使用 Droplet 时避免不必要的付费。

配置 nginx、PM2 和域名

我们的 Droplet 已准备好用于生产,但正如你所见,我们仍在使用端口 3000。我们需要配置 nginx 并实现代理以将流量从端口 80 重定向到 3000;这意味着我们不再需要直接指定端口。

Node PM2 将帮助我们安全地运行 Node 服务器。通常,如果我们直接使用 nodebabel-node 命令运行 Node,并且应用程序中发生错误,那么它将崩溃并停止工作。PM2 在发生错误时重新启动 Node 服务器。

首先,在你的 Droplet 中,你需要全局安装 PM2:

npm install -g pm2 

PM2 将帮助我们以非常简单的方式运行我们的 React 应用程序。

安装和配置 nginx

要安装 nginx,你需要执行以下命令:

sudo apt-get update 
sudo apt-get install nginx 

在你安装 nginx 之后,你可以开始配置:

  1. 我们需要调整防火墙以允许端口 80 的流量。要列出可用的应用程序配置,你需要运行以下命令:

    sudo ufw app list
    Available applications:
    Nginx Full
    Nginx HTTP
    Nginx HTTPS
    OpenSSH 
    
  2. Nginx Full 表示它将允许来自端口 80(HTTP)和端口 443(HTTPS)的流量。我们尚未配置任何带有 SSL 的域名,因此,目前我们应该限制流量仅通过端口 80(HTTP)发送:

    sudo ufw allow 'Nginx HTTP'
    Rules updated
    Rules updated (v6) 
    

    如果你尝试访问 Droplet IP,你应该看到 nginx 正在运行:

    图形用户界面、文本、应用程序、电子邮件  自动生成的描述

    图 17.22:欢迎使用 nginx

  3. 你可以使用以下命令管理 nginx 进程:

    Start server: sudo systemctl start nginx
    Stop server: sudo systemctl stop nginx
    Restart server: sudo systemctl restart nginx 
    

Nginx 是一个非常出色的网站服务器,现在越来越受欢迎。

设置反向代理服务器

如我之前所述,我们需要设置一个反向代理服务器,将端口 80(HTTP)的流量发送到端口 3000(React 应用程序)。为此,你需要打开以下文件:

sudo vi /etc/nginx/sites-available/default 

设置反向代理服务器的步骤如下:

  1. location / 块中,你需要用以下代码替换文件中的代码:

    location / {
      proxy_pass http://localhost:3000;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection 'upgrade'; 
      proxy_set_header Host $host; 
      proxy_cache_bypass $http_upgrade;
     } 
    
  2. 保存文件后,你可以使用以下命令验证 nginx 配置中是否存在语法错误:

    sudo nginx -t 
    
  3. 如果一切正常,你应该看到这个:

文本  自动生成的描述

图 17.23:sudo ngnix-t

  1. 最后,你需要重新启动 nginx 服务器:

    sudo systemctl restart nginx 
    

现在,您应该能够访问不带端口的 React 应用程序,如下面的截图所示:

包含文本的图片  自动生成的描述

图 17.24:不带端口的 React 应用程序

我们几乎完成了!在下一节中,我们将向我们的 Droplet 添加一个域名。

将域名添加到我们的 Droplet

使用 IP 地址访问网站并不方便;我们总是需要使用域名来帮助用户更容易地找到我们的网站。如果您想使用与您的 Droplet 关联的域名,您需要更改域名的名称服务器,使其指向 DigitalOcean DNS。我通常使用 GoDaddy 来注册我的域名。

要使用 GoDaddy 完成此操作,请按照以下步骤进行:

  1. 前往dcc.godaddy.com/manage/YOURDOMAIN.COM/dns,然后进入名称服务器部分:

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

图 17.25:名称服务器

  1. 点击更改按钮,选择自定义,然后指定 DigitalOcean DNS:

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

图 17.26:DigitalOcean 名称服务器

  1. 通常,DNS 更改需要 15 到 30 分钟才能生效;现在,在您更新了名称服务器后,前往您的Droplet仪表板,然后选择添加域名选项:

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

图 17.27:添加域名

  1. 然后,输入您的域名,选择您的 Droplet,并点击添加域名按钮:

图表,雷达图  自动生成的描述

图 17.28:网络

  1. 现在,您必须为CNAME创建一个新的记录。选择CNAME选项卡,在主机名中输入www;在别名字段中,默认写入@;默认情况下,TTL 为43200。所有这些都是为了通过www前缀访问您的域名:

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

图 17.29:创建新记录

如果您一切操作正确,您应该能够访问您的域名并看到 React 应用程序正在运行。正如我之前所说,这个过程可能需要长达 30 分钟,但在某些情况下,可能需要长达 24 小时,具体取决于 DNS 传播速度。

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

图 17.30:运行在域名上的 React 应用程序

太棒了。现在您已经正式部署了您的第一个 React 应用程序到生产环境!

实施 CircleCI 进行持续集成

我已经使用 CircleCI 一段时间了,我可以告诉你,它是最好的 CI 解决方案之一:对个人用户免费,提供无限数量的仓库和用户;你每月有 1,000 构建分钟,一个容器和一个并发作业;如果你需要更多,你可以以每月 50 美元的初始价格升级计划。

你需要做的第一件事是使用你的 GitHub 账户(或者如果你更喜欢,使用 Bitbucket)在该网站上注册。

如果你选择使用 GitHub,你需要在你的账户中授权 CircleCI,如下面的截图所示:

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

图 17.31:授权 CircleCI

在下一节中,我们将把我们的 SSH 密钥添加到 CircleCI。

在 CircleCI 中添加 SSH 密钥

现在,你已经创建了账户,CircleCI 需要一种方式来登录你的 DigitalOcean Droplet 以运行部署脚本。按照以下步骤完成此任务:

  1. 在你的 Droplet 中使用以下命令创建一个新的 SSH 密钥:

    ssh-keygen -t rsa
    # Then save the key as /root/.ssh/id_rsa_droplet with no password.
    # After go to .ssh directory
    cd /root/.ssh 
    
  2. 然后,让我们将密钥添加到我们的 authorized_keys

    cat id_rsa_droplet.pub >> authorized_keys 
    
  3. 现在,你需要下载私钥。为了验证你可以使用新密钥登录,你需要按照以下方式将其复制到你的本地机器:

    # In your local machine do:
    scp root@YOUR_DROPLET_IP:/root/.ssh/id_rsa_droplet ~/.ssh/
    cd .ssh
    ssh-add id_rsa_droplet
    ssh -v root@YOUR_DROPLET_IP 
    

    如果你一切操作正确,你应该能够无密码登录你的 Droplet,这意味着 CircleCI 也可以访问我们的 Droplet。

  4. 复制你的 id_rsa_droplet.pub 密钥的内容,然后转到你的仓库设置(app.circleci.com/settings/project/github/YOUR_GITHUB_USER/YOUR_REPOSITORY):

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

图 17.32:项目设置

  1. 前往 SSH 密钥,如下所示:

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

图 17.33:SSH 密钥

  1. 你也可以访问 URL app.circleci.com/settings/project/github/YOUR_GITHUB_USER/YOUR_REPOSITORY/ssh,然后点击底部的 添加 SSH 密钥 按钮:

图形用户界面,应用程序,电子邮件  自动生成的描述

图 17.34:添加 SSH 密钥

  1. 粘贴你的私钥,然后为 主机名 字段提供一个名称;我们将命名为 DigitalOcean

现在,让我们在下一节中配置我们的 CircleCI 实例。

配置 CircleCI

现在,你已经为你的 Droplet 配置了 CircleCI 的访问权限,你需要向你的项目中添加一个 config 文件来指定部署过程中要执行的作业。

此过程在以下步骤中展示:

  1. 为了这个,你需要创建 .circleci 目录并在 config.yml 文件中添加以下内容:

    version: 2.1
    jobs:
    build:
    working_directory: ~/tmp
    docker:
      - image: cimg/node:14.16.1
    steps:
      - checkout
      - run: npm install
      - run: npm run lint
      - run: npm test
      - run: ssh -o StrictHostKeyChecking=no $DROPLET_USER@$DROPLET_IP 'cd production; git checkout master; git pull; npm install; npm run start:production;'
     workflows:
     build-deploy:
     jobs:
      - build:
     filters:
     branches:
     only: master 
    
  2. 当你有一个 .yml 文件时,你需要注意缩进;它与 Python 类似,如果你没有正确使用缩进,你会得到一个错误。让我们看看这个文件是如何结构的。

  3. 指定我们将使用的 CircleCI 版本。在这个例子中,你正在使用版本 2.1(撰写本书时的最新版本):

     version: 2.1 
    
  4. jobs 中,我们将指定需要配置容器;我们将使用 Docker 创建它,并概述部署过程的步骤。

  5. working_directory 将是我们将用于安装 npm 包和运行部署脚本的临时目录。在这种情况下,我决定使用 tmp 目录,如下所示:

     jobs:
     build:
     working_directory: ~/tmp 
    
  6. 如我之前所说,我们将创建一个 Docker 容器,在这个例子中,我选择了一个包含 node: 18.12.1 的现有镜像。如果你想了解所有可用的镜像,你可以访问 circleci.com/docs/2.0/circleci-images

     docker:
     - image: cimg/node:18.12.1 
    
  7. 对于代码案例,首先使用 git checkout 命令切换到 master 分支,然后在每个 run 语句中,你需要指定你想要运行的脚本:

     steps:
     - checkout
     - run: npm install
     - run: npm run lint
     - run: npm test
     - run: ssh -o StrictHostKeyChecking=no $DROPLET_USER@$DROPLET_IP 'cd production; git checkout master; git pull; npm install; npm run start:production;' 
    

这里是对之前步骤的解释:

  1. 首先,你需要使用 npm install 命令安装 npm 包,以便能够执行后续任务。

  2. 使用 npm run lint 执行 ESLint 验证。如果失败,它将中断部署过程;否则,它将继续下一个运行。

  3. 使用 npm run test 执行 Jest 验证;如果失败,它将中断部署过程。否则,它将继续下一个运行。

  4. 在最后一步,我们连接到我们的 DigitalOcean Droplet,通过传递 StrictHostKeyChecking=no 标志来禁用严格的主机密钥检查。然后,我们使用 $DROPLET_USER$DROPLET_IP ENV 变量来连接到它(我们将在下一步创建这些变量),最后,我们将使用单引号指定我们将在 Droplet 内执行的命令。

这些命令如下列出:

  • cd production: 授予对生产环境(或你的 Git 仓库名称)的访问权限。

  • git checkout master: 这将检出主分支。

  • git pull: 从我们的仓库拉取最新更改。

  • npm run start:production: 这是最后一步,它以生产模式运行我们的项目。

最后,让我们向我们的 CircleCI 添加一些环境变量。

在 CircleCI 中创建环境变量

正如你之前看到的,我们正在使用 DROPLETUSERDROPLET_USER** 和 **DROPLET_IP 变量,但我们如何定义这些变量呢?请按照以下步骤操作:

  1. 你需要再次进入项目设置,并选择 环境变量 选项。然后,你需要创建 DROPLET_USER 变量:

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

图 17.35:添加环境变量

  1. 然后,你需要使用你的 Droplet IP 创建 DROPLET_IP 变量:

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

图 17.36:DROPLET_IP

  1. 现在,你需要将配置文件推送到你的仓库,然后你就可以准备魔法时刻了。现在,由于 CircleCI 已经连接到你的仓库,每次你将更改推送到 master 分支时,它都会触发一个构建。

    通常,前两个或三个构建可能会因为语法错误、配置中的缩进错误,或者可能是因为我们有代码检查错误或单元测试错误而失败。如果你有失败,你会看到类似以下的内容:

    文本描述自动生成,中等置信度

    图 17.37:构建错误

  2. 如前一个屏幕截图所示,底部的第一个构建失败说构建错误,第二个在 WORKFLOW 下说build-deploy,如图 17.38所示。这基本上意味着在第一次构建中,我在config.yml文件中有语法错误。

  3. 在你修复了config.yml文件中的所有语法错误以及所有与代码检查或单元测试相关的问题后,你应该会看到一个成功构建,如下所示:

图 17.38:成功构建

  1. 如果你点击构建号,你可以看到 CircleCI 在发布你 Droplet 中的新更改之前执行的所有步骤:

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

图 17.39:CircleCI 执行的步骤

  1. 如你所见,步骤的顺序与我们指定的config.yml文件中的顺序相同;你甚至可以通过点击它来查看每个步骤的输出:

图形用户界面,文本描述自动生成

图 17.40:代码检查和测试步骤

  1. 现在,假设你在你的代码检查验证或某些单元测试中遇到了错误。让我们看看在这种情况下会发生什么,如下所示:

文本描述自动生成

图 17.41:代码检查错误

如你所见,一旦检测到错误,它将以代码1退出。这意味着它将终止部署并将其标记为失败。请注意,在npm run lint之后的步骤都没有被执行。

另一件很酷的事情是,如果你现在去你的 GitHub 仓库检查你的提交,你会看到所有成功构建的提交和所有失败构建的提交:

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

图 17.42:GitHub 成功构建

这真是太棒了:现在你的项目已经配置好了自动部署,并且它与你的 GitHub 仓库相连。

摘要

恭喜!我们已经完成了部署过程的旅程,你现在拥有了将你的 React 应用程序部署到世界(生产环境)所需的知识和技能。你还学会了如何实现 CircleCI 进行持续集成,简化你的开发流程,并确保你的应用程序保持高性能和可靠性。

通过利用本章中概述的策略和最佳实践,您可以自信地将您的应用程序推向全球受众,安心地知道它已经针对速度、可扩展性和弹性进行了优化。感谢您与我一同踏上这段旅程。我希望您喜欢阅读我的书籍。