React-生产环境应用架构-三-

95 阅读29分钟

React 生产环境应用架构(三)

原文:zh.annas-archive.org/md5/ceb525cb4b1ca0f3a18a581d1eef2dfa

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:配置测试和部署的 CI/CD

我们的应用程序终于准备就绪,可以投入生产并迎接第一批用户。我们已经构建了其功能并实现了所有必需的检查,例如代码检查、测试等,这将让我们有信心应用程序代码正在正确运行。

然而,目前,所有这些检查都必须在我们的本地机器上执行。每次我们想要将新功能推送到生产环境时,都需要运行所有脚本然后手动重新部署应用程序,这是一个非常繁琐的过程。

在本章中,我们将学习什么是 CI/CD。然后,我们将学习什么是 GitHub Actions 以及 GitHub Actions 流水线的主要部分。接着,我们将学习如何创建一个 CI/CD 流水线,该流水线将自动化应用程序的验证和部署到 Vercel。

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

  • 什么是 CI/CD?

  • 使用 GitHub Actions

  • 配置测试流水线

  • 配置部署到 Vercel 的流水线

到本章结束时,我们将知道如何使用 GitHub Actions 配置 CI/CD 流水线并将应用程序部署到 Vercel。

技术要求

在我们开始之前,我们需要设置我们的项目。为了能够开发我们的项目,我们将在计算机上需要以下内容安装:

  • Node.js 版本 16 或更高,以及 npm 版本 8 或更高。

  • 安装 Node.js 和 npm 有多种方式。这里有一篇很好的文章详细介绍了更多细节:www.nodejsdesignpatterns.com/blog/5-ways-to-install-node-js

  • VSCode(可选)是目前最流行的 JavaScript/TypeScript 编辑器/IDE,因此我们将使用它。它是开源的,与 TypeScript 有很好的集成,并且我们可以通过扩展来扩展其功能。可以从code.visualstudio.com/下载。

本章的代码文件可以在以下位置找到:github.com/PacktPublishing/React-Application-Architecture-for-Production

可以使用以下命令在本地克隆存储库:

git clone https://github.com/PacktPublishing/React-Application-Architecture-for-Production.git

一旦克隆了存储库,我们需要安装应用程序的依赖项:

npm install

我们可以使用以下命令提供环境变量:

cp .env.example .env

一旦安装了依赖项,我们需要选择与本章匹配的正确代码库阶段。我们可以通过执行以下命令来完成:

npm run stage:switch

此命令将提示我们每个章节的阶段列表:

? What stage do you want to switch to? (Use arrow
 keys)
❯ chapter-02
  chapter-03
  chapter-03-start
  chapter-04
  chapter-04-start
  chapter-05
  chapter-05-start
(Move up and down to reveal more choices)

这是第九章,所以如果我们想跟随,可以选择chapter-09-start,或者选择chapter-09来查看本章的最终结果。

一旦选择了章节,所有必要的文件都会显示出来以供跟随本章内容。

更多关于设置细节的信息,请查看README.md文件。

什么是 CI/CD?

持续集成/持续部署CI/CD)是一种以自动化方式向用户交付应用程序更改的方法。CI/CD 通常应包括以下部分:

  • 持续集成是自动验证代码是否已构建、测试并合并到存储库的过程

  • 持续交付意味着将更改交付到存储库

  • 持续部署意味着将更改发布到生产服务器,在那里更改对用户可用

现在,让我们考虑如何为我们的应用程序实现 CI/CD。我们已经有了所有部分——我们只需要将它们组合在一起。这个过程将像这样工作:

  • 运行应用程序的所有代码检查(单元和集成测试、代码风格检查、类型检查、格式检查等)

  • 构建应用程序并运行端到端测试

  • 如果两个过程都成功完成,我们可以部署我们的应用程序

下面是如何可视化这个过程:

图 9.1 – 管道概述

图 9.1 – 管道概述

此过程将确保我们的应用程序始终处于最佳状态,并且更改可以频繁且容易地发布到生产环境中。这对于在大型团队中工作特别有用,因为每天都会向应用程序引入许多更改。

要运行 CI/CD 管道,我们需要适当的基础设施。由于我们将存储库保存在 GitHub 上,我们可以使用 GitHub Actions 来处理 CI/CD。

使用 GitHub Actions

GitHub Actions是一个 CI/CD 工具,允许我们自动化、构建、测试和部署管道。我们可以在存储库中的特定事件上创建运行工作流程。

要了解它是如何工作的,让我们在以下部分中查看其一些组件。

工作流程

一个.github/workflows文件夹。当指定的事件被触发时,可以运行工作流程。我们还可以直接从 GitHub 手动重新运行工作流程。一个存储库可以有我们想要的任何数量的工作流程。

事件

当一个事件被触发时,将导致工作流程运行。GitHub 活动可以触发事件,例如向存储库推送或创建拉取请求。除此之外,它们还可以按计划或通过 HTTP POST 请求启动。

作业

一个作业定义了一系列将在工作流程中执行的步骤。一个步骤可以是执行的动作或脚本。

一个工作流程可以有多个可以并行运行的作业,或者它们可以在开始之前等待依赖作业完成。

动作

动作是在 GitHub Actions 上运行以执行重复性任务的应用程序。我们可以使用在github.com/marketplace?type=actions上可用的已构建动作,或者我们可以创建自己的。我们将在我们的管道中使用几个预制的动作。

运行器

运行器是一个在触发时运行工作流程的服务器。它可以在 GitHub 上托管,也可以自行托管。

现在我们已经熟悉了 GitHub Actions 的基础知识,我们可以开始创建我们应用程序的工作流程。

让我们创建 .github/workflows/main.yml 文件和初始代码:

name: CI/CD
on:
  - push
jobs:
# add jobs here

在前面的代码中,我们提供了工作流程的名称。如果我们省略它,名称将被分配给工作流程文件的名称。在这里,我们定义了 push 事件,这将导致代码更改推送到仓库时工作流程运行。

我们将在以下部分定义作业。

对于我们定义的每个作业,我们将提供以下内容:

name: Name of the job
runs-on: ubuntu-latest

这些属性将适用于所有作业:

  • name 设置正在运行的作业名称

  • runs-on 设置运行器,它将运行作业

现在我们已经了解了 GitHub Actions 是什么以及管道的主要部分,我们可以开始为我们的应用程序构建管道。

配置测试管道

我们的测试管道将包括两个作业,它们应该执行以下操作:

  • 运行所有代码检查,如代码风格检查、类型检查、单元测试和集成测试等

  • 构建应用程序并运行端到端测试

代码检查作业

代码检查作业应该像以下图示中显示的那样工作:

图 9.2 – 代码检查作业概述

图 9.2 – 代码检查作业概述

如我们所见,作业应该是直接的:

  1. 首先,我们需要向应用程序提供环境变量。

  2. 然后,我们需要安装依赖项。

  3. 接下来,我们必须运行单元测试和集成测试。

  4. 然后,我们必须运行代码风格检查。

  5. 然后,我们必须检查代码格式。

  6. 最后,我们必须运行类型检查。

jobs 中,让我们添加运行这些任务的作业:

jobs:
  code-checks:
    name: Code Checks
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 16
      - run: mv .env.example .env
      - run: npm install
      - run: npm run test
      - run: npm run lint
      - run: npm run format:check
      - run: npm run types:check

关于作业,有几件事情值得提及:

  • 我们使用市场中的 actions/checkout@v3 动作允许作业访问仓库

  • 我们使用 actions/setup-node 动作来配置要运行哪个节点版本

  • 我们执行脚本以验证一切是否按预期工作

端到端测试作业

我们与测试相关的第二个作业是端到端作业,我们希望在上一章中定义的应用程序构建和端到端测试。

它应该像以下图示中显示的那样工作:

图 9.3 – E2E 测试作业

图 9.3 – E2E 测试作业

如我们所见,作业将按以下方式工作:

  1. 首先,我们需要添加环境变量。

  2. 然后,需要安装应用程序的依赖项。

  3. 然后,我们需要创建应用程序的生产构建版本。

  4. 最后,生产代码得到端到端测试。

为了实现这个作业,让我们添加以下代码:

jobs:
  # previous jobs
  e2e:
    name: E2E Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: mv .env.example .env
      - uses: cypress-io/github-action@v4
        with:
          build: npm run build
          start: npm run start

关于作业,有几件事情值得提及:

  • 我们使用 actions/checkout@v3 动作来检出仓库。

  • 我们使用 cypress-io/github-action@v4 动作,它将抽象化端到端测试。它将安装所有依赖项,构建应用程序,然后启动并运行所有 Cypress 测试。

现在我们已经配置了运行代码检查(如代码检查、格式化、类型检查和测试)的管道,我们可以开始部署应用程序的工作。

配置部署到 Vercel 的管道

当我们的测试作业完成后,我们希望将应用程序部署到 Vercel。要从 GitHub Actions 开始部署到 Vercel,我们需要做一些事情:

  • 拥有 Vercel 账户

  • 禁用 Vercel 的 GitHub 集成

  • 将项目链接到 Vercel

  • 向 GitHub Actions 提供环境变量

  • 创建部署应用程序的作业

拥有 Vercel 账户

Vercel 很容易开始使用。访问 vercel.com/signup 并创建账户,如果您还没有的话。

禁用 Vercel 的 GitHub 集成

Vercel 是一个与 GitHub 集成出色的平台。这意味着每次我们向存储库推送更改时,应用程序的新版本将自动部署到 Vercel。然而,在我们的情况下,我们希望在部署步骤之前验证我们的应用程序是否按预期工作,以便我们可以从 CI/CD 管道执行此任务。

要做到这一点,我们需要在 Vercel 中禁用 GitHub 集成。这可以通过创建包含以下内容的vercel.json文件来完成:

{
  "version": 2,
  "github": {
    "enabled": false
  }
}

将项目链接到 Vercel

由于我们已禁用 GitHub 集成,我们需要在 Vercel 中将项目链接到我们的存储库。这可以通过使用 Vercel CLI 来完成。

让我们执行以下命令:

npx vercel

CLI 将会询问我们一系列问题,如下所示:

? Set up and deploy "~/web/project-name"? [Y/n] y
? Which scope do you want to deploy to? org-name
? Link to existing project? [y/N] n
? What's your project's name? project-name
? In which directory is your code located? ./

一旦 CLI 进程完成,.vercel 文件夹将被生成。这是一个不应该由存储库跟踪的文件夹。在 .vercel/project.json 文件中,我们将找到我们的项目凭据,如下所示:

{"orgId":"example_org_id","projectId":"example_project_id"}

我们将在几分钟后需要将这些值提供给 GitHub Actions。

向 GitHub Actions 提供环境变量

对于我们的管道,我们需要几个环境变量:

  • VERCEL_ORG_ID,我们可以从.vercel/project.json文件中获取

  • VERCEL_PROJECT_ID,我们也可以从.vercel/project.json文件中获取

  • VERCEL_TOKEN,我们可以从vercel.com/account/tokens 获取

一旦我们有了这些值,我们就可以将它们添加到我们项目的 GitHub Actions 中:

图 9.4 – 向 GitHub Actions 添加环境变量

图 9.4 – 向 GitHub Actions 添加环境变量

创建部署应用程序的作业

现在一切都已经设置好了,我们可以开始工作,这个工作将完成所有的工作。我们可以在以下图中看到它应该如何工作:

图 9.5 – 部署作业概述

图 9.5 – 部署作业概述

如我们所见,它将经过几个步骤:

  1. 检查存储库所有者,因为我们不希望从存储库分叉触发工作流程时进行部署。

  2. 将部署状态设置为 开始

  3. 部署到 Vercel。

  4. 将部署状态设置为完成

让我们在定义了其他作业的工作流程文件中添加deploy作业:

jobs:
  # previous jobs
  deploy:
    name: Deploy To Vercel
    runs-on: ubuntu-latest
    needs: [code-checks, e2e]
    if: github.repository_owner == 'my-username'
    permissions:
      contents: read
      deployments: write
    steps:
      - name: start deployment
        uses: bobheadxi/deployments@v1
        id: deployment
        with:
          step: start
          token: ${{ secrets.GITHUB_TOKEN }}
          env: ${{ fromJSON('["Production", "Preview"]')
            [github.ref != 'refs/heads/master'] }}
      - uses: actions/checkout@v3
      - run: mv .env.example .env
      - uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-args: ${{ fromJSON('["--prod", ""]')
            [github.ref != 'refs/heads/master'] }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID}}
          vercel-project-id: ${{ secrets.
            VERCEL_PROJECT_ID}}
          scope: ${{ secrets.VERCEL_ORG_ID}}
          working-directory: ./
      - name: update deployment status
        uses: bobheadxi/deployments@v1
        if: always()
        with:
          step: finish
          token: ${{ secrets.GITHUB_TOKEN }}
          status: ${{ job.status }}
          env: ${{ steps.deployment.outputs.env }}
          deployment_id: ${{ steps.deployment.outputs.
            deployment_id }}

关于作业有几件事情值得提及:

  • 我们通过添加needs: [code-checks, e2e]将此作业设置为依赖于前两个作业。这意味着此作业将在那些作业成功完成后才开始。如果其中一些作业失败,此作业将永远不会运行。

  • 使用if: github.repository_owner == 'my-username',我们检查仓库所有者是否是项目的所有者。这个检查应该可以防止仓库分叉部署应用程序。

  • 在部署任务前后,我们使用bobheadxi/deployments@v1动作来更新 GitHub 中的部署状态。

  • 我们使用amondnet/vercel-action@v25动作部署到 Vercel。根据哪个分支被更新,它将被部署到预览环境或生产环境。

我们的工作流程应该看起来像这样:

图 9.6 – 工作流程

图 9.6 – 工作流程

我们可以在仓库页面右下角跟踪每个环境的部署状态:

图 9.7 – 部署状态

图 9.7 – 部署状态

太棒了!我们的应用程序现在已投入生产并可供用户使用。配置管道可能需要初始时更多的努力,但从长远来看,它可以节省大量时间,因为我们不必担心所有这些步骤。它们都已经自动化了。

摘要

在本章中,我们了解到 CI/CD 管道是一个允许自动化代码更改和交付的过程。我们还介绍了 GitHub Actions 以及允许我们创建 CI/CD 管道以自动化测试和部署我们应用程序的各个部分。

之后,我们为工作流程定义了三个作业。通过这些作业,我们自动化了运行所有必需的检查、测试和部署的过程。最后,我们学习了如何从 CI/CD 管道部署到 Vercel 并将应用程序交付给用户。

这标志着我们应用程序 MVP 版本的完成。在下一章中,我们将介绍一些我们可以对应用程序进行的潜在功能和改进。

第十章:超越

我们的应用程序最终投入生产。可能在我们说话的时候,它已经有了用户。然而,就像每一件软件一样,我们的应用程序可能永远不会完全完成。总有改进的空间,而且由于我们构建的应用程序只是一个 MVP,有很多潜在的改进值得提及。

在本章中,我们将涵盖从功能和技术角度的一些最重要的改进。这些主题可能会给我们一些关于扩展和改进现有应用程序的想法。

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

  • 功能改进

  • 技术改进

  • 附录

到本章结束时,我们将介绍一些可以添加到现有应用程序中的功能,使其更加完整。我们还将提及一些本书未涉及但值得自己探索的主题。

功能改进

由于我们的应用程序目前处于 MVP 阶段,从用户的角度来看,有许多潜在的改进可以使应用程序更加易用。

工作功能改进

工作功能是本应用最重要的功能。我们可以实施一些改进来使应用程序变得更好:

  • 更新工作

  • 以草稿状态添加工作

  • 删除工作

  • 使用 markdown/ WYSIWYG 编辑器添加/更新工作信息

更新工作

目前,我们的应用程序仅支持工作创建。当我们想要更改给定职位发布的信息时会发生什么?如果在创建后能够编辑工作数据,那将非常棒。

这里是我们如何做到这一点的说明:

  • PATCH /jobs/:jobId 创建 更新 端点处理程序,该处理程序将更新数据库中的数据

  • /dashboard/jobs/:jobId/update 创建 更新工作 页面,这是更新表单应该放置的地方

  • 创建 更新 表单,包含我们想要能够更新工作所需的所有字段

  • 在成功提交后,我们应该使工作查询无效,以便重新获取其数据

以草稿状态添加工作

目前,当我们为我们的组织创建一个工作,它将立即对公众可用。然而,如果我们能扩展其功能,以便我们可以选择何时将职位发布给公众,那将非常棒。

这可以通过以下方式完成:

  • 使用 status 属性扩展工作模型。

  • status 值设置为 draftpublished

  • 在提交工作创建表单时,新创建的工作将最初具有 draft 状态。

  • 然后,我们可以通过 更新 表单更新工作的状态,在那里我们发送期望的状态作为值。另一种我们可以做到的方法是公开一个单独的端点,该端点只会更新工作的状态。

删除工作

大多数时候,职位空缺会被关闭。在这种情况下,没有人想要一个不再相关的职位发布,因此允许组织管理员删除不再相关的职位可能是个好主意。

这可以通过两种方式实现:

  • 有一个删除端点,将处理从数据库中删除职位。点击按钮会发送请求,在请求成功的情况下,将用户重定向到职位列表。

  • 扩展status属性,现在它可能具有额外的archiveddeleted值。这种方法被称为软删除,因为我们并没有从数据库中删除条目,但从应用程序的角度来看,它看起来就像被删除了。存档职位发布可能有助于跟踪以前招聘的不同统计数据。

使用 Markdown/WYSIWYG 编辑器添加/更新职位信息

目前,职位信息是通过textarea输入字段填充的,这对于纯文本值来说很方便。然而,管理员添加尽可能多的信息的能力仅限于文本。

如果我们能够允许管理员添加诸如不同的标题、列表、链接等内容到职位信息中,那么职位发布将提供尽可能多的信息。

解决方案是将textarea输入字段替换为富文本编辑器,这将使我们能够添加不仅仅是文本的内容。只需确保在提交之前对输入进行清理,以使申请尽可能安全。

组织改进

目前,组织管理员无法更新组织信息。组织应该能够随时更改任何信息。

要实现这一点,我们可以做以下事情:

  • PATCH /organizations/:organizationId创建更新组织的端点

  • /dashboard/organization/update创建一个页面,我们可以在这里填写更新表单

添加职位申请

我们还可以改进的一点是添加职位申请的能力。

目前,没有直接在应用程序中申请职位的机制。当用户点击申请按钮时,电子邮件客户端会打开,并设置正确的主题。然后,用户会向组织的电子邮件地址发送电子邮件,这就是整个流程。

要将其提升到下一个层次,我们可以创建另一个名为Application的实体,当用户申请工作时将提交此实体。这种方法将允许管理员跟踪其组织的职位申请。

让我们重新思考一下,使用这个新功能,应用程序的数据模型将看起来是什么样子:

图 10.1 – 数据模型中的应用

图 10.1 – 数据模型中的应用

如我们所见,申请应包含有关候选人的基本信息、一条消息、面试官的报告等等。

一旦数据模型被更新,我们可以构建应用程序功能,这将处理所有相关事务。这包括以下内容:

  • 创建和浏览应用的端点。

  • 仪表板上的页面,管理员可以浏览所有应用。它们可以定义为 /dashboard/applications/dashboard/applications/:applicationId,分别对应列表和详情页面。

过滤和分页数据列表

在表格中显示数据列表是好的,但当条目数量开始显著增长时会发生什么?一次性加载所有条目并不是很优化,因为一开始可能并不需要所有条目。

为了优化数据列表,我们可以添加对过滤和分页数据的支持。这将帮助用户缩小搜索结果,以满足他们的需求。过滤和分页都应该在服务器上发生。

应该通过 URL 参数处理当前筛选和分页值。这将使应用程序能够轻松地深度链接搜索结果以供进一步使用。

添加用户注册

这一点相当直接。到目前为止,我们一直依赖于测试数据,其中有一个测试用户,我们用它来登录仪表板。然而,没有方法可以注册新用户。如果我们想使这个应用程序被多个组织使用,我们应该添加这个功能。这可以通过以下方式实现:

  • 在 POST /auth/register 上创建注册端点,它将从表单中获取所需数据并创建用户及其对应组织在数据库中的记录

  • /auth/register 创建注册页面,其中包含注册表单,提交后调用注册端点

技术改进

我们的应用状态良好,但在应用开始增长时,有几件事情应该牢记在心。让我们看看。

服务器端渲染和缓存

我们可以进一步优化如何在服务器上渲染公共页面,我们可以做出以下改进。

目前,我们正在每个请求上渲染页面,如果数据频繁更改,这是好的;否则,它可能会增加加载时间和服务器成本,因为服务器上的渲染是一个计算密集型操作。

幸运的是,Next.js 支持另一种名为 增量 静态重新生成 的渲染策略。

它的工作方式如下:

  1. 用户 1 请求一个页面。

  2. 服务器返回缓存的页面版本并将其返回。

  3. 在那次请求期间,Next.js 被触发以使用最新数据重新生成相同的页面。

  4. 用户 2 请求一个页面。

  5. 服务器返回页面的新版本。

以我们的公共工作详情页面为例,它的工作方式如下。

首先,我们需要使用 getStaticPaths 来生成所有工作的所有路径:

export const getStaticPaths = async () => {
  const jobs = await getJobs();
  const paths = jobs.map((job) => ({
     params: { jobId: job.id }
  }));
  return { paths, fallback: true };
}

这将为数据库中存在的所有作业生成路径列表。这里的关键是fallback属性,它将使 Next.js 不返回 404 页面,而是尝试生成一个新的页面。

我们还必须将getServerSideProps替换为getStaticProps,其外观可能如下所示:

export const getStaticProps = async ({
  params,
}: GetStaticPropsContext) => {
  const jobId = params?.jobId as string;
  const job = await getJob({ jobId });
  return {
    props: {
      job
    },
    revalidate: 60,
  };
};

注意我们如何可以将revalidate属性添加到return值中。这将强制页面在 60 秒后重新验证。

由于作业和组织的数据变化不是很频繁,这种渲染策略从长远来看听起来更优,尤其是在请求数量开始增加之后。

这在性能和数据新鲜度之间提供了一个良好的折衷方案。

React Query 的 SSR 解冻

目前,我们正在使用 React Query 来处理客户端的数据获取,但服务器端的数据获取则没有使用它。我们只是在页面上获取数据并传递和渲染它。如果我们没有很多层级的组件,这没问题,但还有更好的方法来做这件事。

React Query 支持两种在服务器上获取数据并将其传递到客户端的方式:

  • 在服务器上获取数据,然后将其作为initialData传递给查询

  • 在服务器上预取,解冻缓存,并在客户端重新解冻

第一种方案适用于较小型的应用,其中组件之间没有非常复杂的层次结构,因此没有必要将服务器数据向下传递多个层级到所需的查询。

第二种方案可能需要更多的初始设置,但最终会使代码库变得更加简单。

pages/_app.tsx文件中,我们应该将QueryClientProvider内部的任何内容都包裹在Hydrate中,如下所示:

import { Hydrate, QueryClient, QueryClientProvider }
  from '@tanstack/react-query'
export const App = ({ Component, pageProps }) => {
  const [queryClient] = React.useState(() => new
    QueryClient())
  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={pageProps.dehydratedState}>
        <Component {...pageProps} />
      </Hydrate>
    </QueryClientProvider>
  )
}

这将使应用程序准备好处理任何解冻状态。但我们如何向页面提供解冻状态呢?

在特定页面上,我们可以修改getStaticPropsgetServerSideProps,如下所示:

export const getServerSideProps = async () => {
     const queryClient = new QueryClient()
  await queryClient.prefetchQuery(['jobs'], getJobs)
  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  }
}

然后,我们可以像在客户端获取它们一样消费作业:

const JobsPage = () => {
     const jobs = useJobs();
     // ...
}

这将使使用 React Query 处理所有服务器状态变得更加容易。

使用查询键工厂

当查询的数量开始增加时,整个应用中遍布许多查询可能会变得难以管理。跟踪所有查询的变体及其使用位置可能很困难。防止重复查询键可能也是一个问题。

正因如此,我们应该考虑使用查询键工厂而不是故意在各个地方添加查询键。

我们可以在src/lib/react-query.ts中定义所有潜在键:

首先,我们可以定义工厂的简化版本:

const getQueryKeys = (baseKey: string) => {
  return {
    all: [baseKey],
    many: (params: Record<string, unknown>) => [baseKey,
      params],
    one: (id: string) => [baseKey, id],
  };
};

然后,我们可以为查询创建键:

export const queryKeys = {
  auth: {
    authUser: ['auth-user'],
  },
  jobs: getQueryKeys('jobs'),
  organizations: {
    one: getQueryKeys('organizations').one,
  },
};

如您所见,并非所有功能都具有相同的键结构,但我们可以结合不同的工厂来创建所需的内容。

然后,如果我们想在查询中使用一个键,可以这样做:

const useJobs = () => {
     const { data, isLoading } = useQuery({
    queryKey: queryKeys.jobs.many(params),
    queryFn: () => getJobs({ params }),
    enabled: !!params.organizationId,
    initialData: [],
  });
  //...
}

这种方法的优点是我们对所有密钥有一个集中的概览,这减少了因误输入密钥或类似情况而犯错的概率。

这是一个简化查询密钥工厂的例子。如果您需要一个更健壮的解决方案,有一个非常好的库可以在www.npmjs.com/package/@lukemorales/query-key-factory找到。

代码脚手架

当查看我们的应用程序时,我们可能会注意到存在一定程度的样板代码。例如,创建组件需要这样一个文件夹:

- my-component
     - index.ts
     - my-component.tsx

我们必须记住从index.ts重新导出组件,使其可用。

对于 API 请求也可以这么说。我们需要创建请求函数,然后是消费它的钩子。这些事情可以通过帮助我们通过 CLI 更容易生成这些类型文件的工具来自动化。

拥有一些脚手架工具,如 Plop.js 和 Hygen.io,也给代码库带来了更好的一致性。

使用 Zod 验证表单输入和 API 响应

让我们简要地谈谈验证。通过验证,我们想确保数据处于预期的形式。对于我们的应用程序,我们可以验证表单输入和 API 响应。

对于验证,我们可以使用 Zod,这是一个以 TypeScript 为先的出色验证库。这意味着我们可以定义一个模式,从中我们可以推断出我们可以使用的类型。

表单输入验证

react-hooks-form库为 Zod 提供了很好的支持,我们可以利用它来做这件事。以当前的登录表单为例,我们可以修改它使其看起来像这样:

import { z } from 'zod';
import { yupResolver } from '@hookform/resolvers/yup';
const schema = z.object({
  email: z.string().min(1, 'Required'),
  password: z.string().min(1, 'Required'),
});
const LoginForm  = () => {
     const { register, handleSubmit } = useForm({
          resolver: yupResolver(schema);
     })
     // ...
     return (
          <Stack
      as="form"
      onSubmit={handleSubmit(onSubmit)}
      spacing="5"
      w="full"
    >
      <InputField
        label="Email"
        type="email"
        {...register('email')}
        error={formState.errors['email']}
      />
      <InputField
        label="Password"
        type="password"
        {...register('password')}
        error={formState.errors['password']}
      />
      <Button
        isLoading={login.isLoading}
        isDisabled={login.isLoading}
        type="submit"
      >
        Log in
      </Button>
    </Stack>
     )
}

这里,我们正在创建一个对象模式,并借助yupResolver将其提供给useForm

这将确保表单只有在所有字段都有有效值的情况下才会提交。

API 请求验证

我们确实有 TypeScript 类型,但它们不能保护我们免受运行时错误的影响。这就是为什么在某些情况下我们应该考虑验证 API 响应。让我们看看以下例子:

import { z } from 'zod';
const JobSchema = z.object({
     position: z.string(),
     info: z.string(),
     location: z.string()
});

由于 Zod 是一个以 TypeScript 为先的库,我们可以用它来推断给定对象的形状类型:

type Job = z.infer<typeof JobSchema>

这可能有助于减少重复的类型定义。最后,我们可以按照以下方式验证我们的请求:

const getJob = async () => {
     const jobResponse = await apiClient.get('/jobs/123');
     const job = JobSchema.parse(jobResponse);
     return job;
}

如果任何作业属性与模式不匹配,Zod 将抛出一个运行时错误,然后我们可以妥善处理。

Next.js 13

Next.js 13 即将到来!它最近发布,带来了一些重大变化,包括以下内容:

  • 带有应用文件夹的新路由系统

  • 服务器组件

  • 新的数据获取方法

值得注意的是,它与旧版本向后兼容,因此它允许增量升级。可能需要一些时间来完善所有内容,但值得关注,并在某个时候升级到新方法。

附录

有几个主题与我们所构建的应用程序没有直接关系,但它们值得提及。

GraphQL

在当今,拥有 GraphQL API 是非常普遍的,尤其是在微服务架构中。我们在我们的应用程序中使用了 REST API,但如果它是 GraphQL API,我们将如何构建我们的 API 层?

好吧,实现将非常相似。我们可以选择使用不同的库,例如 Apollo,但我们将坚持使用 React Query。

看以下请求:

import { request, gql } from "graphql-request";
import { useQuery } from '@tanstack/react-query';
const jobsQuery = gql`
     query {
          jobs {
               data {
                    position
                    department
                    location
               }
          }
     }
`;
const getJobs = () => {
     return request('/api/graphql', jobsQuery);
};
const useJobs = () => {
     const { data, isLoading } = useQuery({
          queryKey: ['jobs'],
          queryFn: getJobs
     })
     // ...
};

如您所见,首先,我们定义了 GraphQL 查询,然后我们使用它来定义请求函数。最后,我们使用请求函数来创建useJobs钩子。

单仓库

单仓库是一个包含多个项目且这些项目之间有明确关系的 Git 仓库。这意味着一个好的单仓库设置应该提供以下功能:

  • 项目间易于代码共享

  • 项目约束和可见性

  • 计算缓存

  • 项目清晰的边界

值得探索单仓库,因为它们被用于一些最大的软件项目中,并使这些大型项目更容易管理。

一些最受欢迎的单仓库工具有以下:

  • Lerna

  • Nx

  • Turborepo

  • Yarn 工作空间

微前端架构

微前端架构是一个非常有趣的概念。这意味着我们可以将应用程序的组件作为独立的应用程序构建和部署,它们看起来和感觉就像它们是同一应用程序的一部分。

使用这种架构的一些好处如下:

  • 当在一个拥有许多不同团队的平台工作时很有用。

  • 不限制应用程序使用特定的技术。每个微前端应用程序都可以有不同的堆栈,并且它们可以真正地很好地协同工作。

然而,也有一些缺点:

  • 尽管使用不同的技术构建微前端架构是可能的,但应该予以劝阻。最好选择一个框架,并制定应用程序构建的标准。

  • 微前端架构需要更复杂的工具,对于大多数用例来说可能并不值得。

一些值得探索的工具如下:

  • 模块联邦

  • 单 SPA

摘要

在这一章中,我们探讨了在完成这本书之后值得探索的其他主题。比如功能改进和技术改进可以将您的应用程序提升到下一个层次。希望您能将在这里学到的知识应用到类似的真实世界场景中。