Flask-和-React-全栈开发-全-

Flask 和 React 全栈开发(全)

原文:zh.annas-archive.org/md5/94d6a245cda033cba86eab2eb23510f1

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

《全栈 Flask 和 React》旨在成为你掌握使用动态组合——React 和 Flask——进行全栈式网页开发艺术的终极指南。

不论你是想同时学习前端和后端开发的求知欲旺盛的学习者,还是想扩展技能集的经验丰富的开发者,这本书的结构旨在带你踏上构建现代网页应用的旅程。

在当今这个数字时代,构建能够与用户交互、动态响应且无缝适应用户交互的网页应用至关重要。Flask 是一个用 Python 编写的开源微型网页框架,它促进了快速开发并采用了干净且实用的设计方法。另一方面,React 是一个用于构建用户界面的 JavaScript 库,它提供了一种高效且灵活的方式来创建交互式 UI。通过结合 React 和轻量级的 Flask 框架的力量,你将能够轻松地创建现代全栈式网页应用。

在这本书中,我们将首先探讨以组件驱动的开发模式 React,并探索 React 的基础知识。我们将探讨诸如组件、属性和状态、JSX、使用 React Hooks 管理状态、React 中的数据获取、显示列表、事件处理、使用 React Router 进行路由以及使用 Jest 进行单元测试等概念。然后,我们将继续使用 Flask 进行全栈应用开发。

我们将涵盖从 SQL 和数据建模到创建 RESTful API、将 React 客户端与 Flask 后端集成、身份验证和授权、使用 Blueprint 构建模块化和可重用 Flask 应用、错误处理、Flask 中的单元测试,以及了解你的网页应用如何部署到云端的各个方面。我们将把复杂的概念分解成小块,提供清晰的解释,并提供实用的示例,以确保你在学习过程中掌握每个主题。无论你更喜欢通过实践学习,还是喜欢深入研究这些技术的内部工作原理,我们都为你提供了相应的支持。

这本书面向的对象

这本书非常适合对全栈式网页开发技能和知识感兴趣的开发者。无论你是想学习 React 进行前端开发的后端开发者,还是想学习 Flask 进行服务器端开发的前端开发者,这本书都将为你提供必要的指导和实用的示例,帮助你精通全栈式开发。

为了从这本书中获得益处,需要具备基本的 HTML、CSS、JavaScript 和 Python 知识。

为了从这本书中获得益处,需要具备基本的 HTML、CSS、JavaScript 和 Python 知识。

本书涵盖的内容

本书分为 16 章,每章都专注于你需要掌握的、成为精通全栈 Flask 网页开发与 React 所必需的基本概念和实践技巧。

让我们更深入地看看这些章节:

第一章使用 React 和 Flask 准备全栈开发,为你全栈开发之旅奠定基础。我们将讨论使用 React 和 Flask 开发 Web 应用程序的原因。你将学习如何设置你的开发环境、安装 React 和 Flask。你还将学习 Git 的源代码版本控制和本书中我们将构建的项目。

第二章React 入门,介绍了 React 的基础知识,包括探索 React 项目结构、组件、props 和状态。我们将讨论在 React 中经常使用的 JavaScript 概念,如解构、箭头函数和默认及命名导出。你将为创建 React 应用程序打下坚实的基础。

第三章使用 React Hooks 管理状态,深入探讨了 React Hooks 的强大功能,如useStateuseEffectuseContextuseRefuseMemouseCallbackuseReducer。你将发现 Hooks 如何简化状态管理,并使你能够创建更可重用和可扩展的代码。

第四章使用 React API 获取数据,专注于在 React 应用程序中获取数据。你将探索使用 React Query、async/await 语法、Fetch API 和 Axios 的不同技术;处理加载和错误状态;并实现缓存以高效检索数据。

第五章JSX 和 React 中的列表显示,涵盖了在 React 中显示动态列表。你将学习如何使用 JSX 作为连接 JavaScript 和 HTML 的桥梁,在 JSX 中实现嵌套列表,在 JSX 中遍历对象,并高效地处理 React 中的事件。

第六章使用 React Router 和表单,指导你处理表单输入和验证、表单提交,以及使用 React Router 实现客户端路由的过程。

第七章React 单元测试,教你如何使用 Jest 和 React Testing Library 为 React 组件编写全面的单元测试。你将对你代码的质量和可靠性充满信心。

第八章SQL 和数据建模,向你介绍 SQL 和数据库建模。你将学习如何设置数据库、执行 CRUD 操作,并为你的应用程序设计高效的数据模型。

第九章API 开发和文档,深入 Flask API 开发的领域。你将理解 RESTful API 概念,实现 CRUD 操作,并有效地记录你的 API。

第十章将 React 前端与 Flask 后端集成,专注于建立 React 客户端与 Flask 后端之间的通信。你将学习如何无缝处理 API 调用和请求。

第十一章在 React-Flask 应用程序中获取和显示数据,探讨了在全栈 React-Flask 应用程序中获取和显示数据的过程。你将学习如何在 Flask-React 应用程序中处理 CRUD 操作。你还将学习如何在 Flask 应用程序中处理分页。

第十二章身份验证和授权,涵盖了用户身份验证和授权的基本主题。你将实现安全的登录和注册功能,识别系统用户并管理他们的信息,了解会话管理,创建受密码保护的资源,在 Flask 中实现闪存消息,并确保你应用程序的安全性。

第十三章错误处理,深入探讨了 Flask 应用程序中的有效错误处理技术。你将了解不同类型的错误以及如何使用 Flask 调试器,创建错误处理器,创建自定义错误页面,跟踪应用程序中的事件,并向管理员发送错误邮件。

第十四章模块化架构 – 蓝图的威力,介绍了 Flask 中的模块化架构和蓝图的概念。你将学习如何将你的代码库组织成可重用的模块,并创建可扩展的应用程序结构。

第十五章Flask 单元测试,探讨了单元测试在 Flask 应用程序中的重要性。你将发现为你的 Flask 组件编写全面测试的技术,确保你后端的健壮性。

第十六章容器化和 Flask 应用程序部署,通过涵盖 Flask 应用的部署和容器化来结束本书。你将学习如何在服务器上部署你的应用程序,并利用容器化技术进行高效的部署。

每一章都精心设计,以提供清晰的解释,以开发交互性强且高效的快速企业级网络应用程序。你将深入理解 React 和 Flask,并成为开发全栈网络应用程序的专家。

我们希望这本书能成为你在成为熟练的全栈网络开发者旅程中的必备 definitive 指南。不要犹豫去尝试、提问,并探索我们涵盖主题的边界之外。这本书是一个起点,但可能性是无限的。让我们深入其中,解锁 全栈 Flask Web 开发 与 React 的无限潜力!

为了充分利用这本书

我们建议使用 Python 的最新版本。Flask 支持 Python 3.8+,React,以及 Node.js 16+,并且需要安装 Docker 23+。本书中的所有代码和示例都使用 Flask 2.3.2 和 React 18.2 进行测试。

本书涵盖的软件/硬件 操作系统要求
Python Windows, macOS, 或 Linux
React Windows, macOS, 或 Linux
Flask Windows, macOS, 或 Linux
PostgreSQL Windows, macOS, 或 Linux
Docker Windows, macOS, 或 Linux
JavaScript Windows, macOS, 或 Linux

如果您正在使用本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/Full-Stack-Flask-and-React。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他丰富的图书和视频的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

使用的约定

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

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“设置完成后,导航到 Bizza 文件夹。”

代码块设置如下:

bizza/--node_modules/
--public/
----index.html
----manifest.json

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

function App() {  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.

任何命令行输入或输出应如下所示:

$ node -v$ npm -v

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“登录并点击 仓库。”

小贴士或重要提示

看起来是这样的。

联系我们

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

一般反馈:如果您对本书的任何方面有疑问,请通过 customercare@packtpub.com 给我们发邮件,并在邮件主题中提及书名。

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

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

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

分享您的想法

一旦您阅读完全栈 Flask 和 React,我们非常乐意听到您的想法!请点击此处直接进入该书的亚马逊评论页面并分享您的反馈。

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

下载此书的免费 PDF 副本

感谢您购买此书!

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

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

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

优惠不仅限于此,您还可以获得独家折扣、时事通讯和丰富的免费内容,每天直接发送到您的收件箱

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

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

packt.link/free-ebook/9781803248448

  1. 提交您的购买证明

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

第一部分 – 使用 React 进行前端开发

欢迎来到本书的第一部分。在本部分中,我们将开始一段激动人心的旅程,探索 React 前端开发的世界。在本节中,我们将深入研究构成现代 Web 开发 React 库核心概念和技术的基本概念。

您将学习到关键原则和最佳实践,这将使您能够构建动态和交互式的用户界面。我们将涵盖核心概念,从设置您的开发环境到创建可重用组件以及在 React 中管理状态。

本部分包含以下章节:

  • 第一章, 使用 React 和 Flask 准备全栈

  • 第二章, React 入门

  • 第三章, 使用 React Hooks 管理状态

  • 第四章, 使用 React API 获取数据

  • 第五章, JSX 和 React 中的列表显示

  • 第六章, 使用 React Router 和表单进行工作

  • 第七章, React 单元测试

第一章:使用 React 和 Flask 准备全栈开发

首个网站创造者,蒂姆·伯纳斯-李爵士,设想了互联网作为一个开放平台,将允许互联网用户在没有地理和文化限制的情况下共享信息、获取机会和协作。有趣的是,软件开发者正在创新地推动这一使命的实现。

作为开发者,我们使功能丰富的 Web 应用程序成为可能,对全球的个人和企业产生积极影响。除了共享信息外,互联网已经从单纯的静态网页转变为动态和数据库驱动的 Web 应用程序。Web 技术专家正在提出新的工具和技术,使互联网上信息的访问变得无烦恼且方便。

到本章结束时,你将更好地理解在客户端-服务器架构背景下全栈 Web 开发。我们将讨论 Web 应用程序的前端与数据库驱动的后端之间存在的重大交互。

拥有这些技能集将使你进入全栈 Web 开发者的名人堂。这包括从零开始启动 Web 应用程序开发项目并将其转变为完整 Web 应用程序所需的所有知识。无论你是独立开发者还是在协作团队中担任开发者角色,全栈 Web 开发的知识将增强你高效执行的能力。此外,你将拥有灵活性,以适应团队设置中分配的任何角色。

此外,我们将深入了解使用 React 的原因,这是一个用于构建 Web 应用程序用户界面的 UI 库。你将简要了解 React 的世界以及为什么 React 对于构建复杂的现代 Web 应用程序界面组件至关重要,这些组件使用户能够拥有流畅的体验。

开发 Web 应用程序需要设置开发环境。在全栈 Web 应用程序开发中,前端和后端有各自独立的开发环境。我们将讨论如何设置 React 作为前端以及 Flask 作为后端技术,以支持基于服务器的处理和数据库交互。

此外,我们还将深入了解如何准备使用Git,这是一个源代码版本控制工具,帮助开发者跟踪代码库的变化。你应掌握足够的基本知识,以便启动将代码部署到GitHub,一个在线版本控制平台。

在这个技术创新和创意软件开发盛行的时代,源代码版本控制是开发的一个基本组成部分。它促进了软件开发者之间的协作,以解决开源或商业项目中的问题。

我们将以讨论本书中将要构建的实战项目Bizza来结束本章。该项目将带你从前端 Web 应用的角度,到数据库驱动的后端,连接到 REST API 层以促进通信。

因此,无需多言,让我们开始体验使用两个热门技术栈——ReactFlask——的全栈 Web 应用开发世界。到本书结束时,你将能够开发全栈应用。

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

  • 全栈 Web 开发的介绍

  • 我们为什么选择 React?

  • 我们为什么选择 Flask?

  • 准备使用 Git

  • 我们在构建什么?

技术要求

本章的完整代码可在 GitHub 上找到:

github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter01

全栈 Web 开发的介绍

现代 Web 应用复杂且快速演变。商业社区的需求和系统要求正在推动软件开发者超越仅作为前端或后端开发者的能力。Web 开发者开发全栈应用的能力现在比以往任何时候都更加重要,并且正在上升。

在本书中,我们将专注于全栈 Web 开发,这指的是 Web 开发的客户端和后端部分。前端,有时被称为客户端,是用户可以看到并与之交互的任何 Web 应用的可见部分。后端,有时被称为服务器端,是程序员代码所在的部分,与数据库和其他服务器基础设施相结合。

精通客户端(前端开发)和服务器端(后端开发)的 Web 开发者通常被称为全栈开发者。

在本书中,我们将使用 React 作为库来开发直观的用户界面UI)或前端,以及使用微框架Flask 来构建后端业务逻辑组件。

让我们更仔细地看看这些工具以及我们选择它们的原因。

我们为什么选择 React?

为 Web 应用构建用户界面是 Web 开发的一个基本部分。有趣的是,大多数 Web 开发者发现很难选择最合适的 JavaScript 前端库或框架来构建用户界面。稍后我们将看到为什么选择 React 将有助于你的职业发展和项目。

React 是一个流行的开源库,Meta Platforms(前身为 Facebook)的出色开发者社区正在积极维护它。根据 Stack Overflow 2021 开发者调查 报告,React 是最常用的库,其中 41.4% 的专业开发者表示他们在过去一年中使用了 React (insights.stackoverflow.com/survey/2021#section-most-popular-technologies-web-framewors)。那么,React 的争议在哪里呢?

React 简单易用,可以开发丰富的交互式用户界面。你可以立即开始为你的网络项目构建可重用的界面组件。它也易于学习,正如你在本书中将要开始的 frontend 项目实现中将会看到的那样。如果你已经熟悉 JavaScript,学习 React 真的非常简单,因为 React 以 JavaScript 为中心。

React 能够与互联网共存这么长时间的主要原因是它在像 FacebookUber EatsSkypeNetflix 这样的技术巨头中的使用。此外,作为一个库,React 专注于构建 UI 组件——仅此而已。它以组件为基础的方法开发 Web 和移动应用程序,使其在开发者中非常受欢迎。

React 中对 文档对象模型DOM)的进一步抽象化,称为虚拟 DOM,提高了 React 应用程序中的效率和性能。React 使用一种特殊的语法,称为 JavaScript XMLJSX),允许你在 JavaScript 中编写 HTML 元素,这与在 HTML 元素中放置 JavaScript 的传统做法相反。

你不必学习像 Handlebars、EJS 和 Pug 这样的复杂模板语言。JSX 帮助你使用熟悉的 HTML 语法编写 React 组件,这得益于一个名为 Babel 的转换器。一些 JavaScript 框架非常具有意见性——例如 Angular、Ember.js 和 Vue。这些框架有严格的构建 Web 应用程序的结构方式,与 React 不同,React 给你选择库、架构和工具的自由和灵活性。

此外,如果你对开发移动应用程序感兴趣,React Native 可以是一个非常有价值的工具。你对 React 及其组件的了解,这些组件可以无缝地与原生视图集成,使你能够高效地创建 Android 和 iOS 应用程序。

现在,让我们动手设置 React 环境。

使用 React 设置开发环境

在本节中,你将为本书中我们将构建的 React 应用程序项目设置开发环境。

要在你的本地机器上编码和测试 React 应用程序,你需要采取以下几步:

  1. 安装 Node.js:

    1. 要下载和安装 Node.js 的稳定版本,请访问 nodejs.org/en/

    Node.js 是 JavaScript 及其扩展(如 React 应用程序)的运行时开发环境。Node.js 随附一个命令行实用工具和包管理器,称为 Node 包管理器NPM)。Node.js 和 NPM 是成功构建和运行任何 React 应用程序所需的工具。

    1. 点击并下载为大多数用户推荐的版本。按照安装步骤进行安装。

    2. 要检查 Node.js 是否成功安装,请在您的命令提示符中输入以下内容:

      $    node -v
      
    3. 要检查 NPM 的版本,请在终端或 Windows 的命令提示符 (cmd) 中输入以下内容:

      $    npm -v
      

    以下截图显示 node 和 npm 正在运行。

图 1.1 – 显示 Node.js 和 NPM 正在工作的截图

图 1.1 – 显示 Node.js 和 NPM 正在工作的截图

  1. 安装 Visual Studio CodeVS Code)。

    VS Code 是一个免费的代码编辑器,您可以使用它来构建和调试网络应用。VS Code 编辑器的即点即用的代码方法使其成为开发的一个优秀工具。VS Code 具有内置的 IntelliSense 代码补全和代码重构功能。VS Code 中的第三方扩展和数百种网络技术工具可以让您更高效地工作。

注意

开发者还有其他代码编辑器可供选择,但 VS Code 非常推荐。

  1. 安装 Git 客户端。

    Git 客户端是用于与 Git 仓库交互的命令行界面。在本章的后面部分将有更多关于 Git 的内容。我们需要这个工具来跟踪项目文件中的更改。要安装 Git 客户端,从 git-scm.com/downloads 下载它:

    1. 选择您的 操作系统OS) 类型并安装软件。

图 1.2 – Git 下载页面的截图

图 1.2 – Git 下载页面的截图

  1. 要测试您是否成功安装了 Git,请在您的系统命令提示符中输入以下内容:
$    git --version

图 1.3 – 显示 Windows 中 Git 客户端版本的截图

图 1.3 – 显示 Windows 中 Git 客户端版本的截图

我们现在已经为将要构建的 React 应用程序设置了开发环境。这完成了前端开发环境。让我们也为 Flask 做同样的事情,并深入探讨为什么你需要选择 Flask 来构建你的后端。

为什么我们应该选择 Flask?

Flask 是一个用于开发现代 Python 网络应用的简约框架。它是一个构建企业级、可扩展、可靠和可维护应用的优秀工具包。

此外,该框架易于学习。Flask 没有开发者必须使用的样板代码,与许多替代框架(如 Django)不同。它核心上绝对轻量。作为微框架的 Flask 只为开发者提供构建网络应用的起始组件,而 Django 则倾向于建议你使用其框架内完整的一套齿轮或组件以某种结构来构建你的网络应用。

使用 Flask,开发者有惊人的自由度来选择他们的数据库、模板引擎和部署流程;他们还可以决定如何管理用户、会话、网络应用和安全。

Flask 的可扩展性促使一些科技公司迁移到 Flask 以高效地实施他们的微服务基础设施。微服务是一个小型、独立且松散耦合的软件组件,专注于在更大的应用架构中执行特定功能。

微服务就像拥有一支由专家组成的团队,每个专家专注于特定任务,和谐地共同工作以创造惊人的成果。正如你可能会同意的,云计算已经不可逆转地改变了应用开发和部署。云计算中的规模科学正在使其成为初创公司和企业的常态。Pinterest 就是这样一个例子。

Pinterest 是世界上访问量最大的网站之一。它是一个图片分享和社交媒体服务平台。根据 Statista 的数据,截至 2021 年第四季度,Pinterest 平均每月有 4.31 亿活跃用户 (www.statista.com/statistics/463353/pinterest-global-mau/)。他们最初使用 Django 框架启动平台,后来选择了 Flask 来开发他们的 API 并构建一个更稳定的微服务架构。

Flask 仍然是 Pinterest,一个高流量社交网络应用的主要核心后端技术。总的来说,在 Flask 中开发 API 和集成各种数据库更容易。你可以凭借这样的简单性和灵活性保证来信赖它。如果你精通 Python,那么你应该能够轻松地为 Flask 应用做出贡献。

Flask 更少地强加观点,因此需要学习的标准更少。相反,Django 提供了你开发网络应用所需的一切——一个盒子里的完整解决方案。然而,扩展问题是最有经验的 Python 开发者在他们的 Django 项目中必须处理的问题。

当你在项目中实施现成的解决方案时,你将不得不处理一个庞大的 Django 框架,这可能会对你的项目上市时间和性能产生负面影响。

当你在项目中结合这些经过实战考验的技术栈,React 和 Flask,你可以确信在可扩展性、可靠性、可重用性、可维护性以及安全的网页和移动应用开发方面将获得开发上的收益。

在本节中,我们讨论了为什么你应该将 React 和 Flask 添加到你的网络应用程序开发工具包中。

使用 Flask 设置开发环境

如果你想在本地计算机上使用 Flask 作为后端框架来开发网络应用程序,你需要安装 Python 和一些其他包。在本节中,我们将设置 Flask 开发环境。为此,请按照以下步骤操作:

  1. 安装 Python。

    $    python –version
    

    你将得到以下输出:

图 1.4 – 显示 Python 版本的截图

图 1.4 – 显示 Python 版本的截图

或者,你可以使用以下命令:

$    python -c "import sys; print(sys.version)"

注意

在 macOS 或 Linux 中,Python3 —version 命令也可以用来检查 Python 版本号,以及扩展到 Python 安装。

如果你的计算机系统上尚未安装 Python,请访问 www.python.org/downloads/,选择适合你操作系统的最新版本,并在你的系统上下载和安装它。

  1. 更新 pip

    pip,在你的终端中输入此命令:

    $      python -m pip install --upgrade pip
    
  2. 创建虚拟环境。

python3 -m venv venv to explicitly specify Python 3 to create the virtual environment.

对于 Windows 用户,如果你遇到问题,请尝试输入以下内容:

$      py -m venv venv

注意

根据你本地机器上的 Python 版本,使用 venv 用于 Python 3,使用 virtualenv 用于 Python 2。

  1. 在 Windows 中激活虚拟环境:

    $    venv\Scripts\activate
    

注意:

如果执行命令 $ venv\Scripts\activate 没有按预期工作,我建议读者尝试使用 $ venv\Scripts\activate.bat

在 macOS 或 Linux 上激活虚拟环境:

$    source venv/bin/activate
  1. 安装 Flask:

    $    pip install flask
    

    以下截图显示了 Flask 安装命令的操作。

图 1.5 – 显示 Flask 安装终端命令的截图

图 1.5 – 显示 Flask 安装终端命令的截图

  1. 要测试 Flask 开发环境,在你的项目目录中创建一个名为 app.py 的文件。在 VS 代码编辑器中打开 app.py 文件,并粘贴以下内容:

    from flask import Flaskapp = Flask(__name__)@app.route('/')def index():    return 'Welcome to Bizza Platform!'if __name__ == '__main__':    app.run()
    
  2. 打开终端并设置你的环境变量:

    使用 .env.flaskenv 来存储你的环境变量和秘密。在 .flaskenv 中,添加以下内容:

    FLASK_APP=app.pyFLASK_ENV=developmentFLASK_DEBUG=true
    
  3. 然后,在终端中输入 pip install python-dotenv 命令来安装 Python-dotenv。使用 python-dotenv,你可以将 .env.flaskenv 文件中的变量加载到你的应用程序环境中,使它们像直接设置系统环境变量一样可访问。

  4. 要运行 Flask 应用程序,使用以下命令,你将得到类似于 图 1**.6 的输出:

    $    flask run
    

图 1.6 – 显示如何运行 Flask 应用程序的截图

图 1.6 – 显示如何运行 Flask 应用程序的截图

注意

要取消激活虚拟环境,只需运行 $ deactivate

在为 Flask 设置并测试了开发环境之后,我们将简要讨论 Git,以了解源代码版本控制在 Web 应用开发中的位置以及 GitHub 如何提供了一个在线协作平台来处理源代码并鼓励团队合作。

准备使用 Git

Git是软件开发中的版本控制工具。那么,版本控制是什么意思呢?

作为一名专业开发者,你需要尽可能多地编写代码。假设你正在处理一个项目,并且已经完成了 80%。项目负责人要求你向代码库添加一个新功能,这是紧急的,因为客户希望你的团队将此作为将在几天内展示的最小可行产品所需的功能之一。

你迅速放弃了之前正在处理的工作,开始着手这个新功能。你更改了一个或两个文件以整合新功能。在尽可能短的时间内,你使新功能工作。不幸的是,在尝试添加新功能时,你不小心修改了其他文件中的代码,甚至不知道哪个文件受到了影响。

现在想象一下,你有一个可以告诉你代码中更改了什么以及确切更改的代码行的神灯。那不是太棒了吗?生活将变得超级简单,节省你大量的开发时间。这就是版本控制的魔力!

版本控制帮助您跟踪软件项目中代码库的变化。这是帮助开发者监控其源代码变化的一种很好的方式。此外,它简化了开发团队的协作工作。有了版本控制,您可以跟踪代码库的变化、谁在更改代码库以及何时发生更改。而且,如果更改不可取,您可以快速撤销它们。

开发者多年来使用了许多不同的版本控制工具。Git 恰好是当前的市场领导者。

Git 是如何工作的?

Git 被称为分布式版本控制软件。在一个需要团队成员之间协作的工作环境中,整个源代码的完整副本将存储在每个贡献者的本地计算机系统上;我们可以称之为本地仓库。

Git 跟踪本地仓库,记录本地仓库中发生的所有更改。它节省了你将多个项目版本保存在计算机上不同本地目录中的时间和精力。这使得在协作者之间共享源代码更改变得轻而易举。

Git 中有三个主要的状态,你应该了解:

  • 修改状态:在这个状态下,文件已经更改,但这些更改尚未被 Git 添加到本地数据库中。这些更改是自上次提交到文件以来的更改。

  • 暂存状态:在这个状态下,Git 已经跟踪了这些更改,将在下一次提交中将这些更改添加到 Git 本地数据库中。

  • 已提交:在此状态下,更改的文件已成功添加到 git 本地数据库中。

让我们深入了解版本控制概念,并学习如何创建本地和远程仓库。在此之前,了解 Git 和 GitHub 之间的区别将很有帮助。

Git 与 GitHub 的区别

如前所述,Git 是一个开源的版本控制工具。它简单用于跟踪代码库中的更改,跟踪更改的人的身份,并允许开发者之间的团队编码协作。当你将项目设置在本地机器上时,Git 用于跟踪所有活动的更改 – 添加文件、更新现有文件或创建新文件夹。

它基本上保存了你的源代码的历史记录。相反,GitHub 是一个基于云的源代码托管和项目管理服务。它简单允许你使用 Git 作为工具,将你的代码库保存在远程托管环境中以跟踪代码库中的更改,或允许开发者协作工作在项目上。

设置本地仓库

git-scm.com/download/ 安装 Git 客户端。如果你已经在你的机器上安装了它,则忽略此步骤:

  1. 让我们在终端中创建一个名为 local_repository 的工作目录:

    $      mkdir local_repository
    
  2. 将目录设置为工作目录:

    $      cd local_repository
    $      touch index.html
    

    你将得到以下输出:

图 1.7 – 创建 index.html 的屏幕截图

图 1.7 – 创建 index.html 的屏幕截图

注意

如果你遇到错误,'touch' 不是一个内部或外部命令,不是一个可操作的程序或批处理文件,使用 touch index.html,如果你的终端上安装了 Node.js,请输入 npm install -g touch-cli

  1. 设置你的全局凭据:

    $    git config --global user.name "Name of User"$    git config --global user.email "test@test.com"
    

    使用前面的命令,你设置了全局用户名和电子邮件地址作为凭据,以跟踪你在项目源代码中的贡献。

  2. 你的工作目录现在有一个新文件,index.html。在终端中输入此命令,你将得到类似于 图 1.8 的输出:

    $    git init
    

图 1.8 – 显示创建空 Git 仓库的屏幕截图

图 1.8 – 显示创建空 Git 仓库的屏幕截图

使用 git init,你创建了一个空的本地 Git 仓库。Git 现在有一个本地数据库或目录,其中包含跟踪工作目录中更改的所有元数据。.git 文件夹通常隐藏在你的工作目录中。

  1. 要将工作目录的内容添加到你的仓库中,请输入以下命令:

    $    git add index.html
    

    这代表了 Git 中的暂存状态。更改将由 Git 跟踪,并在下一次提交时添加到 Git 本地数据库中。

  2. 为了验证这一点,输入以下命令,你将得到类似于 图 1.9 的输出:

    $    git status
    

图 1.9 – 显示 Git 暂存状态的屏幕截图

图 1.9 – 显示 Git 暂存状态的屏幕截图

注意

要添加多个内容,请输入 git add

  1. 现在,你需要将本地仓库提交。这个提交阶段可以帮助你通过友好的消息跟踪代码库中的更改。要使用消息标志进行提交,请输入以下命令,你将得到类似于 图 1**.10 的输出:

    $    git commit -m "first commit"
    

图 1.10 – 展示 Git 提交状态的截图

图 1.10 – 展示 Git 提交状态的截图

注意

在提交命令中包含消息始终是最好的实践。这有助于跟踪更改,如果你需要回滚,你可以使用提交消息作为你的保存点。

现在你已经了解了如何创建本地仓库,向其中添加文件,以及将文件从暂存区过渡到提交状态。让我们简要讨论如何你在 GitHub 上创建远程仓库,以在云端存储你的源代码,以便可能的协作。

使用 GitHub 设置远程仓库

在当今的数字时代,GitHub 已经成为软件开发项目中无缝协作和版本控制的一项基本技能。让我们深入了解如何使用 GitHub 设置远程仓库:

  1. 在 GitHub 网站上创建一个开发者账户:github.com/

图 1.11 – 展示 GitHub 注册页面的截图

图 1.11 – 展示 GitHub 注册页面的截图

  1. 登录并点击 新建,你将看到以下屏幕:

图 1.12 – 展示 Git 预发布状态的截图

图 1.12 – 展示 Git 预发布状态的截图

  1. 一旦你创建了新的仓库,请在当前工作目录中输入以下命令:

    $    git remote add origin https://github.com/your-git-username/your-repository-name.git$    git branch -M main$    git push-u origin main
    

    前面的命令将你的本地仓库移动到一个远程云端仓库,以跟踪你的代码库更改。

总结来说,我们讨论了 Git 作为现代网络开发者所需的一项工具。你现在知道了 Git 和 GitHub 之间的区别。我们还讨论了用于版本控制操作的基本、有用的命令,无论是在本地还是远程仓库中。接下来,我们将深入探讨本书中我们将使用 React 和 Flask 构建的实际项目。

我们将构建什么?

在本书中,我们将构建一个全栈、数据库驱动的网络应用程序,用于会议演讲者。它被称为 Bizza。用户将能够查看他们感兴趣的活动演讲者目录、活动、日程以及演讲者所展示的论文标题。解决方案将包括使用 React 进行前端开发、身份验证、授权以及使用 Flask 设计 REST API。

我们将首先在本书的前几章中实现前端,然后在后面的章节中实现后端。

摘要

在本章中,我们简要介绍了现代全栈 Web 开发,重点介绍了前端和后端开发者的区别。我们讨论了 React 在构建 Web 应用程序用户界面中的重要性,并解释了为什么 React 和 Flask 是开发全栈 Web 应用程序的完美工具,因为与行业中的竞争对手相比,它们具有简单、高效和高性能的特点。同时,我们也涵盖了 React 和 Flask 的开发环境。

最后,我们讨论了 Git 作为版本控制工具的重要性以及本书中将要构建的项目Bizza

在下一章中,我们将更深入地解释 React 中的组件、属性和状态,以便更好地理解 React 应用程序是如何构建的。我们将讨论典型的 React 项目结构,目的是学习文件和目录的功能。

第二章:React 入门

到目前为止,我们已经为使用现代软件栈理解全栈 Web 开发奠定了坚实的基础。希望您已经在本地机器上设置了 React 开发环境。如果没有,您可以回到第一章使用 React 和 Flask 准备全栈开发,并重新阅读使用 React 设置开发环境部分。

在本章中,我们将系统地以微妙的方式开始向您介绍 React 的世界。您将学习许多酷炫的概念和技术,这将帮助您开发直观的用户界面,使用户能够与您的 Web 应用程序进行交互。您将学习如何在无需复杂配置的麻烦下启动您的第一个 React 项目,并了解每个 React 项目所需的基本目录结构。然后,您将学习如何在 React 中使用 ES6 特性。

组件是任何 React 应用程序的构建块。在本章中,您将了解组件的概念以及如何使用它们在 React 中构建用户界面。这些知识对于在项目中构建现代前端技术至关重要。通过详细的使用案例,您将了解 React 中 props 的使用,用于在组件之间传递信息,以及状态如何为 React 应用程序添加交互性。

到本章结束时,您将以实际的方式掌握开始任何 React 项目所需的技能集。您还将对 React 的核心功能(组件、props 和状态)有更深入的理解,以开发任何 Web 应用程序的交互性。

在本章中,我们将涵盖以下主要内容:

  • 探索 React 项目目录结构

  • React 中的箭头函数

  • 理解解构是什么

  • 默认和命名导出

  • 什么是 React 组件?

  • 什么是 props?

  • React 状态

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter02

探索 React 项目目录结构

稍后,我们将设置一个 React 应用程序项目。创建 React 应用程序有许多方法。您可以使用Create React App工具生成一个没有构建配置的样板应用程序。这不需要您具备复杂的配置知识。

您可以直接关注应用程序的实现。这正是本书将要使用的。如果您愿意探索Vite(vitejs.dev/),它也是另一个用于快速设置 React 应用程序的下一代前端工具。

创建 React 应用程序的另一种方式需要了解 Webpack 和 Babel 配置。现在,不再拖延,让我们深入了解创建我们的 React 项目应用程序。你被期望跟随操作。

打开你的终端并输入以下命令:

npx create-react-app frontend

你将得到以下输出:

图 2.1 – create-react-app 命令的截图

图 2.1 – create-react-app 命令的截图

设置完成后,导航到frontend文件夹:

$ cd frontend

现在,我们可以在命令行中使用code .打开应用程序:

图 2.2 – create-react-app 的截图,显示在项目根目录中打开 VS Code 编辑器的代码

图 2.2 – create-react-app 的截图,显示在项目根目录中打开 VS Code 编辑器的代码

在 VS Code 编辑器中会出现以下提取的文件夹和文件结构:

frontend/--node_modules/
--public/
----index.html
----manifest.json
--src/
----App.css
----App.js
----App.test.js
----index.css
----index.js
--.gitignore
--package-lock.json
--package.json
--README.md

以下截图显示了 VS Code 项目目录结构的截图:

图 2.3 – 显示项目目录结构的截图

图 2.3 – 显示项目目录结构的截图

因此,让我们快速浏览一下前面的文件和文件夹列表,以了解它们的各自用途:

  • node_modules/:这个文件夹包含使用 Create React App 工具安装的所有 node 包。所有的dependenciesdevdependencies都存储在这里。值得注意的是,我们将来安装的所有后续包也将保存在这个文件夹中。

  • public/:这个文件夹包含重要的公共文件,如public/index.htmlpublic/manifest.json

    • index文件在开发环境或托管域名中显示在localhost:3000上。本质上,这个文件将 React 组件的执行结果放在公共视图的index文件根div容器中。

    • 该文件夹还包含manifest.json文件,其中包含应用程序元数据和响应式屏幕显示的详细信息。

  • src/:这是 React 应用程序开发中最关键的文件夹。你超过 80%的编码活动时间都将在这里度过。因此,了解你在这个文件夹内确切地做什么非常重要。这个文件夹包含组件和一些其他文件,例如src/App.jssrc/App.csssrc/index.csssrc/App.test.jssrc/index.js

    • src/App.js文件用于实现 React 组件。如果你正在处理一个小项目,你可以用它来实现你的应用程序,而无需创建其他组件文件。你所有的组件代码都将放在一个单独的文件中。随着你的应用程序的增长,你可能会考虑将组件拆分成多个组件文件。通过这种拆分,每个文件将维护一个或多个组件。

    • src/App.css文件用于设置组件的样式。同样,src/index.css文件也用于设置整个应用程序的样式。这两个文件都可以编辑以满足您的样式要求。

    • src/App.test.js文件用于编写 React 应用的单元测试。

    • index.js文件是您的 React 应用的入口点。

  • .gitignore: 此文件包含不应跟踪和添加到项目中 Git 仓库的文件和文件夹列表。例如,node_modules总是列在文件中,因为它仅在开发环境中需要。

  • package.json: 此文件包含项目中使用的节点包依赖和其他元数据。这使得使用Node 包管理器npm)在另一台计算机系统上设置相同内容变得非常容易,无需太多麻烦。

  • package-lock.json: 此文件存储了您通过npm安装的包的版本号。这确保了从npm到其他开发者的本地计算机系统安装包的一致性。

  • README.md: 这是一个 Markdown 文件。README.md文件帮助开发者提供有关他们项目的说明和必要信息。在 GitHub 上,您可以使用README.md文件显示项目存储库中包含的内容。Create React App 工具会为我们自动生成此文件。

现在您已经了解了 React 项目中文件夹和文件结构的一些用途,让我们在终端中运行npm start命令,以查看默认的 React 应用:

图 2.4 – 本地 host:3000 上 React 应用主页的截图

图 2.4 – 本地 host:3000 上 React 应用主页的截图

注意

以下显示了某些消息,例如成功消息、可用脚本以及安装 React 后如何启动开发服务器:

  • npm start: 启动开发服务器

  • npm run build: 将应用打包成用于生产的静态文件

  • npm test: 启动测试运行器

总结来说,我们讨论了如何使用 create-react-app 工具来增强您的 React 应用。现在您已经详细了解了每个文件夹和文件的含义。有了这些,您可以有信心知道如何设置 React 应用。让我们通过讨论箭头函数来开始下一节。箭头函数是ECMAScript 2015ES6)的一个特性。它们是 JavaScript 中编写函数的一个便捷的补充,使得编写函数变得轻而易举!

React 中的箭头函数

箭头函数为 JavaScript 中定义函数提供了更简洁、更易读的语法。箭头函数因其简洁的语法和隐式返回而成为 React 开发中广泛使用的一个特性:您将在稍后更好地理解这些含义。

在传统的 JavaScript 中,您必须定义一个常规函数来将两个数字相加,如下所示:

function addNumbers(a, b) {    return a + b;
}

不错,对吧?但箭头函数可以使这更加简单和优雅。看看这个:

const addNumbers = (a, b) => {    return a + b;
};

非常酷?将 function 关键字替换为看起来很酷的箭头 =>,如果你的函数只是一行代码,你可以省略花括号和 return 语句:

const addNumbers = (a, b) => a + b;

基本上,你通过遵循以下语法规则来定义箭头函数:

const functionName = (parameters) => {    return returnValue;
};

或者,对于函数体仅由一个表达式组成时的更短函数定义和隐式返回,可以使用这个规则:

const functionName = (parameters) => returnValue;

箭头函数由于其简洁的语法和好处,常在 React 应用中使用,尤其是在定义函数组件、处理事件回调和映射数组时。在这本书的学习过程中,我们将看到更多箭头函数的使用。

然而,需要注意的是,箭头函数并不是常规函数的完全替代品。它们有一些限制,例如没有自己的 this 上下文,这使得它们不适合某些用例,例如定义对象方法。

当在定义对象方法时使用箭头函数,它们没有自己的上下文,因此无法正确访问对象内的名称属性。让我们通过以下代码示例来更好地说明这一点。

const speaker = {    name: 'Alice Andrew',
    sayHi: function () {
        console.log(`Hi, I'm ${this.name}!`);
    },
};
speaker.sayHi(); // Output: Hi, I'm Alice Andrew!

现在,让我们使用箭头函数来定义对象方法:

const speaker = {    name: 'Alice Andrew',
    sayHi: () => {
        console.log(`Hi, I'm ${this.name}!`);
    },
};
speaker.sayHi(); // Output: Hi, I'm undefined!

总结来说,箭头函数是 ES6 中的一个绝佳特性,可以使你的代码更简洁、更易于工作。它们非常适合短函数,并且可以使 React 开发者的生活变得更加轻松。

接下来,我们将探讨另一个酷炫的 ES6 功能:解构。你需要解构技术来能够以更简洁和可读的方式从数组或对象中提取值。

理解解构是什么

解构是一种简单的 JavaScript 表达式,确保你能够从数组中提取多个值或从对象中提取属性到单独的独特变量中。解构是 React 中用于管理数据的一些令人惊叹的 JavaScript ES6 功能之一。

ES6 是改进 JavaScript 语言标准过程中的一个重要里程碑。解构将从数组和对对象提取数据提升到一个新的简洁水平。在 ES6 之前,你可以这样声明并从数组中提取数据:

const speakers = ["John", "Walke", "Dan", "Sophie"];const firstspeakerName = speakers[0];
const secondspeakerName = speakers[1];
const thirdspeakerName = speakers[2];
const fourthspeakerName = speakers[3];
console.log(firstspeakerName); // "John"
console.log(secondspeakerName); // "Walke"
console.log(thirdspeakerName); // "Dan"
console.log(fourthspeakerName); // "Sophie"

使用解构,代码看起来是这样的:

const speakers = ["John", "Walke", "Dan", "Sophie"];const [firstspeakerName, secondspeakerName,
    thirdspeakerName,fourthspeakerName] = speakers
console.log(firstspeakerName) // "John"
console.log(secondspeakerName) // "Walke"
console.log(thirdspeakerName) // "Dan"
console.log(fourthspeakerName) // "Sophie"

如果我们想在数组中跳过 "John" 并将数组中剩余的项输出到控制台,我们可以这样做:

const speakers = ["John", "Walke", "Dan", "Sophie"];const [, ...rest] = speakers // the … used is called the
                                spread operator
console.log(rest)// output: "Walke", "Dan", "Sophie" John
                    will be skipped

此外,不仅数组可以进行解构;你还可以在对象中进行解构。例如,以下代码展示了如何解构对象属性:

const speakers = {    id: 1,
    name: "Juliet Runolf",
    jobTitle: "Director, Marketing",
    company: "Abernatny Group",
    address: {
    street: "Okaland Dtuse",
    city: "Greenland",
    state: "Houston",
    country: "USA",
    }
}
function App()
{
    const {name, jobTitle, company} = speakers;
    //destructuring of object properties
    return (
        <>
            <div>
                <h2>Name: {name}</h2>
                <h4>Position: {jobTitle}</h4>
                <h4>Company: {company}</h4>
            </div>
        </>
    );
}

你可以看到我们如何在前面的代码片段中提取属性值。甚至可以解构对象中的嵌套 address 属性:

const {name, jobTitle, company, address} = speakers;// destructuring of object properties
const {street, city, state, country} = address;
// destructuring of nested address property
    return (
    <div> <h2>Name: {name}</h2>
        <h4>Position: {jobTitle}</h4>
        <h4>Company: {company}</h4>
        <h4>Street: {street}</h4>
        <h4>City: {city}</h4>
        <h4>State: {state}</h4>
        <h4>Country: {country}</h4>
    </div>
  );
}

那么,在 React 应用中使用解构赋值有哪些好处?解构赋值使你的代码更加紧凑且易于理解。它允许你直接从对象中提取特定属性或从数组中提取元素,减少了重复使用点符号或数组索引的需求。

此外,解构赋值允许你为属性设置默认值 - 例如,看看以下代码行:

const { name = 'Anonymous', age = 0} = speaker

上述代码在处理可选或可能未定义的数据时非常有用,并且需要默认值。解构赋值在访问组件中的propsstate时被使用。哦,你不知道componentspropsstate是什么?别担心,我们将在本章稍后讨论这些概念。

总结来说,解构赋值允许程序员以简单的方式访问数据,即使在数组和对象中的复杂嵌套数据。它显著提高了代码的可读性和访问质量。如果你想在 React 应用中缩短代码行数,解构赋值显然有帮助。它有助于减少应用中使用的代码量。

在 React 应用开发中,接下来需要理解的两个酷炫概念是默认导出和命名导出。让我们深入探讨并理解这些 React 概念。

默认导出和命名导出

如前所述,ECMAScript 2015,也称为 ES6,是提高 JavaScript 语言标准的重要里程碑。其中添加的新特性包括模块和能够使用import表达式。模块使我们能够更好地将代码库组织成逻辑单元。基本上,模块可以是执行特定任务的一个或多个相关函数。它们使得在项目间重用代码变得更加容易。

在 React 中,我们使用默认导出来使组件函数、变量、类或对象对其他组件文件可用。每个文件只允许有一个默认导出。

例如,以下代码使得从Speaker组件导入文件成为可能:

import Speaker from './Speaker';

以下代码使得将文件导出到另一个组件文件成为可能:

function App(){
    return (
    <div>  …   </div>
    );
}
    export default App; // Specifically, this code line
                           makes it possible to export the
                           file to another component file

在命名导出中,每个文件可以有多个命名导出。当你想要导入时,你可以用大括号命名特定的导入,如下所示:

import { FirstComponent, SecondComponent } from "./ThirdComponent";

总结来说,默认导出和命名导出是使特定函数在 React 项目的任何组件间可用的一种方式。

接下来,我们将深入探讨 React 组件的核心本质,并对其目的和功能有一个清晰的理解。

什么是 React 组件?

组件是任何 React 应用的核心构建块。有时,你可以把 React 看作是加了点巧克力的 JavaScript。我想巧克力是甜的,React 也是。说真的,用纯 JavaScript 构建 UI 可能会很繁琐。你可能会在昂贵的 DOM 困境中挣扎!

问题是,当使用纯 JavaScript 处理 文档对象模型DOM)时,这可能会相当昂贵——无论是时间还是精力。在非 React 应用程序中,频繁的 DOM 操作很高,这最终导致网站元素更新的缓慢。

虚拟 DOM 在 React 中解决了这个问题。DOM 只更新更改的部分,而不是整个 DOM 树。然而,如果您记得您是如何在纯 JavaScript 中使用函数的,编写组件不会是一个挑战。JavaScript 中的函数本质上是一个代码块,用于执行某些任务。

这同样适用于 React 组件,它们是可重用、可维护且自包含的代码块,用于返回 UI。在 React 中,组件返回混合了 JavaScript 的 HTML 元素。

React 有两种类型的组件:类组件函数组件。在这本书中,我们将采用函数组件编码模式来开发我们的 React 应用程序。

函数组件是 React 的现在和未来,所以如果您刚开始学习 React,函数组件比编写类组件所涉及的额外内容更容易学习。如果您已经熟悉 React 中的类组件,您仍然可以使用函数组件与类组件一起使用。如果您发现自己正在使用遗留的 React 代码库,逐步迁移到函数组件绝对值得考虑。

通过以下步骤,让我们学习如何定义一个函数组件并在 React 应用程序中使用它:

  1. 在 React 项目的 src/ 文件夹中打开 App.js 文件。src/App.js 文件包含以下代码:

    import logo from './logo.svg';import './App.css';function App() {    return (        <div className="App">            <header className="App-header">                <img src={logo} className="App-logo"                    alt="logo" />                <p>                    Edit <code>src/App.js</code> and                        save to reload.                </p>                <a                    className="App-link"                    href="https://reactjs.org"                    target="_blank"                    rel="noopener noreferrer">                    Learn React                </a>            </header>        </div>    );}export default App;
    
  2. 让我们删除文件中的所有样板代码,以便更容易理解代码结构。用以下代码片段替换前面的代码片段:

    function App() {    return (        <div>            <h1>Welcome to Bizza Platform</h1>        </div>    );}export default App;
    

注意

您的 src/App.js 文件现在应该看起来像前面的代码片段。

  1. 保存文件,在命令行中使用 npm start 启动您的应用程序,您将得到以下输出:

图 2.5 – 展示 npm start 命令输出的截图

图 2.5 – 展示 npm start 命令输出的截图

  1. 检查 Welcome to Bizza Platform 是否显示在您的浏览器上。如果是,您仍然走在正确的道路上。

图 2.6 – 本地主机 3000 上的 React 应用程序主页截图

图 2.6 – 本地主机 3000 上的 React 应用程序主页截图

那么,让我们更深入地研究代码块中的每个元素,以更好地理解函数组件。

函数组件

如前所述,函数组件在概念上是一个典型的 JavaScript 函数,具有接收数据作为 props 并以 JavaScript XML 形式返回 HTML 元素的能力。

在前面的代码中,App 组件在其函数定义中还没有任何参数。

这就是您在 React 应用程序中定义 App 组件的方式:

    function App() {.....}

代码返回以下 HTML 元素:

    return (        <div>
            <h1>....</h1>
        </div>
    );

App 组件返回 HTML 代码。返回的 HTML 代码是 HTML 和 JavaScript 的混合体。这被称为JavaScript XMLJSX)。JSX 是 React 中用于在 JavaScript 中直接编写类似 HTML 代码的语法扩展。JSX 使得在 React 组件中描述用户界面结构变得更加容易。在第五章 React 中的 JSX 和显示列表中,我们将深入讨论 JSX。

如前所述,我们将在这本书中更多地关注函数组件。如果你熟悉用纯 JavaScript 编写函数,React 中的函数组件肯定会让你感到熟悉。在 React 函数组件中,你可以在函数定义和 return 语句之间有实现细节。

例如,让我们检查 src/App.js 中的 App 组件:

function App() {    // you can perform some operations here.
    return (
        <div>
            <h1>Welcome to Bizza Platform</h1>
        </div>
    );
}
export default App;

在函数体中定义的任何变量都会在每次函数运行时重新定义:

function App() {    const speakerName = 'John Wilson';
    // variable declared inside function component body
    return (
        <div>
            <h1>Welcome to Bizza Platform, {speakerName}
                </h1>
        </div>
    );
}
export default App;

使用 npm start 启动浏览器并检查浏览器中的更新内容。

如果你的服务器仍在运行,你不需要再次使用 npm start。每次你保存文件时,你的应用程序都会重新编译并显示更新后的视图:

图 2.7 – 展示前一个代码片段输出的截图

图 2.7 – 展示前一个代码片段输出的截图

此外,你可以在组件体外部定义你的变量:

const speakerName = 'John  Wilson';// variable declared outside function component body
function App() {
    return (
        <div>
            <h1>Welcome to Bizza Platform, {speakerName}
                </h1>
        </div>
    );
}
export default App;

在函数组件中,函数操作的结果可以是第一次运行在更新时重新渲染。然而,如果你不需要函数组件体内的任何内容,你可以在函数组件外部定义你的变量;否则,考虑在函数组件体内定义它。

总结来说,我们已经能够讨论 React 中的函数组件以及如何编写和显示函数组件的内容。接下来,我们将讨论类组件以及为什么它们被称为有状态组件

类组件

React 提供了使用函数或类来构建 UI 组件的灵活性。类组件是扩展 React.Component 的 JavaScript 类,并调用返回 HTML 元素的 render 方法。类是有状态的,在 React 团队提出 Hooks 之前,这是 React 中管理状态的唯一方式。关于 Hooks 的更多内容将在第三章 使用 React Hooks 管理状态中讨论。让我们看看编写最小化 React 类组件的类代码语法。

src/ 中创建一个 SpeakerProfile.js 文件,并在 src/SpeakerProfile.js 中输入以下代码:

import React from 'react';class SpeakerProfile extends React.Component {
    render() {
        return <h1>This is the class component expression
            from Speaker Profile!</h1>;
    }
}
export default SpeakerProfile;

让我们逐行分析代码以了解它们的功能:

  • import React from 'react':这允许我们在代码文件中使用 React 的核心功能

  • class SpeakerProfile extends React.Component { }:这允许我们创建继承自 React.Component 基类 SpeakerProfile 类组件

  • render() { return <h1>...</h1>;}:每个类组件都必须有一个返回 HTML 元素的render函数

上述代码行解释了 React 类组件的基本结构。

src/App.js中添加以下内容:

import React from 'react';import SpeakerProfile from './SpeakerProfile';
function App(){
    return (
        <div style={{backgroundColor: 'gray', margin:20,
            color:'white'}}>
        <SpeakerProfile  />
        </div>
    );
}
export default App;

如果你的http://localhost:3000/服务器仍在运行,你将看到以下屏幕。如果没有,请打开你的终端并转到你的工作目录,然后输入npm start命令:

图 2.8 – 展示类组件输出的截图

图 2.8 – 展示类组件输出的截图

注意

<SpeakerProfile />组件被添加到src/App.js中,用于渲染SpeakerProfile组件的内容。

让我们比较一下我们在src/SpeakerProfile.js中编写的类组件的函数组件等价物:

import React from 'react';const SpeakerProfile =()=> {
return <h1>This is a function component equivalent to a
    class component !</h1>;
}
export default SpeakerProfile;

因此,你可以看到函数组件的代码行数更少——这意味着它们可以更加优雅、简洁和易于阅读。当使用函数组件时,你不需要定义一个render()方法,组件本身就是一个返回 JSX 的函数,这使得代码更加简单和易于阅读。

让我们简要讨论一下类组件和函数组件之间的细微差别。

类组件与函数组件

创建类组件时,你必须使用extend关键字从React.Component继承,并创建一个负责返回 React 元素的render方法。在函数组件中,没有render函数。它接受props作为参数并返回 HTML 元素。

与无状态的函数组件相比,类组件是有状态的。在 Hooks 出现之前,使函数组件有状态的唯一方法是将其重写为类组件。现在,随着 React 16.8 版本的推出,Hooks 的出现使得函数组件也可以有状态。

在类组件中,你可以利用 React 的生命周期方法,如componentDidMount()componentDidUpdate()componentWillUnmount()。然而,在 React 函数组件中,你不能使用这些生命周期方法。

总结来说,我们讨论了 React 中的类组件以及如何编写和显示类组件的内容。接下来,我们将讨论组件生命周期,以便更好地设计可重用和可维护的 React 组件。

组件生命周期

到目前为止,我们已经看到了每个 React 应用程序的组件化特性。组件之间相互交互,为 Web 应用程序提供用户交互的界面。然而,组件——我们与之交互的每个 UI 的构建块——也有生命周期。

就像生活一样,涉及不同的阶段;我们出生,然后成长,然后死亡。React 组件也会经历阶段。它们经历三个阶段:挂载、更新和卸载。每个阶段都将简要讨论。

挂载阶段

这代表了 React 组件的出生阶段。这是组件实例被创建并插入到 DOM 中的阶段。在这个阶段有四个方法,按照顺序如下列出:

  1. constructor(): 这个方法在挂载阶段被调用,实际上是在组件挂载之前。调用 constructor() 方法接受 props 作为参数。这之后会调用 super(props)。然而,constructor() 方法有两个主要目的:

    • 通过将对象赋值给 this.state 在类组件中初始化本地状态

    • 绑定事件处理方法

  2. static getDerivedStateFromProps(): 这个方法在渲染 DOM 中的元素之前立即被调用。

  3. render(): 这个方法是最重要的,它总是需要用于输出 React HTML 元素。它通常将 HTML 注入到 DOM 中。你界面上看到的内容取决于这个 render 方法以 JSX 形式返回的内容。

  4. componentDidMount(): 这个方法在 React 组件渲染到 DOM 树后启动,基本上是在第一次 render() 调用之后。componentDidMount() 方法允许你在组件挂载后执行用户操作和其他副作用。例如,componentDidMount() 方法可以运行需要从外部源加载数据的语句。此外,当组件渲染后,你可以触发用户或系统事件。

更新阶段

这是 React 组件的假设成长阶段。这个阶段发生在组件生命周期挂载阶段之后。在这个阶段通常使用 componentDidUpdate() 方法。每当数据状态或属性发生变化时,React 组件都会被更新。使用 render() 方法重新渲染状态,并将更新的状态返回到 UI。

卸载阶段

这个阶段被认为是组件生命周期过程的死亡阶段。在组件从 DOM 树卸载之前,立即调用 componentWillUnmount() 方法。

通常,Web 应用程序有许多用户界面,例如按钮、表单输入、手风琴标签页,甚至在我们的应用程序中还有导航栏。用户倾向于从一个组件交互移动到另一个组件,总体上有一个反应性的体验。

因此,当用户停止与组件交互时——比如说,从我们的应用程序主页上的 联系 组件移动到 手风琴标签页——从 联系 页面切换到主页上的 手风琴标签页 意味着 联系 页面组件生命周期的结束。

在用户交互周期的每一个点上,React 组件要么被插入到 DOM 树中,要么是状态变化,或者是组件生命周期的完成。

下图显示了 React 生命周期方法:

图 2.9 – React 生命周期方法(来源:https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/)

图 2.9 – React 生命周期方法(来源:https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/)

总结一下,我们简要讨论了 React 类组件中的各种生命周期方法以及它们在渲染 React 组件中存在的原因。在本书的后面,我们将探讨函数组件中如何处理生命周期。同时,我们将讨论属性props)。Props 是允许 React 组件与其他组件通信的关键特性之一,使得 React 应用如此可重用。

props 是什么?

Props是传递给 React 函数和类组件的参数。如果这听起来太技术化,让我们稍微解释一下。基本上,你使用 props 从一个组件传递数据到另一个组件。所以,props 指的是存储属性值的对象。这类似于 HTML,当你将值传递给标签的属性时。

在 React 的世界里,props 用于自定义和配置组件,并且从父组件传递到子组件,沿着组件树向下传递。这意味着父组件只能将信息传递给子组件。这是 React 中单向数据流的概念。本质上,props 是只读的,这意味着接收它们的组件不能直接修改它们的值。

将数据作为 props 传递

让我们看看一个 props 在 React 组件中使用的例子。正如我们讨论过的,props 在 React 中用于从一个组件传递信息到另一个组件。在下面的片段中,我们将解释如何在 React 中将数据作为 props 传递。

src/App.js中添加以下内容:

import React from 'react';function SpeakerProfile(props) {
    return(
        <>
            <h3>{props.name}</h3>
            <p>Position: {props.jobTitle}</p>
            <p>Company: {props.company}</p>
        </>
    );
}
//Parent component
function App() {
    return (
        <>
            <h1>Speaker Profile</h1>
            // Child component with attributes
               name,jobTitle and company inside parent
               component
            <SpeakerProfile
                name='Juliet Runolf'
                jobTitle='Director,
                Marketing' company='Abernathy Group'
            />
        </>
    );
}
export default App;

在前面的代码中,我们向父函数组件App.js中添加了一个子组件<SpeakerProfile />,并将一些 props 传递给了<SpeakerProfile/>组件。

现在,让我们深入了解SpeakerProfile片段的内部结构:

const SpeakerProfile = (props)=>{    return(
        <>
            <h3>{props.name}</h3>
            <p>Position: {props.jobTitle}</p>
            <p>Company: {props.company}</p>
        </>
    );
}

SpeakerProfile是一个子组件。我们在父组件中定义一个SpeakerProfile函数,并将数据 props 传递给它。在SpeakerProfile组件体中,我们返回 HTML 元素,<h3>{props.name}</h3>

我们然后将属性值传递给这些属性(props),从App父组件传递到SpeakerProfile子组件:

    <p>Position: {props.jobTitle}</p>    <p>Company: {props.company}</p>

下面的屏幕输出显示了传递给SpeakerProfile组件的 props:

图 2.10 – 展示组件中 props 的屏幕截图

图 2.10 – 展示组件中 props 的屏幕截图

总结一下,我们学习了如何在函数组件中从一个组件传递信息到另一个组件。在 React 中,从父组件到子组件的信息流按照规则应该是单向的:从父组件到子组件。

属性是只读的。子组件不能直接修改。在本章的后面部分,我们将讨论如何通过从子组件传递属性到父组件来覆盖此规则。

现在,我们将讨论 React 中的状态,作为使 React 组件动态和交互式的一种方式。

React 状态

状态 是 React 中的一个内置对象,用于存储有关组件的信息。它是组件交互性的责任所在。在 React 应用程序中,状态会发生变化。当组件状态发生变化时,React 会重新渲染组件。

这种变化也影响了组件在屏幕上的行为和渲染方式。有一些因素可以导致状态变化 – 例如,对用户操作的响应或系统生成的事件。属性和状态是 React 的双胞胎特性。虽然属性本质上是从父组件传递信息到子组件,但状态改变组件的内部数据。

让我们看看组件中状态实现的搜索用例。任何时候用户在 HTML 搜索输入字段中输入某些内容,用户都希望看到这些输入的信息,这代表了一种新的状态,在应用程序的其他地方显示。

默认状态是空白的搜索输入字段。因此,我们需要一种方法来更改输入字段中的信息并通知 React 重新渲染其组件。这意味着显示组件的新状态。

让我们在 src/SearchSpeaker/ 目录下添加一个名为 SearchSpeaker.js 的新组件:

import React,{useState} from "react";const SearchSpeaker = () =>{
    const [searchText, setSearchText] = useState('');
    return (
        <div>
             <label htmlFor="search">Search speaker:
                 </label>
             <input id="search" type="text" onChange={e =>
                 setSearchText(e.target.value)} />
             <p>
                 Searching for <strong>{searchText}
                     </strong>
             </p>
        </div>
    );
}
export default SearchSpeaker;

在前面的代码片段中,SearchSpeaker 组件有两个变量:

const [searchText, setSearchText] = useState('');

searchTextsetSearchText 变量管理 searchSpeaker 组件在状态变化时如何更新其状态。此行代码是我们之前讨论过的数组解构的例子,其中我们将 useState 钩子返回的值分配给两个变量一行。在 第三章使用 React Hooks 管理状态,我们将详细讨论 useState

searchText 变量用于设置当前状态,并告诉 React 在事件处理程序通知状态发生更改并带有新值或由 setSearchText 设置的状态时重新渲染其 searchSpeaker 组件。

在 React 中,useState 是一个接受初始状态作为参数的实用函数。在这种情况下,初始状态是空的:useState('')。空的初始状态通知 React 状态将随时间变化。因此,useState 包含一个两元素数组。第一个元素 searchText 代表当前状态;第二个元素是一个函数,用于使用 setSearchText 来更改状态。

这些是我们用来在 React 组件内部显示当前状态或更改状态的机制。

App.js 中添加以下代码:

…import SearchSpeaker from './SearchSpeaker';
function App()
{
    return (
        <div style={{backgroundColor: 'blue', margin:20,
            color:'white'}}>
            <h1>...</h1>
            …..
            <SearchSpeaker   />
        </div>
    );
}
export default App;

以下截图显示了 React 组件中状态的变化:

图 2.11 – 展示表单字段 onChange 状态的截图

图 2.11 – 展示表单字段 onChange 状态的截图

通过理解状态作为 React 组件用来持有和管理随时间变化的数据的对象,开发者可以创建交互式和动态的用户界面,这些界面能够响应用户操作,并提供更好的用户体验。

摘要

在本章中,我们讨论了 React 应用程序中的某些核心概念。我们从 Create React App 工具生成的 React 项目结构的基本结构开始。解释了文件和文件夹的一些用途。我们讨论了一些 ES6 特性,例如使用箭头函数、解构和默认及命名导出。

我们还定义了组件作为任何 React 应用程序的核心构建块。讨论了两种类型的组件:组件和函数组件。此外,我们讨论了 props 以及如何在 props 中传递信息。在 React 中明确了单向信息流。最后,我们讨论了状态作为 React 管理内部数据的方式。

在下一章中,我们将通过讨论 React 中可用的一些 hooks 来深入探讨 React 应用程序的开发。这将使我们接触到 React 的一些高级主题。

第三章:使用 React Hooks 管理状态

第二章React 入门,是启动 React 前端开发的好方法。到目前为止,你应该熟悉项目目录结构和 React 中的几个其他概念。在本章中,我们将进一步深化你对 React 核心概念的理解。

这为什么重要?简单来说,如果你没有掌握 React 核心功能和它们的使用方法,就无法在 React 开发中成为你想要的光芒。本章重点介绍使用 React Hooks 管理状态。

React 中的状态是我们添加用户界面交互的媒介。在 React v16.8 之前,开发类组件是唯一能够为你的组件添加状态和状态转换的方法。

函数组件最初是无状态的;它们只能显示 JavaScript XMLJSX)元素,即仅作为展示组件。但通过 Hooks API,你可以为你的函数组件添加状态和状态转换。

在本章中,你将了解各种 React Hooks 以及我们如何使用它们为函数组件添加状态。任何 React 应用程序的构建块都是组件,而使它们具有状态性是提升 Web 应用程序用户体验的关键。

到本章结束时,你将能够使用如 useStateuseEffectuseContextuseMemouseReducer 等 Hooks 构建有状态的函数组件,甚至能够开发你自己的自定义 Hooks。

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

  • React 中的 Hook 是什么?

  • 为什么要在 React 中使用 Hooks?

  • 使用 useState 开发有状态的组件

  • 使用 useEffect 创建有用的副作用

  • 使用 useContext 管理 React 应用程序的全局状态

  • 使用 useRef 直接访问 DOM 元素并持久化状态值

  • 使用 useReducer 进行状态管理

  • 使用 useMemo 提高性能

  • 使用 useCallback 避免函数重新渲染

  • 使用自定义 Hooks 实现代码复用

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter03.

由于页面数量限制,代码块已被截断。请参阅 GitHub 以获取完整源代码。

React 中的 Hook 是什么?

Hook 是 React 提供的一个特殊函数,它允许你在函数组件中使用 React 核心功能——状态和组件生命周期方法。虽然状态是 React 中内置的对象,它为组件添加了交互性和动态机制,但生命周期跟踪组件经历的各个阶段,从它们的初始化到最终消亡(当用户离开或退出应用程序 UI 时)。

React 组件经历三个主要的循环阶段,如在第 2 章 React 入门 中所述:挂载、更新和卸载。每个阶段都有我们所说的生命周期方法,可以在 React 组件的渲染过程中使用。

我们在类组件的生命周期中观察到某些方法的存在,例如 componentWillMount()componentDidMount()componentWillUpdate()componentDidUpdate()。React Hooks 用于使函数组件具有状态,而无需使用类组件的生命周期方法。

在 React 版本 16.8 之前,如果你在使用具有状态的组件,你除了使用类组件作为将状态性引入组件的唯一方式外别无选择。

让我们看看一个在按钮点击时将名字从首字母更改为全名的组件:

import React from 'react';class App extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      name: "Andrew",
    }
    this.updateNameState = this.updateNameState.bind(this);
  }
  updateNameState(){
    this.setState({
      name: "Andrew Peter"}
);
  }
  render() {
    return(
      <div>
        <p>{this.state.name}</p>
        <button onClick={this.updateNameState}>Display Full
          Name</button>
      </div>
    );
  }
}
export default App;

让我们详细理解前面的代码:

  • import React from 'react':这一行将 React 库的核心功能引入作用域。

  • class App extends React.Component:这声明了我们的 class App,它扩展了 React 组件的基本类。

  • 以下代码片段定义了一个接受 props 作为参数的构造函数:

    constructor(prop) {  super(props);  this.state = {    name: "Andrew",}
    

    这是一个普通的 JavaScript 类构造。任何扩展基本类的类都必须定义一个 super() 方法。this.state={name:"Andrew",} 这部分将初始状态设置为 Andrew。这是我们想要在代码中稍后更新的状态。

  • 以下代码片段确保当函数被调用时,函数的 this 上下文将指向组件的正确实例:

    this.updateNameState = this.updateNameState.bind(this);
    

    updateNameState 函数使用 .bind(this) 绑定到组件实例上。

  • 以下代码片段演示了状态更新方法:

            updateNameState(){          this.setState({            name:"Andrew Peter"          });        }
    

    它在按钮中被调用,将状态从 name:"Andrew" 更改为 name: "Andrew Peter"

  • render():这是 React 中每个类组件的强制方法。

  • <p>{this.state.name}</p>:这设置了我们的初始状态,即 Andrew,并以 JSX 的形式返回它供我们查看。

  • 根据以下代码片段,当按钮被点击时,updateNameState() 类方法被调用,并设置为更新后的状态,即 Andrew Peter

    <button onClick={this.updateNameState}>  ChangeToFullName</button>
    

在本章中,我们将使用 Hook 将前面的代码片段重构为函数组件。但在深入探讨之前,让我们看看指导我们在 React 函数组件中编写 Hooks 的两个规则:

  • 规则 1Hooks 只能在 顶层调用。

    你不能在条件语句、循环或嵌套函数内部调用 Hooks。相反,你应该始终在 React 函数的顶层调用 Hooks。

  • 规则 2Hooks 只能从 React 组件函数中调用。

    你不能在常规的 JavaScript 函数中调用 Hooks,同样,你也不能在 React 的类组件中调用 Hooks。你只能从函数组件中调用 Hooks。你也可以从自定义 Hooks 中调用 Hooks。

为什么要在 React 中使用 Hooks?

在 React 的历史中,Hooks 代表了我们在处理状态组件和管理副作用方面的方法上的重大转变。在 Hooks 之前,编写或重构类组件是使组件能够表现出交互性和处理其他副作用的主要方法。组件是 React 应用程序 UI 的构建块,创建交互式界面需要使用类组件。

然而,对于初学者来说,类的语法和结构可能难以理解。Facebook React 团队前经理 Sophie Alpert 在 2018 React 大会 的主题演讲 (React 今天和明天) 中说:

“我声称类对人类来说很难……但不仅人类,我还声称类对机器来说也很困难”

– Sophie Alpert (https://bit.ly/37MQjBD)

在类组件中使用 thisbind 增加了困惑的清单。虽然 JavaScript 提供了面向对象编程(OOP)和函数编程的世界,但在 React 类组件中,如果不理解面向对象范式,就无法编写代码。

这严重强调了 React 新手面临的挑战。至少在 React Hooks 出现之前是这样的。有了 Hooks,你只需编写常规的 JavaScript 函数,这些函数更容易编写,并且你只需将 React Hooks 钩入即可实现状态化。

你可能选择 React Hooks 的另一个原因是跨多个组件重用状态逻辑。Hooks 允许你将状态逻辑与组件渲染逻辑分离,这使得在不同组件中重用逻辑变得更加容易。

这种分离确保了更好的模块化和可重用性,因为您可以将包含状态逻辑的自定义 Hooks 在不同的 React 应用程序以及更广泛的 React 社区中共享。另一方面,在基于类的组件中,状态逻辑和 UI 往往交织在一起,这可能会使有效地提取和重用逻辑变得更加困难。

总的来说,React Hooks 引发了一种关于 React 组件设计的新思考方式。在现有的代码库中逐步采用的可能性(如果你仍在运行遗留的 React 源代码),使得坚定的类组件 React 开发者能够继续编写他们的状态类组件,同时系统地将其代码库迁移到面向功能的方案。

React 的未来指向了函数组件架构。我无法想象在这个阶段,任何合理的人还会追求类组件。通过了解和编写函数组件,开发者可以更有效地利用 React 的优势。

在下一节中,我们将开始使用 Hooks 开发状态组件的过程。我们将从最受欢迎的 React Hook useState 开始,它将状态引入函数组件。

使用 useState 开发状态组件

useState Hook 允许你在 React 应用程序中管理状态。函数组件依赖于useState Hook 来向它们添加状态变量。状态是 React 中的一个对象,可以存储用于 React 组件的数据信息。当你对现有数据进行更改时,该更改被存储为状态。

这就是这样工作的:你将初始状态属性传递给useState(),然后它返回一个包含当前状态值的变量和一个用于更新此值的函数。以下是useState Hook 的语法:

const [state, stateUpdater] = useState(initialState);

让我们看看useState的一个简单用法示例:

import React, {useState} from 'react';const App = () => {
  const [count, setCount] = useState(0);
  const handleIncrementByTen = () => {
    setCount(count + 10);
  };
  const handleDecrementByTen = () => {
    setCount(count - 10);
  };
  const resetCountHandler = () => {
    setCount(0)
  };

前面的代码片段展示了如何开发一个具有增加、减少和重置状态的组件。当点击IncrementByTen按钮时,计数器将数字增加10,而当点击DecrementByTen按钮时,减少状态被激活,数字减少10

重置到初始状态做了它应该做的事情——将值重置为其初始值。以下完成了代码片段:

  return (    <div>
      Initial Count: {count}
      <hr />
      <div>
        <button type="button"
          onClick={handleIncrementByTen}>
          Increment by 10
        </button>
        <button type="button"
          onClick={handleDecrementByTen}>
          Decrement by 10
        </button>
        <button type="button" onClick={resetCountHandler}>
          Reset to Initial State
        </button>
      </div>
    </div>
  );
};
export default App;

让我们更详细地了解前面的代码:

  • 导入useState:要使用useState Hook 函数,我们首先需要从Import React, { useState } from 'react' React 对象中将其导入到我们的组件中。

  • 初始化useState:我们通过在组件中调用useState来初始化我们的状态,如下所示:

    const [count, setCount] = useState(0);//using destructuring array to write a concise code.
    

    useState<number>接受一个初始状态为零(useState(0))并返回两个值:countsetCount

    • count:当前状态

    • setCount:状态更新函数(此函数负责初始状态的新状态)

  • useState(0):具有初始值0useState <number>

    useState中,你一次只能声明一个状态属性。然而,数据可以是任何类型:原始数据类型、数组,甚至是对象。

  • onClick事件函数被添加以帮助发出按钮的事件操作。当按钮被点击时,将根据预期的操作调用不同的事件函数。

  • handleIncrementByTen()handleDecrementByTen()resetCountHandler()函数用于更改状态值,如下面的代码片段所示:

      const handleIncrementByTen = () => {    setCount(count + 10);  };  const handleDecrementByTen = () => {    setCount(count - 10);  };  const resetCountHandler = () => {    setCount(0)  };
    

useState<number>可以包含原始数据和对象数据,这些数据可以在 React 组件之间访问。在此阶段,建议你启动你的 VS code 或你喜欢的 IDE,并在开发有状态的组件时实验useState

将状态作为 props 传递

状态不仅限于在定义它的组件内使用。你可以将状态作为 props 传递给子组件,允许它们显示或使用父组件的状态数据。

让我们考虑以下示例:

import React, { useState } from 'react';const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const handleIncrementByTen = () => {
    setCount(count + 10);
  };
  return (
    <div>
      <p>Parent Count: {count}</p>
      <ChildComponent count={count} />
      <button onClick={handleIncrementByTen}>Increment
      </button>
    </div>
  );
};
const ChildComponent = ({ count }) => {
  return <p>Child Count: {count}</p>;
};

前面的代码展示了使用useState Hook 的 React 函数组件。它由两个组件组成,ParentComponentChildComponent,并演示了如何从父组件传递状态数据到子组件。

当你在应用程序中使用 ParentComponent 时,它将以初始的 count: number0 渲染。count: number,并且 ChildComponent)也会显示它通过 count 属性接收到的相同值。当你点击按钮时,count 状态会增加 10,并且两个计数都会反映相同的更新值。

条件渲染与状态

使用状态进行条件渲染 在 React 中允许您根据状态变量的值显示或隐藏用户界面的特定部分。通过使用条件语句,您可以控制根据应用程序的当前状态显示哪些内容或组件。

这种技术对于创建动态和交互式用户界面非常有用。想象一下,有一个 登录 按钮一旦用户登录就会变成 注销 按钮。这是一个经典的条件渲染例子!当你点击按钮时,React 会自动更新 UI 以反映新的状态,使其非常响应。哦,还有更多!

你甚至可以使用这种魔法来切换不同元素的可见性,例如根据用户操作显示或隐藏一个酷炫的模态框或下拉菜单。例如,假设你有 isLoggedIn 状态变量,并且你想根据用户是否登录显示不同的内容。

以下代码演示了如何使用 useState 钩子实现这一点:

import React, { useState } from 'react';const Dashboard = () => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const handleLogin = () => {
    setIsLoggedIn(true);
  };
  const handleLogout = () => {
    setIsLoggedIn(false);
  };
  return (
    <div>
      {isLoggedIn ? (
        <button onClick={handleLogout}>Logout</button>
      ) : (
        <button onClick={handleLogin}>Login</button>
      )}
      {isLoggedIn && <p>Hey friend, welcome!</p>}
      {!isLoggedIn && <p>Please log in to continue.</p>}
    </div>
  );
};

以下代码演示了一个名为 Dashboard 的 React 组件。它主要关于处理用户身份验证并向用户显示个性化消息。

Dashboard 组件内部,我们有 isLoggedIn 状态变量,该变量使用 useState 钩子进行管理。该变量跟踪用户是否当前已登录。当组件首次渲染时,isLoggedIn 的初始状态设置为 false,表示用户尚未登录。

现在,让我们深入探索条件渲染的魔法!当你查看 return 语句内部的 JSX 时,你会看到一些有趣的事情发生。我们使用 {} 括号来包裹我们的条件语句。

如果 isLoggedIntrue,我们将显示一个触发相应 handleLoginhandleLogout 函数的 onClick 事件。乐趣还没有结束!

我们还使用更多的 isLoggedIn 条件渲染来显示针对用户的个性化消息。当 isLoggedIntrue 时,我们显示一个温馨的问候,例如 handleLogin 函数被调用,而且你知道吗?

它将 isLoggedIn 设置为 true,表示用户现在已登录!同样,当用户点击 handleLogout 函数被触发时,它将 isLoggedIn 返回到 false,意味着用户现在已注销。

在下一节中,我们将检查 React 中的另一个钩子 useEffect。它广泛用于操作 DOM 和从外部源获取数据。

使用 useEffect 创建副作用

useEffect 钩子允许您从外部源获取数据,更新 DOM 树,并设置数据订阅。这些操作被称为副作用。在类组件中,您有我们所说的生命周期方法,可以根据组件渲染过程的阶段执行操作。useEffect 接受两个参数:一个函数和一个可选的依赖项。

重要的是要注意,useEffect 在一个地方完成了旧 componentDidMountcomponentDidUpdatecomponentWillUnmount 的工作。使用 useEffect 钩子可以缩短函数组件中编写以实现相同副作用的代码量。

以下是对 useEffects 钩子的语法:

- useEffect(<function>, <dependency>)useEffect(() => {
  // This callback function implementation is either to
     update DOM, fetch data from external sources, or to
     manage subscription that happens here.
}, [dependency]);

让我们深入一个使用 useEffect 钩子的例子:

import React, { useEffect, useState } from 'react';const App = () => {
const [data, setData] = useState([]);
    const API_URL = "https://dummyjson.com/users";
      useEffect(() => {
        fetchSpeakers();
    }, []);
     return (
        <ul>
      {data.map(item => (
        <li key={item.id}>
          {item.firstName} {item.lastName}
        </li>
      ))}
    </ul>);
};
export default App;

在前面的代码中,我们正在使用 useEffect 从外部 API 源获取数据。对于这个例子,我们使用了来自 https://dummyjson.com/users 的模拟 API 数据。当我们到达这本书的“后端开发”部分(第九章,API 开发和文档)时,我们将开发自定义 API 端点。接下来,我们将使用 useEffect() 钩子调用 fetchSpeakers 函数。

请参考 GitHub 以获取完整代码,并查看以下要点:

  • import React, { useEffect, useState } from 'react';: 这行代码允许我们使用来自 React 库的 useEffectuseState API。

  • const [data, setData] = useState([]);: 在这一行中,我们声明了一个以空数组作为初始数据的对象状态。

  • useEffect(()=>{...}, []): 这部分代码代表一个签名集,用于从指定的外部源获取数据。useEffect 函数中的第二个参数,依赖项 [],被设置为空数组。空数组确保 useEffect() 只渲染一次,即在组件挂载时第一次渲染。

    要使其根据状态变化进行渲染,您必须通过依赖数组传递状态。这样,您就可以防止组件在没有依赖状态变化的情况下不断进行不必要的重新渲染。

  • fetchSpeakers():Promise<Speaker[]>useEffect 钩子内是对 fetchSpeakers 函数的调用。这个函数是一个异步函数,它从模拟的远程 API 获取数据,并使用 setData 函数将数据设置在组件的状态中。作为 useEffect 的第二个参数传递的空依赖数组 [] 指示,该副作用仅在组件挂载时运行一次,之后不再运行。

    由于没有列出依赖项,副作用不会由任何属性或状态变量的变化触发。这就是为什么它表现得像 componentDidMount 生命周期方法一样,因为它只在组件首次渲染时运行一次。

  • const API_URL = "https://dummyjson.com/users";: API_URL 变量被设置为保存外部源端点信息。

  • try… catch 代码块被设置为执行代码,并在从端点获取数据出错时在控制台显示错误:

      const fetchSpeakers = async () => {    try {      const response = await           fetch(API_URL);      const data = await response.json();      setData(data.users);    } catch (error) {      console.log("error", error);    }  };
    fetch() API to fetch the data from the API_URL endpoint. The try… catch code block is set to execute the code and console error if there is an error fetching data from the endpoint.
    
  • map() 被设置在数据上,用于遍历对象数组数据,并在每个数组元素 item: Speaker 上调用函数时显示新创建的数组:

    {data.map(item => (        <li key={item.id}>          {item.firstName} {item.lastName}        </li>      ))}
    

让我们更新前面代码的 useEffect 钩子函数,并为其依赖项添加一个状态和一个 Cleanup 函数。在 React 应用程序中,在 useEffect 钩子内添加一个 Cleanup 函数起着至关重要的作用。

cleanup 函数在组件卸载时或在 useEffect 钩子中列出的依赖项更改时执行。其主要用途是执行清理任务,释放资源,并防止应用程序中的潜在内存泄漏或意外行为。

现在,按照以下方式更新前面的 useEffect()

useEffect(() => {    const fetchData = async () => {
      const fetchedData = await fetchSpeakers();
      if (isMounted) {
        setData(fetchedData);
      }
    };
    fetchData();
    // Cleanup function
    return () => {
      isMounted = false;
    };
  }, [data]) ;// Adding data state as a dependency

前面的代码使用 useEffect 钩子从 API (fetchSpeakers) 获取数据,并使用获取的结果更新数据状态。它使用一个 isMounted 标志来防止在组件卸载后设置状态,从而有效地避免潜在的问题。数据获取效果在 data 状态更改时运行,并且当组件卸载时,Cleanup 函数将 isMounted 标志设置为 false。

总结一下,我们已经看到了如何通过从外部源获取数据来使用 useEffect 在函数组件中进行副作用操作。接下来,我们将探讨如何使用 useContext 钩子更好地管理 React 应用程序中的全局状态。

在 React 应用程序中使用 useContext 管理全局状态

useContext 钩子用于在组件树中共享应用程序状态数据,而无需在每一层组件级别上显式传递 props。简单来说,useContext 是管理 React 应用程序全局状态的一种方式。记住,我们在 使用 useState 开发有状态组件 部分使用了 useState 钩子来管理局部状态。

然而,随着 React 项目要求的范围扩大,仅使用 useState 钩子来在深层嵌套组件中传递状态数据将变得无效。以下为 useContext 钩子的语法:

const Context = useContext(initialValue);

简要来说,我们将讨论 属性钻取 以了解它带来的挑战。之后,我们将深入探讨上下文 API 的实现,该 API 解决了这些问题。

理解属性钻取

让我们考察一下在不使用 useContext 的情况下,你如何将数据作为 props 传递到组件层次结构中。以下代码片段展示了在不使用 useContext 的情况下如何将数据传递到深层嵌套的内部组件:

  import React, {useState } from 'react';const App = () => {
  const [speakerName]= useState("Fred Morris");
  return (
    <div>
      <h2>This is Parent Component</h2>
      <ImmediateChildComponent speakerName={speakerName} />
    </div>
    );
    }
    function ImmediateChildComponent({speakerName}) {
      return (
        <div>
          <h2>This is an immediate Child
            Component</h2><hr/>
          <GrandChildComponent speakerName={speakerName} />
        </div>
      );
    }
  }
export default App;

前面的代码在一个包含嵌套组件的函数中显示了演讲者的名字。完整的源代码在 GitHub 上。

让我们更详细地理解这段代码:

  • const [speakerName]= useState:这一行用于设置 speakerName 的默认状态。

  • <App /> 是一个父组件,它使用 {speakerName} 作为 props 传递状态,该状态用于 <GrandChildComponent />

      return (    <div>      <h2>This is Parent Component</h2>      <ImmediateChildComponent        speakerName={speakerName}    </div>    );
    

    父组件必须通过 <ImmediateChildComponent /> 组件来达到嵌套在层级中更低的 <GrandChildComponent />。当你有五个或更多中间组件时,这变得更加繁琐,直到我们到达需要状态信息的实际组件。

    这正是 useContext 尝试解决的问题。以下代码显示了中间组件和最终的 GrandChildComponent: React.FC<Props>,其中需要状态:

    function ImmediateChildComponent({speakerName}) {      return (        <div>          <h2>This is an immediate Child            Component</h2><hr/>          <GrandChildComponent            speakerName={speakerName}   />        </div>      );    }    function GrandChildComponent({speakerName}) {      return (        <div>          <h3>This is a Grand Child Component</h3>          <h4>Speakers Name: {speakerName}</h4>        </div>      );}
    

现在我们来看一下 useContext 如何通过维护一个全局状态来解决前面的问题,这样不同的组件可以通信而不会在 React 中引起 prop 钻孔问题。

使用 useContext 解决 prop 钻孔问题

使用 useContext,你会了解如何在不手动使用 props 的情况下跨组件传递状态数据。以下代码显示了 useContext 的使用方法:

import React, {useState, useContext,createContext } from  'react';
const context = createContext(null);
const App = () => {
const [speakerName]= useState("Fred Morris");
  return (
    <context.Provider value={{ speakerName}}>
            <h1>This is Parent Component</h1>
            <ImmediateChildComponent  />
      </context.Provider>
        );}
function ImmediateChildComponent() {
    return (
      <div>
        <h2>This is an immediate Child Component</h2>
        <hr/>
        <GrandChildComponent  />
      </div>);
}
}
  export default App;

让我们详细理解前面的代码。请参考 GitHub 以获取完整的源代码:

  • import React, {useState, useContext, createContext } from 'react';:这行代码允许我们使用 React 库中的 useStateuseContextcreateContext

  • const context = createContext(null);:这行代码创建 Context<ContextType> 并允许我们使用 Provider: React.FC<ProviderProps|null>,其中 null 作为初始值。请注意,null 默认值也可以是 createContext 函数提供的任何值。

  • Context 提供者包围了子组件,并使状态值可用,如下所示:

            return (          <context.Provider value={{ speakerName }}>            <h1>This is Parent Component</h1>            <ImmediateChildComponent  />          </context.Provider>        );
    
  • const {speakerName} = useContext(context);:在这行代码中,我们使用 useContext 钩子来访问 <GrandChildComponent /> 中的 context

    function GrandChildComponent():React.FC<Props> {  const {speakerName} = useContext(context);      return (        <div>          <h3>This is a Grand Child Component</h3>          <h4>Speaker's Name: {speakerName}</h4>        </div>      );
    

总结来说,useContext 钩子使我们能够在函数组件中使用 context,无论组件层级有多深。这在复杂的 React 应用程序中总是必要的,在这些应用程序中,可能需要在全局应用程序状态中跨状态使用状态数据。使用 useContext,我们能够共享作为 props 传递的信息状态,而无需中间组件的直接干扰。

接下来,我们将深入探讨 useRef 钩子,并探讨如何在 React 组件中有效地利用它。

使用 useRef 直接访问 DOM 元素并持久化状态值

useRef 钩子允许你在 React 中直接访问 DOM 元素,并用于在重新渲染之间持久化状态值。React 作为强大的 UI 库,有很多新颖的概念(虚拟 DOM 设计模式、事件处理、属性操作),我们可以使用这些概念来访问和操作 DOM 元素,而无需使用传统的 DOM 方法。

这种声明式 DOM 方法是 React 非常受欢迎的原因之一。然而,使用 useRef,我们可以直接访问 DOM 元素并自由地操作它们,而不会产生后果。React 团队认为,尽管 React 在 DOM 之上提供了抽象,但使用 useRef 可以满足开发人员现在和未来对直接 DOM 访问的需求。

useRef 有两个核心用途:

  • 直接访问 DOM 元素

  • 持久化状态值,当更新时不会触发 React 组件的重新渲染

如果你感兴趣,想知道组件在更新时重新渲染的次数,我们可以使用 useStateuseRef。但使用 useState 可能不是一个好主意。使用它可能会让用户陷入无限循环的重新渲染,因为 useState 在其值每次更新时都会重新渲染。

然而,useRef 钩子在当前场景中非常出色,因为它可以在组件重新渲染之间存储状态值,而不会触发重新渲染机制。

让我们通过自动聚焦渲染组件上的输入字段来深入了解 useRef 的一个用例:

import React, {useRef} from 'react';const App = () => {
    const inputRef = useRef(null);
    const clickButton = () => {
      inputRef.current.focus();
    };
    return (
      <>
        <input ref={inputRef} type="text" />
        <button onClick={clickButton}>click to Focus on
          input</button>
      </>
    );
  }
export default App

让我们详细理解前面的代码:

  • const inputRef = useRef(null);: 这行代码为 useRef 函数创建了一个引用

  • <input ref={inputRef} type="text" />: 在这行代码中,ref 被添加到 input 元素上,以便使用 useRef() 钩子

  • onClick 事件添加到按钮上,该按钮使用 inputRef <button onClick={clickButton}>click</button> 来聚焦到 input

useStateuseRef 在持有状态值方面非常相似。然而,useState 在其值每次变化时都会重新渲染,而 useRef 不会触发重新渲染。

让我们继续到下一个钩子,useReducer。它是另一个用于在 React 应用程序中管理复杂状态的钩子。

使用 useReducer 进行状态管理

useReducer 钩子是 React 应用程序中的状态管理钩子。它比我们在本章前面讨论的 useState 钩子更健壮,因为它将函数组件中的状态管理逻辑与组件渲染逻辑分开。

useState 钩子封装了状态管理函数和组件渲染逻辑,这在需要复杂状态管理的大型 React 项目中可能变得难以处理。以下为 useReducer 的语法:

`const [state, dispatch] = useReducer(reducer, initialState)

useReducer 钩子接受两个参数——reducer,它是一个函数,以及初始应用程序状态。然后,钩子返回两个数组值——当前状态和 Dispatch 函数。

基本上,我们需要理解 useReducer 中的这些核心概念:

  • State: 这指的是随时间变化可以更改的可变数据。State 不一定是对象;它也可以是数组或数字。

  • Dispatch: 这是一个允许我们修改状态的函数。Dispatch 用于触发改变状态的动作。

  • Reducer: 这是一个处理状态如何修改的业务逻辑的函数。

  • IntialState:这指的是应用程序的初始状态。

  • Action:这是一个具有一组属性的对象。类型是一个必需的属性。

  • Payload:这指的是网络数据块中感兴趣的数据。

在解释了这些核心概念之后,我们还需要了解一件事:useReducer 的主要目的是以这种方式管理复杂的多状态,即状态管理的逻辑与组件视图功能分离。我们将通过一个实际例子来详细阐述这一点。

让我们深入探讨 useReducer 的用例:

以下代码片段将展示如何使用 useReducer 来管理不同的状态属性。我们将使用一个事件调度组件。在以下代码片段中,我们从伪造的 JSON API 数据中获取数据。

src 目录中创建 src/db.json 并粘贴此数据对象:

{    "schedules":    [
        {
            "id":1,
            "time":"10.00 AM",
            "speaker": "Juliet Abert",
            "subjectTitle":"Intro to React Hooks",
            "venue":"Auditorium C"
        },
        {
            "id":2,
            "time":"12.00 AM",
            "speaker": "Andrew Wilson",
            "subjectTitle":"React Performance Optimization"
            ,"venue":"Auditorium A"
        },
        {
            "id":3,
            "time":"2.00 PM",
            "speaker": "Lewis Hooper",
            "subjectTitle":"Intro to JavaScript",
            "venue":"Auditorium B"
        }
    ]
}

要安装 JSON 服务器以模拟后端服务,请在终端中输入以下命令:

npm i –g json-server

使用以下命令在端口 8000 上启动服务器:

json-server --watch db.json --port=8000

一旦 JSON 服务器启动,以下内容将在您的终端中显示:

Loading db.json  Done 
  Resources
  http://localhost:8000/schedules 
  Home
  http://localhost:8000

将以下代码片段添加到 App.js 中:

import { useReducer, useEffect } from 'react';import axios from "axios";
const initialState = {
  isLoading: false,
  error: null,
  data: null,
};
const reducer = (state, action) => {
  switch (action.type) {
    case "getEventSchedule":
      return {
        ...state,
        isLoading: true,
        error: null,
      };
              </ul>
    </div>
  );
};
export default App;

完整的源代码可以在 GitHub 上找到。让我们检查代码片段:

  • 组件的初始状态属性首先被指定:

    const initialState = {isLoading: false,error: null,data: null,};
    
  • 然后,我们定义 Reducer 函数为 const reducer = (state, action) => {}

    Reducer 函数接受两个参数:stateaction。然后通过 type 属性定义动作的状态逻辑。在这种情况下,switch 通过一系列基于动作的条件操作运行,并返回一个特定的动作类型。

    reducer 函数中指定的动作类型属性,例如,getEventSchedulegetEventScheduleSuccessgetEventScheduleFailure,允许我们根据动作类型的当前状态修改组件的状态。

  • getEventSchedule<EventSchedule[]> 接受 initialState 的所有属性,并且 isLoading 属性被设置为 true,因为我们正在获取这个状态数据:

    case "getEventSchedule":      return {        ...state,{/*accepts other initial State          properties*/}        isLoading: true, {/*change the initial state          of isLoading*/}      };
    
  • 当通过 action.payload: EventSchedule[] 返回的数据修改 data 属性时,将调用 getEventScheduleSuccess,并且将 isLoading 属性设置回 false

        case "getEventScheduleSuccess":      return {        ...state,        isLoading: false,        data: action.payload,{/*we have useful          returned data at this state*/}      };
    

    如果没有返回数据,将调用 getEventScheduleFailure :Action 并显示错误:

    .catch(() => {  dispatch({ type: "getEventScheduleFailure" });});
    
  • App() 组件处理 useReducer() 定义和执行的组件状态视图部分:

    const [state, dispatch] = useReducer(reducer, initialState);
    

    useReducer() 接受两个参数——reducerinitialState——并返回两个数组变量:statedispatchstate 变量包含状态对象,dispatch 是一个函数,允许根据在 reducer 函数中调用的动作类型更新状态。

  • 使用 useEffect() 从指定的端点获取调度数据:

    useEffect(() => {  dispatch({ type:"getEventSchedule" });  axios.get("http://localhost:8000/schedules/")    .then((response) => {      console.log("response", response);      dispatch({ type: "getEventScheduleSuccess",        payload: response.data });    })
    

    useEffect() 函数体内部,根据动作类型触发 dispatch()。指定了对象类型:dispatch({ type:"getEventSchedule" });

  • 使用 axios() 调用 axios.get("http://localhost:8000/schedules/") 来获取端点数据。

当动作类型为 getEventScheduleSuccess 时,我们期望返回数据,因此有效载荷属性 – dispatch({ type: "getEventScheduleSuccess", payload: response.data })

以下代码片段处理了可能由此承诺请求引发的错误:

.catch(() => {  dispatch({ type: "getEventScheduleFailure" });
});

App() 组件的 return 结构中,我们使用以下代码片段将日程安排渲染到屏幕上:

            <h2>Event Schedules</h2>            {state.isLoading && <div>Loading...</div>}
            {state.error && <div>{state.error}</div>}
            {state.data && state.data.length === 0
              &&   <div>No schedules available.</div>}
            <ul>
              {state.data && state.data.map(({ id, time,
                speaker, subjectTitle, venue }) => (
                <li key={id}>
                  Time: {time} <br />
                  Speaker: {speaker}<br />
                  Subject: {subjectTitle}<br />
                  Venue: {venue}
                </li>
              ))}
            </ul>

我们检查 initialState :State 是否处于加载状态,并显示 <div>Loading…</div>。如果错误状态为 true,则显示错误。如果没有要获取的数据,则显示适当的消息。我们还检查数据状态,并确保我们有数据可以显示。现在,如果服务器没有运行,请使用 npm start 启动服务器:

以下截图显示了 useReducer 的一个示例实现:

图 3.1 – 展示使用  钩子效果的截图

图 3.1 – 展示使用 useReducer 钩子效果的截图

我们已经看到如何使用 useReducer 钩子来管理 React 中的高级多状态。在下一节中,我们将探讨 useMemo 是什么以及我们如何使用它来提高 React 应用程序的性能。

使用 useMemo 提高性能

useMemo 钩子是 React 核心 API 的一部分,旨在提高 React 应用程序的性能。它使用软件开发中已知的技术记忆化

这是一种优化技术,通过在内存中保留资源密集型计算函数调用的结果,并在后续使用相同输入时发送回缓存的输出,来提高软件的性能。那么,为什么 useMemo 在 React 应用程序开发中很重要?useMemo 为 React 开发者解决了两个性能问题。

防止不必要的组件重新渲染

它通过在后续请求(无需状态更新)时发送缓存的函数结果,来对消耗大量资源的计算函数的返回值进行记忆化。

让我们深入一个 useMemo 的用例,以更好地理解它如何在 React 中使用。此代码片段显示了组件在每次字符搜索时如何重新渲染。在一个拥有超过 20,000 用户的庞大应用中,这可能会导致性能问题。

首先,我们来看看没有使用 useMemo 的代码是什么样子:

import React, { useState} from 'react';const speakers = [
  {id: 10, name: "John Lewis"},
  { id: 11, name: "Mable Newton"},
];
const App = () => {
  const [text, setText] = useState("");
  const [searchTerm, setSearchTerm] = useState("");
  const onChangeText = (e) => {
    setText(e.target.value);
  };
  console.log("Text", text);
  const handleClick = (e) => {
    setSearchTerm(e.target.value);
  };
  console.log("Search Term", text);
  ));
  });
  return (
      <div>
        ---
    </div>
  );
};
export default App;

以下截图显示了 list 组件在每次字符搜索时重新渲染:

图 3.2 – 显示组件重新渲染的控制台截图

图 3.2 – 显示组件重新渲染的控制台截图

让我们通过 useMemo 钩子的实现来深入了解,以获得开发者如何显著提高性能和优化 React 应用程序资源使用的见解,确保仅在必要时执行昂贵的计算:

  • speakers 被声明为包含对象数据数组的持有者:

    const speakers = [  {id: 10, name: "John Lewis"},  { id: 11, name: "Mable Newton"},];
    
  • textsearchTerm 被声明为具有它们的设置方法作为状态变量:

    const [text, setText] = useState("");const [searchTerm, setSearchTerm] = useState("");
    
  • onChange 处理器:此事件处理器将初始状态更新为当前状态:

    const handleClick = (e) => {    setSearchTerm(e.target.value);  };
    
  • filteredSpeakers 函数用于根据 searchTerm 使用不区分大小写的搜索过滤演讲者数组。通过这种过滤,您能够通过记忆化过滤结果来优化过滤性能:

    const filteredSpeakers = speakers.filter((speaker) => {  console.log("Filtering speakers...");  return speaker.name.toLowerCase()    .includes(searchTerm.toLowerCase());}
    
  • useMemo

    <div>  <input type="text" onChange={onChangeText} />  <button onClick={handleClick}>Search</button></div>{filteredSpeakers.map((filteredSpeaker) => (  <li key={filteredSpeaker.id}>    {filteredSpeaker.name}</li>))}</div>
    

如您在前面的代码片段中所见,speaker 组件的依赖属性并未改变。无需重新渲染,但控制台显示存在重新渲染的情况。

现在让我们看看使用 useMemo 的代码是什么样的。将前面代码中的 filteredSpeakers 函数更新为以下代码片段:

  const filteredSpeakers = useMemo( () =>    speakers.filter((speaker) => {
    console.log("Filtering speakers...");
    return speaker.name.toLowerCase()
      .includes(searchTerm.toLowerCase());
  },[searchTerm]));

前面的代码片段显示了在 filteredSpeakers 函数上使用 useMemo。此函数仅在 searchTerm 状态改变时执行。filteredSpeakers 函数在 text 状态改变时不应该运行,因为显然这不是 useMemo 钩子依赖项数组中的依赖项。

接下来,我们将探索 useCallbackuseCallback 钩子与 useMemo 钩子类似,旨在使 React 应用程序性能更优。useCallbackuseMemo 都优化了 React 应用程序。让我们深入了解 useCallback,以避免组件函数的重新渲染。

使用 useCallback 来避免函数重新渲染

在 React 函数组件中,还有一个额外的优化钩子,称为 useCallback。它与 useMemo 具有相似的功能,但在输出行为上略有不同。在 useCallback 中,返回一个记忆化的函数,而 useMemo 返回函数的记忆化返回值。

useMemo 类似,useCallback 在函数组件内部更新其依赖项时会被调用。这确保了函数组件不一定会不断重新渲染。useCallback 的关键亮点包括:

  • useCallback 中返回一个记忆化的回调函数。这通过记忆化技术提高了 React 应用的性能。

  • useCallback 钩子的依赖项变化决定了它是否会更新。

现在,让我们深入一个简单的 useCallback 用例,以加深理解。

以下代码片段显示了一个演讲者列表,以模拟高计算需求,考虑使用 useCallback 进行性能优化。同时,需要注意的是,这种说明绝对不足以作为用例来展示现实生活中的性能瓶颈场景,但它对于解释这种场景很有帮助。

假设我们有一个包含大量 speakers 组件的列表,用于处理显示和搜索演讲者,如果没有使用 useCallback,那么在输入字段中的每次字符搜索都会导致 AppListListItem 无需重新渲染。

完整的代码片段可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/blob/main/Chapter03/08

import React, {useState,useCallback} from 'react';  const handleRemoveSpeaker = useCallback(
    (id) => setSpeakers(speakers.filter((user) =>
      user.id !== id)),
    [speakers]
  );

之前的代码片段展示了 useCallback 的使用。代码结构基本上与 useMemo 相似,只是 useCallback 将要缓存或记忆化的函数包裹起来。

在下面的图中,我们可以看到 AppListListItem 组件在搜索输入框中的每次字符搜索后都会重新渲染。

图 3.3 – 展示 useCallback 使用截图

图 3.3 – 展示 useCallback 使用截图

handleRemoveSpeaker: React.FC<ButtonProps> 通过 useCallback 优化,以防止由于搜索输入状态的变化而重新渲染 ListListItem 组件。如果应用程序的用户点击 添加演讲者删除 按钮,组件的重新渲染是预期的。

useCallback Hook 在 React 中解决了一个主要问题:防止由于引用相等性检查而导致组件不必要的重新渲染。

接下来,我们将剖析自定义 Hooks 的使用,以将组件的业务逻辑与渲染函数分离。这允许函数的可重用性,并在 React 应用程序中更好地组织代码。

使用自定义 Hooks 进行代码复用

我们已经广泛讨论了 React 中的一些内置 Hooks。Hooks 自 v16.8 版本以来一直是 React 核心库的一部分,它允许 React 组件在不使用类方法的情况下表现出状态性。例如,useStateuseEffectUseMemouseRefuseContextuseCallback 是用于管理状态、共享状态逻辑以及允许与 React 核心 API 进行其他交互的特定函数。

现在我们来了解什么是自定义 Hook 以及使用它们可以获得的益处。

use 和通常调用一个或多个内置 React Hooks。例如,自定义 Hooks 可以命名为任何东西,只要它以 use 开头,例如 useCustomHookuseFetchSpeakersuseUpdateDatabase。传统上,自定义 Hook 名称前必须包含 use

那么,为什么你想构建自己的自定义 Hooks 呢?让我们来看看经验丰富的 React 开发者构建自定义 Hooks 的原因:

  • 作为一名 React 开发者,你将编写大量的函数来解决你在 React 项目中的问题。如果不遵循最佳 React 实践,一些函数将在你的项目中的许多组件中频繁重复。使用自定义 Hook,你可以在项目的多个组件之间重用状态性逻辑。

  • 自定义 Hooks 鼓励在状态逻辑函数和组件视图层之间分离关注点。

  • 调试简单。

让我们看看自定义 Hook 的实现示例:

App.js 中,输入以下内容:

import React from 'react';import useFetchSpeakers from "./useFetchSpeakers";
const App = () => {
  const API_URL = "https://dummyjson.com/users";
  const [data] = useFetchSpeakers(API_URL);
  return (
    <>
      <ul>
        {data.map((item) => (
          <li key={item.id}>
            {item.firstName} {item.lastName}
          </li>
        ))}
      </ul>
    </>
  );
};
export default App;

现在,让我们分解前面的代码片段:

  • import useFetchSpeakers from "./useFetchSpeakers" 将自定义 Hook 带入此应用程序的作用域。像任何其他 Hook 一样,我们在命名约定中使用 use

  • useFetchSpeakers Hook 根据作为端点的 API_URL : string 返回数据变量状态。这个端点作为参数传递给自定义 useFetchSpeakers Hook。

  • 我们随后使用 map() 迭代数据对象,以显示返回的 firstName:stringlastName:string

useFetchSpeakers.js 中,我们定义了具有其本地管理状态的自定义 Hook 函数:

import { useEffect, useState } from 'react';const useFetchSpeakers = (url) => {
  const [data, setData] = useState([]);
  useEffect(() => {
    const fetchSpeakers = async () => {
      try {
        const response = await fetch(url);
        const data = await response.json();
        setData(data.users);
      } catch (error) {
        console.log("error", error);
      }
    };
    fetchSpeakers();
  }, [url]);
  return [data];
};
export default useFetchSpeakers;

在前面的代码片段中,涉及以下步骤:

  1. useFetchSpeakers 自定义 Hook 签名被定义。它接受 url 作为参数。

  2. useFetchSpeakers Hook 使用 useEffect() 从端点异步获取数据——传递给自定义 Hook 的 url 参数。

  3. 返回一个包含 jsonified 结果数据的承诺,该数据可通过 setData(data.users) 状态提供。

  4. 它有一个依赖项(url),当数据发生任何变化时,都会导致组件重新渲染组件状态。

通过这种方式,你可以看到自定义 Hook 如何使组件的逻辑部分与渲染部分分离,以及代码重用是如何被鼓励和实现的。

摘要

在本章中,我们已经能够理解 Hooks 作为我们在 React 中添加状态性组件的新思维转变。在 Hooks 之前,只有类组件可以提供状态性功能。从 React 16.8 开始,我们现在能够在 React 应用程序中开发出更优雅、更简洁的状态性功能组件。

学习曲线很平缓,因为我们可以利用我们对常规 JavaScript 函数的理解来开发功能组件,为我们的 Web 应用程序提供用户界面。在 React 中使用 Hooks,用户和开发者的体验得到了极大的提升。

在下一章中,我们将重点介绍如何利用 React API 从外部源将数据提取到我们的 Web 应用程序中。我们今天使用的应用程序大多严重依赖外部数据。毫无疑问,React 在这个领域表现出色。

第四章:使用 React API 获取数据

在过去的几年里,对数据库驱动 Web 应用程序的需求有所增加。这种增加是当前数据丰富性的结果。随着互联网的广泛采用,企业利用 Web 应用程序与客户、员工和其他利益相关者进行互动。

比以往任何时候,Web 开发者都面临着诸如数据组织和消费等任务。内部和外部数据都需要我们拥有智能且以业务为导向的数据库驱动 Web 应用程序。

作为全栈软件工程师,你的前端任务之一将是消费数据,无论是来自内部开发的 API 还是第三方 API。在我们深入探讨在 React 项目中获取数据的方法或工具之前,让我们简要地讨论一下 API 是什么以及为什么它们正在重新定义构建用户界面和 Web 应用程序的方式。

API 简单地说就是允许系统之间通过一组标准接受的格式中的规则进行通信。在 Web 开发中,HTTP 协议定义了基于 Web 的系统通信的规则集。HTTP 是一种用于在互联网上获取资源的数据交换协议。

数据交换有两种主要格式:XMLJSON。JSON 在这两种广泛使用的数据交换格式中赢得了人气竞赛。JSON 专门设计用于数据交换,无缝处理数组,并且在开发者中被广泛使用。

在 React 生态系统内,开发者可以访问一系列公开的接口,旨在简化从各种来源获取数据。这些 API 旨在赋予 React 开发者创建直观用户界面并提升与 Web 应用程序交互的整体用户体验。

在本章中,我们将学习一些在 React 前端开发中用于从不同来源获取数据的方法和技术。在本章中,我们将涵盖以下主题:

  • 在 React 中使用 Fetch API 获取数据

  • 使用async/await语法获取数据

  • 使用 Axios 获取数据

  • 使用 React Query 获取数据

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter04.

在 React 中使用 Fetch API 获取数据

Fetch API是 Web 浏览器内建的一个 API,它提供了使用 HTTP 进行互联网通信的 JavaScript 接口。每个 Web 浏览器都有一个 JavaScript 引擎作为运行时来编译和运行 JavaScript 代码。

React 生态系统无疑依赖于 JavaScript。这是一个事实,也是为什么在深入 React 应用程序开发之前,你被期望理解现代 JavaScript 的原因之一。

作为一名 React 开发者,你需要网络资源来构建网络应用。fetch()方法为你提供了访问和操作 HTTP 对象请求以及 HTTP 协议响应的手段。假设在我们的网络应用中,我们想要显示会议演讲者和他们相关的数据。这些信息存储在另一个资源数据库服务器上。

从第三方公开 API 中,我们将消费用户的资源以获取假设数据,用于我们的 React 应用,如下所示:

import React, { useEffect, useState } from 'react';const App = () => {
  const [data, setData] = useState([]);
  const getSpeakers = ()=>{
    fetch("https://jsonplaceholder.typicode.com/users")
       .then((response) => response.json())
       .then((data) => {
         setData( data);
       })
    }
    useEffect(() => {
      getSpeakers()
    },[]);
    return (
      <>
        <h1>Displaying Speakers Information</h1>
        <ul>
          {data.map(speaker => (
            <li key={speaker.id}>
              {speaker.name},  <em> {speaker.email} </em>
            </li>
          ))}
        </ul>
      </>
    );
};
export default App;

让我们详细讨论一下前面的fetch数据片段:

  • import React, { useEffect, useState } from 'react':这一行导入了 React 的核心函数和一些 Hooks,用于在我们的组件中使用。

  • 初始化useState:我们在组件中通过调用useState来初始化我们的状态,如下所示:

    const [data, setData] = useState([]);//using a destructuring array to write concise code.
    

    useState接受一个初始状态为空数组(useState([])),并返回两个值,datasetData

    • data:当前状态

    • setData:状态更新函数(此函数负责初始状态的新状态)

  • useState([])是带有初始值空数组[]useState

  • 以下代码片段包含一个全局的fetch()方法,它接受端点 URL,https://jsonplaceholder.typicode.com/users,其中包含我们假设的资源,用于演讲者:

    const getSpeakers = ()=>{  fetch("https://jsonplaceholder.typicode.com/users")    .then((response) => response.json())    .then((data) => {      setData( data);    })
    

    上一段代码中的 URL 是我们的资源端点。它返回 JSON 格式的数据。setData()函数接受新的状态,即返回的 JSON 数据。

  • 使用useEffect钩子来调用getSpeaker函数:

    useEffect(() => {getSpeakers()    },[]);
    
  • 在数据数组上调用map()函数,用于遍历演讲者的数据并在屏幕上显示详细信息:

    {data.map(speaker => (        <li key={speaker.id}>          {speaker.name},  <em> {speaker.email} </em>        </li>      ))}
    

总结来说,fetch()函数接受资源 URL(jsonplaceholder.typicode.com/users)作为参数,这是我们所感兴趣的网络资源路径,并且当请求的资源响应可用时,返回一个状态为已满足的 Promise。

注意

在现实世界的应用中,有效地管理网络错误至关重要,尤其是在数据检索遇到问题或数据缺失时。此外,实现加载状态可以显著提升整体用户体验。

接下来,我们将探讨在 React 项目中使用async/await和 ECMAScript 2017 特性来获取数据的另一种技术。

使用async/await语法获取数据

在纯 JavaScript 中编写异步代码有三种方式:回调函数、Promise 和async/await。在本节中,我们将重点关注async /await,并探讨它如何在 React 网络应用中使用。async/await是对 Promise 的改进。

以下代码片段解释了如何使用基于 Promise 的方法通过async/await从 API 获取数据:

import React, { useEffect, useState } from 'react';const App = () => {
    const [data, setData] = useState([]);
    const API_URL = "https://dummyjson.com/users";
    const fetchSpeakers = async () => {
        try {
            const response = await fetch(API_URL);
            const data = await response.json();
            setData(data.users);
        } catch (error) {
            console.log("error", error);
        }
    };
    useEffect(() => {
        fetchSpeakers();
    },[]);
    return (
      <> [Text Wrapping Break]
           <h1>Displaying Speakers Information</h1>
[Text Wrapping Break]
           <ul>
               {data.map(item => (
                   <li key={item.id}>
                       {item.firstName} {item.lastName}
                   </li>
               ))}
           </ul>
      </>
    );
};
export default App;

让我们讨论一下前面的代码片段,它展示了如何使用async/await异步获取数据:

  • import React, { useEffect, useState } from 'react': 这行代码导入 React 核心函数和一些 Hooks 以用于我们的组件。

  • 初始化 useState: 我们通过在组件中调用 useState 来初始化我们的状态,如下所示:

    const [data, setData] = useState([]);//using a destructuring array to write a concise code.
    

    useState 接受一个空数组的初始状态(useState([]))并返回两个值,datasetData

    • data: 当前状态

    • setData: 状态更新函数(此函数负责初始状态的新状态)

  • useState([]) 是带有空数组初始值 []useState

    const API_URL = "https://dummyjson.com/users";  const fetchSpeakers = async () => {      try {          const response = await fetch(API_URL);          const data = await response.json();          setData(data.users);      } catch (error) {          console.log("error", error);      }  };
    

    在前面的端点中,我们有假设的演讲者资源。这是我们的资源端点。它返回 JSON 格式的数据。setData() 接受新的状态,即返回的 JSON 数据。

  • useEffect 钩子用于调用 fetchSpeakers 函数,该函数从端点 const API_URL = "dummyjson.com/users" 异步获取数据:

      useEffect(() => {        fetchSpeakers();    },[data]);
    

    数组依赖项提供数据状态。当数据状态发生变化时,可能是由于列表中添加或删除演讲者,组件会重新渲染并显示更新后的状态。

  • 最后,map() 在数据上被调用,它用于遍历演讲者的数据以将详细信息渲染到屏幕上:

    return (        <>        <ul>      {data.map(item => (        <li key={item.id}>          {item.firstName} {item.lastName}        </li>      ))}    </ul>
    

使用 async/await 方法获取数据可以为你的代码提供更好的组织结构,并提高你的 React 应用的响应性和性能。async/await 的非阻塞模式意味着你可以在等待大量数据运行任务响应的同时继续执行其他代码操作。

接下来,我们将探讨另一种从 API 获取数据的方法,使用名为 Axios 的第三方 npm 包。

使用 Axios 获取数据

Axios 是一个轻量级的基于 Promise 的 HTTP 客户端,用于消费 API 服务。它主要用于浏览器和 Node.js。要在我们的项目中使用 Axios,请打开项目终端并输入以下命令:

npm install axios

现在,让我们看看如何在以下代码片段中使用 Axios:

import React, { useEffect, useState } from 'react';import axios from 'axios';
const App = () => {
    const [data, setData] = useState([]);
    const getSpeakers = ()=>{
        axios.get(
            "https://jsonplaceholder.typicode.com/users")
            .then(response => {
                setData(response.data)
            })
    }
    useEffect(() => {
        getSpeakers()
    },[]);
    return (
        <>
           <h1>Displaying Speakers Information</h1>
           <ul>
               {data.map(speaker => (
                   <li key={speaker.id}>
                       {speaker.name},  <em>
                           {speaker.email} </em>
                   </li>
               ))}
           </ul>
        </>
    );
};
export default App;

让我们检查前面的代码片段,看看 Axios 如何在数据获取中使用:

  • import React, { useEffect, useState } from 'react': 这行代码导入 React 核心函数和一些 Hooks 以用于我们的组件。

  • import axios from "axios": 这行代码将已安装的 Axios 包引入到项目中以便使用。

  • 初始化 useState: 我们通过在组件中调用 useState 来初始化我们的状态,如下所示:

    const [data, setData] = useState([]);//using a destructuring array to write a concise code.
    
  • useState 接受一个空数组的初始状态(useState([]))并返回两个值,datasetData

    • data: 当前状态

    • setData: 状态更新函数(此函数负责初始状态的新状态)

  • useState([]) 是带有空数组初始值 []useState

    const getSpeakers = ()=>{    axios.get(        "https://jsonplaceholder.typicode.com/users")        .then(response => {            setData(response.data)        })}
    

    这是我们的资源端点。它返回 JSON 格式的数据。setData() 接受新的状态,即返回的 JSON 数据。

    getSpeakers 函数使用 axios.get() 从端点获取外部数据并返回一个承诺。状态值被更新,我们从响应对象中获取一个新的状态 setData

    useEffect(() => {getSpeakers()    },[]);
    
  • 使用 useEffect 钩子调用 getSpeaker() 并渲染组件:

    <ul>    {data.map(speaker => (        <li key={speaker.id}>            {speaker.name},  <em> {speaker.email}                </em>        </li>    ))}</ul>
    

    最后,使用 map() 函数遍历演讲者的数据,并在屏幕上显示姓名和电子邮件。

接下来,让我们看看 React 中的数据获取技术,我们将探讨使用 React Query 获取数据的一种新方法。

在 React 中使用 React Query 获取数据

React Query 是一个用于数据获取目的的 npm 包库,其中包含大量功能。在 React Query 中,状态管理、数据预取、请求重试和缓存都是开箱即用的。React Query 是 React 生态系统的一个关键组件,每周下载量超过一百万。

让我们重构我们在 使用 Axios 获取数据 部分使用的代码片段,并体验 React Query 的神奇之处:

  1. 首先,安装 React Query。在项目的根目录中,执行以下操作:

    npm install react-query
    
  2. App.js 中添加以下内容:

    import {useQuery} from 'react-query'import axios from 'axios';function App() {  const{data, isLoading, error} = useQuery(    "speakers",    ()=>{ axios(      "https://jsonplaceholder.typicode.com/users")  );  if(error) return <h4>Error: {error.message},    retry again</h4>  if(isLoading) return <h4>...Loading data</h4>  console.log(data);  return (      <>         <h1>Displaying Speakers Information</h1>         <ul>             {data.data.map(speaker => (                 <li key={speaker.id}>                     {speaker.name},  <em>                         {speaker.email} </em>                 </li>             ))}         </ul>      </>  );}export default App;
    

检查前面的 使用 Axios 获取数据 部分,比较代码片段。React Query 的代码片段要短得多,更简洁。useStateuseEffect 钩子的需求已经被 useQuery() 钩子开箱即用处理。

让我们分析前面的代码:

  • useQuery 接受两个参数:查询键(speakers)和一个使用 axios() 从资源端点获取假设演讲者的回调函数。

  • useQuery 使用变量进行解构 – {data, isLoading, error}。然后我们检查是否有来自错误对象的错误消息。

  • 一旦我们有了数据,return() 函数就返回一个演讲者数据的数组。

index.js 中添加以下代码。假设现有的 index.js 代码已经存在:

import { QueryClient, QueryClientProvider } from    react-query";
const queryClient = new QueryClient();
root.render(
    <QueryClientProvider client={queryClient}>
        <App /> </QueryClientProvider>
);

让我们对 index.js 中的代码片段进行一些解释:

  • 从 React Query 导入 { QueryClient, QueryClientProvider }QueryClient 允许我们在 React Query 中利用所有查询和变更的全局默认值。QueryClientProvider 连接到应用程序并提供一个 QueryClient

  • 创建一个新的 QueryClient 实例 queryClient:将组件包裹在 QueryClientProvider 中——在这个例子中,<App/> 是组件,并将新实例作为属性值传递。

如果 localhost:3000 没有运行,现在运行 npm start。屏幕上应该显示以下内容:

图 4.1 – 屏幕截图展示了 React Query 在获取数据中的应用

图 4.1 – 屏幕截图展示了 React Query 在获取数据中的应用

React Query 在从 API 资源获取数据方面非常有效。它封装了可能由 useStateuseEffect 需要的函数。通过引入带有 queryKey 的强大缓存机制,React Query 根本重新定义了我们在 React 应用程序中获取数据的方式。

与手动管理数据获取和缓存不同,React Query 以透明的方式处理这些操作。React Query 允许开发者仅用几行代码就轻松地获取和缓存数据,从而减少样板代码并提高性能。

图书馆提供了各种钩子和实用工具,这些工具简化了数据获取、错误处理以及与服务器之间的数据同步,从而带来了更高效、更流畅的用户体验。进一步探索 React Query 可以打开处理复杂数据获取场景和优化 React 应用程序数据管理的新世界。

摘要

处理数据是任何网络应用程序的关键组成部分。React 已经证明在处理大量数据时非常高效和可扩展。在本章中,我们讨论了您可以在项目中利用的各种处理数据获取的方法。我们讨论了使用 Fetch API、async/await、Axios 和 React Query 获取数据。

在下一章中,我们将讨论 JSX 以及如何在 React 中显示列表。

第五章:JSX 和在 React 中显示列表

组件化是 React 应用程序开发中的设计范式。作为一名开发者和 React 热衷者,你将开发大量有用的组件。你需要一组单元的组合来提供用户可以无缝交互的界面。

JavaScript 语法扩展JSX)是描述现代网络应用程序用户界面(UI)的一种创新方法。在本章中,我们将深入探讨为什么 JSX 是开发生产就绪的 React 应用程序的核心要求之一。此外,你还将学习如何在 React 中显示列表。

我们在几乎每一个网络应用程序开发项目中都会使用列表,了解如何渲染列表是网络开发者所需技能集的一部分。HTML 和 JavaScript 作为网络的语言,从一开始就伴随着我们,帮助网络开发者构建网络应用程序。

然而,在最近一段时间,对复杂且高度丰富的交互式网络应用程序的需求促使使用 JSX 作为构建用户界面组件的一种创新方法。

在本章中,我们将了解 JSX 是什么以及它与 HTML 的不同之处。我们将使用 JSX 来描述本章中将要构建的用户界面。然后,我们将检查我们在 React 中如何处理事件操作。

作为一名 React 开发者,你将为用户消费内部和外部 API 数据。到本章结束时,你将能够向用户显示列表对象,处理 React 中的常见事件,并使用循环函数渲染列表。

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

  • JSX 是什么?

  • JSX 与 HTML 的比较

  • JSX 如何抽象 JavaScript

  • React 中的事件处理

  • 在 React 中显示列表

  • JSX 中的嵌套列表

  • 在 JSX 中遍历对象

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter05

JSX 是什么?

你已经接触并看到了一些 JSX。让我们更深入地讨论 JSX 作为一种新方法,在设计和用户界面时将 HTML 添加到 JavaScript 中意味着什么。

JSX 简单来说是一种类似于 XML 的 JavaScript 语法扩展。JSX 允许前端开发者将 HTML 元素与 JavaScript 结合使用。这种混合的效果通常是一个令人印象深刻的用户友好界面。正如我们所知,React 的主要目的是为我们提供一组用于构建用户界面的 API。

几乎没有争议,React 已经接受了这一挑战,成为前端 JavaScript 库和框架丛林中的领先璀璨宝石。React 通过改进的用户体验,为大规模、生产级别的网络和移动应用程序提供动力。

有趣的是,React 正在使用我们已熟悉的相同工具、语言和技术(HTML 和 JavaScript)来实现这种改进的效率和性能:React 利用 HTML 元素和 JavaScript 函数来构建可重用的 UI 组件。JSX 作为一个允许我们混合标记和显示逻辑以构建 React 组件的方法而发展起来。

您可以安全地将 JavaScript 代码作为 JSX 表达式运行。考虑以下简单的 React 组件片段,以了解您可以在 React 中使用 JSX 的几种方式:

import React from 'react';export function App() {
    const speakerName = "John Holt"
    return (
        <div className='App'>
            <h2>{speakerName}</h2>/* This outputs  John
                Holt */
            <h2>{5 + 5 }</h2>/* This outputs the sum of 5 +
                5 = 10 */
        </div>
    );
}

让我们检查这段代码片段中正在发生的事情。

以下代码解释了如何在 React 中使用 JSX:

  • import React from 'react' 用于将 React 引入作用域

  • export function App() 描述了一个名为 App() 的函数组件,该组件可以被其他组件访问

  • 声明并赋值了 const speakerName 变量,其值为 John Holt

  • 以下代码片段的前一部分描绘了 component App() 代码的 JSX 部分:

    return (    <div className='App'>        <h1>Displaying name of a conference            speaker:</h1>        <h2>{speakerName}</h2>   /* This outputs John                                   Holt */        <h2>{5 + 5 }</h2>   /* This outputs number 10                           */    </div>);
    

以下代码是 HTML 元素(h2div)和花括号内的 JavaScript 表达式({speakerName})的混合。这显示了文本 John Holt,而 {5 + 5} 显示了 5 + 5 的和。

无论您有什么 JavaScript 表达式,都可以将其放在 JSX 中的花括号内,它将返回预期的有效 JavaScript 输出。然而,浏览器默认情况下不知道如何处理 JSX 语法;但借助 Babel 编译器的帮助,JSX 代码被转换成浏览器原生理解的等效 JavaScript 语法。

Babel 的 JSX 转译对使 React 应用程序如此快速的因素做出了重大贡献。它不仅将 JSX 代码转译成浏览器 JavaScript,而且还进行了优化。

您还可以看到 <div className='App'> 属性在 JSX 中的使用;类属性的命名约定很重要。我们以驼峰式格式编写它 – React 中的 classNameclassName 属性被分配了一个值为 App 的值,该值在 CSS 文件中使用,以向组件添加样式。

此外,我们还需要理解 JSX 和 文档对象模型 (DOM) 之间存在高级连接。DOM 是一个面向对象的网络文档表示。它是一组用于操作可以在网络浏览器上加载的 Web 文档的 API。典型的 Web 应用程序页面代表一个 Web 文档,DOM API 使用它来维护 DOM 结构和内容。

DOM 操作通常由 JavaScript – 一种脚本语言来完成。您可以使用 JavaScript 对象来创建、更新和删除 HTML 元素。DOM 操作是大多数网络应用程序中交互性的基石。但 React 对 DOM 的处理方式不同,并且具有一些创新性。

React 团队能够识别出在每次 HTML 元素操作(创建、更新和删除)中对 DOM 树重新渲染的挑战,并决定开发一个虚拟 DOMVDOM)。VDOM 是原生浏览器 DOM 的抽象,使得 React 应用程序能够快速高效,并表现出跨浏览器的兼容性。

React 组件只重新渲染 DOM 中变化的节点(h1divp – 所有这些在 HTML 中代表节点)而不是在单个节点变化时重新渲染整个 Web 文档。

接下来,我们将讨论如何使用 JSX 和 HTML 元素来设计 UI 组件,以及 JSX 和 HTML 之间的固有差异。

JSX 与 HTML 的比较

React.createElement()的底层实现。JSX 使得组件接口开发变得轻松,同时优化了效率。

HTML 是构建 Web 的标准语言。HTML 元素为你在互联网上看到的每个网页提供动力。HTML 语法易于理解,它是浏览器原生理解的语言。

下表清楚地说明了 JSX 和 HTML 之间存在的细微差异,以便更好地理解和在 React 应用程序中使用:

HTML JSX
原生于浏览器 HTML 元素是浏览器原生的。 JSX 在使用 Babel 将其转换为 JavaScript 之前,浏览器才能理解其语法。
属性使用 你在命名 HTML 属性方面有灵活性,尽管这通常是小写,如onmouseoveronclickonsubmitonloadonfocus等等。 在 JSX 中命名属性和事件引用(如onClickonChangeonMouseOver等)时,你必须遵循驼峰命名法规则。
classfor属性命名 在 HTML 中命名 CSS 类时必须使用小写class,在命名输入标签时使用for 在 JSX 中,你必须使用className(驼峰命名法)和htmlFor为输入标签。
处理 JavaScript 代码 你必须使用<script>...</script>脚本标签或外部 JS 文件来向 HTML 添加 JavaScript。 在 JSX 中,你可以在花括号内编写 JS 表达式;例如,{ new Date().toString }
返回单个父元素 在 HTML 中,你可以返回 HTML 元素而不将其包含在单个父元素中;例如:<div > </div><p>...</p><ul>...</ul><span>...</span>。所有这些标签都可以独立地存在于网页上,带有封装标签。 在 JSX 中,你必须返回一个单个父元素;否则,你会得到 JSX 错误;例如:<div></div>或一个片段标签<> </>必须包含你所有的 HTML 元素:<div><p>...</p><ul>...</ul></div><> <p>...</p><ul>...</ul> </>
自闭合标签 在 HTML 中,你可以有一个不带斜杠的自闭合标签;例如,<br> 在 JSX 中,你必须给任何自闭合标签添加一个斜杠;例如,<br />

表 5.1 – JSX 与 HTML 之间的差异

JSX 和 HTML 允许你结构化网络内容,并使用户能够与网络应用程序界面进行交互。作为一名 React 开发者,你必须熟悉 HTML 和 JSX 元素之间的固有差异,以避免被 JSX 编译器标记为红旗。

接下来,我们将讨论 JSX 如何允许我们使用类似 HTML 的标签来描述 UI,同时它利用了底层 JavaScript 的强大功能。

JSX 如何抽象 JavaScript

现在,不使用 JSX 编写 React 应用程序不被推荐,尽管这是可能的。例如,你可以编写一个React.createElement(component, props, ...children)函数来描述 UI。

然而,你可以使用以下代码轻松地在 JSX 中描述一个按钮 UI:

<Button color="wine">    Click a Wine Button
</Button>

如果不使用 JSX 编写前面的代码,你需要使用以下代码来描述一个按钮 UI:

React.createElement(Button,
    {color: 'wine'},
    ' Click a Wine Button')

在大型 React 项目中这样做可能会导致多个问题,例如需要处理代码库中的更多错误,并且需要面对更陡峭的学习曲线,以成为能够优化编写此底层代码来描述 UI 的熟练开发者。然而,尽管意见分歧很少,你可能会同意 JSX 是描述 UI 组件的更好途径,而不是使用纯 React 修改后的 JavaScript。

让我们通过在低级 React 函数React.createElement()之上提供语法糖来检查 JSX 如何在其数据表示中抽象 JavaScript。这暗示了 React 如何通过 Babel 将 JSX 转换为 JavaScript,以实现无缝的 DOM 交互。

src/index.js中,使用以下代码片段更新文件,以了解如何在不使用 JSX 的情况下将React Conference 2024写入屏幕:

import React from 'react';import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement('div', {}, 'React Conference 2024'));

在前面的代码片段中,React.createElement()是一个带有三个参数的函数调用:div{}和预期的输出文本React Conference 2024

  • React.createElement函数中的div参数代表我们正在创建的 HTML 元素的类型。这可以是任何 HTML 元素或标签(h1pulli等等)。你甚至可以将组件作为第一个参数添加。

  • 空的括号参数{}代表 props。它可以是对象或 null。

  • 第三个参数代表我们希望在屏幕上看到的内容。这可以是普通文本或子组件。

src/app.js中,使用以下代码片段更新文件,以解释 JSX 的使用:

import React from 'react';export function App() {
    return (
        <div className='App'>
            <h1>React Conference 2024</h1>
        </div>
    );
}

前面的代码片段显示了显示React Conference 2024的 JSX 代码结构,即<div className='App'><h1>React Conference 2024</h1></div>

虽然这些看起来像常规的 HTML 元素,但它们是 JSX 表达式。现在,按照以下方式重置index.js以使用 JSX 描述 UI:

import React from 'react';import ReactDOM from 'react-dom/client';
import { App } from './App.jsx'
ReactDOM.createRoot(document.querySelector('#root'))
    .render(<App />)

使用npm start运行代码;你将在浏览器屏幕上看到React Conference 2024文本:

图 5.1 – 展示 JSX 输出的截图

图 5.1 – 展示 JSX 输出的截图

总结来说,JSX 是 React 社区中的一个有用工具,它允许开发者在不进行严格的 DOM 操作的情况下满足组件的展示需求。它促进了用户界面的流畅体验和丰富的交互性。有了这个,我们就有了一个快速、高效且独立的广泛兼容浏览器的 Web 应用程序。

接下来,我们将讨论 React 中的事件处理。传统的事件方法,如onclickonfocusonblur等,与 React 中的一些细微命名差异密切相关。

我们将讨论这一点以及更多内容,以了解我们如何利用本章涵盖的知识,并将其添加到构建本书互动会议 Web 应用程序项目所需的技能集中。

React 中的事件处理

React 的事件系统是 React 核心 API 提供的一个强大功能。它被称为SyntheticEvent。作为 React 开发者,我们在 React 应用程序开发项目中会每天遇到事件处理。如果你熟悉 JavaScript 的基础知识,处理事件对你来说不应该陌生。你可以使用浏览器原生方法向 HTML DOM 添加事件。

让我们看一下这个代码片段:

<html><body>
<h1>HTML DOM Operations</h1>
<p><strong>Click here to see my message.</strong></p>
<div id="root"></div>
<script>
document.addEventListener("click", function(){
document.getElementById("root").innerHTML =
    "This is a text added to the DOM tree!";
});
</script>
</body>
</html>

<div id="root"> </div>表示 DOM 将注入我们创建的文本的位置。div元素有一个id属性,其值为root。这有助于事件对象知道文本应该出现在哪里。document.addEventListener()方法使用两个参数添加一个事件监听器:click和一个回调函数。

当我们点击一个按钮时,我们会触发一个事件。这个事件被称为点击事件。在这种情况下,有一个点击以触发事件的Post消息:<p><strong>点击这里查看我的消息。</strong></p>。一旦这个消息被点击,回调函数(第二个参数)就会被触发,并导致getElementById窗口方法使用div元素的id属性值root来传递innerHTML新分配的文本 – 那就是添加到DOM 树中的文本!

在 React 中,我们有各种遵循驼峰命名约定的事件:onClickonChangeonInputonInvalidonResetonSubmitonFocusonBluronToggle等。你可以在 React 文档中找到整个事件列表 – React 合成事件https://reactjs.org/docs/events.html)

让我们深入以下代码片段,看看我们如何在 React 中处理表单事件。此代码片段展示了onChange表单事件:

import React,{useState} from 'react';const App = ()=> {
const [username,setUsername]= useState("");
const [name,setName]=useState("");
const [email,setEmail]=useState("");
const handleSubmit=(e)=>{
    e.preventDefault()
    alert(`Username:${username}, Name: ${name} and Email:
        ${email} submitted`)
  }
    return (
        <div>
            <form onSubmit={handleSubmit}>
                <label htmlFor="username"> Username</label>
                    <br />
                <input type="text" placeholder="Username"
                    onChange={(e)=>setUsername(
                        e.target.value)} /><br />
                <label htmlFor="name">Name</label><br />
                <input type="text" placeholder="Name"
                    onChange={(e)=>setName(e.target.value)}
                        /><br />
                <label htmlFor="email"> Email</label><br />
                <input type="email" placeholder="Email"
                     onChange={ (e)=>setEmail(
                         e.target.value)}/><br />
                <button>Submit</button>
            </form>
            <div>
                <p>Username: {username}</p>
                <p>Name: {name}</p>
                <p>Email: {email}</p>
            </div>
        </div>
    );
}
export default App;

让我们仔细检查这个片段:

  • import React, {useState} from 'react'使useState钩子在App()组件中可用于状态跟踪。

  • const [username,setUsername]=useState("") const name, setName]=useState("")const [email,setEmail]=useState("") 这段代码片段使我们能够访问用户名、邮箱和姓名的状态。通过 setUsernamesetNamesetEmail,我们可以跟踪变量的新状态。所有变量的初始状态都设置为空字符串。

    const handleSubmit=(e)=>{  e.preventDefault()    alert(`Username:${username}, Name: ${name} and        Email: ${email} submitted`)}
    

    在前面的代码片段中,handleSubmit() 是一个接受 e 作为事件对象的处理器。e.preventDefault 阻止表单字段提交时的浏览器默认行为。表单提交时不会刷新页面。handleSubmit 被添加到表单中作为属性以执行 alert(),在屏幕上显示 usernamenameemail 的状态。

  • 在输入标签 <input type="text" placeholder="用户名"onChange={(e)=>setUsername(e.target.value)} />中,向输入元素添加了onChange 事件属性。onChange事件有一个接受e 作为参数的回调函数。setUsername(e.target.values)监听输入字段中的变化事件。同样,为姓名和邮箱的输入标签添加了相同的onChange` 事件回调。

  • <p>用户名: {username}</p>,<p>姓名: {name}</p>,<p>邮箱: {email}</p> 这段代码片段显示了输入表单字段中的当前输入文本。

    以下屏幕截图显示了表单中事件处理的效果:

图 5.2 – 屏幕截图显示事件处理器对表单字段的影响

图 5.2 – 屏幕截图显示事件处理器对表单字段的影响

总结来说,React 中的事件处理由一个跨浏览器的包装器 SyntheticEvent 管理。我们在 React 应用程序中将事件处理器作为实例传递给 SyntheticEvent。最佳实践是使用 preventDefault() 来防止默认的浏览器行为。

接下来,我们将讨论如何在 React 应用程序中使用列表。列表是任何企业级网络应用程序中的常见功能。让我们看看 React 如何通过精心设计的 UI 列表来提升用户体验。

在 React 中显示列表

我们今天看到的绝大多数网络应用程序在描述用户界面时都会使用列表组件。在任何复杂的网络应用程序项目或生产级应用程序中,你都会看到列表功能,通常用于数据展示。在 React 中,你可以使用列表来展示你的组件数据。

我们将使用模拟数据来展示如何使用 map() 获取数据项列表。我们还将讨论 React 列表管理中 keyid 属性的本质。本书项目的 GitHub 仓库(https://github.com/PacktPublishing/Full-Stack-Flask-Web-Development-with-React/tree/main/Chapter-05/06/frontend)包含模拟的会议演讲者 data 源;你可以在公共文件夹中找到 images,在 src 文件夹内找到 css (index.css)。

然而,本书的后端部分(第九章API 开发和文档)将解释我们如何从 Flask 开发的 API 端点中拉取这些数据。

url 将您带到演讲者页面 – http://localhost:3000/speakers

图 5.3 – 屏幕截图显示从数据源拉取的会议演讲者列表

图 5.3 – 屏幕截图显示从数据源拉取的会议演讲者列表

让我们检查显示演讲者列表的代码片段,使用几个组件:

src/components 目录内创建 SpeakersCard/SpeakersCard.jsx 并添加以下代码片段:

import React from 'react'const SpeakersCard= ({name, jobTitle, company, profileImg}) => {
    return (
        <>
            <div className="card">
                <div className="speaker-card">
                    <div className="speaker-info">
                        <img src={profileImg} alt={name} />
                        <span>
                            <h3>{name}</h3>
                        </span>
                        <p>{jobTitle}</p>
                        <p>{company}</p>
                    </div>
                </div>
            </div>
        </>)
}
export default SpeakersCard;

在前面的代码片段中,我们创建了一个 SpeakersCard 组件,该组件接受一个包含四个属性的对象:namejobTitlecompanyprofileImg。这些属性将通过即将创建的 SpeakersPage 组件(父组件)作为 props 传递给组件。

SpeakersCard 组件的返回语句包含 JSX,它表示渲染输出的结构。

我们需要这个 SpeakersCard 组件来封装和表示会议网页应用中演讲者实体的视觉外观和信息显示。通过创建这个组件,我们可以在需要显示演讲者信息时,在整个应用程序中重用它。

现在,在 src/pages 目录内创建 SpeakersPage/SpeakersPage.jsSpeakersPage 组件将用于通过为 speakerList 中的每个演讲者渲染 SpeakersCard 组件来显示演讲者列表。

将以下代码添加到 SpeakersPage.js

import React from 'react';import SpeakersCard from
    '../../components/SpeakersCard/SpeakersCard';
import speakerList from '../../data/SpeakerList';
import Breadcrumb from
    '../../components/Common/Breadcrumb/Breadcrumb'
import Header from '../../components/Header/Header';
import Footer from '../../components/Footer/Footer';
const SpeakersPage = () => {
    return (
        <>
            <Header/>
            <Breadcrumb title={"Speakers"}/>
            <div className="speakers-container">
                <div className="section-heading" >
                    <h1>Meet Our Speakers</h1>
                </div>
                <div className="card">
                    {speakerList.map((speaker) => (
                        <SpeakersCard
                            key={speaker.id}
                            name={speaker.name}
                            jobTitle={speaker.jobTitle}
                            company={speaker.company}
                            profileImg={speaker.profileImg}
                        />
                    ))}
                </div>
            </div>
            <Footer/>
        </>
    )
}
export default SpeakersPage;

在前面的代码片段中,我们导入依赖项以使页面功能正常工作:

  • import SpeakersCard from '../../components/SpeakersCard/SpeakersCard':此代码行从组件目录中的 SpeakersCard 目录导入 SpeakersCard 组件。使用 ../../ 符号导航到适当的目录级别。

  • import speakerList from '../../data/SpeakerList':此代码行从位于数据目录中的 SpeakerList.js 文件导入 speakerList 数据。您可以在本章的 GitHub 仓库中找到数据文件。此数据包含一个对象数组,每个对象代表一个演讲者,具有 namejobTitlecompanyprofileImg 等属性。

然后,我们添加 <SpeakersCard ... />。此代码行渲染 SpeakersCard 组件,并将每个演讲者从 speakerList 中的必要 props (namejobTitlecompanyprofileImg) 传递。每个 SpeakersCard 组件都添加了 key={speaker.id} props。key props 帮助 React 在列表更改时高效地更新和重新渲染组件。

SpeakersPage 还包括 header面包屑导航footer 组件,为演讲者部分提供完整的布局。HeaderBreadcrumbFooter 组件的代码可以在本章的 GitHub 仓库中找到。

接下来,我们将检查如何通过使用键唯一标识列表中的项目来遵循 React 的最佳实践处理列表项。

在 JSX 中使用键和 id

React 列表项中的 key 是列表中项目状态的唯一标识符。我们使用键来跟踪列表中已更改、已添加或已删除的项目。通常期望它是一个列表中的唯一项。

看看我们前面示例中使用到的对象数组:

    const speakerList = [        {
        id: 1,
        name: 'Advon Hunt',
        jobTitle:'CEO',
        company:'Robel-Corkery',
        profileImg: 'https://images.unsplash.com/photo-
            1500648767791' },
]

此数组中的 id 属性应该是一个唯一的数字。这使我们能够适当地跟踪对象数据状态。我们在前面的 speakersList 示例中使用了 {speaker.id} ID 作为 key 属性的值。

现在,我们将深入 JSX 中的嵌套列表,并学习如何在 React 中使用嵌套列表来处理复杂的数据结构。

JSX 中的嵌套列表

如前所述,列表是大多数 Web 应用程序的关键组件。列表通常用于结构化数据和整齐地组织信息。我们对 Web 开发中的一些列表陈词滥调很熟悉:待办事项列表任务列表,甚至是 菜单列表。所有这些列表都可能变得复杂,这取决于数据结构和您期望如何向最终用户展示列表项。在 React 应用程序中处理列表需要理解如何处理以对象数组形式出现的数据。

在本节中,我们将学习如何在 React 应用程序中渲染 JSX 中的嵌套项目列表。您将看到这样的复杂嵌套数据结构,以及更多来自您的 API 数据源的结构,因此了解嵌套列表将使包含复杂数据的 React 应用程序更容易处理。

以下代码片段显示了一个组件中嵌套的 Web 技术栈项目列表。

src/App.js 内编辑 App.js

import React from "react";import {webStacksData} from "./data/webStacksData";
import WebStacks from "./components/WebStacks/WebStacks";
const App = () => {
    return (
        <ul>
            {webStacksData.map(i => (
                <WebStacks item={i} key={i.id} />
            ))}
        </ul>
    );
}
export default App;

那么,这段代码中发生了什么?我们正在处理一个名为 webStacksData 的命名嵌套对象数据列表,该列表可以在本书的 GitHub 仓库中找到:

  • 数据通过 import {webStacksData} from "./data/webStacksData"; 引入作用域。

  • 我们还引入了 WebStacks 组件到作用域中。

  • webStacksData.map 函数遍历 webStacksData 数组中的每个项目,为每个项目创建一个新的 WebStacks 组件。key 属性设置为每个项目的 id 属性,以帮助 React 在需要时高效地更新列表。对于 webStacksData 数组中的每个项目,都会渲染一个 WebStacks 组件,并将 item 属性设置为来自数组的当前 item

让我们创建一个名为 WebStacks 的组件来查看组件的内部工作原理:

import React from "react";const  WebStacks = ({ item })=> {
let children = null;
if (item.values && item.values.length) {
    children = (
        <ul>
            {item.values.map(i => (
                <WebStacks item={i} key={i.id} />
            ))}
        </ul>
    );
}
return (
    <li>
        {item.name}
        {children}
    </li>
);
}
export default WebStacks;

WebStacks 组件接收 props 项目。在组件体函数中,我们检查父列表项是否存在以及是否有子项。然后我们调用 map() 递归遍历具有有效子列表项的列表项。

这个 <li>{item.name}{children}</li> 返回列表项的名称及其所有子项。接下来,我们将看到如何在 React 中遍历对象并在 JSX 中显示输出。

在 JSX 中遍历对象

遍历复杂的数据对象是经验丰富的 React 开发者需要轻松处理的一部分。你无疑会遇到需要处理来自你的 API 端点的简单和嵌套对象数据的情况,以提取对应用程序有用的数据。在本节中,我们将了解如何在应用程序中无缝地遍历数据对象。

在 JavaScript 中,对象是不可迭代的。你无法使用 for ... of 语法遍历对象属性。Object.Keys() 是 JavaScript 中用于遍历对象数据的一个内置标准对象方法。然而,在 ES2017 中,添加了新的对象方法,可以用来遍历对象属性:Object.values()Object.entries()

让我们简要地检查这些方法,并学习如何使用对象数据来使用它们。

创建用于遍历的对象数据,并将其命名为 speakersData

const speakersData = {name:"Juliet Abert",
company:"ACME Group",
street:"1st Avenue",
state:"Pretoria",
country:"South Africa"
}

接下来,我们将检查用于高效遍历对象属性的各种技术,这些技术允许你使用 Object.keys()Object.values()Object.entries() 等方法访问和操作对象中的数据。我们将简要探讨这些技术,从 Object.keys() 开始。

使用 Object.keys()

Object.keys 方法返回一个包含对象键的数组。正如你所知,对象包含键和值对,因此 Object.keys() 将返回键/属性的数组。

让我们在以下代码片段中将我们的数据对象作为参数传递:

console.log(Object.keys(speakersData));

我们将得到以下输出:

图 5.4 – 展示使用 Objects.keys() 方法效果的截图

图 5.4 – 展示使用 Objects.keys() 方法效果的截图

在这里,你可以看到键的数组。你可以使用循环函数检索键的值:

for (const key in speakersData){  console.log(`${key}: ${speakersData[key]}`);
}

以下截图显示了对象数据的键和值:

图 5.5 – 展示对象键和值的截图

图 5.5 – 展示对象键和值的截图

之后,你可以调用 map() 来检索 React 组件中键的值。这将在本节后面进行解释。

现在,让我们学习如何使用 Object.values()

使用 Object.values()

Object.values() 方法返回一个包含对象属性值的数组:

console.log(Object.values(speakersData));

这只返回没有键的属性值,因此在需要键和值的情况下,它的实用性较低。

图 5.6 – 显示使用 Objects.values()方法效果的截图

图 5.6 – 显示使用 Objects.values()方法效果的截图

让我们看看我们可以用来遍历对象数据的最后一种技术。

使用Object.entries()

Object.entries()方法返回一个包含对象键值对的数组 – [key, value]。使用Object.entries()遍历对象更容易,因为有一个[key, value]对。例如,考虑以下代码:

for (const  key of Object.entries(speakersData) ){    console.log(`${key[0]} : ${key[1]}`)  }

以下截图显示了在对象数据上使用Object.entries()的输出:

图 5.7 – 显示使用 Objects.entries()方法效果的截图

图 5.7 – 显示使用 Objects.entries()方法效果的截图

我们可以看到返回了包含对象属性键和值的二维数组。

使用 Object.keys 的循环示例

现在,我们将处理一个包含有用演讲者信息的对象数据,其数据格式为对象。它可以在本书的项目 GitHub 仓库中找到(https://github.com/PacktPublishing/Full-Stack-Flask-Web-Development-with-React/blob/main/Chapter-05/data/objSpeakersData.js),并在 React 组件中显示输出。

以下代码遍历speakers对象数据,并在 JSX 中显示输出:

import React from 'react';import {simpleSpeakerData} from
    '../../data/objSpeakersData';
const Speakers = () => {
    return (
        <>
            <h1>Speakers</h1>
            <div>
                <ul>
                    {Object.keys(s).map(key => (
                        <li key=
                           {key}>{simpleSpeakerData[key]
                               .name}
                           {simpleSpeakerData[key].company}
                           {simpleSpeakerData[key].street}
                           {simpleSpeakerData[key].state}
                           {simpleSpeakerData[key].country}
                        </li>
                    ))}
                </ul>
            </div>
        </>
    );
}
export default Speakers;

以下代码的解释如下:

  • import {simpleSpeakerData} from '../../data/objSpeakersData'将数据引入作用域,以便可以在代码中使用。

  • 然后,我们声明一个Speakers组件,它返回一个对象数据列表。

  • simpleSpeakerData被传递给Object.keys(simpleSpeakerData)

  • map()函数随后被调用在Object.keys()返回的键上。这会遍历返回的键数组。

    现在,我们可以访问对象的各个键和值。

  • {simpleSpeakerData[key].name}指向对象数据的 name 属性值。

以下图显示了在 React 中使用 JSX 遍历复杂对象数据的输出:

图 5.8 – 显示对象数据的截图

图 5.8 – 显示对象数据的截图

在 JSX 中使用Object.keys()Object.values()Object.entries()遍历对象是基本且涉及遍历对象的属性以动态渲染 JSX 元素。这种方法允许你生成列表、表格或其他 UI 组件,以结构化的方式显示对象中的数据。

概述

在本章中,我们广泛讨论了 React 中的 JSX。我们深入解释了 JSX 是什么以及指导在 React 中使用 JSX 的规则。然后,我们讨论了 DOM 以及 React 中的 VDOM 如何抽象原生的浏览器 DOM,以便 React 开发者构建更高效、跨浏览器的用户界面。JSX 提高了 React 应用中的 DOM 交互,并优化了 React 组件中元素的渲染速度。

我们还检查了 React 中的事件处理以及如何使用 SyntheticEvent 事件包装器来处理 React 中的事件操作。我们讨论了 JSX 和 HTML 之间的细微差别以及指导 React 中使用的规则。

最后,我们通过实际案例讨论了如何在 React 项目中显示列表,以及如何使用 keyid 来唯一管理列表项。我们还探讨了如何在 React 中遍历对象以及显示复杂的嵌套对象。

在下一章中,我们将深入讨论如何在 React 网络应用程序中处理表单操作和路由。

第六章:与 React Router 和表单一起工作

React Router是一个用于客户端和服务器端路由的库。想象一下网站通常是如何工作的;当你点击一个链接时,你的浏览器向 web 服务器发送一个请求,接收一大堆数据,然后花费时间处理所有这些,最后才最终显示新页面的内容。

每次你从网站请求新页面时,你都会获得相同的体验。使用客户端路由,事情会变得非常顺畅!每次你点击链接时,你不必经历整个过程,你的 Web 应用程序可以立即更新 URL,而不必打扰服务器获取新文档。这意味着你的 Web 应用程序可以快速显示应用程序的新部分,而没有任何延迟。这正是 React Router 所提供的。

在本章中,我们将探索 React Router v6 作为一个神奇的工具来处理导航。你还可以使用 React Router 进行数据获取,但我们将本书的范围限制在组件导航。你将在 React 中实现简单和复杂的嵌套路由。你还将使用useParamsuseNavigate钩子进行动态和程序性路由。

接下来,我们将深入探讨 React 应用程序中的表单处理。表单是任何 Web 应用程序中的关键组件。没有表单,你无法拥有一个完整的 Web 应用程序。有趣的是,我们使用表单来完成各种目的,这些目的取决于业务或项目需求。

在 React 中,表单用于组件中,以允许用户登录、注册、搜索、联系表单、购物结账页面、活动参与者表单等活动。表单为浏览器-数据库服务器交互提供了一个媒介。

我们通过表单从我们应用程序的用户那里收集数据;有时,我们将用户数据发送到数据库或发送/保存到其他平台,如电子邮件和第三方应用程序。这完全取决于我们打算如何处理表单数据。

简而言之,你将学习如何使用表单元素来促进你的 React 应用程序中的用户交互。你还将了解如何利用 React Router,这是一个流行的客户端路由库。

到本章结束时,通过使用 React Router 库来导航你的不同应用程序端点,你将了解 React 应用程序中路由是如何工作的。最后,你将能够开发优雅的 React 表单,并以 React 的方式处理用户信息。

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

  • 使用 React Router 进行路由

  • 在 React 中添加 React Router

  • 处理动态路由

  • 在 React 中使用表单

  • 受控和非受控表单组件

  • 处理用户输入 – 输入字段文本区域选择

  • 在 React 中验证和清理用户数据

技术要求

本章的代码可以在github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter06找到。

由于页面限制,一些代码块已被截断。请参阅 GitHub 以获取完整代码。

使用 React Router 进行路由

路由在 React Web 应用程序中是指无缝导航到和从多个应用程序组件、URL、页面和资源的能力,无论是内部还是外部。默认情况下,React 不包括页面路由在其库中。事实上,React 的主要目标是允许开发者设计单页 Web 应用程序的视图显示。

我们都知道 Web 应用程序需要多个视图,因此需要像 React Router 这样的外部库来允许组件导航。处理大型应用程序需要多个专业视图。这意味着我们必须解决 React 库中未处理的导航问题,但这就是 React Router 发挥作用的地方。

React Router是一个开源包,用于在 React 应用程序中进行基于组件的路由。它在 React 开发者中很受欢迎,并在各种 React 项目中广泛使用。有趣的是,你可以在任何打算运行 React 应用程序的地方使用 React Router:客户端使用浏览器、在 Web 服务器上使用 NodeJS,甚至通过 React Native 在移动应用程序中使用。

到目前为止,我们已逐步处理了Bizza应用程序,挑选组件及其交互。现在,我们将浏览我们项目的页面,并使用 React Router 将它们链接起来。

React Router 由一些路由功能组成。这些功能是 React Router 内部工作的螺丝钉。了解它们将有助于我们理解 React Router。在以下章节中,我们将解释 React Router 的一些常用功能。

让我们从Router组件开始,它允许在 React 应用程序中进行导航和路由。

路由器

React Router 提供了不同类型的路由器,允许你在 React 应用程序中处理路由和导航。每个路由器都有其特定的用例和优势。我们将简要讨论一些常用的 React Router 路由器:

  • CreateBrowserRouter:这是 React Router v6 中的一个专用函数,它作为在 Web 项目中生成浏览器路由的首选方法。通过利用 DOM 历史 API,它有效地更新 URL 并维护历史堆栈。此外,它解锁了对 v6.4 数据 API 的访问,包括加载器、操作、fetchers 和其他 React Router 功能。

  • RouterProvider:这是 React Router 中的一个组件,旨在为其作用域内渲染的所有组件提供路由实例。这确保了路由可以被用于高效管理应用程序的导航和路由需求。

    RouterProvider 组件需要一个 router 属性作为参数,这个属性作为将要分布到 RouterProvider 内渲染的组件中的路由实例。将 RouterProvider 定位在组件树的最顶层是至关重要的,以确保所有应用程序组件都能有效地访问路由实例。

  • NativeRouter: 这是一个在 React Native 中运行 React Router 所必需的接口,它是移动应用程序的路由解决方案。这超出了本书的范围。

接下来,我们将讨论 React Router 中的组件。React Router 中的组件允许您在单页应用程序中渲染特定路由的用户界面。

组件

React Router 中的组件使您能够在 React 应用程序内创建一个灵活且动态的路由系统,这使得管理导航和状态以及用户与您的 UI 交互变得更加容易。我们将简要讨论一些常用组件。

  • Link: Link 是一个组件元素,允许用户在点击时导航到另一个组件页面。在底层,react-router-dom 将一个 <Link> 标签渲染为一个带有真实 href 的锚点元素 <a>,该 href 将用户引导到它所指向的资源。

  • NavLink: 它作为一个 <Link> 标签工作,但增加了指示菜单中活动元素的功能。这在构建标签菜单时常用,您想显示当前选中的菜单部分。

  • Route: 这用于根据当前位置在 React Router 中渲染 UI。Route 有一个路径和一个元素作为属性。这是它的工作方式:每当 Route 组件的路径与当前 URL 匹配,基于用户的点击操作,它将渲染其元素。这个元素可以是应用程序中的任何组件。我们很快将看到一个实时示例。

  • Routes: 它有 Route 作为其子元素。Routes 在逻辑上以简单的方式工作,就像 Route 一样,只不过 Route 或一系列 RouteRoutes 的子元素。因此,每当 UI 组件的路径发生变化时,Routes 会检查其所有子 Route 元素,以确定用户请求或点击路径的最佳匹配,并渲染该特定 UI。

接下来,我们将讨论 React Router 中的钩子。钩子提供了一种与路由状态交互并在组件内直接执行导航操作的机制。我们将讨论 useLocationuseParamsuseNavigate 等钩子。

钩子

React Router 提供了一系列钩子,使开发者能够以高效的方式管理组件内的路由、状态和导航。我们将简要讨论一些常用钩子:

  • useLocation: 您可以使用此钩子来在需要跟踪当前位置变化时执行一些副作用。useLocation 钩子通常返回当前位置对象。

  • UseParams:您可以使用此钩子通过当前 URL 匹配<Route path>从浏览器获取参数。useParams钩子返回一个包含动态参数键值对的对象。

  • UseNavigate:您可以使用此钩子在不使用history对象或Link组件的情况下,在 React 应用程序的不同路由之间进行编程式导航。

现在,是时候将 React Router 添加到我们的根应用程序中,并将我们的页面连接起来。

在 React 中添加 React Router

您需要安装 React Router 才能在项目中使用它。我们将为Bizza项目构建导航功能,以连接不同的组件。导航标签将包括主页、关于页、演讲者页、活动页、赞助商页和联系页。让我们通过在项目目录的终端中输入以下命令开始编码:

npm install react-router-dom@latest

一旦我们在项目的根目录中安装了该包,我们就可以创建主页、关于页、演讲者页、新闻页和联系页的组件。

现在,我们将为这些组件中的每一个添加内容:

  • src/pages/HomePage/HomePage.js内部,添加以下代码片段:

    import React from 'react';const HomePage = () => {    return <div> Home page </div>;};export default HomePage;
    
  • src/pages/AboutPage/AboutPage.js内部,添加以下内容:

    import React from 'react';const AboutPage = () => {    return <div> About page </div>}export default AboutPage;
    
  • src/pages/SpeakersPage/SpeakersPage.js内部,添加以下内容:

    import React from 'react';const SpeakersPage = () => {    return <div>Speakers </div>}export default SpeakersPage;
    
  • src/pages/EventsPage/EventsPage.js内部,添加以下内容:

    import React from 'react';const EventsPage = () => {    return <div>Events page </div>}export default EventsPage;
    
  • src/pages/SponsorsPage/SponsorsPage.js内部,添加以下内容:

    import React from 'react'const SponsorsPage = () => {    return <div>Sponsors Page</div>}export default SponsorsPage
    
  • src/pages/ContactPage/ContactPage.js内部,添加以下内容:

    import React from 'react'const ContactPage = () => {    return <div>Contact Page</div>}export default ContactPage
    

现在这些组件已经设置好了,让我们开始在我们的应用程序中实现 React Router 的功能:

  1. src/index.js内部,添加以下代码:

    import React from 'react';import { createRoot } from 'react-dom/client';import {    createBrowserRouter,    RouterProvider,} from 'react-router-dom';
    import statements required for the client-side routing using React Router:
    
    • createRoot:导入创建用于渲染的根 React 组件的函数

    • createBrowserRouterRouterProvider:导入与 React Router 相关的组件和函数,它提供了路由功能

  2. 我们还需要导入我们之前创建的所有各种组件。仍然在index.js内部,添加以下组件导入:

    import HomePage from './pages/HomePage/HomePage';import AboutPage from './pages/AboutPage/AboutPage'import SpeakersPage from './pages/SpeakersPage/SpeakersPage';import EventsPage from './pages/EventsPage/EventsPage';import SponsorsPage from './pages/SponsorsPage/SponsorsPage';import SponsorsPage from './pages/SponsorsPage/SponsorsPage';
    

    上述导入是将在应用程序中使用的各种文件和组件。

请注意,我们可能希望 React Router 了解的所有未来组件都可以作为导入的文件和应用程序的组件添加。接下来,我们将设置路由配置。

设置路由配置

在网络应用程序开发和如 React Router 之类的库的背景下,路由配置指的是设置规则或映射的过程,这些规则或映射定义了网络应用程序中不同的 URL(或路由)应该如何被处理。这包括指定为特定 URL 渲染哪些组件或视图,使用户能够无缝地浏览应用程序的不同部分。

使用 React Router,您可以定义一系列路由,并将每个路由与要显示的相应组件关联起来。这些路由可以是静态的、动态的(带有占位符)或嵌套的,以创建一个层次结构。

让我们将这些应用到实际中。将以下代码添加到 index.js 文件中:

const router = createBrowserRouter([  {
    path: "/",
    element: <HomePage />,
  },
  {
    path: "/about",
    element: <AboutPage/>,
  },
  {
    path: "/speakers",
    element: <SpeakersPage/>,
  },
  {
    path: "/events",
    element: <EventsPage/>,
  },
{
    path: "/sponsors",
    element: <SponsorsPage/>,
  },
{
    path: "/contact",
    element: <ContactPage/>,
  },
    ],
);
createRoot(document.getElementById("root")).render(
  <RouterProvider router={router} />
);

上述代码展示了使用 createBrowserRouter 函数创建的 router 对象,该函数定义了应用程序的路由配置。这个 router 对象设置了不同的路径及其对应的 React 组件,当这些路径匹配时,将渲染这些组件。

这意味着当用户在应用程序中导航到不同的 URL 时,将根据定义的路由渲染相应的组件;例如:

  • 导航到 / 将渲染 HomePage 组件

  • 导航到 /about 将渲染 AboutPage 组件

同样,其余的组件也是根据路由及其对应的组件进行渲染和显示的。react-dom 库中的 createRoot() 函数,'react-dom/client',用于创建一个用于渲染的 root 组件。这是一个较新且更高效的替代方案,用于 ReactDOM.render()createRoot() 函数接受一个目标 DOM 元素作为参数,并返回一个可以用于将 React 元素渲染到该目标元素的 root 组件。

在这种情况下,createRoot(document.getElementById("root")) 创建了一个将在具有 "root" ID 的 <div> 元素内部渲染其内容的 root React 组件。本质上,createRoot 函数用于创建 Root 对象并将 RouterProvider 组件渲染到 root DOM 元素中。

然后,RouterProvider 组件渲染 HomePage 组件,这是应用程序的默认路由。<RouterProvider router={router} /> 使用了 React Router 的 RouterProvider 组件。RouterProvider 接收一个名为 router 的属性,该属性的值是之前定义的包含先前路由配置的 router 对象。这使得 router 可用于整个应用程序,并基于定义的路由进行导航。

我们将在下一节中添加到路由的链接。

添加链接

让我们通过向元素添加链接来改进导航菜单。要向元素添加链接,请使用 <Link to="" >elementName </Link>to="" 允许我们插入我们打算导航到的导航路径。让我们看看典型链接定义的细节:

<nav className="nav">  <ul>
    <li>
      <Link to="/" className='navlink'>Home</Link>
    </li>
    <li>
      <Link to="/about" className='navlink'>About</Link>
    </li>
    <li>
      <Link to="/speakers" className='active
        navlink'>Speakers</Link>
    </li>
    <li>
      <Link to="/events" className='navlink'>Events</Link>
    </li>
    <li>
      <Link to="/sponsors" className='navlink'>
        Sponsors</Link>
    </li>
      <li><Link to="/contact" className='navlink'>
        Contact</Link>
    </li>
  </ul>
</nav>

检查 GitHub src/components/Header/Header.jsx 文件以了解更多关于 Link 定义的信息。

以下截图显示了带有菜单和链接的 HomePage

图 6.1 – 展示路由和链接的截图

图 6.1 – 展示路由和链接的截图

接下来,让我们学习如何将一个路由嵌入到另一个路由中,以便我们得到所谓的嵌套路由。

添加嵌套路由

React Router 中的嵌套路由 提供了一种在应用程序中组织路由的结构化方法。它们便于对相关路由进行分组,简化不同部分之间的导航。要实现嵌套路由,必须在 Route 组件上使用 children 属性。

此属性接受一个路由组件数组作为其值,定义了当父路由匹配时将渲染的子路由。例如,考虑以下代码片段,它演示了为演讲者创建嵌套路由。

src/index.js内部,更新/speakers路由,如下所示:

const router = createBrowserRouter([  {
    path: "/speakers",
    children: [
      {
        index: true,
        element: <SpeakersPage />,
      },
      {
        path: "/speakers/:speakerId",
        element: <SpeakerDetail />
      },
    ],
  },
]);

在前面的代码中,我们有一个名为speakers的父路由,路径为/speakersSpeakerDetail的子路由路径为/speakers/:speakerId。路径中的:speakerId占位符是一个动态参数,当用户导航到该路由时,它将被演讲者的:speakerId值替换。

SpeakerDetail组件将使用 URL 中speakerId的详细信息进行渲染。在src/pages内部,创建SpeakerDetail/SpeakerDetail.js;然后,添加以下代码:

const SpeakerDetail = () => {    return (
        <div className='page-wrapper'>
            <h1>This is SpeakerDetail with the ID: </h1>
        </div>
    )
}
export default SpeakerDetail

以下截图显示了带有http://localhost:3000/speakers/234的嵌套路由:

图 6.2 – 展示嵌套路由的截图

图 6.2 – 展示嵌套路由的截图

从本质上讲,嵌套路由可以用来以对应用程序有意义的方式组织路由。它们还可以用来使在相关路由之间导航更容易。

接下来,我们将探讨如何使用useParamsuseNavigate处理动态和程序化路由。

处理动态路由

在 Web 应用程序开发中,使用speakerIdproductIdpostId等来表示变化的值。

例如,让我们考虑我们之前更新的带有/speakers/:speakerId的演讲者路由。在动态路由前添加冒号是一种惯例,如下所示::speakerId。那么,我们如何从 URL 中检索这个speakerId的值呢?这就是useParams钩子的作用所在。

使用 useParams

React Router 中的useParams钩子提供了对从路由中提取的动态参数的访问。这些参数是与动态路由路径中的占位符相对应的值。

例如,在以下代码片段中,使用了useParams钩子从/speakers/:speakerId路由中检索SpeakerId。以下代码展示了代码实现。

按照以下方式更新src/pages/中的SpeakerDetail组件:

import React from 'react'import { useParams } from 'react-router-dom'
const SpeakerDetail = () => {
    const {speakerId} = useParams()
    return (
        <div className='page-wrapper'>
            <h1>This is SpeakerDetail with the ID:
                {speakerId} </h1>
        </div>
    )
}
export default SpeakerDetail

在前面的代码片段中,我们有SpeakerDetail组件,它用于根据从 URL 中提取的speakerId动态参数显示演讲者的详细信息。useParams钩子将返回一个包含路由中动态参数的对象。在这种情况下,对象中的speakerId属性将包含 URL 中的演讲者 ID。

以下截图显示了从 URL 中提取的speakerId

图 6.3 – 展示提取的 speakerId 的截图

图 6.3 – 展示提取的 speakerId 的截图

useParams 钩子是一个强大的工具,可以用来访问任何路由的动态参数。接下来,我们将简要讨论 useNavigate 钩子用于编程导航。

使用 useNavigate

useNavigate 是 React Router v6 中引入的一个新钩子。它提供了一种在 React 应用程序中通过编程方式导航或重定向用户到不同路由的方法。与之前版本中提供对历史对象访问的 useHistory 钩子不同,useNavigate 提供了一种更直接和明确的方式来在路由之间导航。

使用 useNavigate,你可以响应某些事件(如按钮点击、表单提交或其他用户操作)来启动导航。与 React Router v5 中直接修改 URL 不同,你现在可以使用 useNavigate 返回的 navigate 函数来实现导航。

例如,在 src/components/Header/Header.jsx 文件中,我们有以下代码来展示 useNavigate 的实现:

import React from 'react';import {Link, useNavigate } from 'react-router-dom';
const Header = () => {
  const navigate = useNavigate();
  const handleLoginButtonClick = () => {
    navigate('/auth/login');
  }
  return (
    <header className="header">
      ...
      <div className="auth">
        <button onClick={handleLoginButtonClick}
          className="btn">Login</button>
      </div>
    </header>
  );
}
export default Header;

在前面的代码片段中,调用了 useNavigate 钩子以获取 navigate 函数。当按钮被点击时,会执行 handleLoginButtonClick 函数,该函数反过来调用 navigate('/auth/login')。这将通过编程方式将用户导航到 '/auth/login' 路由。

与直接操作历史对象相比,useNavigate 提供了一种更声明性和简洁的方式来处理导航。当使用 React Router v6 时,它提高了代码的可读性和可维护性。

本节总结了 React Router 的路由功能。接下来的部分将重点转向 React 库中表单管理的领域。

在 React 中使用表单

传统上,表单用于收集用户输入。没有表单的严肃生产级 Web 应用程序是不存在的。在 React 中使用表单与使用 HTML 表单元素略有不同。如果你已经开发了一段时间的 React 应用程序,这可能对你来说并不陌生。

React 表单元素与普通 HTML 表单元素之间的细微差别是由于 React 处理表单内部状态的独特方式。HTML DOM 以浏览器 DOM 的方式管理原生 HTML 表单元素的内部状态。另一方面,React 通过其组件的状态来处理表单元素。

那么,这个状态究竟是什么呢?我们所说的状态是一个在表单提交前持有用户输入的对象。表单元素有一个内部状态,它可以防止在将用户输入提交到处理通道之前数据丢失。

在为表单元素的内部状态管理奠定基础之后,让我们快速了解一下 React 如何通过其基于组件的方法以及 React 的 VDOM 机制来增强用户体验。我们将使用 React 开发表单,而不使用任何外部库;相反,我们将专注于纯 React,并利用其基于受控组件的方法来管理表单状态。目前,我们将设计一个简单的注册表单组件。

下面的代码片段显示了一个 SignUp 表单组件,以帮助你了解如何在 React 中创建一个简单的表单。

在你的项目目录 src/pages/Auth/SignUp.js/ 中创建 SignUp

import React from 'react';const SignUp = () => {
  return (
    <>
    <div className="signUpContainer">
    <form>
    <h2>Create an account</h2>
      <div className="signUpForm">
        <label htmlFor="name">Name</label>
        <input
          id="name"
          type="text"
          name="name"
        />
      <label htmlFor="email">Email Address</label>
        <input
          id="email"
          type="email"
          name="email"
        />
      <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          name="password"
        />
            <button>Register</button>
      </div>
    </form>
    </div>
</>
  );
};
export default SignUp;

前面的代码片段应该看起来很熟悉——这里没有特别之处,除了 <label> 属性的 htmlFor。这是 React 添加 for 属性到表单标签的方式。htmlFor 属性用于将相应的 ID 与输入表单元素匹配。

接下来,让我们更新 index.js 中的路由配置,并添加 signup 路由。在 src/index.js 中,添加注册路径及其相关组件:

{    path: "/auth/signup",
    element: <SignUp/>,
  },
};

下面的图显示了注册表单代码片段的输出:

图 6.4 – 展示渲染的注册表单截图

图 6.4 – 展示渲染的注册表单截图

当你导航到 http://localhost:3000/auth/signup 时,你会看到 SignUp 表单组件已经被渲染。有了这个,React 只是渲染表单元素,并允许原生浏览器 DOM 在每次表单提交时继续正常工作。如果你填写表单并点击 注册,你会看到页面重新加载的效果。

这显然是一种反 React 设计模式,这意味着这不是 React 设计表单元素的方式。那么,在构建直观的用户表单体验方面,React 的设计期望是什么?这个问题的答案就是我们将在下一节中关注的重点。在那里,你将学习如何使用 React 的所有要素构建引人入胜且可重用的表单。

在 React 中,有两种表单组件的方法:受控和不受控表单组件。在下一节中,我们将深入探讨这个问题,并学习如何设计在 React 网络应用程序项目中增强与表单元素交互的表单组件。

受控和不受控表单组件

到目前为止,在这本书中,我们已经熟悉了组件以及它们是如何成为任何 React 应用程序的构建块的。当你将独立设计的组件片段混合在一起时,你会得到一个 UI 组件或一个完整的 React 网络应用程序,具体取决于你正在做什么。

React 的组件驱动方法在不久的将来不会改变。为应用程序构建高质量的 UI 是 React 最擅长的。作为开发者,无论哪种方式,您都需要一个高性能的表单。React 通过两种构建防漏数据并提高表单交互用户体验的表单组件的方法来满足您的需求。

这两种方法分别是受控和非受控表单组件。让我们从受控表单组件开始,以便我们充分理解它们的实现方式和为什么它们是 React 推荐的表单处理方法。

受控表单

在受控表单方面,React 组件维护表单元素中用户输入的内部状态。我们的意思是什么?本质上,React 有一个内置的事件包装器,称为 SyntheticEvent,它是 React 事件系统的一个关键组件。

我们在 第五章React 中的 JSX 和列表显示React 的事件处理 部分详细讨论了 SyntheticEvent。在受控表单方法中,受控表单组件的事件处理函数接受 SyntheticEvent 的实例,例如 onChangeonInputonInvalidonResetonSubmit,以控制表单数据的状态。

例如,onChange 事件监听组件表单状态值的变化:这种变化可能是用户在表单输入中输入某些内容或尝试替换表单输入的值。onChange 事件被触发,并且相应地更改状态数据值。

让我们探索事件处理程序如何使我们拥有受控表单组件。以下代码片段演示了 React 中的受控表单,以及管理 onChangeonSubmit 事件的相关事件处理程序。更新 SignUp.js 文件代码,以在 src/pages/Auth/SignUp.js 中的表单组件内演示事件处理程序的使用:

import React,{ useState } from 'react';const SignUp = () => {
  const[name, setName ] = useState("");
  const [ email, setEmail ] = useState("" );
  const [password, setPassword] = useState("");
  const nameHandler = (e) => {
    setName(e.target.value);
  };
  const onSubmitHandler = (e) => {
    e.preventDefault();
    alert(`Name: ${name}: Email: ${email} Password:
      ${password}`);
  };
  return (
    <>
      <div className="signUpContainer">
        <form onSubmit={onSubmitHandler}>
          <h2>Create an account</h2>
          <div className="signUpForm">
            <label htmlFor="name">Name</label>
            <input
              id="name"
              type="text"
              name="name"
              value={name}
              onChange={nameHandler}
            />
            <button>Register</button>
          </div>
        </form>
      </div>
    </>
  );
};
export default SignUp;

请参阅 GitHub 以获取完整的源代码。在前面的代码片段中,我们通过向其添加一些输入属性来更新我们的默认表单——即 valueonChange 事件,并为受控表单做好了准备。表单输入使用 value 作为当前值的属性,并使用 onChange 作为回调来更新值的内部状态。

如您所知,用户操作,如轻触键盘、点击网页上的按钮或鼠标悬停在 HTML 元素上,都会引发事件。但 React 有我们所说的合成事件,具有一些实例方法和属性,用于监听用户交互并发出某些事件。

在 React 中,onChangeonClick 是这些实例中比较流行的。我们将使用更多这样的功能。onChange 事件会在表单输入元素发生变化时触发。Web API 的 event.target 属性用于访问这个变化的价值。

此外,onClick 事件在每次点击 HTML 元素时都会被激活;例如,当按钮被点击时。在我们的代码片段中,事件处理程序指定在组件函数的主体中:nameHandleremailHandlerpasswordHandler。这些事件处理程序监听表单输入中的相应值变化并控制表单的操作。

nameHandler 的情况下,它监听用户输入的内容,使用 e.target 属性访问值,并通过 setName() 更新其状态:

const nameHandler = (e) => {          setName(e.target.value);
};

在带有 Name 标签的表单输入中,添加了以下内容:

        <input              …
              value={name}
          onChange={nameHandler}
        />

注意

电子邮件和密码的 input 元素同样更新了适当的值和 onChange 事件处理程序。

onSubmitHandler() 处理表单元素的 onSubmit 事件:

<form onSubmit={onSubmitHandler}>  const onSubmitHandler = (e) => {
        e.preventDefault();
        alert(`Name: ${name}: Email: ${email} Password:
          ${password}`);
  };

e.preventDefault() 阻止浏览器默认的重新加载行为。我们还使用 alert() 输出提交的表单数据。控制表单组件的好处是什么?

有几个原因会让你想要使用控制表单组件:

  • React 推荐这样做。这是 React 处理 React 中用户输入的最佳实践方式。

  • 组件紧密控制表单的行为,从而确保用户和开发者有反应性的体验。

  • 由于事件处理程序监听表单元素并适当地发出事件,我们从表单中获得即时反馈。

控制表单组件提升了 React 应用中表单交互的体验,并且在 React 社区中得到了广泛的应用。接下来,我们将学习未控制表单组件为 React 开发者社区提供了什么,以及我们如何使用它们。

未控制表单

在未控制表单中,原生 DOM 直接维护和存储用户输入的状态。它是通过在 DOM 中存储表单元素的值并引用表单元素来做到这一点的。这是 HTML 表单元素维护其内部状态的常规方式。

React 组件通过维护对 DOM 中底层表单元素的引用来简单地与未控制表单元素交互。让我们复制我们之前的注册表单,并重构代码片段以使用未控制表单组件。

此代码片段使用 useRef 钩子来引用 DOM 中表单元素的值:

import React,{ useRef } from 'react';const SignUp = () => {
  const onSubmitHandler = (e) => {
    e.preventDefault();
    console.log("Name value: " + name.current.value);
    console.log("Email value: " + email.current.value);
    console.log("Password value: " +
      password.current.value);
  };
  return (    <>
    <div className="signUpContainer">
    <form onSubmit={onSubmitHandler}>
    <h2>Create an account</h2>
      <div className="signUpForm">
        <label htmlFor="name">Name</label>
        <input
          id="name"
          type="text"
          name="name"
          ref={name}
        />
      <button>Register</button>
      </div>
    </form>
    </div>
</>
  );
};
export default SignUp;

请参阅 GitHub 以获取完整的源代码。让我们简要解释前面的代码片段。

在使用未控制表单 React 组件时,我们需要 useRef 钩子来访问 DOM 中的表单元素。import React, { useRef } from 'react'; 将 React 和 useRef() 钩子引入作用域。

然后,我们创建了一个引用变量来保存 DOM 中表单元素的引用:

const name = useRef();const email = useRef();
const password = useRef();

input 标签中,我们将引用变量绑定到输入元素的 ref 属性:

<label htmlFor="name">Name</label>        <input
          id="name"
          type="text"
          name="name"
          ref={name}
        />

对于电子邮件和密码输入元素,采取了相同的步骤。

为了提取表单字段的当前值,我们必须在 onSubmitHandler() 中使用 useRefcurrent.value 属性:

  const onSubmitHandler = (e) => {    e.preventDefault();
    console.log("Name value: " + name.current.value);
    console.log("Email value: " + email.current.value);
    console.log("Password value: " +
      password.current.value);
  };

以下图显示了 React 中未受控表单组件的控制台日志:

图 6.5 – 未受控表单组件的控制台日志

图 6.5 – 未受控表单组件的控制台日志

未受控表单组件有哪些好处?

虽然 React 推荐使用受控方法,但使用未受控表单组件的好处并不多。以下是一些在您的 React 项目中需要考虑的好处:

  • 在复杂的 React 表单应用中,每次用户输入都重新渲染表单 UI 可能会对应用程序的性能造成昂贵的 DOM 操作。使用未受控表单组件可以防止与表单元素组件重新渲染相关的性能瓶颈。

  • 当您需要在 React 应用程序中处理file类型的表单输入时,例如进行文件上传,未受控表单更为合适。

  • 当您从遗留的非 React 代码库项目迁移时,未受控表单的使用非常快捷。由于 DOM 维护表单的状态,因此从遗留代码库中处理表单字段更容易。

现在我们已经充分了解了 React 项目中未受控表单的相关内容,并介绍了一些使用未受控表单的好处,让我们深入了解一些常用的输入元素:InputTextAreaSelect

处理用户输入 – Input、TextArea 和 Select

处理 React 表单元素的方式与非 React 应用程序处理用户输入的方式略有不同。在本节中,我们将查看在遵循 React 最佳实践的同时处理用户输入时使用的常见表单元素。

Input

表单中的Input字段是任何 Web 应用程序中最广泛使用的标签。输入字段允许收集用户数据。根据在表单中的用途,输入字段有不同的类型。在受控输入表单元素中,组件状态始终通过表单输入字段的值或 checked 属性来设置。您还有一个回调函数,用于监听用户输入导致的价值变化。

对于单选按钮和复选框的输入类型,我们使用 checked 属性。要访问输入字段的值,我们可以使用event.target.checked

TextArea

Textarea是一个允许用户写入多行文本字符的标签。Textarea通常用于收集用户数据,如 Web 应用程序中的评论或评论部分。React 中的Textarea元素工作方式不同。状态是通过表单输入字段中的值或 checked 属性来设置的,类似于单个输入字段。textarea没有子元素,这是 HTML 的典型特征。

您可以使用回调函数来检索表单元素状态值的更改:

<textarea value={textarea} onChange={onChangeCallback} />

Select

select表单元素用于设计下拉列表。React 有自己独特的处理select的方式。选中值通过select表单元素上的value属性设置。在 React 中,没有selected属性。

这个选择是由select表单元素上的set value属性决定的。您可以在处理表单元素中的选中值时使用回调函数:

<select value={user} onChange={onChangeCallback}><option value="user">User</option>
<option value="admin">Admin</option>
</select>

接下来,我们将讨论 React 应用程序如何处理用户数据验证,以及当用户在填写表单元素时,您如何对用户数据进行清理。

在 React 中验证和清理用户数据

验证是一个确保用户数据质量、完整性和系统期望的适当格式的过程。您永远不能盲目信任您应用程序用户提供的数据。虽然我们期望他们信任我们的代码,但我们不能通过不指导他们如何处理我们的表单和表单数据来回报这种信任。

从初级开发者开始,短语“永远不要相信用户会始终用你的表单做正确的事”将永远适用。您永远不能信任用户数据原样。来自用户的数据必须经过彻底审查和清理,并确保其处于期望的格式。

表单字段是通向您在 Web 开发中可能称之为后端的一切的开放窗口。因此,在没有规则的情况下信任用户输入可能会对您作为开发者的精神健康以及您 Web 应用程序的健康状况造成损害。

作为 React 开发者,您始终可以遵循一些标准的验证规则。这些验证规则指导您和您的应用程序避免应用程序中的不良行为者。

让我们看看在用户输入数据存入您的数据库或任何后端基础设施之前,您可能想要勾选的一些验证规则:

  • 数据类型:您需要确认用户是否为表单字段输入了正确的数据类型。他们甚至是否填写了任何内容?您需要检查。例如,在一个期望字符串字符的表单字段中,确保您没有接收到数值数据。

  • 一致性:您需要确保用户输入的数据一致性,确保获取一致数据的一种方法是通过强制执行验证规则。例如,您可能添加一个正则表达式来检查密码长度是否不少于 8 个字符,并且包含符号。

    或者,您可能只是允许用户从选项下拉列表中选择他们想要访问的国家,而不是要求他们输入他们想要访问的国家名称。如果您这样做,您将对您的好意得到的回报感到粗鲁的震惊。

  • 数据格式:您可能希望应用程序用户的出生日期以YYYY-MM-DDDD-MM-YYYY格式。我和您都知道我们不能将此留给用户的任意选择!

  • 范围和约束:你也可能想要检查数据是否与某些参数范围或某些数据是否落在某些预期约束内。因此,你需要通过一些机制来强制执行这一点 - 正则表达式就是这样的机制之一。你应该始终记住,用户也容易犯真正的错误,即使他们无意于做出不良行为。

话虽如此,在 React 表单设计中,你会在两个主要情况下想要强制执行验证规则:

  • 在用户输入时:当用户与表单元素交互时,你会检查其合规性并提供即时反馈。React 在这里使用受控表单组件表现最佳,该组件使用回调来收集用户值并将它们传递给具有检查错误能力的事件处理器。我们将在我们的注册表单中很快实现这一点。

  • 在用户提交时:在这种情况下,当用户点击提交按钮时,表单数据将受到验证。这曾经是过去的黄金标准。然而,如今,由于前端技术的阵列使得即时反馈变得容易实现,这在企业应用开发中似乎发生得越来越少。

实现表单验证

让我们检查使用 React 受控表单组件实现表单数据验证的实现方式。

我们将首先导入 useStateuseEffect 钩子,并将它们引入作用域以分别管理状态和副作用:

import React,{ useState,useEffect } from 'react';

然后,我们必须设置状态变量为 initialValuesformValuesformErrorsisSubmit

const SignUp = () => {const initialValues = { name: "", email: "", password: "" };
const [formValues, setFormValues] = useState(initialValues);
const [formErrors, setFormErrors] = useState({});
const [isSubmit, setIsSubmit] = useState(false);

initialValues 被声明为一个对象,用于保存初始表单输入状态,设置为空字符串。useStateinitialValues 变量的值作为初始状态,并将其分配给 formValues,这样从开始,所有表单输入值都是空字符串。formErrors 的初始状态设置为空对象,isSubmit 的初始状态设置为 false。这意味着还没有提交任何表单。

我们需要一个 onChange 函数来跟踪表单输入值的变化。我们必须设置 onChangeHandler(),它接受事件对象的参数 e 并解构 e.target 对象,该对象返回两个属性 - namevaluesetFormValues 函数接受所有当前的 formValues,使用 ...formValues 扩展运算符,并将它们更新为新值:

const onChangeHandler = (e) => {const { name, value } = e.target;
setFormValues({ ...formValues, [name]: value });  };

useEffect() 用于在表单没有错误时记录成功提交的值:

useEffect(() => {    if (Object.keys(formErrors).length === 0 && isSubmit) {
      console.log(formValues);
    }
  }, [formErrors]);

接下来,使用 validateForm 函数设置表单数据验证规则。这是一个简单的验证规则,检查 nameemailpassword 表单输入是否已填写。它还检查是否使用了正确的格式来处理电子邮件,使用 regex.test()

对于密码,我们必须检查密码是否超过 8 个字符但不超过 12 个字符:

const validateForm = (values) => {    const errors = {};
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i;
    if (!values.name) {
      errors.name = "Name is required!";
    }
      ...(This ... represents omitted code which can be
          found on GitHub)
    return errors;
  };

然后,调用onSubmitHandler(),这确保了setFormErrors()函数被运行,该函数以validateForm()作为参数。如果没有表单错误,setIsSubmit被设置为true,允许表单提交:

  const onSubmitHandler = € => {    e.preventDefault();
    setFormErrors(validateForm(formValues));
    setIsSubmit(true);
  };

下面是表单组件返回的 JSX,其中包含onSubmitHandler以及formErrors对象显示的每个错误:

return (  <div className="signUpContainer">
    <form onSubmit={onSubmitHandler}>
      <h2>Create an account</h2>
      <div className="signUpForm">
        <label htmlFor="name">Name</label>
        <p style={{color:'red', fontWeight:'bold'}}>
          {formErrors.name}</p>
        <input
          id="name"
          type="text"
          name="name"
          value={formValues.name}
          onChange={onChangeHandler}
        />
  ...(This ... represents omitted code which can be found
      on GitHub)
        <button>Register</button>
      </div>
    </form>
  </div>
);
};
export default SignUp;

下图显示了在提交前填写表单字段时的表单条目输出:

图 6.6 – 包含验证规则的表单截图

图 6.6 – 包含验证规则的表单截图

我们为每个表单元素添加了formErrors属性,以便在存在错误时输出错误信息。例如,{formErrors.name}会在表单中没有填写名称时显示错误。

该验证规则的完整代码可以在本书的 GitHub 仓库中找到:https://github.com/PacktPublishing/Full-Stack-Flask-Web-Development-with-React/blob/main/Chapter-06/

这总结了如何在 React 应用程序中添加验证而不使用外部库,从而最小化应用程序可能依赖的潜在依赖项数量。

摘要

在本章中,我们讨论了 React 中的两个重要概念:表单和路由。我们强调了在非 React 和 React 应用程序中设置的表单之间的细微差别。React 通过使用受控和非受控表单组件来处理表单元素,从而在用户体验方面提供了大量的改进。

然后,我们深入探讨了验证概念以及如何在 React 中实现验证规则。然后,我们讨论了 React Router。我们展示了第三方库 React Router 如何使我们能够导航复杂的 React 应用程序。我们讨论了RouteLinks和嵌套Routes的使用,并探讨了它们在 React 项目中的应用。

在下一章中,我们将学习并理解如何在 React 应用程序中实现测试。测试是软件开发的一个基本部分,因为它确保应用程序的组件按预期工作,并且在开发中遵循了相关的最佳实践。

第七章:React 单元测试

我们在前面的章节中充分讨论了 React 的基础知识。你已经接触到了 React 工具和资源,以便在完整的全栈开发旅程中掌握现代前端技术。我们深入探讨了构建丰富交互式界面所需的 React 的有用信息。我们讨论了 React 中的组件、属性和状态、JSX、事件处理、表单和路由等主题。

在本章中,我们将专注于 React 应用程序中的单元测试,这是一种专注于隔离代码片段的软件测试类型。我们还将探索基于 Node 的测试运行器 Jest。测试运行器允许你发现测试文件、运行测试,并自动找出测试是否通过或失败。你将以一个非常清晰、表达丰富且易于阅读的格式结束报告。

Jest 是一个针对 React 开发的流行测试框架。该项目最初由 Meta 公司拥有,该公司也是 React 的背后公司。然而,随着 Jest 从 Meta 转移到 OpenJs Foundation,Jest 现在有一个独立的内核团队正在积极工作,以确保其稳定性、安全性和经得起时间考验的代码库。

此外,我们还将简要探讨 React 生态系统中的一个有用的测试工具——React 测试库RTL)。这个库为我们提供了一系列方法,可以在虚拟环境中对 React 组件进行测试。

最后,在本章中,我们将深入探讨测试驱动开发TDD),这是一种软件开发范式,它将测试实现放在实际编码之前。编码和测试是交织在一起的。测试始终是第一位的,尽管它显然不是软件开发过程中的有趣部分。

即使是经验丰富的开发者,在测试方面仍然会像初学者一样遇到困难。公司对测试有不同的政策,但在现实世界的开发中,没有测试就无法拥有工业级的 Web 应用程序。

测试确保你遵循软件开发生命周期的最佳实践,并且你和你的应用程序最终用户对你的 Web 应用程序功能有信心。通过适当的测试,你的 Web 应用程序的功能将像预期的那样高效运行。

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

  • 什么是软件测试?

  • Jest 简介

  • 在 Jest 中编写测试

  • 单元测试 React 组件

  • TDD

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter07

什么是软件测试?

软件测试是一个确保软件或应用开发中的所有代码片段都能按照所有利益相关者的预期工作的过程。测试过程是软件开发生命周期中的关键阶段之一,它解释了设计和开发应用程序的标准方法论。

软件测试确保网络应用程序具有更少的错误,技术需求被高效地按预期实现,开发成本降低,最终满足业务需求。

有趣的是,如果有效地执行,测试通常会给软件工程师足够的信心,使他们确信他们向生产环境推出的产品是可维护的、可读的、可靠的,并且结构良好。

这减少了关于应用程序中可能出现的昂贵错误所带来的恐慌,这些错误可能使公司蒙羞或损害客户信心。有许多值得注意的软件测试类型——单元测试、集成测试、性能测试、可用性测试、验收测试和回归测试。

让我们简要讨论一下我们在软件开发中拥有的测试类型,以刷新我们的思维,并为后续章节提供一个坚实的基础。

  • 单元测试:在这种测试中,软件或应用开发中最小的代码片段或单元被测试。在单元测试中,你系统地经过三个阶段——规划、用例脚本编写和测试。你主要关注你应用程序中独立单元的功能。在“单元测试 React 组件”部分,我们将深入探讨单元测试以及如何在你的 React 应用程序中实现单元测试。

  • 集成测试:在这种测试中,将代码的各个独立单元组合起来,并测试它们之间的有效交互。

  • 性能测试:在这种测试中,你的应用程序的速度和效率与给定的负载进行测试。这种方法用于识别软件应用程序平稳运行中的早期瓶颈。

  • 可用性测试:在这种测试中,你的应用程序的预期用户被允许体验你的产品。通过可用性测试对应用程序设计进行直接评估的反馈确保你能够尽早捕捉到用户在使用应用程序时的挫败感。可用性测试的报告还可以提供其他可能改进产品的有用见解。

  • 验收测试:在这种测试中,客户满意度被衡量——客户需求与开发的软件或应用程序进行比较。客户能够检查这些需求是否被正确捕捉并按预期工作。

  • 回归测试:在这种测试中,我们检查将新功能添加到你的软件或应用程序中引起的代码更改是否破坏了之前测试过的正常功能。回归测试可以防止在修改应用程序代码库时引入错误或意外的副作用。通过回归测试,所有测试用例都会重新运行,以确保不会引入新的错误。

接下来,我们将探索你可以在测试前端应用程序的功能时,有信心与之合作的领先测试框架之一。

介绍 Jest

Jest是一个开源的、健壮的、文档齐全的、快速且安全的 JavaScript 测试框架。Jest 提供了你需要的工具和资源,让你对你的 JavaScript 代码库有绝对的信心。测试是编写高质量代码的基础,而 Jest 几乎无需配置,就能愉快地处理测试实现。

Jest 在 JavaScript 社区中非常受欢迎。你可以利用 Jest 丰富的 API 集编写测试,例如匹配器、模拟函数、代码覆盖率以及快照测试。Jest 可以用来测试 React 组件,实际上 React 团队也推荐使用 Jest 来测试 React 项目。

Jest 是一个 Node.js 测试运行器,这意味着测试总是在 Node 环境中运行。Jest 在构建时考虑了性能。你可以有效地并行运行一系列测试。Jest 工具包附带代码覆盖率,它使你能够了解项目中已测试和未测试的文件。并且当你的测试失败时,你会得到关于为什么它们失败的有见地的信息。

让我们为测试设置一个 Jest 环境。

设置 Jest 环境

Jest 运行需要 Node.js。Jest 附带了我们用于第二章,“使用 React入门”中create-react-app脚手架代码的npx create-react-app命令。

在本节中,我们将创建一个名为bizza-test的新工作文件夹,以便在继续使用它进行 React 组件的单元测试之前,更好地探索 Jest 如何与 JavaScript 函数协同工作:

  1. path/to/bizza-test/目录下,让我们在终端中执行以下命令:

    1. 在命令终端中运行npm init -y

    2. 使用npm install --save-dev jest在工作文件夹(bizza-test)中安装 Jest 作为依赖项。

  2. 更新package.json以能够通过npm test运行 Jest:

    "scripts": {              "test": "jest"}
    

    完整的package.json应如下所示:

    {  "name": "bizza-test",  "version": "1.0.0",  "description": "",  "main": "index.js",  "scripts": {    "test": "jest"  },  "keywords": [],  "author": "",  "license": "ISC",  "devDependencies": {    "jest": "²⁹.2.1"  }}
    

下面的截图显示了在终端中安装 Jest 的过程。

图 7.1 – Jest 库设置截图

图 7.1 – Jest 库设置截图

配置完成后,你就可以正确地运行测试了。接下来,我们将探讨一些关键的测试术语,并编写实际的测试。

在 Jest 中编写测试

现在我们已经设置了测试运行环境,让我们简要了解一下在这个部分我们将遇到的一些关键字。在 Jest 中,你有一些来自 Jest API 的测试关键字和函数来构建你的测试;让我们来检查它们:

  • test()it(): 这是一个描述性的单元测试或测试用例。它包含三个参数——一个描述性的测试用例字符串、一个包含要测试的期望值的函数,以及一个可选的超时参数,指定在终止测试用例之前要等待多长时间。默认超时时间为五秒:

    test(name, fn, timeout)test("<test case name>", ()=>{              ...    })
    
  • describe(): 这将相关测试用例组合在一起。它用于组合几个相关的测试并描述它们的行为。describe() 接受两个参数——一个描述你的测试组的字符串和一个包含测试用例的回调函数:

    describe(name, fn)describe("<your test group name>", ()=>{    test("<test case name>",()=>{      ...    });    ...    test("<test case name>",()=>{      ...    });})
    
  • beforeAll(): 这个函数在测试文件中的任何测试运行之前执行。当你从函数中返回一个 promise 或 generator 时,Jest 会等待该 promise 解决后再运行测试:

    beforeAll(fn, timeout)
    
  • beforeEach(): 这个函数在测试文件中的每个测试运行之前执行。当你从函数中返回一个 promise 或 generator 时,Jest 也会等待该 promise 解决后再运行测试。

  • expect(): 在编写测试时,你至少需要检查一个值是否符合某些条件。expect() 允许你使用匹配器有条件地检查一个值,我们稍后会讨论匹配器。值得注意的是,存在这样的情况,一个单独的测试用例可能有多个 expect() 函数:

    test("<test case name>",()=>{    ...    expect(value);    ...})
    
  • Matchers: Jest 使用匹配器来测试值验证。匹配器让你以不同的方式测试值,以便做出正确的断言。

  • Assertion: 断言被定义为包含可测试逻辑的表达式,用于验证程序员对特定代码块所做的假设。这允许你识别应用程序中的错误和其他缺陷。

因此,在所有这些函数和术语都定义清楚之后,让我们编写我们的第一个测试套件:

  1. bizza-test 工作目录中,创建一个 __tests__ 文件夹。Jest 会搜索你的工作目录中的 __tests__ 或以 .test.js.spec.js 结尾的测试套件文件,然后运行该文件或 __tests__ 文件夹中的所有文件。__tests__.test.js.spec.js 这三个是测试命名约定。

    为了命名本书中的测试目录,我们将采用 __tests__ 的命名约定;在 path/to/bizza-test/ 内创建一个 __tests__ 目录。这是我们存放测试文件的地方。

  2. 现在,在 bizza-test 目录中创建 basicFunctions.js 并将以下片段添加到 basicFunctions.js 中:

    const basicFunctions = {    multiply:(number1, number2) => number1 * number2}Module.exports = basicFunctions;
    

    这个片段描述了一个简单的 JS 函数,用于将两个数字相乘。

  3. __tests__ 中创建一个测试套件文件,basicFunctions.test.js,并粘贴以下代码片段:

    const basicFunctions = require ('../basicFunctions');test ('Multiply 9 * 5 to equal 45', () => {    expect (basicFunctions.multiply(9, 5)).toBe(45);});
    

    让我们简要解释一下前面的测试文件代码的工作原理:

    • 第一行将我们要测试的basicFunctions.js模块导入到作用域中。

    • Test()函数设置测试用例描述,并使用其功能来验证9乘以5等于45

    • toBe(45)让我们知道我们期望得到45作为预期结果。如果结果不是45,测试将失败。

    • 然后,在终端中运行npm test命令。

    下面的截图显示了运行npm test命令后的输出:

图 7.2 – 显示测试用例输出的截图

图 7.2 – 显示测试用例输出的截图

上述内容展示了 Jest 中测试是如何工作的。

现在,有了这些关于如何测试简单 JS 函数的坚实基础,让我们深入了解 React 组件的单元测试。

注意

可以在这里找到 Jest 的有用函数列表:jestjs.io/docs/api

可以在这里找到常用匹配器的列表:jestjs.io/docs/expect

单元测试 React 组件

React 依赖于组件驱动的开发哲学,测试 React 组件的单元进一步使我们接近构成 React 组件的基本元素。单元测试的本质是测试单个代码块,以确保其功能符合用户的预期。

如前所述,在单元测试中,你需要系统地经历三个阶段——规划、用例脚本编写和测试。编写单元测试应该进行彻底的规划,应该实现描述性的测试用例,并且断言应该足够清晰,以便团队中的每个人都能理解。

然而,在我们深入探讨 React 组件的单元测试之前,我们如何知道在 React 应用程序中要测试什么?很简单。每个 React 应用程序都有一个或多个具有特定功能的组件。因此,在 React 应用程序中要测试的内容是主观的。每个项目都是不同的,应用程序的功能也是如此。在电子商务 Web 应用程序中要测试的应用程序功能将不同于在社交媒体应用程序中要测试的感兴趣的功能。

然而,在选择要测试的功能时有一些一般性的经验法则。在应用程序开发中,测试为我们提供了信心和保证,即我们的软件产品仍然按预期工作,即使在代码重构之后也是如此。这基本上归结为那些函数的商业价值,以及那些对最终用户使用应用程序的体验有显著影响的功能。

要测试 React 组件,还有一个简单而有用的测试工具,称为 RTL。RTL 可以与 Jest 一起使用,以实现 React 组件测试目标。RTL 允许你像你的应用程序的实际用户一样测试你的组件单元,与你的应用程序 UI 交互。

虽然 Jest 是一个测试运行器,用于在测试环境中查找测试、运行测试并确认测试是否失败或通过,但 RTL 在 VDOM 上提供了实用函数。你将遇到如 render() 这样的方法来模拟组件的渲染,fireEvent 来派发用户与浏览器交互时希望触发的事件,以及一个用于查询渲染元素的 screen

RTL 还使用 waitFor() 方法等待异步代码,以及 expect(),这是一个用于进行断言的函数,用于确定预期的结果是否与实际结果匹配,并指示成功或失败。使用 npx create-react-app 设置的 React 项目,你不需要显式安装 RTL。在实际的应用程序开发环境中,你希望测试你的组件是否按预期工作。RTL 促进了模拟用户如何与你的组件交互。

这可以通过 RTL 内置的实用工具实现,它允许你编写直接与 DOM 节点交互的测试,而无需实际渲染 React 组件。简而言之,我们很快将深入了解 RTL 如何在无状态和有状态组件中模拟人类与 React 应用程序 UI 的交互。

让我们从编写一个简单的单元测试来检查组件是否渲染开始。

为无状态组件编写单元测试

为了测试目的,让我们使用 npx create-react-app bizzatest 创建一个新的 React 测试项目。删除一些样板文件,让目录结构如下所示:

    /bizzatest        /.git
        /node_modules
        /public
        /src
        /App.js
        /index.js
        /setupTests.js
          .gitignore
            package.json
        package-lock.json
        README.md

App.js 文件应包含以下代码:

function App() {    return (
        <div>
            <h1>Bizza Tests</h1>
        </div>
    );
}export default App;

Index.js 文件应包含以下代码:

import React from 'react';import ReactDOM from 'react-dom/client';
import App from './App';
const root =
    ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

现在我们已经设置了测试环境,让我们创建一个无状态组件,并编写一个单元测试来检查 WelcomeScreen.js 是否渲染了预期的文本段落。

按照以下步骤创建组件,并对其是否存在指定的段落文本进行单元测试。

创建一个 WelcomeScreen.jsx 组件

让我们创建一个 src/WelcomeScreen.jsx 组件,并添加以下代码片段。此组件显示 <h1>React</h1> 元素:

import React from "react";const WelcomeScreen = () => {
    return  <h1>React</h1>
};
export default WelcomeScreen;

创建测试用例文件

src/__tests__ 文件夹内,创建一个测试用例文件,并将其命名为 WelcomeScreen.test.js。使用它来存储所有的测试用例。你也可以在每个组件的文件夹中编写测试。然而,在这种情况下,我们将将其存储在 src/__tests__ 文件夹中。添加以下代码片段:

import {render,screen,cleanup} from    "@testing-library/react";
import WelcomeScreen from "../WelcomeScreen";
afterEach(() => {
    return cleanup();
});
test("should show Welcome text to screen", () => {
    render(<WelcomeScreen />);
    const showText = screen.getByText(/Welcome to Bizza
        Conference Platform/i);
    expect(showText).toBeInTheDocument();
});

让我们简要讨论一下前面代码片段中发生的事情。

  • renderscreencleanup 工具是从 @testing-library/react 导入的。render 帮助你在 HTML 文档(DOM)的 body 中附加一个容器中虚拟渲染一个组件。

    RTL 提供的 screen 对象为您提供了查找渲染 DOM 元素的方法,以便进行必要的断言。本质上,screen 用于与渲染组件交互,而 cleanup 函数用于在每个测试后清理渲染组件。

  • WelcomeScreen 组件作为测试所需的文件被导入。这是要测试的组件。

  • afterEach() 方法被添加到卸载每个渲染的组件测试之前,以便在渲染下一个组件之前防止内存泄漏。afterEach() 块是 Jest 生命周期方法,在每个测试之后运行。在这种情况下,它调用 cleanup 函数来清理每个测试后渲染的任何组件。使用来自 TRL 的此清理实用工具被视为 React 组件测试的最佳实践。

  • test() 函数定义了实际的测试,命名为 "should show Welcome text to screen",以及一个回调函数来包含测试用例。测试首先调用 render 函数来渲染 WelcomeScreen 组件。然后,它使用 screen.getByText 函数获取包含文本的 DOM 元素,使用 expect() 函数来验证文本是否在文档中,使用 toBeInTheDocument 匹配器。

  • WelcomeScreen 组件被渲染时,我们期望它包含 Welcome to Bizza Conference Platform

现在,使用 npm test 运行测试。

以下截图显示了输出:

图 7.3 – 展示失败的测试用例输出

图 7.3 – 展示失败的测试用例输出

如预期的那样,测试失败了。在 WelcomeScreen.jsx 中 RTL 模拟的渲染容器中的段落文本是 <h1>React</h1>,而不是 <h1>Welcome to Bizza Conference Platform</h1>

更新 WelcomeScreen 组件

现在让我们更新 WelcomeScreen.jsx,以包含要在屏幕上渲染的预期文本:

import React from "react";const WelcomeScreen = () => {
    return <h1>Welcome to Bizza Conference Platform</h1>
};
export default WelcomeScreen;

太棒了,测试通过了!更新的测试报告现在显示一个通过测试:

图 7.4 – 展示通过测试用例输出的截图

图 7.4 – 展示通过测试用例输出的截图

您可以为您的各种组件的元素编写单元测试,并使用 RTL 的适当查询。这些 RTL 查询允许您以用户与应用程序 UI 交互相同的方式查找元素:

  • getByRole(): 此函数用于通过其角色属性定位元素,例如按钮、链接、复选框、单选按钮和标题。getByRole() 函数对于测试组件的可访问性以及一般测试目的都很有用。

  • getByLabelText(): 此函数用于通过 for 属性定位与标签元素关联的表单元素。getByLabelText() 对于测试表单组件及其可访问性非常有用。

  • getByPlaceholderText(): 此函数用于通过其占位符属性定位输入元素。getByPlaceholderText()对于测试输入字段及其行为非常有用。

  • getByText(): 此函数用于通过其文本内容定位元素。getByText()对于测试特定文本的渲染或定位通过其文本识别的按钮或链接非常有用。

  • getByDisplayValue(): 此函数用于通过其显示值定位表单元素,例如具有预填充值的输入或选择元素。getByDisplayValue()对于测试表单组件及其行为非常有用。

  • getByAltText(): 此函数用于通过其alt属性定位图像元素,该属性提供了图像的文本描述。getByAltText()对于测试组件中图像的可用性非常有用。

  • getByTitle(): 此函数用于通过其title属性定位元素。getByTitle()对于测试具有title属性的元素的可用性和行为非常有用。

  • getByTestId(): 此函数用于通过其data-testid属性定位元素。getByTestId()对于测试通过唯一测试 ID 识别的特定元素非常有用,而不依赖于其他属性,如 class 或 ID。

让我们来看看如何编写有状态组件的单元测试。我们将使用演讲者卡片并测试其单元,直到我们满意。

编写有状态组件的单元测试

components文件夹中,创建一个名为SpeakerCard的有状态组件。SpeakerCard组件是一个函数组件,它渲染一个包含演讲者信息的卡片。该组件接受一个speaker对象作为属性,该对象包含诸如演讲者的姓名、职业、公司、电话号码和电子邮件地址等属性。

现在,以下代码片段显示了SpeakerCard组件的测试片段:

import {useState} from 'react'const SpeakerCard=speaker=>{
const {name, occupation, company, phone, email}= speaker;
const [showDetails, setShowDetails]= useState(false);
const toggleDetails = () => setShowDetails(!showDetails);
return(
<div className="card" data-testid="card">
<span><h2>Name:{name}</h2></span>
<span><h2>Occupation:{occupation}</h2></span>
<span><h2>Company:{company}</h2></span>
<button data-testid="toggle-test" onClick={toggleDetails}>
{showDetails? "Hide Details":"Show Details"}
</button>
{showDetails && (<div data-testid="test-details">
<h2>Email:{email}</h2>
<h2>Phone:{phone}</h2>
</div>
)}
</div>
)
}
export default SpeakerCard;

之前提供的代码片段在此处解释:

  • 组件接受一个speaker对象作为属性,该对象包含诸如演讲者的nameoccupationcompanyphone numberemail address等属性。

  • 组件使用useState钩子来管理一个名为showDetails的布尔状态变量,该变量控制是否显示关于演讲者的额外详细信息。当切换按钮被点击时,toggleDetails函数会切换showDetails变量的值。

    如果showDetails变量为true,组件将在具有data-testid属性为test-details的嵌套div元素中渲染关于演讲者的额外详细信息,包括他们的电子邮件和电话号码。

  • toggleDetails()函数的初始状态是false;当点击切换按钮时,状态变为true。在<div data-testid="card">中的data-testid属性用于识别测试的 DOM 节点。data-testid属性具有card作为其值。这个值允许expect(),一个 Jest 实用工具,对测试是否失败或通过进行断言。

  • 使用切换按钮,我们将data-testid属性设置为toggle-test以断言尚未点击任何按钮。

  • 使用data-testid="test-details"来断言切换按钮被点击并且详细信息在屏幕上显示。

  • 条件性,当showDetails设置为true时,电子邮件和电话详情将在屏幕上显示;否则,它们将被隐藏。

现在,让我们编写单元测试来显示<SpeakerCard/>可以在屏幕上渲染,并且当点击切换按钮时,我们可以看到关于speaker对象的更多详细信息。

/src/__tests__/内部,创建一个测试文件,SpeakerCard.test.js

import {render,screen, fireEvent,cleanup} from    "@testing-library/react";
import SpeakerCard from
    '../components/SpeakerCard/SpeakerCard'
const speaker= {
    name: "Olatunde Adedeji",
    occupation: "Software Engineer",
    company: "Mowebsite",
    email:"admin@admin.com",
    phone: "01-333333",
}
afterEach(() => {
    return cleanup();
});
test("should render the SpeakerCard component", ()=>{
    render(<SpeakerCard/>);
    const card = screen.getByTestId("card");
    expect(card).toBeDefined()
});
test("should make sure the toggle button shows or hides
    details", ()=>{
    render(<SpeakerCard speaker={speaker}/>);
    const toggleButton = screen.getByTestId("toggle-test");
    expect(screen.queryByTestId(
        "test-details")).toBeNull();
    fireEvent.click(toggleButton);
    expect(screen.getByTestId(
        "test-details")).toBeInTheDocument();
});

让我们回顾一下前面的代码片段:

  • SpeakerCard组件。

    1. SpeakerCard.jsx作为测试所需的文件导入。

    2. 使用来自@testing-library/react库的test()函数定义test()函数,其中包含一个描述测试应执行什么的test字符串,在这个测试用例中是"should render the SpeakerCard component",以及一个包含测试代码的函数。

    3. 然后,导入并使用renderscreen实用工具来模拟<SpeakerCard/>组件的渲染。

    4. SpeakerCard组件接受定义的speaker对象属性。

    5. 使用.getByTestId("card")查询节点并将其值分配给card。这允许你访问进行断言所需的 DOM 节点。然后,你可以使用 Jest 实用工具中的expect()来确认<SpeakerCard/>已被渲染。期望它被定义!

    以下屏幕截图显示了通过渲染 React 组件的测试:

图 7.5 – 展示通过渲染组件测试的屏幕截图

图 7.5 – 展示通过渲染组件测试的屏幕截图

  • test()函数来自@testing-library/react库,其中包含一个描述测试应执行什么的test字符串,在这个测试用例中是"should make sure the toggle button shows or hides details",以及一个包含测试代码的函数。

  • 渲染SpeakerCard组件。

  • 当没有按钮被点击时,我们期望data-testid属性值toggle-testn。第一个断言检查详情部分最初没有显示,通过检查具有test-details data-testid的元素不在文档中。这是通过使用screen.queryByTestId()函数完成的,如果找不到元素,它将返回null

  • 然后,导入并使用fireEvent函数。fireEvent.click(toggleButton)模拟用户在SpeakerCard组件中点击切换按钮。fireEvent实用工具是@testing-library/react包的一部分,它提供了一种在测试环境中模拟用户交互的方法。fireEvent.click()函数用于模拟点击切换按钮。这将触发组件中的toggleDetails函数,该函数应根据showDetails状态显示或隐藏详细信息部分。

  • 使用getByTestId查询节点并将其值分配给toggleButtonSpeakerCard组件中的data-testid属性通过使用screen.getByTestId搜索test-details元素来检查是否显示详细信息。

  • 我们期望data-testid中的test-details在屏幕上显示。如果文档中存在test-details,则测试通过;否则,测试失败。

以下截图显示了切换按钮的测试:

图 7.6 – 展示通过切换按钮测试的截图

图 7.6 – 展示通过切换按钮测试的截图

接下来,我们将讨论 TDD。这是一种软件开发实践,它鼓励在编码之前首先测试应用程序的每个功能单元。

TDD

TDD 是一种将编写测试放在首位的开发范式。你先写测试,然后编写代码来验证。TDD 的主要目的是快速反馈。你写一个测试,运行它,然后它失败。然后你编写最小的代码来通过测试。一旦测试通过,你然后适当地重构你的代码。

这些过程是迭代重复的。在代码实现之前专注于编写测试,使开发者能够从用户的角度看待产品,从而确保一个符合用户需求的工作功能。

TDD 使软件开发者能够创建具有单一职责的代码库单元 – 允许代码只做一件正确的事情。然而,传统的做法是先编码再测试。在开发过程结束时测试代码库的想法已被证明是错误的,并且伴随着高昂的代码维护成本。

大多数软件开发者比测试驱动更敏捷。将产品推向市场并经常面临不切实际的截止日期的迫切需求,使得对支撑这些软件产品的代码单元的质量关注较少。使用传统的开发方法,错误会悄悄地进入生产环境。

如我们所知,基于业务需求,生产中的 Web 应用程序确实会不时添加额外的功能。然而,如果没有质量测试,新功能添加或修复可能会带来更多问题。

传统方法另一个问题是产品开发团队与测试团队不同。这种团队分离可能会使代码库的长期维护变得困难,并可能导致代码质量严重下降,这是由于意图混乱的冲突。

使用 TDD,你和其他利益相关者有证据证明你的代码确实工作并且质量很高。现在,我们将扮演一个敏捷且同时是测试驱动的网络开发者的角色。我们将研究一个案例研究,其中我们使用 TDD 方法在我们的Bizza项目中构建登录组件。

组件开发将遵循 TDD 原则:

  1. 编写单个测试用例。这个测试预计会失败。

  2. 编写满足测试并使其通过的最小代码。

  3. 然后,重构你的代码并运行它以通过/失败。

这些步骤会重复进行。

因此,现在让我们编写我们的初始测试。我们预计它们无论如何都会失败。这就是 TDD 的本质。

编写单个测试用例

在测试目录bizzatest下的bizzatest/src/components/Auth/SignInForm/SignInForm.jsx创建SignInForm组件,并按以下方式更新文件。这是一个没有标签和功能的SignInForm组件:

import React from 'react';const SignInForm = () => {
    return (
        <div className="signInContainer">
            <form>
                <div className="signInForm">
                    <label htmlFor="email"></label>
                    <input
                        type="email"
                    />
                    <label htmlFor="password"></label>
                    <input
                        type="password"
                    />
                    <button></button>
                </div>
            </form>
        </div>
    );
};
export default SignInForm;

App.js内部添加以下代码片段:

import React from "react";import "./App.css";
import SignInForm from
    "./pages/Auth/SignInForm/SignInForm";
const App = () => {
    return <SignInForm />;
};
export default App;

然后,运行npm s以渲染以下组件:

图 7.7 – 展示渲染后的 SignInForm 组件的屏幕截图

图 7.7 – 展示渲染后的 SignInForm 组件的屏幕截图

在 TDD(测试驱动开发)中,你希望从一个将失败的组件单元测试开始,然后在组件功能实际开发之后,围绕它来使其通过。

注意

检查本章的技术要求部分以获取此表单组件的样式(github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter07/05)。

现在,让我们编写并运行SignInForm组件的测试。

bizzatest/src/__tests__目录内创建SignInForm.test.jstest文件。

按以下方式更新文件:

import { render, screen,cleanup } from    '@testing-library/react';
import  SignInForm from
    '../components/Auth/SignInForm/SignInForm';
afterEach(() => {
    return cleanup();
  });
test("Email Address should be rendered to screen", () => {
    render(<SignInForm />);
    const linkElEmailInput =
        screen.getByText(/Email Address/i);
    expect(linkElEmailInput).toBeInTheDocument();
});
test("Password should be rendered to screen", () => {
    render(<SignInForm />);
    const linkElPasswordInput =
        screen.getByText(/Password/i);
    expect(linkElPasswordInput).toBeInTheDocument();  });
test("Sign In should be rendered to screen", () => {
    render(<SignInForm />);
    const linkElSignInBtn = screen.getByTest(/SignIn/i);
    expect(linkElSignInBtn).toBeInTheDocument();
});

让我们解释前面代码片段的工作原理:

Import { render, screen } from '@testing-library/react' 将 RTL(React Testing Library)中的renderscreen函数引入作用域。这允许我们在测试环境中使用 VDOM 渲染组件。

然后导入测试下的所需SignInForm组件文件。

test("Email Address should be rendered to screen", () => {render(<SignInForm />)定义了一个带有描述的测试用例并渲染了SignInForm-componentrender()方法来自 RTL。同样,对于PasswordSign In按钮,使用单独的测试用例描述重复此操作。

在第一个测试中,测试使用@testing-library/react中的getByText()方法检查屏幕上是否渲染了Email Address标签。如果标签在屏幕上找到,则测试通过。

在第二个测试中,测试通过使用getByText()方法检查屏幕上是否渲染了Password标签。如果标签在屏幕上找到,则测试通过。

第三个测试通过使用getByTestId()方法检查屏幕上是否渲染了带有文本SignIn的按钮。如果按钮在屏幕上找到,则测试通过。

expect(linkElEmailInput).toBeInTheDocument()用于断言。这是为了验证声明的变量值是否存在于SignInForm组件中。

现在,在终端中运行npm test命令。测试用例描述显示为失败状态。

图 7.8 – 失败的 SignInForm 测试截图

图 7.8 – 失败的 SignInForm 测试截图

以下截图显示了 Jest 测试运行器的详细报告。它显示了一个测试套件和三个失败的测试。

图 7.9 – 失败的 SignInForm 组件测试截图

图 7.9 – 失败的 SignInForm 组件测试截图

编写满足测试的最小代码并使其通过

现在,更新SignInForm组件以满足测试套件中测试用例的期望:

import React from 'react';const SignInForm = () => {
    return (
        <>
            <div className="signInContainer">
                <form>
                    <div className="signInForm">
                        <label htmlFor="email">
                            Email Address</label>
                        <input
                            type="email"
                        />
                        <label htmlFor="password">
                            Password</label>
                        <input
                            type="password"
                        />
                        <button>Sign In</button>
                    </div>
                </form>
            </div>
        </>
    );
};
export default SignInForm;

在前面的代码片段中,我们重构了代码,以期望的方式通过测试并完成 TDD 的原则。Jest 测试运行器会自动运行并根据SignInForm组件的重构通过测试。

在下面的图中,我们有我们测试驱动组件开发的详细 Jest 成功报告。

图 7.10 – SignInForm 组件测试成功通过截图

图 7.10 – SignInForm 组件测试成功通过截图

SignInForm组件现在如图图 7.11所示:

图 7.11 – 测试通过后渲染的 SignInForm 组件截图

图 7.11 – 测试通过后渲染的 SignInForm 组件截图

代码重构

代码重构是指修改现有代码以提高其质量、可维护性和效率,同时不改变其行为的过程。代码重构涉及分析并改进代码结构、设计和实现,使其更容易理解、修改和维护。重构通常是为了提高代码可读性、消除代码重复并提高性能。

在代码重构过程中,测试你的代码以确保所做的任何更改都不会影响系统的行为非常重要。在每次重构步骤之后,代码都会运行多次,以确保它仍然通过所有正面测试或失败所有负面测试。

如果你期望测试通过但测试失败了,这意味着重构步骤引入了一个错误,代码需要恢复到其之前的状态,或者需要做出额外的更改来修复问题。重构可以手动完成,但也有一些自动化工具,如 Jest、RTL、Enzyme 和 React 测试工具,可以帮助识别需要重构的代码区域。

作为 TDD 方法的一部分的重构允许你重构代码并运行它,直到你对测试结果有信心。这些代码重构步骤可以重复多次。

让我们重构SignForm.test.js并检查你可以重构测试代码的一种方法。

bizzatest/src/__tests__/ RefactorSignInForm.test.js内部创建test文件:

按照以下方式更新文件:

import { render, screen, cleanup } from    '@testing-library/react';
import SignInForm from
    '../components/Auth/SignInForm/SignInForm';
describe('SignInForm component', () => {
    afterEach(() => {
        cleanup();
    });
    it('should render the email address input', () => {
        render(<SignInForm />);
        const emailInput =
            screen.getByLabelText(/Email Address/i);
        expect(emailInput).toBeInTheDocument();
    });
    it('should render the password input', () => {
        render(<SignInForm />);
        const passwordInput =
            screen.getByLabelText(/Password/i);
        expect(passwordInput).toBeInTheDocument();
    });
    it('should render the sign in button', () => {
        render(<SignInForm />);
        const signInButton =
            screen.getByRole('button', { name: /Sign In/i
            });
        expect(signInButton).toBeInTheDocument();
    });
});

这里是一些在重构的测试代码中所做的更改:

  • 测试被包裹在一个describe块中,以便将它们分组在同一个标题下

  • test()替换为it()以保持一致性

  • 文本匹配从getByText更改为getByLabelText,用于电子邮件和密码输入,因为这是一种更合适的方式来定位表单输入

  • Signin按钮的getByTest查询替换为getByRole和名称选项

摘要

测试对于将应用程序产品成功部署到生产至关重要。在本章中,我们探讨了各种测试类型,特别是单元测试对于代码库质量和易于维护性的重要性。这确保了以自信的方式生产软件的成本较低。

我们还探讨了 Jest,这是一个令人愉快的 Node 测试运行器,它可以测试 JavaScript 代码,以及由此扩展的 React 应用程序。Jest 测试框架确保你在集成的测试环境中工作,几乎所有测试工具都在一个地方,而且触手可及。

我们讨论了 RTL 及其实现,然后深入探讨了使用 Jest 和 RTL 对 React 组件进行单元测试,这些测试包含在Create React AppCRA)模板代码中。

我们编写了有用的组件测试来展示 Jest 和 TLR 结合工具的能力。最后,我们讨论了软件开发中的测试方法 TDD,以及它如何在基于 React 的应用程序中使用示例。

接下来,我们将暂时转移本书的焦点,深入探讨全栈 Web 开发的后端开发方面,这正是本书的主题。我们将在下一章开始一个后端应用程序,讨论 SQL 和数据建模。

第二部分 – 使用 Flask 进行后端开发

欢迎来到我们书籍的第二部分,我们将深入探讨使用 Flask 进行后端开发的动态。在本节中,我们将探讨使用 Flask 框架在现代 Web 开发中使用的根本概念和技术。

你将获得解决后端开发复杂性的知识和工具。从设置你的 Flask 环境到设计高效的 RESTful API,实现数据库集成,以及将 React 前端与 Flask 集成,我们将涵盖所有内容。

到这部分结束时,你将牢固掌握 Flask 的核心概念,并朝着成为一名熟练的全栈 Web 开发者迈进。让我们共同踏上这段激动人心的旅程吧!

本部分包含以下章节:

  • 第八章, SQL 和数据建模

  • 第九章, API 开发与文档

  • 第十章, 整合 React 前端与 Flask 后端

  • 第十一章, 在 React-Flask 应用程序中获取和显示数据

  • 第十二章, 身份验证和授权

  • 第十三章, 错误处理

  • 第十四章, 模块化架构 – 蓝图的威力

  • 第十五章, Flask 单元测试

  • 第十六章, 容器化与 Flask 应用程序部署

第八章:SQL 和数据建模

到目前为止,我们已经探讨了 React,它是前端技术堆栈中的一个关键库。现在,我们将探索后端开发的世界,从结构化查询语言SQL)和数据建模开始。

SQL 和数据建模是任何后端开发人员的关键技能,在后端开发之旅中从这些技能开始将为你打下构建稳健和可扩展的 Web 应用的基础。SQL 是一种标准语言,用于管理关系数据库中的数据,这些数据库本质上用于存储结构化数据。

掌握 SQL 将帮助你编写优化的 SQL 查询,从而提高应用程序的性能并减少服务器负载。数据建模将帮助你设计一个反映应用程序将处理的数据的数据库模式。数据建模可以帮助你避免性能问题、可维护性问题以及其他在数据库工作中可能出现的常见问题。

我们将深入探讨关系数据库以及数据库表如何相互关联。我们将检查 SQL,这是许多数据库管理系统使用的标准语言,包括流行的开源关系数据库管理系统RDBMSPostgreSQL

对关系数据库有深入的理解,并了解数据库关系如何工作,将帮助你设计可扩展和可维护的数据结构。我们将不遗余力,从设置 PostgreSQL 开始,探索 Flask 应用程序如何与 PostgreSQL 通信。

我们还将深入讨论 SQLAlchemy 通过提供一个允许使用 Python 对象与数据库进行 SQL 交互的接口,来处理各种关系数据库方言的能力。SQLAlchemy 是一个工业级对象关系映射器,它为与各种关系数据库方言(包括 PostgreSQL)的交互提供了一个强大的接口。

SQLAlchemy 使得编写复杂的数据库查询和管理数据库事务变得更容易。我们将检查您如何提出数据模型,并将数据从 Flask 应用程序发送到数据库。

除了数据建模和 SQL,迁移也是后端开发的一个关键方面。我们将通过 Alembic 来检查迁移,作为跟踪和更新数据库结构的一种方式。Alembic 是一个迁移工具,它提供了一种可靠的方式来跟踪和更新数据库结构,对于 Python 开发人员来说是一个必不可少的工具。

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

  • 什么是关系数据模型?

  • 探索不同的数据库关系

  • 设置 PostgreSQL

  • 理解 Flask 应用程序的数据库概念

  • 理解 SQLAlchemy ORM 基础

  • 为演讲者会议 Web 应用程序建模数据

  • 从 Flask 应用程序向 PostgreSQL 数据库发送数据

  • 使用 Alembic 进行迁移

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter08

什么是关系数据模型?

关系数据模型是一种概念方法,用于将数据库表示为一组关系。大多数 Web 应用程序都是高度数据驱动的。开发者必须处理代码级别的数据存储,在数据结构的情况下,或者找到一种方法在关系型数据库管理系统(如 PostgreSQL 或 MySQL)中持久化存储数据。

在关系型数据库管理系统中,你可以将表格称为关系。因此,关系模型将数据表示为关系或表格的集合。进一步分解数据库结构,你将得到构成表格的行和列。然后,你有一个记录,它由行和列的组合组成。

让我们看看一个名为customers的假设表格,它以清晰的方式描述了典型表格的结构:

Id firstname lastname email phone
1 Joel Doe Joel@admin.com 404-228-5487
2 Edward Spinster Edward@admin.com 403-268-6486
3 Mabel Emmanuel Mabel@admin.com 402-248-4484

表 8.1 – 展示客户信息的表格

在前面的customers表格中,我们有五列和三行。表格中的每一行称为一个元组。列标题如Idfirstnamelastnameemailphone被称为属性或字段。在数据库中,表格被创建来高效地存储数据。每个表格通常代表一个业务实体或对象,例如演讲者、场地、主题、客户、产品、订单等。

为了清晰起见,业务实体代表我们打算在应用程序业务数据模型中封装的事物,包括所有规则、关系和能够在数据库中持久化的能力。在前面的表格中,我们有一个客户业务实体,具有以下属性 – Idfirstnamelastnameemailphone。通常情况下,你会在你的 Web 应用程序中拥有不止一个表格。

预计你需要能够使用主键在你的数据库中的不同表格或关系之间建立联系。主键是表格中整行唯一的标识符,指代一个或多个列。如果有多个列用于主键,那么主键列的集合被称为复合键。在我们的例子中,客户表中的Id可以设置为主键。

此外,数据关系中的另一个概念是外键。外键指的是另一个(外部)表格中的主键。外键用于映射表格或关系之间的关系。表格关系帮助你高效地存储需要存储的数据,并在需要时更容易访问相关数据。

数据库设计中存在许多关系——一对一一对多多对多。例如,数据库中的相关表可以帮助你找出客户下的订单,了解有多少会议参与者注册了每个主题,等等。

在下一节中,我们将广泛讨论数据库中的关系以及它们在 Web 应用程序架构设计中的应用。

探索不同的数据库关系

在客户端-服务器模型中,数据库位于基础设施的服务器端。数据库是任何生产级 Web 应用程序收集和存储应用程序数据的核心。理解数据库中存在的关系对于组织、管理和从数据库中检索有用数据至关重要。

如前所述,数据库中存在三种类型的关系——一对一一对多多对多。我们将从深入研究一对一关系的概念开始。

一对一(1:1)关系

speakersspeakerExtras

图 8.1 – 显示一对一关系的实体关系图

图 8.1 – 显示一对一关系的实体关系图

前面的图示展示了speakers表和speakerExtras表。speakers表详细说明了演讲者对象的基本必要信息,而speakerExtras表则展示了可以添加到演讲者信息中的可选信息。speakers表中的speaker_id是主键,可以用来唯一标识一个演讲者记录。

speakerExtras表中,增加了一个名为speaker_id的列。speakerExtras表中的这个speaker_id列是一个具有唯一属性的外键。这个speaker_id列被用作对speakers表中的speaker_id的引用键,以在speakerExtras表和speakers表之间形成一个一对一关系。

大多数情况下,一对一关系的数据模型实际上可以合并。合并一对多关系的数据模型通常意味着将表合并为一个单一的表。这通常是在两个表有强烈的联系,且表中的数据并不复杂到需要单独的表时进行的。

然而,在某些情况下,数据库设计的要求可能需要一个单独的表来处理可能的可选信息,因此,而不是在表中留有空列,你可以创建一个不同的表来处理这些可选信息,以获得更好的数据库性能。

让我们来看看另一种数据库关系,一对多。

一对多(1:M)关系

数据模型中的一对多关系解释了两个表之间的关系,其中一个表中的一行可以引用另一个表中的一行或多行。一对多关系是一种父子关系,其中子记录只能有一个父记录。

大多数时候,父记录在另一个表的行中有多于一个子记录。然而,在现实场景中,我们可能会遇到一个父记录没有任何子记录的情况。没有子记录的父记录意味着子表中的外键列将为该父记录为空。

例如,考虑一个商店的数据库设计,每个客户可以有多个订单。如果一个新客户记录被创建,但还没有下订单,那么将有一个没有对应订单记录的客户记录。在这种情况下,客户记录是父记录,订单记录是子记录。

此外,在某些情况下,一对多关系也允许另一个表中的单个记录。存在一些情况,一对多关系可以被限制为功能上作为一对一关系。例如,你可以在子表的外键列上添加唯一约束,确保父表中的每个记录最多与子表中的一个记录相关联。

这将有效地将关系约束为作为一对一关系,尽管底层的数据模型是一对多关系。这种一对多关系几乎类似于一对一关系,但有一个唯一约束的细微差别。为什么这种一对多关系在数据建模中很重要?

就像其他数据库关系一样,一对多关系是关系数据库设计的基本构建块,对于组织、管理和分析复杂数据结构至关重要。一对多关系通过确保子表中的每个记录都与父表中的一个有效记录相关联,强制执行数据的引用完整性。这种方法有助于防止由孤立记录或对不存在记录的引用引起的数据不一致性和错误。

让我们来看一个一对多关系的例子。想象一下,在我们的数据库中有customers(客户)和orders(订单)表。一个客户在一段时间内可能有很多 1:M 的订单。

一个企业可能希望保留这个记录。例如,客户往往有不同的订单;特定的orderID不能属于多个客户。每个客户的订单都是唯一的。以下图展示了客户和订单之间的一对多关系。

图 8.2 – 显示一对多关系的实体关系图

图 8.2 – 显示一对多关系的实体关系图

在前面的图中,orders 表中的 customer_id 代表外键,是两个实体——customersorders 之间的主要链接因素。如果你想了解某个特定客户有多少订单,你只需要编写一个 JOIN 查询,查找 customersorders 表,并检查 orders 表中作为参考键的 customer_id 的出现情况。

在你的网络应用程序开发数据库设计中,你将遇到更多这种数据关系,因为它是最常用的。接下来,我们将看看另一种数据库关系,多对多。

多对多(M:M)关系

数据建模中的 多对多关系 简单来说是指一个表中的多条记录与另一个表中的多条记录相关联的数据关系。以会议数据库设计为例,你可能会有以下场景:

  • 一个参与者可以注册多个会议

  • 一个会议可以有许多注册的参与者

这意味着一个参与者可以参加多个会议,一个会议也有许多参与者。因此,会议参与者和会议之间存在多对多关系。

有趣的是,与一对一关系和一对多关系不同,你不能只用两个表来建模多对多关系。在多对多关系建模中,需要一个第三张连接表,其中包含两个表的主键值添加到连接表中。

让我们检查会议参与者和他们注册的会议的多对多关系的 实体关系图ER):

图 8.3 – 显示多对多关系的实体关系图

图 8.3 – 显示多对多关系的实体关系图

让我们分解前面的 ER 图,以便更好地理解多对多关系。我们有三张表,如上图所示——attendeesenrollemnt_tblconf_sessions

attendees 表包含会议参与者的记录。同样,conf_sessions 表包含会议的记录。然后你有一个连接表 enrollment_tbl,它在技术上与两个表各自形成一个一对一关系。

enrollment_tbl 包含参与者和 conf_sessions 表的主键作为外键。通过 enrollment_tbl,我们可以查询参与者和 conf_sessions 表的相关记录。在这种情况下,我们可以访问特定参与者参加的所有会议。

接下来,我们将深入探讨数据库设置,使用 SQLAlchemy 和 Alembic 来处理网络应用程序的数据库部分。

设置 PostgreSQL、SQLAlchemy 和 Alembic

我们将首先设置后端数据库所需的数据库工具。PostgreSQL 是一个流行的免费开源关系型数据库管理系统。它与存在的其他 SQL 数据库方言类似 – 例如,MySQL、MariaDB 和 Oracle。此数据库可以用于存储任何 Web 应用的数据。PostgreSQL 具有企业级功能,使其强大、可扩展和可靠。

我们还将设置 SQLAlchemy,一个用于读取插入更新删除的工具,而不是直接编写 SQL 查询。最后,在本节中,我们将设置 Alembic 以处理数据库迁移。

设置 PostgreSQL

要在本地机器上开始使用 PostgreSQL,请从 www.postgresql.org/download/ 下载它并选择您的操作系统包。

图 8.4 – 一张显示 PostgreSQL 下载页面的图表

图 8.4 – 一张显示 PostgreSQL 下载页面的图表

按照说明运行安装向导以设置数据库。在 PostgreSQL 安装过程中,您将被提示输入超级用户密码。非常重要,您需要保留您输入的超级用户密码,因为这将用于登录默认的 PostgreSQL 数据库。一旦安装完成,请登录到数据库。

打开您机器上的命令终端并输入以下命令:

$     psql -d postgres -U postgres

psql 从终端调用与 PostgreSQL 的连接。然后,您可以使用 -d 选项选择您想要访问的数据库,并使用 -U 选择具有数据库访问权限的用户。

注意

如果命令终端回复“psql不是内部或外部命令”,您可能需要将 Postgres bin/ (C:\Program Files\PostgreSQL\14\bin) 添加到环境变量的系统路径中。

然后,输入安装期间为超级用户创建的密码。

以下屏幕截图显示了尝试登录 PostgreSQL 时的终端。

图 8.5 – 一张显示如何从终端访问 PostgreSQL 的屏幕截图

图 8.5 – 一张显示如何从终端访问 PostgreSQL 的屏幕截图

总是创建一个与安装期间创建的超级用户不同的新用户在 Postgres 上是一个最佳实践。现在您已使用默认的 Postgres 用户登录,让我们创建另一个名为 admin 的用户角色,并为其分配密码 admin123

CREATE ROLE admin WITH LOGIN PASSWORD 'admin123';

我们现在将给新用户 admin 授予权限以创建数据库:

ALTER ROLE admin CREATEDB;

在设置好所有这些之后,从 Postgres 用户注销并作为 admin 登录。要注销,请输入 \q 命令。

现在,以 admin 用户身份登录:

$    psql -d postgres -U admin

输入管理员密码并登录。登录后,创建一个名为 bizza 的新数据库:

$    CREATE DATABASE bizza;

要连接到新创建的数据库,请运行以下命令:

$    \connect bizza

以下屏幕截图显示了如何在 PostgreSQL 中连接以创建数据库:

图 8.6 – 显示连接到 bizza 数据库的屏幕截图

图 8.6 – 显示连接到 bizza 数据库的屏幕截图

欢呼!我们已经成功设置了 PostgreSQL。现在,让我们深入了解设置 SQLAlchemy。

注意

角色名称,admin,和密码,admin123,可以是您想要的任何名称;它们不必按照建议命名。

设置 SQLAlchemy

为 Flask 应用程序设置 SQLAlchemy 简单直接。要在您的项目中设置 SQLAlchemy,请在终端中运行以下命令,直接在您的项目 root 目录内。在运行安装命令之前,请确保您的虚拟环境已激活:

pip install SQLAlchemy

注意

SQLAlchemy 是一个 Python SQL 工具包和 ORM 库,它提供了一套高级 API,用于与关系数据库交互。

此外,我们将使用一个名为 Flask-SQLAlchemy 的 Flask 扩展。让我们在终端中运行以下命令来安装它:

pip install flask-sqlalchemy

注意

Flask-SQLAlchemy 扩展为 SQLAlchemy 提供了一个包装器,使得在 Flask 应用程序中使用 SQLAlchemy 更加容易。Flask-SQLAlchemy 提供了额外的功能,例如自动会话处理、与 Flask 的应用程序上下文集成以及支持 Flask 特定的配置选项。

在本章的后续部分,我们将深入讨论 SQLAlchemy,理解 SQLAlchemy ORM 基础

简而言之,让我们演示如何无缝地将 SQLAlchemy 集成到您的 Flask 应用程序中。集成 SQLAlchemy 的过程如下:

  1. 创建一个新的目录,并将其命名为 sqlalchemy

  2. 然后,按照 第一章使用 Flask 设置开发环境 部分的 Flask 项目设置进行操作。第一章。

  3. 确保您的开发虚拟环境已激活。

  4. 在终端中运行以下命令:

    pip install flaskpip install flask-sqlalchemy
    
  5. sqlalchemy 目录中创建一个名为 app.py 的文件,并添加以下代码片段:

    from flask import Flask, render_templatefrom flask_sqlalchemy import SQLAlchemyapp = Flask(__name__, template_folder='templates')app.config["SQLALCHEMY_DATABASE_URI"] =    "sqlite:///bizza.db"db = SQLAlchemy(app)@app.route('/users')def get_users():    users = User.query.all()    return render_template('users.html', users=users)class User(db.Model):__tablename__= "users"    id = db.Column(db.Integer, primary_key=True)    username = db.Column(db.String(55), unique=True)    name = db.Column(db.String(55), unique=False)    email = db.Column(db.String(100), unique=True)    def __repr__(self):        return '<User {}>'.format(self.username)
    

    上述代码设置了一个基本的 Flask 应用程序,该应用程序使用 SQLAlchemy 进行数据库操作。让我们解释一下上述代码中发生了什么:

    • from flask import Flask: 这从 Flask 包中导入 Flask 类。

    • from flask_sqlalchemy import SQLAlchemy: 这从 Flask-SQLAlchemy 包中导入 SQLAlchemy 类。

    • app = Flask(__name__, template_folder="templates"): 这将创建一个新的 Flask 应用程序实例。template_folder 被添加以确保 Flask 能够找到 templates 目录。App.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///bizza.db": 这设置了 SQLite 数据库的数据库 URI。在这种情况下,数据库文件名为 bizza.db,位于 Flask 应用程序文件 app.pyinstance 目录中。我们在这个例子中使用 SQLite。您可以使用任何数据库方言 – MySQL、Oracle、PostgreSQL 等。

    • db = SQLAlchemy(app): 这将创建一个新的 SQLAlchemy 实例,并将其绑定到 Flask 应用程序。

    • 然后,我们有一个 Flask 路由处理器,它响应/users URL 端点。当用户访问/users URL 时,Flask 将执行get_users函数,从数据库中检索所有用户,并渲染users.html模板,将用户变量传递给模板以进行显示。

    • 最后,我们有包含四个列的User模型——idusernamenameemail。每一列代表相应数据库表中的一个字段,并指定其数据类型和可选约束,如唯一性。这代表了我们在 Flask 中使用 SQLAlchemy 定义我们的模型作为 Python 类,指定它们的属性和关系,并执行数据库操作的方式。

    现在,让我们在sqlalchemy目录的终端中创建users表。

  6. 在终端中输入以下命令:

    users table with sample data with python sample_data.py. The sample_data.py is located inside the project root directory in GitHub.
    
  7. 然后,输入flask run命令,并访问http://127.0.0.1:5000/users来查看数据库中的用户列表。

    以下是一个屏幕截图,展示了你如何使用 SQLAlchemy 定义和检索用户数据。

图 8.7 – 展示如何使用 SQLAlchemy 进行数据库操作的屏幕截图

图 8.7 – 展示如何使用 SQLAlchemy 进行数据库操作的屏幕截图

通过这种设置,我们展示了你如何定义 SQLAlchemy 模型,并使用 Python 对象在 Flask 应用程序中与数据库交互。

接下来,我们将深入了解如何设置 Alembic,这是一个数据库迁移工具。

设置 Alembic

pip install alembic或从另一个迁移包Flask-Migrate安装它,使用pip install flask-migrateFlask-Migrate依赖于 Alembic 进行数据库迁移,我们将在这里将其作为数据库迁移的首选工具。

Flask-Migrate迁移工具允许你跟踪数据库的变化。迁移工具带有使用迁移脚本管理数据库结构和操作的能力。迁移工具 Alembic 的本质是简化迁移 SQL 脚本的自动生成。

在本章的最后部分,我们将深入讨论迁移,并提供一些实现细节。接下来,我们将讨论如何从 Flask 网络应用程序与数据库交互。

理解 Flask 应用程序的数据库概念

现在我们已经设置了数据库并使用终端连接到它,对一些数据库概念有一个扎实的理解对于能够设置能够收集、存储和检索用户数据的后端服务至关重要。

大多数现代网络应用都有一个数据库来存储用户数据。作为一名全栈网络开发者,你的部分责任是能够设置后端服务来收集、存储和检索用户数据。我们将在不久的将来深入探讨从 Flask 应用程序与数据库的交互,但在那之前,有一些数据库概念你需要理解。

让我们快速概述以下数据库概念。

关系型数据库管理系统(RDBMS)

在生产环境中与数据库工作时,您需要一个关系型数据库管理系统(RDBMS)。RDBMS 是一种软件包,允许您与数据库交互。RDBMS 软件允许您定义、操作、检索和管理数据库中的数据。

您拥有数据库功能,可以用来管理数据库中的数据和数据库本身的架构。市场上有很多不同的关系型数据库管理系统(RDBMS)版本——MySQL、PostgreSQL、Oracle、MsSQL 和 MariaDB——但我们将要使用的是称为 PostgreSQL 的。

让我们来看看另一个与数据库交互所需的相关工具,称为数据库应用程序编程接口DB-API)。

DB-APIs

DB-API 是一个应用程序编程接口API),它允许编程语言或 Web 服务器框架与数据库服务器之间进行通信,使用 TCP/IP 等协议。Python 是一种我们与 Flask 框架一起使用的语言,它使用一个名为 psycopg2 的库,允许 Python 模块与 PostgreSQL 数据库服务器交互。

DB-API 充当数据库适配器,一个 Web 接口,允许 Web 服务器编程语言(在我们的例子中是 Python)在数据库上运行 SQL 查询,使用 psycopg2 作为 DB-API 的库。

对于每种编程语言或服务器框架以及我们拥有的 SQL 数据库的各种方言,都存在不同类型的 DB-API。例如,我们有 psycopg2 用于 PostgreSQL,mysql-pythonPyMySQL用于 MySQL,adodbapipymssql用于 MS SQL Server,以及mxODBCpyodb用于 Oracle。

PEP 248 DB-API 版本 1.0 的文档指定了 DB-API 的目标:

“这个 API 被定义为鼓励用于访问数据库的 Python 模块之间的相似性。通过这样做,我们希望实现一致性,从而使得模块更容易理解,代码在数据库之间更通用,并且从 Python 扩展数据库连接的范围。” (www.python.org/dev/peps/pep-0248/)

从本质上讲,DB-API 提供了一种低级方式来编写 SQL 查询语句,使得与不同数据库的交互始终简单且容易。

客户端-服务器模型交互

客户端-服务器模型交互是一种通信范式,其中客户端通过网络从服务器请求资源或服务。在这个模型中,客户端向服务器发起请求,服务器响应请求提供所需的服务或数据,从而形成一个客户端-服务器交互,这是各种应用程序和网络通信的基础。

在客户端-服务器模型交互中,浏览器充当客户端,Web 服务器充当服务器。当最终用户从服务器请求资源或网页时,请求通过浏览器(客户端端)在 Web 服务器(服务器端)上通过 HTTP 协议进行。这种相同的客户端-服务器架构也可以用于最终用户请求包含数据库数据的资源或网页。

当浏览器发起一个需要从数据库获取数据的请求时,Web 服务器接收请求并与数据库服务器建立连接。Web 服务器成为客户端,数据库服务器成为服务器,从而完成从后端基础设施的客户端-服务器模型(www.python.org/dev/peps/pep-0248/)。

在本章的后面部分,我们将创建表格以全面了解如何与数据库交互。以下图示展示了客户端-服务器交互。

图 8.8 – 展示客户端-服务器交互

图 8.8 – 展示客户端-服务器交互的图示

在前面的图示中,客户端通过向服务器发送请求来启动交互。在基于 Web 的应用程序中,请求可以是 HTTP 请求。服务器接收请求,对其进行解释,并执行必要的操作以生成响应。

一旦响应准备就绪,服务器将其发送回客户端。客户端接收响应并对其进行处理,利用提供的信息来完成客户端-服务器交互。接下来,我们将深入了解 ORM 基础知识,了解您如何使用 Python 类与关系型数据库管理系统(RDBMS)交互。

理解 SQLAlchemy ORM 基础知识

SQLAlchemy 为开发者提供了完全使用 Python 代码来创建、读取、更新和删除表的能力。SQLAlchemy 是 Python SQL 工具包和 ORM,允许应用程序开发者与数据库交互,而无需编写直接的 SQL 语句。

由于 SQLAlchemy 是一个 ORM 库,Python 类被映射到表上,而这些类的实例被映射到关系数据库中的表行。然后,您就可以使用 Python 面向对象编程代码在应用程序中执行数据库 SQL 创建读取更新删除CRUD)操作以及其他必要的操作。

SQLAlchemy 的 ORM 功能为 Python 开发者提供了利用函数调用直接生成 SQL 语句的能力。以这种方式思考数据库,开发者能够解耦对象模型和数据库模式,从而实现更灵活的数据库结构、优雅的 SQL 语句以及可以与不同类型的数据库方言交互的 Python 代码。

SQLAlchemy 不介意你使用哪种数据库系统。SQLAlchemy 使你更容易在不更改代码的情况下从一个数据库切换到另一个数据库。SQLAlchemy 无疑是一个强大的工具,它简化了 Web 开发的过程,使其更快、更高效。

由于 SQLAlchemy 的用户友好界面,开发者可以轻松创建数据库模型,并从他们的 Flask 应用程序中与之交互。此外,SQLAlchemy 旨在减少将错误引入代码库的机会,这对于大规模应用程序尤为重要。

提供了一个强大的框架来处理数据库交互,SQLAlchemy 允许开发者专注于构建应用程序的逻辑,而无需担心底层的数据库操作。这导致了一个更流畅的开发过程和更好的整体代码质量。

最后,SQLAlchemy ORM 库自带自动缓存功能。SQLAlchemy 在对象首次加载后缓存集合和对象之间的引用。这无疑提高了性能,并防止你在每次调用时都向数据库发送 SQL 查询。

在 SQLAlchemy 中,有三个主要的抽象层。这些层是引擎、方言和连接池。这个三重奏描述了你是如何选择与数据库交互的。

图 8.9 – SQLAlchemy 的抽象层

图 8.9 – SQLAlchemy 的抽象层

让我们深入挖掘 SQLAlchemy 的抽象层,以更好地理解它们在 SQLAlchemy 中的使用。

SQLAlchemy 引擎

一个 SQLAlchemy 应用程序从创建一个引擎开始。在 SQLAlchemy 能够连接和与数据库交互之前,你需要创建一个引擎。引擎是 SQLAlchemy 中抽象层的最低层,它的工作方式与我们在使用 Psycopg2 与数据库交互时几乎相同。

让我们创建一个引擎来启动数据库连接和交互。

以下代码片段展示了使用 SQLAlchemy 的create_engine进行 PostgreSQL 数据库连接。SQLAlchemy 引擎连接到 PostgreSQL 数据库,并在 Python 中执行 SQL 查询:

from sqlalchemy import create_engine# create the engine with the database URL
engine = create_engine
    ("postgresql://admin:admin123@localhost:5432/bizza")
# create a connection to the database
conn = engine.connect()
# execute a SQL query
result = conn.execute('SELECT * FROM speakers')
# loop through the result set and print the values
for row in result:
    print(row)
# close the result set and connection
result.close()
conn.close()

这就是前面代码中发生的事情:

  • sqlalchemy库中导入create_engine函数:

    from sqlalchemy import create_engine
    
  • 然后,在 SQLAlchemy 中调用create_engine()方法并将其分配给引擎实例。接着,你传入一个连接 URL 字符串,该字符串指定了后端服务器细节,例如用于连接的数据库的nameusernamepasswordhostport

    engine = create_engine(    'postgresql://admin:admin123@localhost:5432/bizza'    )Database name-bizzaUsername-adminPasswordadmin123
    
  • 为当前数据库创建一个连接池,并且这个连接池在整个应用程序生命周期中只创建一次:

    connection = engine.connect()
    
  • 我们使用建立的连接对象执行 SQL 语句:

    result= connection.execute('SELECT * FROM speakers')
    
  • 最后,我们遍历查询返回的结果集并打印值。

使用result.close()conn.close()方法关闭结果集和连接,以释放资源并防止内存泄漏是很重要的。

SQLAlchemy 连接池

连接池是软件工程中对象池设计范式的实现,其中连接在需要时被重用而不是每次都需要创建。在 SQLAlchemy 中,create_engine()方法的通常使用会生成一个连接池对象,该对象在创建后会被重用,在执行数据库事务时后续连接会使用它。

这种连接池设计模式有助于提高性能并更好地管理应用程序的并发连接。连接池的默认设置可以根据需要调整,以更有效地服务于商业 Web 应用程序的最终用户。

要更新连接池,将连接池参数添加到create_engine()中:

engine = create_engine('postgresql://user:admin123@localhost/bizza', pool_size=20, max_overflow=0)

以下项目符号列表显示了如何调整连接池参数以优化与 SQLAlchemy 连接池一起工作的性能。

  • 使用pool_size,你可以设置连接池可以处理的连接数。

  • 使用max_overflow,你可以指定连接池支持的溢出连接数。

  • 使用pool_recycle,你可以配置连接池中连接的最大年龄(以秒为单位)。

  • 使用pool_timeout,你可以指定应用程序在放弃从连接池获取连接之前需要等待多少秒。

SQLAlchemy 方言

使用 SQLAlchemy 的主要好处之一是,你可以使用不同类型的 DB-API 实现(如 Psycopg2)和关系数据库,而无需更改你的 Python 代码以适应它们的内部工作方式。

因此,方言是 SQLAlchemy 中的一种系统,它使得与不同数据库的交互成为可能。你可以在开发环境中使用 SQLite,并决定在生产环境中使用 MySQL 或 PostgreSQL,而无需更改现有的代码库。

以下是一些关系数据库方言的示例:

  • PostgreSQL

  • MySQL 和 MariaDB

  • SQLite

  • Oracle

  • Microsoft SQL Server

为了使用这些数据库方言,你必须在你的应用程序中安装适当的 DB-API 驱动程序。当与 PostgreSQL 连接时,SQLAlchemy 使用 Psycopg2 作为 DB-API 规范的实现。Psycopg2 是 Python 的 PostgreSQL 适配器。Psycopg2 提供了一个简单高效的方法,使用 Python 代码与 PostgreSQL 数据库进行通信。

我们已经讨论了 SQLAlchemy 方言的工作原理,但那些方言中使用的数据类型是什么呢?SQLAlchemy 提供了多种数据类型,现在我们将探讨其中的一些。之后,我们将讨论如何将它们映射到 Python 类中。

SQLAlchemy 数据类型 – 表与类之间的映射

SQLAlchemy 为我们可能选择在应用程序中使用的任何底层关系数据库提供了高级抽象。SQLAlchemy 对我们熟悉的常见传统数据库提供了数据类型支持。例如,日期、时间、字符串、整数和布尔值都得到了 SQLAlchemy 的良好支持。

SQLAlchemy 数据类型允许精确的数据存储和检索。数据库表中的每一列都有一个特定的数据类型,它定义了可以存储在该列中的数据类型,例如整数、字符串或日期。SQLAlchemy 提供了一系列数据类型,可用于定义数据库表中的列,包括 NumericStringDateTimeBoolean

通过使用这些数据类型,我们可以确保我们的数据被准确且高效地存储,从而实现更快的检索和更可靠的性能。此外,表和类之间的映射很重要,因为它允许我们以面向对象的方式处理数据库表。

SQLAlchemy 提供了一个 ORM 系统,允许我们将数据库表映射到 Python 类。这意味着我们可以使用 Python 对象和方法来处理数据库数据,这使得构建和维护我们的 Web 应用程序更加容易。让我们看看 SQLAlchemy 数据类型是如何与 Python 类和实例属性映射的,分别用于表和列的创建。

本书中的 Flask 应用程序将使用这种方法从 Python 类定义表:

class Speaker(Base):__tablename__ = 'speakers'
speaker_ id=Column(Integer, primary_key=True)
first_name=Column(String)
last_name=Column(String)
email=Column(String)
created_at = Column(DateTime(timezone=True),
    server_default=datetime.now)
updated_at = ColumnDateTime(timezone=True),
    default=datetime.now, onupdate=datetime.now)

在前面的代码片段中,我们定义了一个名为 Speaker 的类,它具有一些属性。让我们更深入地了解这段代码:

  • __tablename__ 属性被设置为 speakers,表示 Speaker 类的实例应该存储在数据库中的 speakers 表中。

  • speaker_id 属性指定这是表中的 primary_key,并且具有 Integer 类型。

  • first_name 属性指定表中的一个列具有名称 first_name,类型为 String

  • last_name 属性指定表中的一个列具有名称 last_name,类型为 String

  • email 属性指定表中的一个列具有名称 email,类型为 String

  • created_atupdated_at 指定了表中具有所述名称的列,其类型为 date。然而,方法被传递以获取当前的 timezone

定义了类和实例后,我们可以利用 SQLAlchemy 的内部 API 和配置的方言,将类属性映射到数据库的相应原生结构。例如,如果我们类中有字符串数据类型,SQLAlchemy 将将其映射到 PostgreSQL 中的 varchar 列。

这确保了在类中使用的数据类型被正确地转换为适当的数据库列类型,从而使得 Python 代码和数据库之间的通信无缝。

为了构建一个健壮的演讲者会议 Web 应用程序,我们需要正确地建模数据。SQLAlchemy 为我们提供了一个强大的 ORM,这使得将我们的数据库表映射到类变得容易。

然后,我们可以定义这些类之间的属性和关系,这使得处理复杂的数据结构变得容易。在下一节中,我们将探讨如何使用 SQLAlchemy 为我们的演讲者会议 Web 应用程序建模数据。

为演讲者会议 Web 应用程序建模数据

数据建模是创建数据结构的概念性、逻辑性和视觉表示的过程,以便理解、分析和管理复杂的信息系统。数据建模涉及识别将在系统中表示的实体(对象、概念或事物),并定义它们的属性以及与其他实体之间的关系。

数据建模的主要目的是创建一个清晰且精确的数据表示,这些数据将由信息系统存储、处理和管理。在 Web 应用程序中,一个设计良好的数据模型可以确保 Web 应用程序具有可扩展性、效率,并满足其用户的需求。

让我们快速检查在设计会议演讲者 Web 应用程序的数据模型时需要考虑的一些最佳实践:

  • 识别实体:首先根据系统需求识别将在 Web 应用程序中表示的实体或对象,例如用户、演讲者、演讲、日程、与会者、会议、场地和赞助商。

  • 定义关系:确定这些实体之间的关系,例如一对一、一对多或多对多关系。例如,一个演讲者可以在会议上进行多次演讲,但一次演讲只能由一个演讲者进行。

  • 确定属性:定义每个实体的属性或特性——例如,一个演讲者可能具有姓名、联系地址、传记或演讲主题等属性。

  • 考虑性能和可扩展性:设计数据模型以优化性能和可扩展性,例如使用索引、缓存或反规范化以减少查询时间和提高系统响应。

  • 规划数据存储:考虑可用的数据存储选项,例如关系数据库、NoSQL 数据库或平面文件,并根据 Web 应用程序的需求选择合适的选项。在这种情况下,选择了 PostgreSQL。

  • 数据安全:设计数据模型以确保数据安全,例如使用加密或访问控制来保护敏感数据。例如,当用户登录时,他们输入的密码可以被散列并与存储的散列进行比较以验证其身份。

  • 考虑未来变化:为 Web 应用程序的未来变化和更新做出规划,例如添加新的实体或属性,或修改现有关系。

你是否准备好通过建立和组织数据模型来开始开发 Bizza 项目的后端?Bizza 项目旨在创建一个以数据库驱动为主的完整栈网络应用,专注于会议演讲者。

在这个数据模型中,我们将更详细地查看数据库、表以及表之间的关系。

bizza 数据库有以下表和关系:

  • usersuserExtras,用户和角色之间的多对多关系,usersspeakers 之间的一对一关系,以及 usersattendees 之间的一对多关系。

  • users 表 —— usersuserExtras 之间的一对一关系。

  • users 表 —— usersroles 之间的多对多关系。

  • Speaker 模型将与 users 模型相关联,以便将每个演讲者与他们的用户账户关联起来。与其他表存在以下关系——speakerssessions 之间的一对多关系,usersspeakers 之间的一对一关系,speakersschedules 之间的一对多关系,以及 speakerspresentations 之间的一对多关系。

  • Presentation 模型将与 Speaker 模型相关联,以便将每个演示与其演讲者关联起来。与其他表存在以下关系——presentationsspeakers 之间的多对一关系,以及 presentationsschedules 之间的一对多关系。

  • Schedule 模型将与 Presentation 模型相关联,以便将每个演示与其日期和时间关联起来。与其他表存在以下关系——schedulespresentations 之间的多对一关系,schedulessessions 之间的一对多关系,schedulesvenues 之间的多对一关系,以及 schedulesattendees 之间的多对多关系。

  • attendeesschedules

  • Session 模型将与 Presentation 模型相关联,以便将每个会议与它的演示关联起来。与其他表存在以下关系——sessionsschedules 之间的多对一关系,以及 sessionspresentations 之间的一对一关系。

  • venuesschedules

  • 赞助商模型:此模型将包括有关每个会议赞助商的数据,例如他们的名称、标志、网站以及任何其他相关细节。

现在我们已经定义了我们的数据模型,在下一节中,我们将探讨如何通过几个简单的步骤无缝地将数据发送到 PostgreSQL 数据库。

从 Flask 应用程序向 PostgreSQL 数据库发送数据

与数据库交互是大多数网络应用的一个常见方面。从 Flask 应用程序向 PostgreSQL 数据库发送数据简单直接。一个网络应用如果没有存储和从数据库检索数据的能力,将是不完整的。

将数据发送到数据库的第一步是确保 Flask 应用和数据库之间存在连接。这涉及到安装所需的库,并确保您在一个虚拟环境中工作,以包含您的安装并防止其他库的干扰导致意外发生。

让我们利用在 第一章使用 Flask 设置开发环境 部分中创建的 bizza/backend 目录。如果您还没有创建,可以创建一个。安装并激活虚拟环境。

要创建一个虚拟环境,请在终端中打开您的项目根目录,并添加以下命令:

python –m venv venv

现在,让我们激活虚拟环境:

  • 对于 Windows 用户

    Venv\Scripts\activate
    
  • 对于 Linux/Mac 用户

    source venv/bin/activate
    

要从 Python 类创建数据库表,您需要安装 flask-sqlalchemy。我们已经安装了它。如果没有安装,请输入以下命令:

pip install flask-sqlalchemy

在 Flask 应用模块中——即 app.py ——更新 app.py 的内容,使用以下代码在 bizza 数据库中创建 users 表。在本节中,我们只创建 users 表以演示如何从 Flask 应用向 PostgreSQL 发送数据。

其他模型将在 GitHub 上展示,并在书中稍后介绍,直到我们完成演讲者会议网络应用 Bizza 的完整实现:

# Import flaskfrom flask import Flask
# Import datetime
from datetime import datetime
# Import SQLAlchemy
from flask_sqlalchemy import SQLAlchemy
# Create a Flask instance
app = Flask(__name__)
# Add the PostgreSQL database
app.config['SQLALCHEMY_DATABASE_URI'] =
    'postgresql://<db_username>:<db_password>@localhost:
    5432/<database_name>'
# Initialize the database
db = SQLAlchemy(app)
# User model definition
class User(db.Model):
    __tablename__ = 'users'
    user_id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(100), unique=True,
        nullable=False)
    email = db.Column(db.String(120), unique=True,
        nullable=False)
    password = db.Column(db.String(128), nullable=False)
    first_name = db.Column(db.String(100), nullable=False)
    last_name = db.Column(db.String(100), nullable=False)
    roles = db.Column(db.String(100))
    is_active = db.Column(db.Boolean, default=True)
    is_superuser = db.Column(db.Boolean, default=False)
    created_at = db.Column(db.DateTime,
        default=datetime.utcnow())
    updated_at = db.Column(db.DateTime,
        default=datetime.utcnow,
        onupdate=datetime.utcnow())
    def __repr__(self):
        return '<User %r>' % self.username

让我们深入探讨前面用于在数据库中创建 users 表的 Flask 应用代码片段:

  • from flask import Flask 从 Flask 模块导入 Flask 类。

  • from flask_sqlalchemy import SQLAlchemyflask_sqlalchemy 模块导入 SQLAlchemy 类。

  • app = Flask(__name__) 创建了一个名为 appFlask 实例。

  • app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://<db_username>:<db_password>@localhost:5432/<database_name>'app.config[] 字典定义了数据库的路径,其中设置了 db usernamepassword

  • db = SQLAlchemy(app) 使用 Flask 应用作为参数初始化 SQLAlchemy 类的一个实例。

  • 然后,我们开始定义 Usermodel 类,使用 class User(db.Model):User(db.Model) 类定义了一个继承自 db.Model 类的 User 模型。

  • __tablename__ = 'users' 允许我们为表指定一个自定义名称——一个与 Class User 模型对应的表名 users。如果没有指定,将使用小写类名(user)。

  • 然后,我们为表添加列。每个列都是 SQLAlchemy 的 Column 子类的对象:

    • User_id = db.Column(db.Integer, primary_key=True, nullable=False) 定义了一个名为 user_id 的主键列,其数据类型为 Integer

    • username = db.Column(db.String(50), unique=True, nullable=False) 定义了一个名为 username 的列,其数据类型为 String,并强制要求它必须是唯一的且不可为空。

    • email = db.Column(db.String(120), unique=True, nullable=False) 定义了一个名为 email 的列,其数据类型为 String,并强制要求它必须是唯一的且不可为空。

    • password = db.Column(db.String(256), nullable=False) 定义了一个名为 password 的列,其数据类型为 String,并强制要求它不可为空。

    • first_name = db.Column(db.String(50), nullable=False) 定义了一个名为 first_name 的列,其数据类型为 String,并强制要求它不可为空。

    • last_name = db.Column(db.String(50), nullable=False) 定义了一个名为 last_name 的列,其数据类型为 String,并强制要求它不可为空。

    • is_active = db.Column(db.Boolean, default=True) 定义了一个名为 is_active 的列,其数据类型为 Boolean,并设置默认值为 True

    • is_superuser = db.Column(db.Boolean, default=False) 定义了一个名为 is_superuser 的列,其数据类型为 Boolean,并设置默认值为 False

  • def __repr__(self) 定义了一个返回 User 对象字符串表示的方法。

  • return '<User %r>' % self.username 返回一个包含 User 对象用户名的字符串。

既然我们已经定义了 User 类模型,现在是时候在数据库中创建 users 表并传入一些数据了:

  1. 在项目根目录中打开一个终端,并输入 flask shell 命令。

图 8.10 – 显示 Flask shell 的屏幕截图

图 8.10 – 显示 Flask shell 的屏幕截图

  1. 输入 from app import db 以连接到数据库对象。

  2. 输入 db.create_all() 以创建 users 表。

图 8.11 – 显示 db.create_all() 的屏幕截图

图 8.11 – 显示 db.create_all() 的屏幕截图

  1. 输入 from app import User 以获取对 User 模型的访问权限。

  2. 使用 flask shell 终端向表中添加用户数据:

    user1=User(username='john',first_name='John',last_name='Stewart',email='admin@admin.com',password='password')
    
  3. 添加 db.session.add(user1)db.session.commit() 以将用户添加到数据库会话中,并在数据库中提交以持久化数据。

  4. 输入 User.query.all() 以查看带有添加信息的 users 表。

图 8.12 – 显示插入数据的 Flask shell 的屏幕截图

图 8.12 – 显示插入数据的 Flask shell 的屏幕截图

接下来,我们将讨论如何使用迁移添加和跟踪数据库结构的更改。

使用 Alembic 进行迁移

如前所述,Alembic 是一个迁移工具,它使 Flask 开发者跟踪数据库更改变得不那么复杂。由于我们预计数据模型会发生变化,我们需要一个工具来跟踪这些更改并确保它们在数据库中得到更新。

这与我们在使用 Git 进行源代码版本控制的方式类似。同样的方法也适用于数据库模式变更管理,我们在此保持数据库结构的增量可逆更改。在与数据库表一起工作时,您可能想要添加或删除列,从而在 Python 模型类中更改模式。

完成此操作后,您需要一个自动过程来确保您的数据库表和数据模型状态保持同步。Alembic 优雅地处理模式迁移,并确保 Python 文件中的数据模型与数据库结构相同。

让我们看看如何在 Flask 应用程序中实现迁移。我们将添加另一个模型,并使用迁移来跟踪对数据库所做的更改。在这里,我们添加了Speaker模型类。

如在设置 Alembic部分所述,使用pip安装Flask-Migrate

pip install Flask-Migrate

app.py内部,添加以下片段:

from flask import Flaskfrom flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] =
    'sqlite:///bizza.db'
db = SQLAlchemy(app)
migrate = Migrate(app, db)
class Speaker(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    bio = db.Column(db.Text, nullable=False)
    photo = db.Column(db.String(100))
    contact_info = db.Column(db.String(100))
    user_id = db.Column(db.Integer,
        db.ForeignKey('users.user_id'), nullable=False)
    user = db.relationship('User',
        backref=db.backref('speaker', uselist=False))
    def __repr__(self):
        return f"Speaker('{self.name}', '{self.bio}',
            '{self.photo}', '{self.contact_info}')"
if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=5000)

app.py文件进行了一些更改,包括安装flask-migrate包,使用appdb实例作为参数创建Migrate类的一个实例,以及添加一个将在其包含到数据库中时被跟踪的speaker模型类。

运行迁移

根据对app.py中添加的speaker模型类的更改,让我们使用 Alembic 实现迁移:

  1. 首先,在项目目录中,输入pip install flask-migrate命令。flask-migrate扩展为 Flask 应用程序提供数据库迁移支持。

  2. 在 Flask 中生成初始迁移,请在项目根目录的命令终端中输入以下内容:

    migrations in your Flask application directory.
    
  3. 然后,一旦您初始化了迁移存储库,您可以使用flask db migrate命令根据对模型所做的更改生成第一个迁移。我们已将新的演讲者模型添加到app.py文件中。

  4. 现在,使用flask db migrate -m 'first migration message, speaker model added'命令根据对数据库模型所做的更改生成一个新的迁移脚本。-m标志用于指定描述迁移中更改的消息。以下命令将创建一个包含模型中指定更改的新迁移文件:

    flask db initflask db migrate -m "first migration speaker model"
    

    以下屏幕截图显示了命令的效果:

图 8.13 – 显示迁移命令的屏幕截图

图 8.13 – 显示迁移命令的屏幕截图

  1. 将模式数据模型更改提交到基于迁移脚本状态的数据库中,请运行以下命令:

    flask db upgrade
    

    您将得到以下输出:

图 8.14 – 显示 flask db upgrade 命令的屏幕截图

图 8.14 – 显示 flask db upgrade 命令的屏幕截图

这将应用所有挂起的迁移到您的数据库模式中。从这一点开始,您可以使用flask db migrate命令根据需要继续生成新的迁移。

注意

总结 Alembic 迁移所需的命令,请按照以下步骤操作:

pip install flask-migrate

flask db init

flask db migrate -m “first migration message speakers model”

flask db upgrade

Alembic 是一个用于 SQLAlchemy 的数据库迁移工具,它帮助保持数据库模式与应用程序的数据模型同步。当使用 Alembic 时,你通过一系列迁移来定义数据模型的变化,这些迁移是修改数据库模式的 Python 脚本。

这些迁移存储在迁移目录中,每个迁移都与数据库模式的一个特定版本相关联。当你运行一个迁移时,Alembic 会将数据库模式的当前状态与迁移中定义的目标状态进行比较。然后,它生成一系列 SQL 语句来修改数据库模式以匹配目标状态。

这些 SQL 语句在事务中执行,这确保了数据库模式以一致的方式被修改。使用 Alembic 管理数据库迁移,你可以确保数据库模式随着时间的推移与应用程序的数据模型保持同步。这有助于防止数据不一致和其他问题,这些问题可能在数据库模式和数据模型不同步时出现。

摘要

在本章中,我们广泛讨论了用于网络的 SQL 和关系数据建模。关系数据库帮助我们以关系组的形式设计数据库。我们还讨论了数据库中可能存在的关系,例如一对一、一对多和多对多关系,这使得我们能够在数据库中逻辑地分组关系并强制执行数据引用完整性。

此外,我们还探讨了如何设置 PostgreSQL。我们介绍了 SQLAlchemy 的基础知识及其相关的数据库适配器,以及它们在 Flask 应用程序开发中的使用。我们讨论了数据模型设计,以 Bizza 项目为例。最后,我们讨论了 Flask 应用如何与数据库和迁移进行通信,以及在 Flask 中如何跟踪数据库的变化。

在下一章中,我们将广泛讨论后端开发中的 API 以及如何使用 Flask 框架来实现 API 设计。

第九章:API 开发和文档

应用程序编程接口API)是许多开发者用来处理数据和促进不同系统间通信的技术核心。API 启用的数字商业模式正在快速发展。对有经验的开发者来说,构建创新企业解决方案的需求也在不断上升。

API 经济正在演变成为一种新的可持续业务增长商业模式,为商业主和明智的执行者提供了大量机会。如果曾经有成为开发者的时机,那就是现在,因为公共 API 和有价值的商业 API 如此之多,它们可以使得应用开发和部署变得更容易实现。

在本书的前面部分,我们讨论了如何使用数据库和数据建模来有效地存储和检索所需的应用数据。本章提供了深入后端开发的机会,利用 API 技术实现各种客户端应用程序和后端服务之间的无缝通信。

你将学习在 Flask Web 应用程序中进行 API 设计和开发。我们将涉及常见的 API 术语,以提升你对 API 设计的理解水平。你将学习 REST API 最佳实践以及如何在 Flask 和 SQLAlchemy 中实现数据库 CRUD 操作。

到我们结束本章时,你将更好地理解 RESTful API 架构以及如何在 Flask Web 应用程序中设计和实现 RESTful API。你将获得对端点和有效载荷结构的改进理解,以便有效地处理数据。

最终,你将能够构建能够处理 HTTP 请求和响应的 Flask Web 应用程序。你将能够使用 Flask 的 SQLAlchemy 扩展与数据库交互并执行 CRUD 操作。

最后,你将测试一些 API 端点,并使用 Postman 编写清晰简洁的实现 API 端点的文档。

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

  • 什么是 API?

  • 为什么在 Web 开发中使用 API

  • 端点和有效载荷结构

  • 理解 HTTP 请求/响应

  • 理解 HTTP 状态码

  • REST API 设计原则

  • 在 Flask 应用程序中实现 REST API

  • 通过 CRUD 操作与数据库进行 API 交互

  • API 文档

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter09.

什么是 API?

API 代表 应用程序编程接口。表面上,API 似乎是一种新的技术术语,旨在使学习应用开发变得困难。事实并非如此。API 的核心目的是根据一套约定的规则、方法和协议,促进不同系统之间的通信。

在 Web 应用程序的背景下,API 帮助全渠道前端应用程序与后端服务进行通信。对数字服务的不断需求正在推动企业组织产生创新想法,通过设计和实现 API 使他们的数字资产可用。

作为一名开发者,你将花费大量时间开发以 API 驱动的解决方案。了解如何设计和实现 API 解决方案可以增加你的技能资本和对你雇主的价值。总的来说,有两种类型的 API:私有 API公共 API

私有 API 有时被称为内部 API。私有 API 描述了一个开放架构接口,允许组织内部的开发者访问关键组织数据。有了 API,自动化业务流程和管理各个业务单位之间的信息流变得容易。

私有 API 允许企业利用现有的可重用平台高效地开发内部解决方案。例如,你可以通过利用相同的后端服务,将你的前端应用程序的范围从 Web 应用程序扩展到移动应用程序。

另一方面,公共 API 描述了一个标准化的接口,允许组织外部的开发者以编程方式访问组织的数据和服务,这些数据和服务是为公众消费而设计的。这一套接口允许开发者构建新的应用程序或为他们的应用程序添加更多功能,而无需重新发明轮子。在这个时代,大量的公共 API 可供开发者学习,并且是开发创新解决方案的明智方式。

以下 GitHub 链接描述了一些你可以利用的公共 API:github.com/public-apis/public-apis。例如,Google、Twitter、Facebook 和 Spotify 等平台允许开发者通过 API 访问平台的数据。有了这个,开发者能够构建按需服务和产品。

此外,其他形式的 API 包括简单对象访问协议SOAP)、JavaScript 对象表示法-远程过程调用JSON-RPC)、可扩展标记语言-远程过程调用XML-RPC)和表征状态转移REST)。这些规则、协议和规范描述了不同的系统如何通过网络进行通信。虽然 JSON-RPC 和 REST 可以一起使用,但探索这种集成超出了本书的范围。

本书下一部分将探讨为什么 API 已成为企业和开发者关键技术的关键,以及它们是如何改变软件、工具和数字服务的构建和消费方式的。

为什么在 Web 开发中使用 API

API 是现代网络应用开发的一个组成部分。你很少会遇到一个没有某种形式 API 实现的以数据驱动的网络应用。API 之所以如此受欢迎,原因并不难理解。API 通过提供标准化的方式,促进了跨不同应用和系统之间高效资源共享的集成、协作和创新。以下是使用 API 进行网络开发的一些好处:

  • API 允许不同的系统进行交互,弥合网络应用不同组件之间的通信差距

  • 以 API 驱动的开发可以访问第三方数据和服务的,促进创新解决方案并减少开发时间

  • API 为开发人员和最终用户提供了安全且可扩展的信息共享方式

  • 以 API 为中心的开发通过利用现有 API 并避免重复造轮子来减少软件开发时间

  • API 具有巨大的财务潜力,正如谷歌地图和 Twilio 通过 API 访问产生的显著收入所证明的那样

  • 在医疗保健领域,以 API 驱动的网络应用促进了关键健康数据的访问和管理

  • API 在旅游和旅游业中非常有价值,可以获取实时航班预订信息并找到最佳价格

  • API 通过集成支付解决方案并实现无缝交易在电子商务中发挥着至关重要的作用

  • API 抽象允许开发者构建具有受控数据暴露和安全的架构设计的网络应用

接下来,我们将简要探讨端点和有效载荷结构,以了解如何在 API 设计中定义清晰和逻辑的路径来访问资源,并确保客户端和服务器之间有效信息交流的数据结构。

端点和有效载荷解剖

端点和有效载荷是任何 API 组件的关键部分。端点通过使用定义良好的路由或 URL 来促进服务器上资源的访问。在客户端-服务器环境中,端点通常充当两个不同应用之间数据交换的实际点。有效载荷允许我们在请求或响应中发送数据。我们将在稍后讨论更多关于有效载荷的内容。

让我们从检查端点结构以及指导 REST API 中端点设置的规则开始。

理解端点结构

端点结构允许你逻辑地组织应用程序的资源。我们将从探索端点结构中的venue资源开始。在 REST API 中,数据通常表示为资源。你可以定义一个venues集合的端点,使用collection/resource路径约定后跟随一个venue资源。

注意

venue资源代表一个可以通过唯一的 URL 端点访问的对象或数据结构,允许客户端检索、创建、更新或删除有关场所的信息。

API 设计者的一个主要目标是将数据清晰建模为资源,其他开发者可以在他们的应用程序中使用。

例如,https://example.com:5000/api/v1/venues 是一个完整的路径,它指向 API 服务器上的 venue 资源。

让我们通过路径的结构来了解:

  • https: 安全协议

  • example.com:域名

  • 500:端口号

  • /api/v1/venues:端点

    /api/ 代表 API 端点的入口点,/v1/ 代表 API 的版本号,/venues 代表资源

我们可以根据 HTTP 方法在端点上执行以下 API 操作:

  • GET /api/v1/venues:返回所有场所的列表

  • GET /api/v1/venues/id:检索由 id 标识的单个场所

  • POST /api/v1/venues/:创建场所资源

  • UPDATE /api/v1/venues/id:更新由 id 标识的单个场所

  • DELETE /api/v1/venues/id:删除由 id 标识的单个场所

让我们使用适当的 HTTP 方法检索有关场所的信息。/api/v1/venues URL 端点用于从数据源获取所有可用场所及其相关信息的概述。响应将以 JSON 格式提供,以结构化的方式表示场所数据。

例如,让我们检查一个场所资源请求和预期的 JSON 格式响应。

使用 GET /api/v1/venues,预期的 JSON 格式响应将是一个所有可用场所的列表:

[{
"id":1
"name": "Auditorium A"
},
{
"id":2
"name": "Auditorium B"
},
]

使用 GET /api/v1/venues/2,预期的 JSON 格式响应将是一个具有 id 2 的特定场所资源:

[{
"id":2
"name": "Auditorium B"
}]

使用 POST /api/v1/venues,预期的 JSON 格式响应将是一个添加的场所资源,其返回的 id3

[{
"id":3
"name": "Auditorium C"
}]

使用 UPDATE /api/v1/venues/3,预期的 JSON 格式响应将是一个更新后的场所资源,其 id3name 属性的新值现在是 Conference Hall

[{
"id":3
"name": "Conference Hall"
}
]

使用 DELETE /api/v1/venues/3,预期的 JSON 格式响应将是一个已删除的资源场所,其 id3

[{
"id":3
}
]

前面的 JSON 响应消息描述了基于对服务器的请求的端点数据表示。使用 GET/api/v1/venues RESTful API 端点将返回可用场所的列表,GET /api/v1/venues/2 将返回具有 id 2 的特定场所,POST /api/v1/venues 将添加一个新的场所并返回其 idUPDATE /api/v1/venues/3 将更新 id 3 的场所并返回更新后的资源,而 DELETE /api/v1/venues/3 将删除 id 3 的场所。

接下来,我们将检查在设计端点时需要遵守的一些黄金法则。有了这些原则,您将能够设计出更直观、用户友好的 RESTful API,这将减少开发和使用 API 的应用程序所需的时间和精力。

API 端点最佳实践

设计一个好的 API 端点有指导原则,这些原则也适用于 API 开发。我们将简要探讨以下设计 API 端点的黄金法则,这些法则可以让团队成员或其他开发者产生共鸣:

  • /venues端点,名词venues解释了资源的相关内容:

    • https://example.com/api/v1/venues

    • https://example.com/api/v1/get_all_venues

  • venues案例中,您可以看到我们使用了/venues来描述集合,例如,https://example.com/api/v1/venues/2,其中id=2,指的是集合中的特定资源:

    • https://example.com/api/v1/venues

    • https://example.com/api/v1/venue

  • collection/resource/collection结构中,端点 URL 以集合名称开头,接着是资源名称,如果适用,然后是另一个集合名称。

    例如,对于一个可能包含一系列论文的speaker资源,推荐的端点 URL 可能类似于/speakers/2/papers,其中speakers是集合名称,2是特定演讲者资源的 ID,而papers是与该特定演讲者关联的论文集合:

    • https://example.com/api/v1/speakers/2/papers

    • https://example.com/api/v1/speakers/2/papers/8/reviews

https://example.com/api/v1/speakers/2/papers/8/reviews违反了推荐的结构,在papers之后包含了另一个集合名称reviews。这种结构暗示reviewspapers的子集合,这与collection/resource/collection模式的规则相矛盾。相反,我们可以将它们视为具有自己端点的独立资源。

以下是一个示例:

  • GET /api/v1/speakers/2/papers

  • GET /api/v1/papers/8/reviews

通过分离端点,可以更清楚地看出评论与论文相关,而不是嵌套在papers集合中。

接下来,我们将探讨负载数据的结构,并检查其在当前上下文中的作用。

理解负载数据结构

负载数据包含 API 设计用于处理的实际数据。在本节中,您将了解 API 发送和接收的数据格式。您将学习如何构建负载数据,包括用于表示数据的键和值。

通过理解负载数据结构,您将能够处理更复杂的 API 并处理更大的数据量。如前所述,API 提供了在 Web 服务之间交换数据的方式。在交互、通信或共享数据时,所涉及的数据是负载数据。

负载数据是希望交换信息的各种 Web 应用程序之间的数据。技术上讲,这是客户端-服务器通信中 HTTP 请求和响应的主体。在 API 生态系统中,当客户端发起请求时,请求的主体中包含数据,这本质上由两部分组成:头部/开销和负载数据。

标头用于描述传输中的数据源或目的地。有效负载有不同的风味:JSON 或 XML。有效负载通过使用花括号{}来识别。在这本书中,我们将重点关注有效负载的 JSON 格式。

我们选择 JSON 格式,因为 JSON 易于阅读和理解,在大多数编程语言中易于解析,支持复杂的数据结构,是平台无关的,并且使用最少的语法。

让我们通过示例描述一个典型有效负载的结构。

以下是一个客户端发送给服务器的有效负载(API 请求有效负载):

POST /venues  HTTP/1.1Host: example.com
Accept: application/json
Content-Type: application/json
Content-Length: 10
{
"id":3
"name": "Conference Hall"
}

注意以下代码中的内容:

  • 有效负载由花括号内的数据表示,并解释了我们要使用POSTHTTP 方法发送到/venuesAPI 端点的信息。

  • "Content-Type: application/json"请求头描述了请求体的 JSON 数据类型。

  • 客户端还使用Accept: application/json描述它从服务器期望接收的响应格式。

    对于服务器返回的有效负载(来自服务器的OK 响应有效负载),我们有以下内容:

    HTTP/1.1 200 OKContent-Type: application/jsonContent-Length: 10{"responseType": "OK","data": {"id":3"name": "Conference Hall"}}
    

注意以下代码片段中的内容:

  • OK和花括号内的内容数据是有效负载。

  • 您可以看到服务器遵守了客户端期望接收的Content-Type: application/json

  • JSON 有效负载用花括号{}括起来,由两个键值对组成:

    • "responseType": "Ok":这个键值对表示 API 成功处理了请求并返回了响应。"responseType"键的值是"Ok"

    • "data": { "id": 3, "name": "Conference Hall" }:这个键值对包含 API 实际返回的实际数据。"data"键的值是一个包含有关 ID 为3的场所信息的对象。在这种情况下,场所名称是"Conference Hall"

    HTTP/1.1 404 Not foundContent-Type: application/jsonContent-Length: 0{"responseType": "Failed","message": "Not found"}
    

    上述代码中的有效负载是"responseType": "Failed""message": "``Not found"

端点和有效负载是 API 开发的重要组成部分。您需要设计简洁直观的 API 端点,以便清楚地传达您的意图给可能希望与您的 API 数据服务交互的开发者。

现在,我们将深化知识体系,快速浏览 HTTP 请求/响应。在构建 Web 应用时,了解 HTTP 请求和响应的工作方式至关重要。

这些是客户端和服务器之间通信的构建块,了解如何使用它们对于构建有效和高效的 Web 应用至关重要。

理解 HTTP 请求/响应

要成功与 API 一起工作,您需要了解 HTTP 请求/响应。因此,让我们揭开 HTTP 请求和响应的结构。

请求行

每个 HTTP 请求都以请求行开始。这包括 HTTP 方法、请求的资源以及 HTTP 协议版本:

GET /api/v1/venues  HTTP/1.1

在这种情况下,GET 是 HTTP 方法,/api/v1/venues 是请求资源的路径,HTTP 1.1 是使用的协议和版本。

让我们进一步深入了解 HTTP 方法,以了解开发人员如何使用不同的 HTTP 方法来指定他们在向 Web 服务器发出请求时想要执行的操作类型。

HTTP 方法

HTTP 方法指示客户端打算在 Web 服务器资源上执行的操作。常用的 HTTP 方法如下:

  • GET:客户端请求 Web 服务器上的资源

  • POST:客户端向 Web 服务器上的资源提交数据

  • PUT:客户端替换 Web 服务器上的资源

  • DELETE:客户端删除 Web 服务器上的资源

让我们快速浏览一下请求头。

HTTP 请求头部

HTTP 头部在 HTTP 请求或响应期间促进客户端和服务器之间的通信中起着关键作用。它们允许双方在传输主要数据的同时包含附加信息,例如元数据、身份验证凭据或缓存指令。

头部充当有效载荷的占位符,并为客户端和服务器提供关键上下文和元数据。例如,它们可以传达有关正在传输的数据的内容类型、语言、编码和大小等信息。

此外,头信息还可以提供有关客户端能力和偏好的详细信息,例如正在使用的浏览器类型或内容交付的首选语言。HTTP 请求头紧随请求行之后。

常见的头部如下:

  • www.packtpub.com/

    Host 头部指定服务器的主机,并指示从哪里请求资源。

  • "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0"

    User-Agent 头部告诉 Web 服务器正在发出 HTTP 请求的应用程序。它通常由操作系统(如 Windows、Mac 或 Linux)、版本和应用供应商组成。

  • "text/html,application/xhtml+xml,application/xml;q=0.9,ima ge/avif,image/webp,*/*;q=0.8"

    Accept 头部告诉 Web 服务器客户端可以接受哪种类型的内容作为响应。

  • en-US,en;q=0.5

    Accept-Language 头部指示语言。

  • text/html; charset=UTF-8

    Content-type 头部指示请求体中传输的内容类型。

接下来,我们将检查请求体。

HTTP 请求体

在 HTTP 中,请求体指的是与 HTTP 请求消息一起发送的附加数据,通常以有效载荷的形式。与提供请求或响应元数据的 HTTP 头部不同,请求体包含客户端发送给服务器的实际数据。请求体可以包含各种类型的数据,包括表单数据、JSON、XML、二进制数据或文本。

例如,当提交网页表单时,用户输入的数据通常作为请求体的一部分发送。同样,当上传文件或发送 API 请求时,正在传输的数据通常包含在请求体中。请求体的格式和结构取决于请求头中指定的内容类型。

例如,如果内容类型设置为application/json,则请求体必须是一个有效的 JSON 对象。如果内容类型为multipart/form-data,则请求体可能包含多个部分,每个部分包含不同类型的数据。

以下是一个使用POST方法向 Web 服务器提交数据的 HTTP 请求示例:

POST /users HTTP/1.1Host: example.com
{
"key":"value",
"array":["value3","value4"]
}

请求包含一个 JSON 格式的请求体,其中包含一个键值对和一个数组。键的值为"value",数组包含两个字符串值,"value3""value4"

以下是一个使用PUT方法更新 Web 服务器上数据的 HTTP 请求示例:

PUT /authors/1 HTTP/1.1Host:example.com
Content-type: text/json
{"key":"value"}

请求包含一个 JSON 格式的请求体,其中包含一个键值对。键的值为"value",这些数据旨在更新指定端点的资源。

接下来,我们将考虑 HTTP 响应。HTTP 响应是服务器对 HTTP 请求的响应方式。

HTTP 响应

理解各种 HTTP 状态码和 HTTP 响应中包含的信息对于构建健壮和有效的 Web 应用至关重要。在 Web 服务器处理完 HTTP 请求后,它应向客户端发送 HTTP 响应。

响应的第一行包含状态,它指示请求是否成功或由于错误而失败。此状态行向客户端提供了关于请求结果的宝贵反馈:

HTTP/1.1 200 OK

HTTP 响应以 HTTP 协议版本开始,接着是状态码,然后是原因消息。原因消息是状态码的文本表示。在即将到来的理解 HTTP 状态码部分,我们将详细探讨状态码的主题。

我们现在将开始讨论响应头。

HTTP 响应头

在 HTTP 中,响应头提供了关于服务器发送的响应消息的额外信息。虽然 HTTP 响应初始行中的状态码提供了关于请求结果的基本信息,但响应头可以提供关于响应的额外元数据,例如内容类型、缓存设置和服务器类型。

响应头通常用于向客户端提供有助于优化响应渲染和处理的信息,例如指定字符编码或内容长度。响应头还可以用于控制客户端的行为,例如设置缓存参数或为 API 请求启用跨源资源共享CORS)。

HTTP 响应头由服务器在响应消息中发送,紧随状态行之后。头由一行或多行组成,每行包含一个头字段名和一个值,由冒号分隔。一些常见的响应头包括 Content-TypeContent-LengthCache-ControlServerSet-Cookie

下面是一个 HTTP 响应头的示例:

Date: Sun, 27 Nov 2022 02:38:57 GMTServer: Cloudflare
Content-Type: text/html

考虑到前面的代码块,我们有以下内容:

  • Date 头指定了 HTTP 响应生成的日期和时间

  • Server 头描述了用于生成响应的 Web 服务器软件

  • Content-Type 头描述了返回资源的媒体类型:在这种情况下,HTML

接下来,我们将讨论 HTTP 响应体。

HTTP 响应体

响应体指的是服务器在响应 HTTP 请求时发送的数据。虽然响应头提供了关于响应的元数据,但响应体包含客户端请求的实际数据,例如 HTML、JSON、XML 或二进制数据。

响应体的结构和内容取决于请求的性质和请求数据的格式。例如,对网页的请求可能会收到包含页面标记和内容的 HTML 响应体,而对 API 数据的请求可能会收到包含请求数据的 JSON 响应体。

在 HTTP 中,响应体可能在某些情况下包含内容,例如当服务器以状态码 200 响应时,这表示请求成功,服务器正在返回内容。在其他情况下,当服务器以状态码 204 响应时,它表示请求成功,但没有内容返回,因此响应体可能为空:

HTTP/2 200 OKDate: Sun, 27 Nov 2022 02:38:57 GMT
Server: Cloudflare
Content-Type: text/html
<html>
    <head><title>Test</title></head>
    <body>Test HTML page.</body>
</html>

在讨论了 HTTP 请求和响应之后,我们现在将开始讨论各种常用的 HTTP 状态码。

理解 HTTP 状态码

HTTP 状态码是服务器在响应 HTTP 请求时发送的三位数。这些代码向客户端提供有关请求结果的反馈,并帮助识别在事务过程中可能发生的任何问题。HTTP 状态码的第一个数字表示响应的类别,可能是 InformationalSuccessfulRedirectionClient ErrorServer Error

每个类别中你可能会遇到的常见状态码如下所示:

1XX 信息性

状态码 描述 原因
100 此代码表示 Web 服务器向客户端发送的临时响应,告知客户端继续请求或如果请求已被处理则忽略响应。 继续

2XX 成功

状态码 描述 原因
200 此代码表示服务器成功处理了请求。 OK
201 此代码表示服务器成功处理了请求并创建了资源。 已创建
202 此代码表示请求已接收,但处理尚未完成。 已接受
204 此代码表示服务器成功处理了请求,但没有返回任何内容。 无内容

3XX 重定向

状态码 描述 原因
301 此代码表示请求以及所有未来的请求都应该发送到响应头中的新位置。 永久移动
302 此代码表示请求应暂时发送到响应头中的新位置。 找到

4XX 客户端错误

状态码 描述 原因
400 此代码表示服务器无法处理请求,因为客户端存在错误。 错误请求
401 此代码表示发起请求的客户端未经授权,应进行认证。 未经授权
403 此代码表示发起请求的客户端没有访问内容的权限;他们未经授权,应获得访问资源的权限。 禁止访问
404 此代码表示网络服务器未找到请求的资源。 未找到
405 此代码表示网络服务器知道方法,但目标资源不支持使用的 HTTP 方法。 方法不允许

5XX 服务器错误

状态码 描述 原因
500 此代码表示在处理请求时,网络服务器遇到了意外错误。 内部服务器错误
502 此代码表示在作为获取响应的网关时,网络服务器从应用程序服务器接收到了无效的响应。 不良网关
503 此代码表示网络服务器无法处理请求。 服务不可用

在我们探讨如何在 Flask 网络应用程序中实现 REST API 之前,理解 RESTful API 设计的基本原则非常重要。通过理解这些基本原则,我们可以确保我们的 API 设计得直观、用户友好且高效。因此,让我们更深入地了解支撑 RESTful API 设计的关键原则。

REST API 设计原则

REST API,或 RESTful API,描述了一个符合 REST 架构风格的 API,它使用基于 HTTP 的接口进行网络通信。API 在其最简单的形式中定义了一组规则,不同的系统或应用程序需要遵守这些规则以交换数据。

罗伊·菲尔丁博士在 2000 年提出了一篇论文(www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm),描述了一种新颖的设计方法,API 设计者预期在构建能够经受时间考验且安全的应用程序时遵循。为了开发 RESTful 系统,有一些值得注意的架构约束。

我们将检查那些 REST 原则,例如客户端-服务器无状态性缓存统一接口分层系统按需代码,以符合 REST 风格指南。

客户端-服务器

REST API 架构鼓励客户端和服务器之间的通信。客户端向服务器发送网络请求,而服务器只能向客户端发送响应。RESTful API 确保所有通信都从客户端开始,然后客户端等待服务器的响应。

RESTful API 强制执行客户端应用程序和服务器之间的关注点分离,从而使得交互流畅且独立。由于关注点分离,Web 应用程序设计不是紧密耦合的,因为客户端和服务器可以独立扩展,而不会无意中影响整体应用程序架构。

注意

关注点分离是一种设计原则,旨在将系统划分为不同的、独立的部分,每个部分负责特定的任务或功能。这个设计原则在软件工程和编程中普遍应用,包括 REST API 的设计。

无状态性

符合 REST API 设计约束要求所有网络请求都必须是无状态的。无状态性意味着服务器不期望记住过去的网络请求。技术上,网络请求的无状态性鼓励客户端和服务器之间独立的交互。

每个客户端向服务器发出的请求都应包含理解并满足该请求所需的所有重要信息。无状态性总是能提高性能,因为服务器不需要存储或记住之前的请求。此外,在 RESTful 应用程序设计中,无状态状态使得架构简单易设,可扩展且可靠。

缓存

RESTful API 在设计时考虑了缓存。缓存是将频繁使用的数据存储在临时位置的过程,以减少访问它所需的时间和资源。REST API 中的缓存原则确保对请求的响应中包含的网络信息被隐式或显式地声明为可缓存或不可缓存。

例如,如果响应是可缓存的,客户端将重用缓存的响应数据来处理类似的后续请求。缓存提高了服务器资源的效率,减少了带宽使用,同时降低了网站页面的加载时间。

统一接口

统一接口是 REST API 设计者需要实现的设计约束之一。REST API 架构风格指出,REST API 应具有单一的通信协议和标准化的数据格式。无论系统环境如何,应用程序和服务器,统一接口都便于顺畅的交互。

统一接口鼓励每个系统组件的易于可扩展性,并为任何客户端应用程序与 REST API 通信提供了一个通用框架。

REST API 采用 HTTP 作为客户端-服务器交互的通信协议。使用 HTTP,客户端以特定格式发送请求,如 JSON 或 XML。让我们看看一个示例请求:

GET https://localhost:5000/api/v1/venues

此 REST API 请求包含两个主要组件——GET和 URL:

  • GET是 HTTP 方法之一。GET方法指定客户端想要在服务器资源上执行的操作。客户端通常使用四种常用的 HTTP 请求来发起请求:

    • GET:用于检索资源

    • POST:用于创建新资源

    • PUT/PATCH:用于更新或编辑现有资源

    • DELETE:用于删除资源

  • URL 部分包含统一资源标识符,指定了感兴趣的资源。在这种情况下,我们感兴趣的是venues资源。因此,我们发出一个 HTTP GET请求来查找该资源的位置。

此外,URL 有时也被称为端点。端点表示 API 实际与客户端交互的位置,数据交换发生的地方。

客户端-服务器交互从主机接收和验证GET请求开始。响应数据从目标资源(/api/v1/venues)返回。返回的数据格式通常是 JSON 或客户端指定的预期响应格式。JSON 允许我们拥有标准化的结构化数据来显示目标资源的内容。

分层系统

现代 Web 应用程序由分层架构组成。客户端-服务器系统可能包含多个层的服务器/服务,每个都有自己的责任,例如负载均衡、安全和缓存层。REST API 设计原则鼓励实现,其中可能存在的系统层不会改变客户端-服务器交互的自然行为。

在此约束下,内部系统/服务器中的任何更改或修改都不会对基于 HTTP 的请求和响应模型的格式产生影响。分层系统强制执行关注点的清晰分离,并随着客户端和服务器的高度独立性和可扩展性而提高。

需求代码(可选)

需求代码是 RESTful API 设计中的一个可选约束,允许服务器在响应请求时向客户端发送可执行代码。这些代码可以是脚本、小程序或其他可执行文件,并可用于扩展客户端的功能。

按需提供代码的约束是可选的,因为并非总是有必要或期望 API 向客户端提供可执行代码。在许多情况下,RESTful API 仅提供数据或资源,这些数据或资源可以被客户端应用程序消费,而不需要可执行代码。

然而,在某些情况下,按需提供代码对于向客户端提供额外功能是有用的,例如数据处理、过滤或可视化。例如,用于数据分析的 RESTful API 可以向客户端提供可执行代码,以执行复杂计算或基于数据生成可视化。

REST API 可以发送代码,如 JavaScript 代码,到客户端应用程序执行。这种按需提供代码的可选功能允许 API 设计者通过增加 API 交付所需业务解决方案的灵活性来进一步定制 API 的功能。

之前提到的 REST API 设计原则确保开发者能够基于在软件开发行业中广泛接受的架构风格构建解决方案。

最后,Roy Fielding 博士曾如下总结 RESTful API 设计原则和软件开发的整体目标:

“REST 是数十年的软件设计:每个细节都是为了促进软件的长期性和独立进化。”

许多约束直接与短期效率相矛盾。遗憾的是,人们通常擅长短期设计,而通常在长期设计方面表现糟糕。大多数人认为他们不需要设计超出当前版本。有许多软件开发方法将任何长期思考描绘为错误的方向,或者是不切实际的象牙塔设计(如果它不是由真实需求驱动的,它确实可以是)。”

roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven

接下来,我们将深入探讨在 Flask Web 应用程序中实现 REST API 的实际操作。

学习如何在 Flask 应用程序中实现 REST API 是希望构建可以被其他应用程序或服务访问和消费的 Web 应用程序的开发者的一项宝贵技能。

在 Flask 应用程序中实现 REST API

Flask 作为流行的 Python 网络框架,为开发者提供了构建 Web 应用程序的灵活和轻量级解决方案。使用 Flask,你可以轻松创建和管理 RESTful API。在 Flask 应用程序中实现 REST API 的过程很简单。

在 Flask 应用程序中实现 REST API 涉及定义 API 端点、请求处理程序和数据模型,以及可能连接到数据库。在本节中,我们将使用 Flask 设计一个 REST API 服务,该服务可以被 React 前端应用程序消费。

我们将遵循一个简单的定义资源的过程,在Bizza应用程序中预期的资源,然后定义用于访问资源的 URL(端点)。此外,我们将使用 Flask 的路由系统将每个端点映射到应用程序中的特定函数。

每个函数应处理 HTTP 请求,与数据库(如有必要)交互,并返回适当的响应。最后,我们将实现数据序列化,以在 Python 对象和 JSON 之间进行序列化和反序列化。这确保了 API 可以使用标准格式与客户端建立通信。

接下来,我们将开始讨论使用定义应用程序资源的概念来实现 REST API。

定义应用程序资源

我们将首先定义创建一个能够处理活动所有方面的会议 Web 应用程序所需的资源,从安排和注册到赞助管理。因此,以下资源被定义:

  • Attendees: 参加会议的人

  • Speakers: 在会议上进行演讲的人

  • Schedules: 会议的日程安排,包括每个会议的开始和结束时间

  • Presentations: 会议演讲者的兴趣领域和主题

  • Users: 事件管理系统中的用户,包括与会者、演讲者和组织者

  • Userextras: 关于参加活动的用户的附加信息,例如饮食偏好或无障碍需求

  • Venues: 活动或会议的举办地点,包括位置、容量和设施信息

  • Sessions: 会议中的单个会议或演讲,包括演讲者、主题和时间

  • Sponsors: 赞助该活动的组织或公司,包括其赞助级别、标志和联系信息

接下来,我们将定义 API 端点。为了实现一个功能性的 REST API,有必要明确地定义 API 端点。

定义 API 端点

定义 API 端点是实现 REST API 的关键步骤。这些端点允许您对会议 Web 应用程序的资源执行各种操作,例如创建、读取、更新和删除记录。我们根据前述章节中指定的资源来定义端点。

现在,具体端点和使用的 HTTP 方法如下所示:

  • Users:

    • GET /users: 获取所有用户的列表

    • GET /users/{id}: 获取特定用户的信息

    • POST /users: 创建新用户

    • PUT /users/{id}: 更新特定用户的信息

    • DELETE /users/{id}: 删除特定用户

  • Userextras: 关于参加活动的用户的附加信息,例如饮食偏好或无障碍需求

    • GET /userextras: 获取所有userextras的列表

    • GET /userextras/{id}: 获取特定userextra的信息

    • POST /userextras: 创建新的userextra

    • PUT /userextras/{id}: 更新特定 userextra 的信息

    • DELETE /userextras/{id}: 删除特定 userextra

  • Attendees:

    • GET /attendees: 获取所有参会者的列表

    • GET /attendees/{id}: 获取特定参会者的信息

    • POST /attendees: 创建新的参会者

    • PUT /attendees/{id}: 更新特定参会者的信息

    • DELETE /attendees/{id}: 删除特定参会者

  • Speakers:

    • GET /speakers: 获取所有演讲者的列表

    • GET /speakers/{id}: 获取特定演讲者的信息

    • POST /speakers: 创建新的演讲者

    • PUT /speakers/{id}: 更新特定演讲者的信息

    • DELETE /speakers/{id}: 删除特定演讲者

  • Schedules:

    • GET /schedules: 获取所有日程的列表

    • GET /schedules/{id}: 获取特定日程的信息

    • POST /schedules: 创建新的日程

    • PUT /schedules/{id}: 更新特定日程的信息

    • DELETE /schedules/{id}: 删除特定日程

  • Presentations:

    • GET /presentations: 获取所有演示文稿的列表

    • GET /presentations/{id}: 获取特定演示文稿的信息

    • POST /presentations: 创建新的演示文稿

    • PUT /presentations/{id}: 更新特定演示文稿的信息

    • DELETE /presentations/{id}: 删除特定演示文稿

  • Venues:

    • GET /venues: 获取所有场馆的列表

    • GET /venues/{id}: 获取特定场馆的信息

    • POST /venues: 创建新的场馆

    • PUT /venues/{id}: 更新特定场馆的信息

    • DELETE /venues/{id}: 删除特定场馆

  • Sessions:

    • GET /sessions: 获取所有会议的列表

    • GET /sessions/{id}: 获取特定会议的信息

    • POST /sessions: 创建新的会议

    • PUT /sessions/{id}: 更新特定会议的信息

    • DELETE /sessions/{id}: 删除特定会议

  • Sponsors:

    • GET /sponsors: 获取所有赞助商的列表

    • GET /sponsors/{id}: 获取特定赞助商的信息

    • POST /sponsors: 创建新的赞助商

    • PUT /sponsors/{id}: 更新特定赞助商的信息

    • DELETE /sponsors/{id}: 删除特定赞助商

一旦定义了 API 端点,下一步就是在 Flask 应用程序中实现它们。

让我们开始挖掘吧!

实现 API 端点

实现 API 端点是开发 RESTful API 的关键步骤。这是所有精彩的部分汇聚在一起,形成您 REST API 的核心和灵魂。API 端点定义了 API 的功能和行为,指定了可以用来访问 API 资源的各种方法。

在 Flask 应用程序中,实现 API 端点涉及将 URL 映射到相关函数,定义 HTTP 方法,并编写将处理请求并生成响应的 Flask 视图函数。此外,还需要指定用于与 API 通信的请求和响应格式。

在本节中,我们将探讨在 Flask 应用程序中实现 API 端点的过程。让我们从创建一个简单的端点开始,该端点从 API 服务器返回基于文本的欢迎消息。

在后端开发环境中,在bizza/backend/内部,在终端中激活虚拟环境:

  • 在 Windows 上使用以下内容

    venv/Scripts/activate
    
  • 在 Mac/Linux 上使用以下内容

    source ./venv/bin/activate
    

如果你在激活虚拟环境时遇到问题,请检查第一章中的使用 Flask 设置开发环境

现在,使用以下代码更新app.py

from flask import Flaskapp = Flask(__name__)
@app.route("/")
def index():
    return "Welcome to Bizza REST API Server"
if __name__ == "__main__":
    app.run()

这就是前面代码片段中发生的事情:

  • 我们从flask包中导入Flask类。

  • 然后,我们创建Flask类的实例,并将其命名为app。然后,我们将一个__name__变量作为参数传递,该变量引用当前模块名称。这是 Flask 内部工作所需的,用于路径发现。

  • 使用@route() Flask 装饰器告诉 Flask,当用户访问 URL"/"(索引 URL)时,实现index()视图函数。Python 中的装饰器简单地说是一种向函数添加额外功能的方法,而无需显式更改方法行为。

  • 这个视图函数向浏览器返回消息欢迎使用 Bizza REST API 服务器。因此,本质上,装饰器能够修改index()视图函数,以返回 HTTP 响应形式的价值,然后客户端可以使用所需的数据表示格式显示它。在这种情况下,返回了text/html

  • 如果代码的条件部分变为真,即app.py是主程序,那么它将运行该模块。这样,Python 可以防止意外或无意中运行导入的模块。

我们可以通过输入以下命令使用curl测试端点:

curl http://localhost:5000/

我们得到以下输出:

图 9.1 – 展示来自 localhost 的 HTTP 响应

图 9.1 – 展示来自 localhost 的 HTTP 响应的屏幕截图

JSON 化响应数据

JSON 化响应数据是指将 Python 数据结构转换为 JSON 字符串的过程,该字符串可以作为 API 端点的响应返回。JSON 是一种轻量级的数据交换格式,易于阅读和编写,因此它是 Web API 的流行选择。

通过将响应数据 JSON 化,数据可以轻松地通过 HTTP 传输并被其他系统和编程语言使用。这是因为 JSON 是一种语言无关的数据格式,许多编程语言都可以解析和生成它。

JSON 还支持复杂的数据结构,如数组和对象,使其成为系统间传输数据的灵活格式。在 Flask 应用程序中,可以使用 jsonify 函数将 Python 数据结构转换为 JSON 格式。

此函数接受数据作为参数,并返回一个包含 JSON 数据和适当的 Content-Type 标头的 Flask 响应对象,指示数据为 JSON 格式。通过从 API 端点返回 JSON 格式的响应,客户端可以轻松地消费和使用数据。

你可以看到,前面的代码中的 content-typetext/html;现在,让我们返回一个序列化的 JSON 格式,因为从现在开始,那将是首选的数据交换格式:

from flask import Flask,jsonifyapp = Flask(__name__)
@app.route("/")
def index():
    return "Welcome to Bizza REST API Server"
@app.route("/api/v1/venues")
def venues():
    return jsonify({"id":1,"name":"Auditorium A"}), 404
if __name__ == "__main__":
    app.run()

在前面的代码中,我们添加了另一个端点,并用 @route("/api/v1/venues") 装饰它。因此,我们告诉 Flask 实现装饰器附加的视图函数的功能。

为了检索 JSON 格式的响应,我们使用 from flask import Flask, jsonifyFlask 包中调用 jsonify(),并将 Python 字典数据传递给它,然后它被转换成可序列化的 JSON 格式。

我们可以使用 curl 测试端点,通过输入以下命令:

curl.exe http://localhost:5000/api/v1/venues

我们得到以下输出:

图 9.2 – 展示正在测试的端点

图 9.2 – 展示正在测试的端点

接下来,我们将通过结合查询参数来增强端点功能。

向端点添加查询参数

查询参数是我们可以与请求一起传递到服务器以允许某些请求处理的附加信息。有了查询参数,我们能够向应用程序用户展示动态内容。

例如,这是一个没有查询参数的普通 URL 端点:

http://localhost:5000/

现在,你可以在 URL 的末尾添加一个 ? 符号,后面跟着一个键值对,来向 URL 添加一个查询参数。让我们向前面的 URL 添加一个查询参数:

http://localhost:5000/?firstname=Jim&lastname=Hunt

让我们在 Flask 应用程序中实现一个简单的查询参数,以更好地说明。

将以下片段添加到 app.py 文件中:

Import flask, jsonify, request@app.route("/api/v1/speakers/")
def speakers():
    firstname = request.args.get("firstname")
    lastname = request.args.get("lastname")
    if firstname is not None and lastname is not None:
        return jsonify(message="The speaker's fullname :" +
            firstname+" "+lastname)
    else:
        return jsonify(message="No query parameters in the
            url")

注意以下代码中的以下几点:

  • 我们正在创建一个新的端点,其 URL 为 /api/v1/speakers/ 资源。

  • 我们使用 Flask 包中的 request 对象。

  • 我们定义了一个视图函数 speakers() 来处理对端点的请求。

  • request 对象用于允许客户端向服务器发送数据以及执行其他端点请求操作。我们使用 request.args 来处理 URL 数据,request.form 来提取表单数据,以及 request.json 来处理 JSON 数据。在这里,我们将使用 request.args 来提取 URL 中的键值对,以便在服务器端处理 URL 数据。

  • firstnamelastname 变量存储从 URL 中提取的数据值。

  • 然后,我们执行一个简单的检查,以确定 URL 中是否存在查询参数。在生产代码中,你预计将对用户发送到服务器的数据进行全面检查。这只是为了演示目的。如果存在查询参数,我们返回 JSON 化的数据。否则,输出消息为 "No query parameters in the url"

测试带有查询参数的端点,例如,http://localhost:5000/api/v1/speakers?firstname=Jim&lastname=Hunt,会得到以下输出:

图 9.3 – 带查询参数的测试截图

图 9.3 – 带查询参数的测试截图

在不提供查询参数的情况下测试 http://localhost:5000/api/v1/speakers? 端点,会得到以下输出:

图 9.4 – 不带查询参数的测试截图

图 9.4 – 不带查询参数的测试截图

现在,让我们检查如何将变量传递到端点。这也有助于在应用程序中动态修改服务器端处理。

将变量传递到端点

向端点提供输入的一种方法是通过在 API 端点的 URL 路径中传递变量,从而提供特定信息。当需要输入来识别特定资源或对象时,例如 ID 号或用户名,这种技术通常被使用。

在 Flask 应用程序中,可以通过在端点 URL 定义中将变量括在尖括号 (<>) 中来将变量包含在 URL 路径中。例如,为了定义一个接受演讲者 ID 作为输入的端点,URL 可以定义如下:

from flask import Flask, jsonifyapp = Flask(__name__)
@app.route('/api/v1/speakers/<int:speaker_id>')
def get_speaker(speaker_id):
    # Use the speaker ID to fetch the appropriate speaker
      data
    # ...
    # Return the speaker data as a JSON response
    return jsonify(speaker_data)
if __name__ == '__main__':
    app.run()

在前面的示例中,get_speaker 函数接受一个 speaker_id 参数,它对应于 URL 路径中包含的变量。当请求 /speakers/123 端点时,get_speaker 函数会以 speaker_id=123 被调用。

将变量传递到端点是一种向 API 端点提供输入的有用技术,并且在 RESTful API 设计中常用。

接下来,我们将进一步扩展我们的 Flask 应用程序中的 REST API。我们将使用 PostgreSQL 数据库执行 CRUD 操作。

通过 CRUD 操作与数据库进行 API 交互

在大多数 Web 应用程序项目中,为了持久化数据存储,通常需要与数据库一起工作。你不会将纯文本硬编码到你的 REST API 中,除非你是那个试图煮海的人。

GETPOSTPUT/PATCHDELETE – 这些进一步描述并简化了与数据库的交互。

在一个全栈 Web 应用程序中,你期望你的用户能够创建一个资源(如果是现有资源,则为 POSTPUT),读取一个资源(GET),更新一个资源(PUT/PATCH),以及删除一个资源(DELETE)。在本节中,我们将使用一个简单的 venue 资源,以下是其端点,以及我们将对其执行的 HTTP 操作是 CRUD。

端点实现的所有代码都将托管在这本书的 GitHub 仓库中。

让我们通过描述我们使用的端点来启动 CRUD 操作:

  • POST /api/v1/venues/:创建一个 venue 资源

  • GET /api/v1/venues:返回所有场馆的列表

  • GET /api/v1/venues/id:检索由 id 标识的单个场馆

  • PUT /api/v1/venues/id:更新由 id 标识的单个场馆

  • DELETE /api/v1/venues/id:删除由 id 标识的单个场馆

前面的端点在意图上很清晰。但在我们开始具体化端点之前,让我们讨论必要的依赖项并确保我们可以连接到数据库。

激活虚拟环境:始终记住你在一个虚拟环境中工作以包含你的项目依赖项。然后,在 bizza/backend 内部,使用以下代码更新 app.py 文件:

from flask import Flaskfrom flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://<username>:<password>@localhost:5432/<database_name>'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

在前面的代码片段中,我们导入了 FlaskSQLAlchemy 模块。然后,app = Flask(__name__) 行创建了一个 Flask 应用程序实例。__name__ 参数代表当前模块的名称。

Flask API 随带一些我们可以修改的配置设置。config 对象是以 Python 字典的形式存在的。我们可以使用 app.config['SQLALCHEMY_DATABASE_URI'] 来设置数据库 URI,并使用 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 来禁用 SQLAlchemy 操作通知设置。

注意

app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://<username>:<password>@localhost:5432/<database_name>' 中,将 <username><password> 更改为您适当的数据库凭据。

使用 db = SQLAlchemy(app),我们创建了 SQLAlchemy 实例,它接受 Flask 实例作为参数。

设置完成后,让我们定义模型类并在数据库中创建 venue 表。

创建 Venue 类模型,如下所示:

class Venue(db.Model):__tablename__ = 'venues'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
    def format(self):
        return {
            'id': self.id,
            'name': self.name
    }

打开命令终端并输入以下内容:

flask shell

然后,输入以下内容:

from app import db, Venue

前面的命令将 db,一个 SQLAlchmy 实例,以及 Venue 类模型引入作用域。

现在,输入以下内容:

db.create_all()

前面的命令从 Venue 类模型创建了 venues 表。或者,你可以像之前那样运行迁移命令来创建表。

最后,使用以下内容验证表创建:

db.engine.table_names()

以下屏幕截图显示了终端命令以显示 venues 表的创建。

图 9.5 – 显示 Flask shell 中命令的屏幕截图

图 9.5 – 显示 Flask shell 中命令的屏幕截图

现在我们已经有了数据库和 venues 表,让我们开始定义端点。

创建场馆资源

让我们在 app.py 中定义 /venues 端点并使用它向数据库中发布条目。

创建新场馆的端点如下:

from flask import Flask, request, jsonify@app.route("/api/v1/venues", methods=['POST'])
def add_venues():
    if request.method == 'POST':
        name = request.get_json().get('name')
        all_venues =
            Venue.query.filter_by(name=name).first()
        if all_venues:
            return jsonify(message="Venue name already
                exist!"), 409
        else:
            venue = Venue(name=name)
            db.session.add(venue)
            db.session.commit()
            return jsonify({
                'success': True,
                'venues': venue.format()
            }), 201

在前面的代码中,我们有以下内容:

  • jsonifyrequest 方法是从 Flask 导入的。

  • @app.route() 装饰器具有指向 '/api/v1/venues' 端点的 URL,并使用 HTTP POST 方法。

  • 当有 POST 请求时,会调用 add_venue() 函数。

  • 我们测试以确保 request.method == 'POST'

  • 我们进行测试以确保新的场馆名称尚未存在于数据库中。如果添加的名称已在数据库中,则返回包含状态码 409- content conflict"Venue name already exist" 消息作为 json 消息。

  • 如果前面的步骤失败,我们使用 db.session.add(new_venue) 将其添加到数据库会话中。此时,条目尚未完全添加到数据库,而是添加到数据库会话中。我们需要再进行一步提交到数据库,即 db.session.commit()

  • Jsonify() 是 Flask 内置的函数,它返回一个 JSON 序列化的响应对象。

返回场馆列表

以下是从数据库中检索所有场馆的端点:

# retrieve all venues endpoint@app.route("/api/v1/venues", methods=['GET'])
def retrieve_venues():
    if request.method == 'GET':
        all_venues = Venue.query.all()
        if all_venues:
            return jsonify({
                'success': True,
                'venues': [venue.format() for venue in
                    all_venues]
            }), 200
        return jsonify(message="No venue record found"),
            404

在前面的代码中,我们有以下内容:

  • 调用 GET 请求方法

  • Venue.query.all() 是从 SQLAlchemy 查询以检索所有场馆

  • jsonify 响应对象输出结果为 200 状态码,如果失败,则显示 "No venue record found" 消息,状态码为 404

返回单个场馆资源

以下是一个用于返回单个场馆的端点:

@app.route("/api/v1/venues/<int:id>", methods=['GET'])def retrieve_venue(id):
    if request.method == 'GET':
        venue = Venue.query.filter(Venue.id == id).first()
        if venue:
            return jsonify({
                'success': True,
                'venue': venue.format()
            }), 200
        return jsonify(message="Record id not found"), 404

在前面的代码中,我们执行以下操作:

  • 调用 GET 请求方法

  • Venue.query.filter(Venue.id == id).first() 使用 retrieve_venue() 函数提供的参数检索具有指定 ID 的第一条记录

  • 如果 ID 存在,jsonify 响应对象输出结果为 200 状态码,如果失败,则显示 "Record id not found" 消息,状态码为 404

更新单个场馆资源

以下是一个用于修改场馆信息的端点:

@app.route("/api/v1/venues/<int:id>", methods=['PUT'])def update_venue(id):
    if request.method == 'PUT':
        name = request.get_json().get('name')
        venue = Venue.query.get(id)
        if not venue:
            return jsonify(message='Venue record not
                found'), 404
        venue.name = name
        db.session.commit()
    return jsonify({
        'success': True,
        'updated venue': venue.format()
    }), 200

在前面的代码中,我们执行以下操作:

  • 使用 PUT 请求方法调用以更新单个资源

  • 我们尝试检查记录 id 的存在性

  • 如果记录存在,我们使用 venue.name = name 更新它,然后提交会话

  • 如果 ID 信息已更新,jsonify 响应对象输出结果为 200 状态码,如果失败,则显示 "Venue record not found" 消息,状态码为 404

删除单个场馆资源

以下是一个用于删除场馆的端点:

@app.route('/venues/<int:id>', methods=['DELETE'])def remove_venue(id):
    venue = Venue.query.filter_by(id=id).first()
    if venue:
        db.session.delete(venue)
        db.session.commit()
        return jsonify(
            {'success': True,
            'message': 'You deleted a venue',
            'deleted': venue.format()
            }
        ), 202
    else:
        return jsonify(message="That venue does not
            exist"), 404

在前面的代码中,我们执行以下操作:

  • 使用 DELETE 请求方法调用以删除单个资源

  • 我们尝试检查记录 id 的存在性

  • 如果记录存在,我们删除它,然后提交会话

  • 如果删除了 ID 信息,jsonify 响应对象输出结果为 202 状态码,如果失败,则显示 "That venue does not exist" 消息,状态码为 404

以下 GitHub 链接包含了app.pyvenue CRUD 操作的完整代码 - github.com/PacktPublishing/Full-Stack-Flask-and-React/blob/main/Chapter09/05/bizza/backend/app.py

接下来,我们将探讨 API 文档的概念,并深入探讨其重要性以及如何使用 Postman 工具来记录您的 API 端点。

API 文档

您是否曾经尝试过在没有查看说明书的情况下组装从商店购买的设备?有很大可能性您是故意这样做的。您聪明地认为这会很容易组装,而大多数时候,您在这个过程中会经历试错,最终弄糟事情。

说明书是一种很好的资源,旨在帮助您了解产品的功能和特性。API 文档与说明书在指导方面并无不同,它是一套指令、参考资料,甚至是教程材料,可以增强开发者对您的 API 的理解。

为什么 API 文档是您 API 的一个关键组成部分?我们将探讨几个原因,并深入探讨如何使用 Postman 客户端工具来记录您的 API,以便其他开发者可以理解其全部内容以及如何使用它。我们将使用前一个部分中我们考察的venues CRUD 操作作为一个非常简单的案例研究。

在一个生产就绪的 API 产品中,您预计在文档中提供更多关于您的 API 的详细信息。您需要编写一份技术指导手册,使使用您的 API 服务变得轻而易举。您还希望包括教程、清晰的参考和示例代码,以帮助开发者轻松集成。

让我们考察一下您为什么想要为您的 API 制定清晰的文档:

  • 增强开发者体验:API 文档为提供快速入门指南、参考资料和案例研究开辟了途径,这些指南和资料可以帮助开发者提高生产力,并使用第三方数据来改善他们的软件产品。因此,精心设计的 API 文档有助于开发者了解实现您的 API 数据点的最佳方法,以解决他们的问题。

    高质量和有用的 API 文档意味着开发者将很容易理解如何实现您的 API,从而增加他们对您的 API 产品的整体情感依恋。Stripe、PayPal、Spotify、Twilio 和 Paystack 是具有出色文档的流行商业 API 的例子。

  • 缩短入职时间:全面的文档允许即使是初级开发者也能快速了解如何实现您的 API 端点和方法,以及如何无缝处理请求和响应,而无需其他开发者的协助。这将节省业务的时间和成本,并提高 API 的采用率。

  • 遵守 API 目标:文档允许 API 设计者和消费者在 API 规范及其旨在解决的问题上有一个共同的基础,避免歧义。

接下来,我们将使用 Postman 来测试和记录 RESTful API。

使用 Postman 进行测试和记录

Postman 是一个高级 API 平台,提供了一系列功能来简化 API 的开发、测试和利用。Postman 提供了一个用户友好的界面来测试 API 端点、生成 API 文档以及协作进行 API 相关操作。

要开始使用 Postman,您可以在本地计算机上下载并安装应用程序。访问 Postman 官方网站www.postman.com/downloads以访问下载页面。从那里,您可以选择适合您操作系统的适当版本并遵循安装说明。

在您安装了 Postman 之后,您可以利用 Postman 的力量来记录 RESTful API 端点、执行请求、分析响应以及进行全面的测试。

让我们利用集合中的venues生成 API 文档,告诉其他开发者如何进行请求以及预期的响应描述:

  1. 在您的计算机上启动 Postman 并创建一个集合。点击 Postman 界面上左侧侧边栏的集合标签;如果不可见,您可以在界面左上角点击三个水平线以展开侧边栏。在对话框中,输入您的集合名称。

图 9.6 – 展示在 Postman 中创建集合的屏幕截图

图 9.6 – 展示在 Postman 中创建集合的屏幕截图

  1. 在创建集合后,创建一个新的请求。在集合内,点击界面左上角的新建按钮。将出现一个下拉菜单:

图 9.7 – 展示如何添加 HTTP 请求的屏幕截图

图 9.7 – 展示如何添加 HTTP 请求的屏幕截图

  1. 从下拉菜单中选择请求类型。要将数据发布到服务器,选择localhost:5000/api/v1/venues

  2. 选择主体标签,然后选择原始,最后从下拉菜单中选择JSON标签。这将允许您以 JSON 格式将数据发送到服务器。在这种情况下,我们将以下数据发送到后端服务器:

    {''name": "Auditorium A"}
    

    以下屏幕截图显示了如何使用 Postman 测试端点。

图 9.8 – 展示向后端发送数据的屏幕截图

图 9.8 – 展示向后端发送数据的屏幕截图

  1. 在配置请求后发送请求。点击POST请求将失败。

    发送请求后,Postman 将显示从服务器收到的响应。您可以在响应面板中看到响应内容、状态码、头部和其他相关信息。以下屏幕截图显示了响应数据。

图 9.9 – 展示服务器对 POST 请求的响应的屏幕截图

图 9.9 – 展示服务器对 POST 请求的响应的屏幕截图

对您集合中的每个请求重复前面的步骤,选择适当的请求类型,并提供描述、参数细节、请求负载和响应格式(如有需要)。

  1. 一旦您测试了所有端点,右键单击集合名称,然后单击查看文档

    这将带您进入可以进一步自定义 API 文档的地方。您可以添加有关 API 的详细信息,例如,说明 API 的简要概述,包括其目的、功能以及任何相关的背景信息。

    此外,您还可以指定 API 的基本 URL,包括协议(HTTP/HTTPS)和域名。如果 API 需要身份验证,请解释支持的认证机制(例如,API 密钥、OAuth 或 JWT)并提供如何进行身份验证的说明。

    根据您 API 的要求,您可以记录 API 公开的每个端点。对于每个端点,您可能包括以下信息:

    • 端点 URL:提供端点的 URL 模式,包括任何必需的路径参数。

    • GETPOSTPUTDELETE等)。

    • 请求参数:指定端点所需的任何查询参数、请求头或请求体参数。包括参数名称、类型、描述以及是否为必需或可选。

    • 响应格式:描述端点返回的响应格式(例如,JSON 或 XML)。

    • 200 OK400 Bad Request401 Unauthorized)。

    • 响应体:提供端点返回的响应体的示例,包括所有相关字段及其描述。

    • 错误处理:解释 API 如何处理错误,并提供错误响应的示例。

  2. 最后,您现在可以将 API 文档发布到公共领域。

图 9.10 – 展示如何发布 API 文档的屏幕截图

图 9.10 – 展示如何发布 API 文档的屏幕截图

您可以在 Postman 文档中了解有关发布文档的进一步自定义,因为您可以将其转换为 HTML 并在自己的服务器上托管。生成的 API 文档链接为 https://documenter.getpostman.com/view/4242057/2s93sjUoYX。

摘要

在本章中,我们探讨了 Flask 应用程序中的 API 开发。我们首先了解 API 是什么以及为什么企业和开发者创建和消费 API 来驱动他们的数据访问。我们通过快速浏览开发者实现 API 设计模式时遇到的常见术语来进一步探讨。

然后,我们揭示了端点和负载的结构,认识到设计路由和交换数据是 API 开发的基础元素。此外,我们批判性地审视了指导 RESTful API 开发的原理。我们讨论了理解 REST API 设计原理如何提升 API 设计和开发中的最佳实践。同时,我们还讨论了 REST API 的实现以及如何在 Flask 应用程序中将 API 后端服务与数据库连接起来。

最后,我们讨论了使用 Postman 进行 API 测试和文档。在 API 设计和开发中,我们认识到测试和记录端点对于构建稳定且可用的 Web 应用至关重要。

在下一章中,我们将连接前端和后端功能,体验 React 和 Flask 的全栈特性。

第十章:集成 React 前端与 Flask 后端

本章代表了我们构建全栈 Web 应用程序过程中的一个关键点。在本章中,你将了解到如何将 Flask Web 服务器连接到 React 前端的一系列指令。你将学习如何将 React 前端表单输入传递到 Flask 后端。在此集成之后,你就可以正式被称为全栈 Web 开发者

React Web 应用程序通常具有简洁的外观和感觉,被认为是现代前端 Web 应用程序的劳斯莱斯。React 拥有一个直观的用户界面库,能够轻松地驱动生产级的 Web 和移动应用程序。

强大的 React 生态系统与 React 的工具和库相结合,促进了端到端的 Web 开发。当你将 React 令人难以置信的基于组件的设计模式与一个简约轻量级的 Flask 框架相结合时,你将得到一个能够经受时间考验并大规模扩展的丰富 Web 应用程序。

本章将帮助你理解在开发有价值的软件产品时,将 React(一个前端库)和 Flask(一个后端框架)集成的动态。你还将学习在本章中 React 如何处理与 Flask 后端相关的表单。

自从 Web 的出现以来,在 Web 应用程序中就需要更多动态和响应式的表单形式。我们将探讨服务器端表单元素的处理、验证和安全问题。

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

  • Bizza应用程序结构

  • 配置 React 前端

  • 准备 Flask 后端

  • 在 React 和 Flask 中处理表单

  • React 前端和 Flask 后端的故障排除技巧

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter10

Bizza应用程序结构

在本节中,我们将深入探讨本书中将要构建的应用程序的结构。正如之前所述,我们将把这个虚构的 Web 应用程序命名为Bizza,一个会议活动 Web 应用程序。

这个Bizza Web 应用程序将作为信息技术行业演讲者的会议活动的数字中心,提供众多功能和特性,以增强演讲者和与会者的整体体验。让我们深入了解Bizza应用程序结构。

应用程序概述

Bizza是一个虚构的数据驱动事件应用程序,允许信息技术行业的主题专家分享他们的见解和经验,为活动参与者提供有价值的知识,以提升他们的技能。

Bizza让你可以看到演讲者和研讨会日程的列表,并查看详细信息。这个网站允许用户注册并浏览研讨会。本质上,该应用程序将具有以下功能:

  • 展示活动演讲者和可用活动日程(包括地点和主题)的主页

  • 事件参加者注册表单

  • 具有感兴趣主题的演讲者注册表单

  • 用户登录应用程序的页面

  • 包含演讲者姓名和详细信息的页面

接下来,我们将深入探讨 Bizza 应用程序,并将其分解为其前端和后端组件。通过这样做,我们将全面了解每个组件在应用程序中扮演的独特角色和功能。

将代码结构分解为前端和后端

在软件开发的世界里,前端和后端就像阴阳一样——相反但相辅相成,共同提供和谐的数字体验。“阴阳”是中国哲学中的一个概念,描述了相反但相互关联的力量。

简而言之,将应用程序分解为其前端和后端组件提供了关注点的清晰分离,促进了代码的重用性和可移植性,实现了可扩展性和性能优化,并促进了协作和并行开发。这种方法最终有助于网络应用程序开发过程的总体成功。

在 20 世纪 90 年代末和 21 世纪初,随着基于 Web 的应用程序的兴起,软件开发中开始重视将前端和后端组件分离。在此期间,Web 技术瞬息万变,对可扩展和模块化应用程序的需求变得明显。

早在 2000 年代初期,JavaScript 框架如 jQuery 的引入使得前端用户界面更加动态和交互。这导致了网络应用程序的表现层(前端)和数据处理层(后端)之间更加清晰的区分。

随着 单页应用程序SPAs)的出现以及 AngularJS、React 和 Vue.js 等 JavaScript 框架和库的普及,前端和后端之间的分离变得更加标准化和广泛采用。SPAs 将渲染和管理 UI 的责任转移到了客户端,而后端 API 处理数据检索和操作。

现在我们已经讨论了分解代码结构的关键原因,让我们来检查 Bizza 网络应用程序的前端和后端组件。

下面的代码结构代表了前端和后端之间的高端级别代码拆分。这使我们能够分离关注点并提高代码的可重用性:

bizza/├── backend/
├── frontend/

前端结构

首先,让我们提供一个详细的 frontend 结构概述:

frontend/├── node_modules
├── package.json
├── public
    ├──favicon.ico
├──index.html
├── src
    ├──components/
    ├──pages/
    ├──hooks/
    ├──assets/
    └──App.js
    └──App.css
    └──index.js
    └──index.css
    └──setupTests.js
├──.gitignore
├──.prettierrc
├──package-lock.json
├──package.json
├──README.md

前端代码结构主要包括 node_modulespackage.jsonpublicsrc.gitignore.prettierrcpackage-lock.jsonREADME.md

让我们快速分析主要的目录和文件:

  • node_modules: 此目录包含你的应用程序所依赖的所有包(库和框架)。这些包列在 package.json 文件的 dependenciesdevDependencies 部分中。

  • package.json: 此文件包含有关你的应用程序的元数据,包括其名称、版本和依赖项。它还包括你可以用来构建、测试和运行应用程序的脚本。

  • public: 此目录包含你的应用程序将使用的静态资源,例如 favicon 和主 HTML 文件(index.html)。

  • src: 此目录包含你的应用程序的源代码。它组织成组件、页面、钩子和资产的子目录。src 目录对于采用的 React 前端设计模式至关重要。components 文件夹包含我们打算在 Bizza 应用程序中使用的所有组件,pages 包含应用程序的展示组件,hooks 包含自定义钩子,最后,assets 文件夹包含应用程序中使用的所有资产,例如 imageslogossvg

  • .gitignore: 此文件告诉 Git 在你将代码提交到仓库时应忽略哪些文件和目录。

  • .prettierrc: 此文件指定了 Prettier 代码格式化工具的配置选项。Prettier 是一款流行的代码格式化工具,它确保你的代码库风格一致。它通常放置在 JavaScript 项目的 root 目录中,并包含用于定义格式化规则的 JSON 语法。

  • package-lock.json: 此文件记录了应用程序所依赖的所有包的确切版本,以及这些包所依赖的任何包。它确保每次安装应用程序时,它都使用其依赖项的相同版本。

  • README.md: 此文件包含你的应用程序的文档,例如安装和运行它的说明。

后端结构

接下来,我们将检查后端的结构:

backend/├── app.py
├── models
├── config
│   ├── config.py
├── .flaskenv
├── requirements.txt

上述内容代表了 Flask 后端应用程序的文件和目录结构。

让我们分解目录和文件:

  • app.py: 此文件包含你的后端应用程序的主要代码,包括处理 HTTP 请求的路由和逻辑。

  • models: 此目录包含数据库模型定义的每个模型的模块。

  • config: 此目录包含应用程序的配置选项文件,例如数据库连接字符串或密钥。

  • .flaskenv: 此文件包含特定于 Flask 应用的环境变量。

  • requirements.txt: 此文件列出了应用程序所依赖的包,包括任何第三方库。你可以通过运行 pip install -r requirements.txt 来使用此文件安装必要的依赖项。

接下来,我们将了解如何配置 React 前端并准备它以消费后端 API 服务。

为 API 消费配置 React 前端

在本节中,您将配置前端 React 应用通过在 React 中设置代理与后端 Flask 服务器进行通信,以从 Flask 服务器消费 API。

为了配置 React 代理以用于 API 消费,您需要更新前端 React 应用的package.json文件中的proxy字段。proxy字段允许您指定一个 URL,该 URL 将用作从 React 应用发出的所有 API 请求的基础。

让我们更新package.json文件:

  1. 使用文本编辑器在project目录中打开package.json文件,然后向package.json文件中添加一个proxy字段,并将其设置为 Flask 服务器的 URL:

    {  "name": "bizza",  "version": "0.1.0",  "proxy": "http://localhost:5000"}
    
  2. 接下来,您需要从 React 前端向 Flask 服务器发送 HTTP 请求。我们将使用Fetch()方法作为 Axios 的替代方案。

    Axios 是一个允许您从浏览器中发送 HTTP 请求的 JavaScript 库。它是一个基于 promise 的库,使用现代技术使异步请求变得容易处理。使用 Axios,您可以发送 HTTP 请求从服务器检索数据,提交表单数据,或将数据发送到服务器。

    Axios 支持多种不同的请求方法,如GETPOSTPUTDELETEPATCH,并且它可以处理 JSON 和 XML 数据格式。Axios 在开发者中很受欢迎,因为它有一个简单直接的 API,使得初学者和经验丰富的开发者都很容易使用。

    Axios 还具有许多使它灵活强大的功能,例如自动转换数据、支持拦截器(允许您在发送或接收之前修改请求或响应),以及取消请求的能力。

  3. 您可以通过在终端中运行以下命令来安装 Axios:

    npm install axios
    

    一旦安装了 Axios,您就可以使用它从 React 前端向 Flask 服务器发送 HTTP 请求。

  4. 确保前端 React 应用和后端 Flask 服务器在不同的端口上运行。默认情况下,React 开发服务器在端口3000上运行,而 Flask 开发服务器在端口5000上运行。

接下来,您需要在 Flask 后端定义路由和函数来处理来自 React 前端发出的 HTTP 请求。

使 Flask 后端准备就绪

第一章使用 React 和 Flask 准备全栈开发环境部分,我们为 Flask 服务器设置了开发环境。请确保您的虚拟环境已激活。您可以通过运行以下命令来实现:

  • 对于 Mac/Linux

    source venv/bin/activate
    
  • 对于 Windows

    Venv/Scripts/activate
    

您的虚拟环境现在应该已激活,并且您的终端提示符应该以虚拟环境名称为前缀(例如,(``venv) $)。

接下来,让我们直接进入定义事件注册路由,该路由作为 Bizza 应用程序模型要求的一部分。

让我们添加一个模型来处理活动参加者的注册。你将在下一节中使用它来接受来自 React 前端的要求,在那里我们将处理 React 和 Flask 中的表单输入。

应用程序根目录中的app.py文件仍然是 Flask 应用程序的主要入口点。更新app.py以以下代码片段定义模型和端点以处理活动注册:

class EventRegistration(db.Model):    __tablename__ = 'attendees'
    id = db.Column(db.Integer, primary_key=True)
    first_name = db.Column(db.String(100), unique=True, nullable=False)
    last_name = db.Column(db.String(100), unique=True, nullable=False)
    email = db.Column(db.String(100), unique=True, nullable=False)
    phone = db.Column(db.String(100), unique=True, nullable=False)
    job_title = db.Column(db.String(100), unique=True, nullable=False)
    company_name = db.Column(db.String(100), unique=True,         nullable=False)
    company_size = db.Column(db.String(50), unique=True,         nullable=False)
    subject = db.Column(db.String(250), nullable=False)
def format(self):
    return {
        'id': self.id,
        'first_name': self.first_name,
        'last_name': self.last_name,
        'email': self.email,
        'phone': self.phone,
        'job_title': self.job_title,
        'company_name': self.job_title,
        'company_size': self.company_size,
        'subject': self.subject
    }

在前面的代码片段中,EventRegistration类代表数据库中活动注册的模型。

__tablename__属性指定了数据库中存储此模型的表的名称。db.Model类是Flask-SQLAlchemy中所有模型的基类,db.Column对象定义了模型字段,每个字段都有一个类型和一些附加选项。

format方法返回模型实例的字典表示形式,键对应字段名称,值对应字段值。

现在,让我们定义路由或端点,/api/v1/events-registration

@app.route("/api/v1/events-registration", methods=['POST'])def add_attendees():
    if request.method == 'POST':
        first_name = request.get_json().get('first_name')
        last_name = request.get_json().get('last_name')
        email = request.get_json().get('email')
        phone = request.get_json().get('phone')
        job_title = request.get_json().get('job_title')
        company_name = request.get_json().get('company_name')
        company_size = request.get_json().get('company_size')
        subject = request.get_json().get('subject')
        if first_name and last_name and email and phone and subject:
            all_attendees = EventRegistration.query.filter_by(
                email=email).first()
            if all_attendees:
                return jsonify(message="Email address already                     exists!"), 409
            else:
                new_attendee = EventRegistration(
                    first_name = first_name,
                    last_name = last_name,
                    email = email,
                    phone = phone,
                    job_title = job_title,
                    company_name = company_name,
                    company_size = company_size,
                    subject = subject
                )
                db.session.add(new_attendee)
                db.session.commit()
                return jsonify({
                    'success': True,
                    'new_attendee': new_attendee.format()
                }), 201
        else:
            return jsonify({'error': 'Invalid input'}), 400

/api/v1/events-registration端点函数处理对/api/v1/events-registration路由的 HTTP POST请求。此端点允许用户通过提供他们的姓名、电子邮件地址、电话号码和主题来注册活动。

端点函数首先检查请求方法是否确实是POST,然后从请求体中提取名称、电子邮件、电话和主题值,预期请求体为 JSON 格式。

接下来,该函数检查所有必需的输入值(first_namelast_nameemailphonesubject)是否都已存在。如果存在,它将检查数据库中是否已存在具有相同电子邮件地址的参与者。如果存在,它将返回一个 JSON 响应,其中包含一条消息指出电子邮件地址已被使用,以及 HTTP 409状态码(冲突)。

如果电子邮件地址未被使用,该函数将使用输入值创建一个新的EventRegistration对象,将其添加到数据库会话中,并将更改提交到数据库。然后,它将返回一个包含成功消息和新的参与者详情的 JSON 响应,以及 HTTP 201状态码(已创建)。

如果任何必需的输入值缺失,该函数将返回一个包含错误消息和 HTTP 400状态码(错误请求)的 JSON 响应。现在,让我们更新数据库并添加一个eventregistration表格。eventregistration表格将接受所有活动注册的条目。

以下步骤在数据库中创建eventregistration表格。在project目录的终端中,输入以下命令:

flask shellfrom app import db, EventRegistration
db.create_all()

或者,你可以继续使用迁移工具:

flask db migrate –m "events attendee table added"flask db upgrade

使用这些选项中的任何一个,后端都将包含新的表格。

在终端中执行flask run以在localhost上使用默认端口(5000)启动 Flask 开发服务器。

就这样!后端现在已准备好接收来自 React 前端的表单条目。让我们在 React 中设计表单组件并将表单条目提交到 Flask 后端。

处理 React 和 Flask 中的表单

在 Web 开发中,处理 React 前端和 Flask 后端的表单是一种常见模式。在这个模式中,React 前端向 Flask 后端发送 HTTP 请求以提交或检索表单数据。

在 React 前端方面,你可以使用表单组件来渲染表单并处理表单提交。你可以使用受控组件,如 inputtextareaselect,来控制表单值并在用户输入数据时更新组件状态。

当用户提交表单时,你可以使用事件处理器来阻止默认的表单提交行为,并使用类似 Axios 的库向 Flask 后端发送 HTTP 请求。在本节中,我们将使用 Axios 库。

在 Flask 后端方面,你可以定义一个路由来处理 HTTP 请求并从请求对象中检索表单数据。然后你可以处理表单数据并向前端返回响应。

EventRegistration 组件为未认证用户提供了一个简单的表单,用于在 Bizza 应用程序的前端注册活动。该表单包括用户姓名、电子邮件地址、电话号码和主题字段——即他们注册的活动主题或标题。

让我们深入了解与 Flask 后端协同工作的 React 表单实现:

  1. 在项目目录中,在 components 文件夹内创建 EventRegistration/EventRegistration.jsx

  2. 将以下代码片段添加到 EventRegistration.jsx 文件中:

    import React, { useState, useEffect } from 'react';import axios from 'axios';const EventRegistration = () => {  // Initial form values  const initialValues = {    firstname: '',    lastname: '',    email: '',    phone: '',    job_title: '',    company_name: '',    company_size: '',    subject: '' };  // State variables  const [formValues, setFormValues] =    useState(initialValues); // Stores the form field                                values  const [formErrors, setFormErrors] = useState({});// Stores the form field for the validation errors  const [isSubmitted, setIsSubmitted] =    useState(false); // Tracks whether the form has                        been submitted{/* Rest of the form can be found at the GitHub link - https://github.com/PacktPublishing/Full-Stack-Flask-Web-Development-with-React/tree/main/Chapter-10/ */}            <div id="btn-section">              <button>Join Now</button>            </div>        </form>      </div>    </div>  </div></>);};export default EventRegistration;
    POST request to the /api/v1/events-registration route with the form data. It then updates the component’s state with the response from the server and displays a success or error message to the user.
    

    EventRegistration 组件还包括一个 validate 函数,用于检查表单值中的错误,以及一个 onChangeHandler 函数,用于在用户输入表单字段时更新表单值。

让我们讨论前面代码中使用的组件状态变量:

  • 表单值: 这是一个对象,用于存储表单字段的当前值(姓名、电子邮件、电话和主题)

  • 表单错误: 这是一个对象,用于存储在表单值中发现的任何错误

  • response: 这是一个对象,用于存储表单提交后从服务器返回的响应

  • 反馈: 这是一个字符串,用于存储要显示给用户的反馈消息(例如,注册成功!

  • 状态: 这是一个字符串,用于存储表单提交的状态(例如,成功错误

我们然后定义以下函数:

  • validate: 这是一个函数,接受表单值并返回一个包含在值中发现的任何错误的对象。

  • onChangeHandler: 这是一个函数,用于在用户在表单字段中输入时更新 表单值 状态变量。

  • handleSubmit:这是一个在表单提交时被调用的函数。它阻止默认的表单提交行为,调用 validate 函数来检查错误,然后使用 sendEventData 函数将表单数据发送到服务器。它还会根据服务器的响应更新反馈和状态状态变量。

  • sendEventData:这是一个 async 函数,它向 /api/v1/events-registration 路由发送带有表单数据的 HTTP POST 请求,并使用服务器的响应更新响应状态变量。

EventRegistration 组件同样包含一个 useEffect 钩子,当 formValues 状态变量发生变化时,会调用 sendEventData 函数。最后,EventRegistration 组件渲染一个包含表单字段的表单元素,并向用户显示反馈信息和状态。

现在,使用 npm start 启动 React 前端并提交您的表单条目。确保 Flask 服务器也在运行。在任何一个开发过程中,问题和错误都是不可避免的。我们将探讨一些有价值的故障排除技巧,帮助您在 React 前端和 Flask 后端集成过程中调试和修复问题。

React 前端和 Flask 后端的故障排除技巧

将 React 前端与 Flask 后端集成可以是一个强大的组合,用于构建动态和可扩展的 Web 应用程序。然而,像任何集成一样,它可能带来自己的一套不可避免的问题。在 React-Flask 集成过程中出现问题时,需要系统的方法来识别和有效地解决问题。

本节将讨论您在将前端与后端集成时可能遇到的某些问题的解决方法。通过遵循这些技巧,您将能够诊断并解决在应用程序的开发和部署过程中可能出现的常见问题。

让我们深入了解 React 前端和 Flask 后端集成的故障排除技巧:

  • 验证 Flask 设置

    • 确保 Flask 已正确配置并在服务器上运行。

    • 检查 Flask 服务器控制台中的任何错误消息或异常,这些可能表明配置错误。

    • 确认已安装必要的 Flask 包和依赖。

    • 通过测试基本端点来验证 Flask 服务器是否可访问并响应请求。

  • 检查 React 配置

    • 确保 React 应用程序已正确配置并运行。

    • 确认 React 项目中已安装必要的依赖和包。

    • 在浏览器开发者工具的控制台中检查任何可能表明前端设置问题的 JavaScript 错误或警告。

    • 确保在 package.json 中添加了代理属性,并指向 Flask 服务器地址 - 例如,http://127.0.0.1:5000

  • 调查 网络请求

    • 使用浏览器的开发者工具来检查 React 应用程序发出的网络请求

    • 确认请求是否发送到正确的 Flask 端点

    • 检查网络响应状态码以识别任何服务器端错误

    • 检查响应负载以确保数据正确传输

    • 如果 React 前端和 Flask 后端托管在不同的域名或端口上,请注意跨源资源共享CORS)问题

通过遵循这些故障排除技巧,您将具备诊断和解决 React-Flask 集成问题的必要知识。这将确保您的 Web 应用程序集成平稳且健壮。

摘要

在本章中,我们广泛讨论了应用程序代码结构以及集成 React 前端与 Flask 后端所需的一些关键步骤。首先,您需要设置前端以与后端通信,使用 HTTP 客户端库,并处理表单和用户输入。

然后,您需要设置 Flask 后端,包括必要的路由和函数来处理前端发出的请求并处理表单数据。最后,您需要测试整个应用程序以确保其正确且按预期工作。

通过这些步骤,您可以成功地将 React 前端与 Flask 后端集成到您的 Web 应用程序中。在下一章中,我们将通过创建更多表格来扩展 React-Flask 交互。这些表格将具有关系,我们将能够获取并显示数据。

第十一章:在 React-Flask 应用程序中获取和显示数据

在上一章中,您成功地将 React 前端集成到 Flask 后端。这是全栈 Web 开发者旅程中的一个重要里程碑。在本章中,您将在此基础上继续学习,并深入探讨全栈 Web 应用程序中的数据获取。

在 Web 应用程序中,数据获取非常重要,因为它允许应用程序从后端服务器、API 或数据库中检索数据并向用户显示这些数据。如果没有获取数据的能力,Web 应用程序将仅限于显示硬编码的数据,这不会很有用或动态。通过从后端服务器或 API 获取数据,应用程序可以向用户显示最新和动态的数据。

此外,数据获取通常与用户交互和数据更新结合使用,使应用程序能够执行诸如在数据库或 API 中插入、更新或删除数据等操作。这使得应用程序能够更加互动并对用户的操作做出响应。

在本章中,您将了解数据获取的复杂性及其在 Web 应用程序中的关键作用,更重要的是,它如何涉及将 React 前端与 Flask 后端集成。您将了解数据获取在使 Web 应用程序能够从后端服务器或 API 获取数据并确保显示当前和动态信息方面的作用。

我们将讨论结合用户交互使用数据获取来执行诸如检索、插入、更新或删除数据库或 API 中的数据等操作。最后,我们将讨论如何在 React-Flask 应用程序中管理分页。

到本章结束时,您将了解如何向数据库中添加数据、显示数据库数据以及如何在 React-Flask Web 应用程序中处理分页。

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

  • 获取和显示数据 – React-Flask 方法

  • 向数据库中添加数据 – React-Flask 方法

  • 编辑数据 – React-Flask 方法

  • 从数据库中删除数据 – React-Flask 方法

  • 在 React-Flask 应用程序中管理分页

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter11

由于页面数量限制,一些代码块已被截断。请参阅 GitHub 以获取完整代码。

获取和显示数据 – React-Flask 方法

在本章中,首先,我们将检索演讲者的数据并将其显示给应用程序的用户。但在进入这一部分之前,让我们进行一些代码重构。你需要重构后端以适应项目目录中app.py文件内容的增长。将代码划分为不同的组件可以改善应用程序的整体结构和组织。

而不是将所有代码放在一个模块中,你可以将代码结构化以分离关注点。我们将在第十四章中讨论更多关于大型应用程序的代码结构化,即模块化架构 – 蓝图的威力。通过这种代码拆分,开发者可以轻松地定位和修改代码库的特定部分,而不会影响其他组件。这种模块化方法也促进了代码的可重用性。

现在,回到代码,你将在后端项目目录(bizza/backend/models.py)中添加models.py,以存放所有数据库交互的模型。这将帮助我们分离应用程序的关注点。app.py文件将用于处理端点和它们相关的逻辑,而models.py文件包含应用程序数据模型。

重新结构化的app.pymodels.py文件可以在 GitHub 上找到,网址为github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter11

实质上,我们将为我们的Bizza应用程序模拟一个管理页面,以便我们可以创建、显示和编辑演讲者数据,并通过管理页面进行分页管理。目前,我们仅为了演示目的设置管理页面;我们不会去烦恼数据验证、身份验证和授权的实现。

在本节中,重点将是如何从后端检索数据并在 React 前端显示它。能够从数据库中显示数据非常重要,因为它允许你以视觉和交互式的方式向用户展示数据。通过在 Web 应用程序中显示数据,你可以创建一个用户友好的界面,使用户能够查看、搜索、过滤和按需操作数据。

为了创建一个功能强大且有用的 Web 应用程序,你需要从数据库中获取并显示数据。为了从后端检索数据,我们将使用 Axios 进行网络请求。你可以使用 Axios 向后端服务器发送GET请求并检索所需的数据。

让我们深入了解如何从后端检索演讲者列表及其详细信息,并在我们的Bizza应用程序的管理页面中显示它们。

从 Flask 中检索演讲者列表

Flask 后端将通过简单的 API 管理演讲者的列表及其详细信息。在models.py中,添加以下代码以创建Speaker模型类:

from datetime import datetimeclass Speaker(db.Model):
    __tablename__ = 'speakers'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(100), nullable=False)
    company = db.Column(db.String(100), nullable=False)
    position = db.Column(db.String(100), nullable=False)
    bio = db.Column(db.String(200), nullable=False)
    speaker_avatar = db.Column(db.String(100),
        nullable=True)
    created_at = db.Column(db.DateTime,
        default=datetime.utcnow)
    updated_at = db.Column(db.DateTime,
        default=datetime.utcnow, onupdate=datetime.utcnow)
    def __repr__(self):
        return f'<Speaker {self.name}>'
    def serialize(self):
        return {
            'id': self.id,
            'name': self.name,
            'email': self.email,
            'company': self.company,
            'position': self.position,
            'bio': self.bio,
            'speaker_avatar': self.speaker_avatar,
            'created_at': self.created_at.isoformat(),
            'updated_at': self.updated_at.isoformat()
        }

上述代码定义了一个Speaker模型,并具有__repr__()serialize()方法。__repr__方法是 Python 中的一个内置方法,用于创建对象的字符串表示。在这种情况下,它用于创建Speaker对象的字符串表示。

serialize()方法用于将Speaker对象转换为字典格式,可以轻松地转换为 JSON。这在您需要将Speaker对象作为 API 端点的响应返回时非常有用。

该方法返回一个包含Speaker对象所有属性的字典,如idnameemailcompanypositionbiospeaker_avatarcreated_atupdated_atcreated_atupdated_at属性使用isoformat()方法转换为字符串格式。

现在,让我们创建一个端点来处理显示演讲者数据的逻辑:

@app.route('/api/v1/speakers', methods=['GET'])def get_speakers():
    speakers = Speaker.query.all()
    if not speakers:
        return jsonify({"error": "No speakers found"}), 404
    return jsonify([speaker.serialize() for speaker in
        speakers]), 200

上述代码使用get_speakers()函数从数据库中检索演讲者列表。现在,您需要更新 React 前端目录以消费 API 演讲者列表端点。

在 React 中显示数据

在 React 前端,您需要创建一个路由,在http://127.0.0.1:3000/admin路径上渲染一个组件。

以下代码片段将创建用于管理员的路由系统:

const router = createBrowserRouter([  {
    path: "/admin",
    element: <AdminPage/>,
    children: [
      {
        path: "/admin/dashboard",
        element: <Dashboard />,
      },
      {
        path: "/admin/speakers",
        element: <Speakers />,
      },
      {
        path: "/admin/venues",
        element: <Venues />,
      },
      {
        path: "/admin/events",
        element: <Events />,
      },
      {
        path: "/admin/schedules",
        element: <Schedules />,
      },
      {
        path: "/admin/sponsors",
        element: <Sponsors />,
      },
    ],
  },
]);

现在,让我们在/src/pages/Admin/AdminPage/AdminPage.jsx文件中创建AdminPageAdminPage将作为管理员的索引组件页面,并渲染必要的组件,包括演讲者的 CRUD 操作。

将以下代码添加到AdminPage.jsx文件中:

import React from "react";import { Outlet } from "react-router-dom";
import Sidebar from
    "../../../components/admin/Sidebar/Sidebar";
import './AdminPage.css'
const AdminPage = () => {
    return (
        <div className="container">
            <div><Navbar/></div>
            <div><Outlet /></div>
        </div>
    );
};
export default AdminPage;

上述代码显示了AdminPage组件,它代表了admin页面的结构和内容。Sidebar组件被导入并作为子组件渲染,以渲染管理员的侧边菜单列表。然后,我们有从react-router-dom包中导入的Outlet组件,它用于渲染当前路由的特定内容。

接下来,我们将创建一个用于查看数据库中演讲者列表的数据获取组件。

使用 ViewSpeakers 组件显示演讲者列表

我们将使用ViewSpeakers组件开始对演讲者的 CRUD 操作,该组件将处理从后端到管理员用户的演讲者数据的显示。

首先,我们将创建一个名为SpeakersAPI.js的模块来处理所有的 API 调用。SpeakersAPI.js模块封装了 API 调用,抽象出了制作 HTTP 请求的低级细节。这还将允许应用程序的其他部分以更直接的方式与 API 交互,而无需直接处理 Axios 库的复杂性。总的来说,您会从拥有这个独立的模块来处理 API 调用中受益,因为它促进了代码的组织、可重用性、错误处理、头部管理以及代码库的可扩展性和可维护性。

现在,让我们深入了解SpeakersAPI模块。

bizza/frontend/src 项目目录中,创建 SpeakersAPI.js 并添加以下代码片段:

import axios from 'axios';const API_URL = 'http://localhost:5000/api/v1';
// Function to handle errors
const handleErrors = (error) => {
    if (error.response) {
    // The request was made and the server responded with a
       status code
    console.error('API Error:', error.response.status,
        error.response.data);
    } else if (error.request) {
    // The request was made but no response was received
    console.error('API Error: No response received',
        error.request);
    } else {
    // Something else happened while making the request
    console.error('API Error:', error.message);
    }
    throw error;
};
// Function to set headers with Content-Type:
   application/json
const setHeaders = () => {
    axios.defaults.headers.common['Content-Type'] =
        'application/json';
};
// Function to get speakers
export const getSpeakers = async () => {
    try {
        setHeaders();
        const response =
            await axios.get(`${API_URL}/speakers`);
        return response.data;
    } catch (error) {
        handleErrors(error);
    }
};

前面的代码为使用 Axios 向 API 发送 HTTP 请求设置了基本配置,并提供了一个从 API 获取演讲者的函数。它处理错误并设置请求所需的必要头信息。

接下来,我们将定义 ViewSpeakers 组件并使用前面的 SpeakersAPI 模块。

src/pages/Admin/Speakers/ 目录中创建 ViewSpeakers.js 组件并添加以下代码:

import React, { useEffect, useState } from 'react';import { getSpeakers } from
    '../../../services/SpeakersAPI';
const ViewSpeakers = () => {
    const [speakers, setSpeakers] = useState([]);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError] = useState(null);
    const fetchSpeakers = async () => {
        try {
            const speakerData = await getSpeakers();
            setSpeakers(speakerData);
            setIsLoading(false);
        } catch (error) {
            setError(error.message);
            setIsLoading(false);
        }
    };
    useEffect(() => {
        fetchSpeakers();
    }, []);

前面的代码设置了一个名为 ViewSpeakers 的 React 组件,该组件使用 getSpeakers 函数获取演讲者数据并相应地更新组件的状态。它处理加载和错误状态,并在组件挂载时触发数据获取过程。ViewSpeakers.js 的完整代码可以在 GitHub 仓库中找到。

接下来,我们将探讨如何使用 Flask-React 方法将数据添加到数据库中。

将数据添加到数据库 – React-Flask 方法

我们将数据添加到数据库以存储和组织易于访问、管理和更新的信息。这是持久化存储数据的一种方式,了解如何执行此操作是任何全栈开发者的关键要求。这种知识使您能够构建动态和交互式网络应用程序。然后您就有能力高效地检索和使用数据,用于各种目的,如报告、分析和决策制定。

将数据添加到 Flask

现在,让我们创建一个端点来处理将演讲者数据添加到数据库的逻辑:

    @app.route('/api/v1/speakers', methods=['POST'])    def add_speaker():
        data = request.get_json()
        name = data.get('name')
        email = data.get('email')
        company = data.get('company')
        position = data.get('position')
        bio = data.get('bio')
        avatar = request.files.get('speaker_avatar')
        # Save the uploaded avatar
        if avatar and allowed_file(avatar.filename):
            filename = secure_filename(avatar.filename)
            avatar.save(os.path.join(app.config[
                'UPLOAD_FOLDER'], filename))
        else:
            filename = 'default-avatar.jpg'
        if not name or not email or not company or not
            position or not bio:
            return jsonify({"error": "All fields are
                required"}), 400
        existing_speaker =
            Speaker.query.filter_by(email=email).first()
        if existing_speaker:
            return jsonify({"error": "Speaker with that
                email already exists"}), 409
        speaker = Speaker(name=name, email=email,
            company=company, position=position, bio=bio,
                speaker_avatar=avatar)
        db.session.add(speaker)
        db.session.commit()
        return jsonify(speaker.serialize()), 201
  # Function to check if the file extension is allowed
    def allowed_file(filename):
        return '.' in filename and \
            filename.rsplit('.', 1)[1].lower(
            ) in app.config['ALLOWED_EXTENSIONS']

之前的代码定义了一个 /api/v1/speakers 路由,该路由处理添加新演讲者的 POST 请求。它从请求中提取所需的演讲者信息,验证数据,如果提供则保存头像文件,检查重复的电子邮件,创建新的演讲者对象,将其添加到数据库中,并返回包含创建的演讲者数据的响应。

前面的代码显示了在向指定路由发出 POST 请求时执行的 add_speaker 函数。

add_speaker 函数使用 request.get_json() 从请求中检索 JSON 数据,并从数据中提取演讲者的姓名、电子邮件、公司、职位、个人简介和 speaker_avatar(一个上传的文件)。

如果提供了 speaker_avatar 并且文件扩展名是允许的(在 allowed_file 函数检查后),则将头像文件以安全文件名保存到服务器的上传文件夹中。否则,将分配一个默认的头像文件名。

函数随后检查是否提供了所有必需的字段(nameemailcompanypositionbio)。如果任何字段缺失,它将返回一个包含错误消息和状态码 400(错误请求)的 JSON 响应。

接下来,add_speaker() 函数查询数据库以检查是否存在具有相同电子邮件的演讲者。如果找到具有相同电子邮件的演讲者,它将返回一个包含错误消息和状态码 409(冲突)的 JSON 响应。

如果演讲者是新的(没有具有相同电子邮件的现有演讲者),则使用提供的信息(包括头像文件)创建一个新的 Speaker 对象。然后,演讲者被添加到数据库会话并提交。

最后,add_speaker() 函数返回一个包含序列化演讲者数据和状态码 201(已创建)的 JSON 响应,以指示演讲者创建成功。该代码还包括一个辅助函数 allowed_file,该函数根据应用程序的配置检查给定的文件名是否具有允许的文件扩展名。

接下来,我们将设置 React 组件以将演讲者数据添加到后端。

使用 CreateSpeaker 组件将演讲者数据添加到后端

在本节中,我们将向后端添加演讲者数据。我们将创建一个名为 CreateSpeaker 的组件。此组件将处理添加新演讲者的表单输入并将数据发送到后端 API 以进行存储。

首先,我们将 AddSpeaker 函数添加到 API 调用服务模块 SpeakersAPI.js

// API function to add a speakerexport const addSpeaker = (speakerData) => {
    const url = `${API_URL}/speakers`;
    return axios
        .post(url, speakerData, { headers: addHeaders() })
        .then((response) => response.data)
        .catch(handleErrors);
};

上述代码提供了一个 addSpeaker 函数,该函数使用 Axios 向后端 API 发送 POST 请求以添加新的演讲者。它适当地处理请求、响应和错误情况。

现在,我们将在 src/pages/Admin/Speakers 内创建 CreateSpeaker.js 组件并添加以下代码:

import React, { useState } from 'react';import { addSpeaker } from
    '../../../services/SpeakersAPI'LP;
import { useNavigate } from 'react-router-dom';
const CreateSpeaker = () => {
    const [name, setName] = useState('');
{/* Rest of inputs states */}
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(null);
    const [successMessage, setSuccessMessage] =
        useState('');
    const navigate = useNavigate();
    const handleSubmit = async (event) => {
        event.preventDefault();
        setIsLoading(true);
        setError(null);
        try {
            ...
            await addSpeaker(formData);
            setIsLoading(false);
            // Reset the form fields
            setName('');
            setEmail('');
            setCompany('');
            setPosition('');
            setBio('');
            setAvatar(null);
            // Display a success message
            ...
        )}
        </div>
    );
};
export default CreateSpeaker;

上述代码定义了一个 CreateSpeaker 组件,该组件处理新演讲者的创建。它管理表单输入值、头像文件选择、加载状态、错误消息和成功消息。当表单提交时,该组件将数据发送到后端 API 并相应地处理响应:

  • 组件导入必要的依赖项,包括 ReactuseState 钩子、从 SpeakersAPI 导入的 addSpeaker 函数以及从 react-router-dom 导入的 useNavigate 钩子。

  • CreateSpeaker 组件内部,它使用 useState 钩子设置状态变量以存储表单输入值(nameemailcompanypositionbio)、头像文件、加载状态、错误消息和成功消息。CreateSpeaker 组件还使用 useNavigate 钩子来处理导航。

  • 组件定义了一个 handleSubmit 函数,当表单提交时触发。它首先阻止默认的表单提交行为。然后,将加载状态设置为 true 并清除任何之前的错误消息。在 handleSubmit 函数内部,组件构造一个 FormData 对象并将表单输入值和头像文件附加到它。

  • SpeakersAPI 导入的 addSpeaker 函数与构造的 FormData 对象一起调用,该对象向后端 API 发送 POST 请求以创建新的演讲者。

  • 如果请求成功,将加载状态设置为 false,并重置表单输入值。显示成功消息,并将用户导航到/speakers页面。如果在 API 请求过程中发生错误,将加载状态设置为 false,并将错误消息存储在状态中。

  • 该组件还包括一个handleAvatarChange函数,用于在头像输入字段中选择文件时更新头像状态变量。

  • 组件的渲染函数返回 JSX 元素,包括带有表单输入和提交按钮的表单。它还根据相应的状态变量显示错误和成功消息。

现在,让我们进入下一节,探索如何在 React-Flask 应用程序中编辑数据。

数据编辑 – React-Flask 方法

除了显示和添加数据外,对于 Web 应用程序来说,允许用户编辑数据也很重要。在本节中,您将学习如何在 React-Flask Web 应用程序中实现数据编辑。

在 Flask 中编辑数据

现在,让我们添加端点来处理在数据库中更新演讲者数据的逻辑。将以下代码添加到app.py中:

from flask import jsonify, requestfrom werkzeug.utils import secure_filename
@app.route('/api/v1/speakers/<int:speaker_id>',
    methods=['PUT'])
def update_speaker(speaker_id):
    data = request.get_json()
    name = data.get('name')
    email = data.get('email')
    company = data.get('company')
    position = data.get('position')
    bio = data.get('bio')
    avatar = request.files.get('speaker_avatar')
    speaker = Speaker.query.get(speaker_id)
    if not speaker:
        return jsonify({"error": "Speaker not found"}), 404
    if not all([name, email, company, position, bio]):
        return jsonify({"error": "All fields are
            required"}), 400
    if email != speaker.email:
        existing_speaker =
            Speaker.query.filter_by(email=email).first()

上述代码定义了一个新的路由,用于在/api/v1/speakers/int:speaker_id端点更新演讲者的信息,该端点接受PUT请求。使用@app.route装饰器定义端点,并将methods参数设置为['PUT'],以指定此路由只能接受PUT请求。《int:speaker_id》部分是路径参数,允许路由接受演讲者 ID 作为 URL 的一部分。

代码定义了update_speaker函数,它接受一个speaker_id参数,该参数对应于端点中的路径参数。

代码首先获取请求的 JSON 有效负载,并从中提取演讲者的信息。然后,使用Speaker.query.get(speaker_id)方法从数据库中检索演讲者的信息。该函数根据提供的speaker_id查询数据库以检索现有的演讲者对象。如果没有找到演讲者,它将返回一个包含错误消息和状态码404(未找到)的 JSON 响应。

update_speaker()检查是否提供了所有必需的字段(nameemailcompanypositionbio)。如果任何字段缺失,它将返回一个包含错误消息和状态码400(错误请求)的 JSON 响应。

如果保存图像时出现异常,它将删除之前的头像图像,并返回错误消息和状态码。然后,update_speaker函数更新数据库中的演讲者信息。update_speaker函数尝试将更改提交到数据库;如果失败,它将回滚事务并返回错误消息和状态码500

最后,如果一切顺利,代码将返回更新后的演讲者信息作为 JSON 对象和状态码200

接下来,我们将创建一个 React 组件来处理更新演讲者数据。

在 React 中显示编辑后的数据

在本节中,我们将提供编辑演讲者信息的功能。要在 React 中编辑数据,我们可以通过修改组件的状态来使用更新的值,并在用户界面中反映这些更改。我们将首先添加UpdateSpeaker组件。在frontend/src/pages/Admin/Speakers/UpdateSpeaker.js中,添加以下代码:

import React, { useState, useEffect } from 'react';import { updateSpeaker } from
    '../../../services/SpeakersAPI';
import { useNavigate } from 'react-router-dom';
const UpdateSpeaker = ({ speakerId }) => {
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');
    const [company, setCompany] = useState('');
    const [position, setPosition] = useState('');
    const [bio, setBio] = useState('');
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(null);
    const [successMessage, setSuccessMessage] =
        useState('');
    const navigate=useNavigate();
    useEffect(() => {
        // Fetch the speaker data based on speakerId
        fetchSpeaker();
    }, [speakerId]);
    const fetchSpeaker = async () => {
        try {
            // Fetch the speaker data from the backend
                based on speakerId
            const speakerData =
                await getSpeaker(speakerId);
            setName(speakerData.name);
            setEmail(speakerData.email);
            setCompany(speakerData.company);
            setPosition(speakerData.position);
            setBio(speakerData.bio);
        } catch (error) {
            setError(error.message);
        }
    };
{/* The rest of the code snippet can be found on GitHub */}

上述代码定义了一个名为UpdateSpeaker的组件。该组件允许用户通过使用SpeakersAPI.js文件中的updateSpeaker函数向服务器发送PUT请求来更新演讲者的信息。

组件首先从 React 库中导入ReactuseStateuseEffect,从SpeakersAPI.js模块中导入updateSpeaker。当表单提交时,将调用handleSubmit函数;它调用SpeakersAPI.js文件中的updateSpeaker函数,并传递speakerId和一个包含更新后的演讲者信息的对象。如果请求成功,它将成功状态设置为 true,如果有错误,它将错误状态设置为error.message

现在,你需要更新src/services/SpeakersAPI.js中的SpeakersAPI.js文件,以添加updateSpeaker API 调用函数:

// API function to update a speakerexport const updateSpeaker = (speakerId, speakerData) => {
    const url = `${API_URL}/speakers/${speakerId}`;
    return axios
        .put(url, speakerData, { headers: addHeaders() })
        .then((response) => response.data)
        .catch(handleErrors);
};

上述代码定义了一个用于在后端更新演讲者信息的updateSpeaker API 函数:

  • 该函数接受两个参数:speakerId(表示要更新的演讲者的 ID)和speakerData(一个包含更新后的演讲者信息的对象)。

  • 它通过将speakerId附加到基本 URL 来构造 API 端点的 URL。

  • 该函数使用 Axios 库向构造的 URL 发送一个PUT请求,将speakerData作为请求负载传递,并使用addHeaders函数包含适当的头信息。

  • 如果请求成功,它返回响应数据。如果在请求过程中发生错误,它捕获错误并调用handleErrors函数来处理和传播错误。

接下来,你将学习如何从数据库中删除演讲者数据。

从数据库中删除数据 – React-Flask 方法

从数据库中删除数据涉及从表中删除一个或多个记录或行。在本节中,你将学习如何在 React-Flask 网络应用程序中处理删除请求。

在 Flask 中处理删除请求

让我们创建一个端点来处理从数据库中删除演讲者数据的逻辑:

@app.route('/api/v1/speakers/<int:speaker_id>',    methods=['DELETE'])
def delete_speaker(speaker_id):
    speaker = Speaker.query.get_or_404(speaker_id)
    if not current_user.has_permission("delete_speaker"):
        abort(http.Forbidden("You do not have permission to
            delete this speaker"))
    events =
        Event.query.filter_by(speaker_id=speaker_id).all()
    if events:
        abort(http.Conflict("This speaker has associated
            events, please delete them first"))
    try:
        if speaker.speaker_avatar:
            speaker_avatar.delete(speaker.speaker_avatar)
        with db.session.begin():
            db.session.delete(speaker)
    except Exception:
        abort(http.InternalServerError("Error while
            deleting speaker"))
    return jsonify({"message": "Speaker deleted
        successfully"}), http.OK

上述代码定义了一个用于删除演讲者的 API 路由,执行必要的检查,从数据库中删除演讲者,处理错误,并返回适当的响应。

接下来,我们将探讨用于处理前端删除请求的 React 组件。

在 React 中处理删除请求

当构建一个 React 应用程序时,你可以通过创建一个与后端 API 交互的组件来处理删除请求以删除演讲者资源。此组件将向适当的端点发送删除请求,处理任何潜在的错误,并根据删除演讲者资源更新组件的状态。

让我们从创建一个 DeleteSpeaker 组件开始。在 frontend/src/pages/Admin/Speakers/DeleteSpeaker.js 中,添加以下代码:

import React, { useState, useEffect } from "react";import { useParams, useNavigate } from "react-router-dom";
import { deleteSpeaker } from "./api/SpeakersAPI";
const DeleteSpeaker = () => {
    const { speakerId } = useParams();
    const navigate = useNavigate();
    const [error, setError] = useState("");
    const [isLoading, setIsLoading] = useState(false);
    const handleDelete = async () => {
        try {
            setIsLoading(true);
            await deleteSpeaker(speakerId);
            setIsLoading(false);
            navigate("/speakers"); // Redirect to speakers
                                      list after successful
                                      deletion
        } catch (err) {
            setIsLoading(false);
            setError("Failed to delete speaker.");
        }
    };
    useEffect(() => {
        return () => {
            // Clear error message on component unmount
            setError("");
        };
    }, []);
    return (
        <div>
            {error && <p className="error">{error}</p>}
            <p>Are you sure you want to delete this
                speaker?</p>
            <button onClick={handleDelete}
                disabled={isLoading}>
                {isLoading ? "Deleting..." : "Delete
                Speaker"}
            </button>
        </div>
    );
};
export default DeleteSpeaker;

上述代码定义了一个组件,允许用户通过 id 删除演讲者。组件首先从 react-router-dom 中导入 useParamsuseNavigate 钩子,以从 URL 中提取 speakerId 值。它还从 src/services/SpeakersAPI.js 中导入 deleteSpeaker 函数,以通过后端 API 调用处理演讲者的删除。然后,组件使用 useState 钩子初始化两个状态变量:errorsuccess

该组件有一个单按钮,点击时会触发 handleDelete 函数。此函数阻止默认的表单提交行为,然后调用 deleteSpeaker 函数,并将 speakerId 作为参数传递。如果删除成功,它将成功状态设置为 true;否则,它将错误状态设置为从 API 返回的错误信息。然后,该组件渲染一条消息,指示删除是否成功或存在错误。

现在,你需要更新 src/api/SpeakersAPI.js 中的 SpeakersAPI.js 文件,以添加 deleteSpeaker API 调用函数:

// API function to delete a speakerexport const deleteSpeaker = async (speakerId) => {
    const url = `/api/v1/speakers/${speakerId}`;
    try {
        const speakerResponse = await axios.get(url);
        const speaker = speakerResponse.data;
        if (!speaker) {
            throw new Error("Speaker not found");
        }
      const eventsResponse = await
          axios.get(`/api/v1/events?speakerId=${speakerId}`
          );
      const events = eventsResponse.data;
      if (events.length > 0) {
        throw new Error("This speaker has associated
            events, please delete them first");
      }
      await axios.delete(url);
      return speaker;
    } catch (err) {
        if (err.response) {
            const { status, data } = err.response;
            throw new Error(`${status}: ${data.error}`);
        } else if (err.request) {
            throw new Error('Error: No response received
                from server');
        } else {
            throw new Error(err.message);
        }
    }
};

上述代码定义了一个 deleteSpeaker 函数,该函数接受一个 speakerId 作为其参数。该函数使用 Axios 库向服务器发送 HTTP 请求。该函数首先尝试通过向 /api/v1/speakers/{speakerId} 端点发送 GET 请求从服务器获取演讲者详细信息。

然后,它会检查演讲者是否存在。如果演讲者不存在,该函数会抛出一个错误,错误信息为向 /api/v1/events?speakerId=${speakerId} 端点发送 GET 请求以获取与演讲者相关的事件列表。然后,它会检查事件长度是否大于 0。如果是,它会抛出一个错误,错误信息为 “此演讲者有关联事件,请先删除” 它们

最后,该函数向 /api/v1/speakers/{speakerId} 端点发送一个 DELETE 请求以删除演讲者。如果在过程中出现错误,该函数会检查错误并抛出一个适当的错误信息。然后,该函数导出 deleteSpeaker 函数,以便在其他应用程序的部分中导入和使用。

接下来,我们将讨论如何在 React-Flask 应用程序中处理分页。

在 React-Flask 应用程序中管理分页

当处理大量数据集时,实现分页对于使大量数据集对用户更易于管理非常重要。分页是一种将大量数据集划分为更小、更易于管理的块(称为页面)的技术。每个页面包含总数据的一个子集,使用户能够以受控的方式浏览数据。

分页提供了一种高效展示大量数据集的方法,通过使数据更易于访问来提高性能并增强用户体验。在本节中,你将学习如何在 React-Flask 网络应用程序中实现分页。要实现分页,你需要对后端服务器进行一些修改以处理分页请求。

你可以使用 Flask-SQLAlchemy 库在后台处理分页。在 Flask 后端,你可以使用 Flask-SQLAlchemy 库的分页功能来实现 speaker 模型的分页。让我们深入了解如何为 Speaker 模型实现分页。

app.py 文件中更新 get_speakers() 函数如下所示:

from flask_sqlalchemy import Pagination@app.route('/api/v1/speakers', methods=['GET'])
def get_speakers():
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 10, type=int)
    speakers = Speaker.query.paginate(page, per_page,
        False)
    if not speakers.items:
        return jsonify({"error": "No speakers found"}), 404
    return jsonify({
        'speakers': [speaker.serialize() for speaker in
            speakers.items],
        'total_pages': speakers.pages,
        'total_items': speakers.total
    }), 200

在前面的代码中,我们使用 Flask-SQLAlchemy 的 paginate() 方法为演讲者集合添加分页功能。pageper_page 参数作为查询参数在 GET 请求中传递。page 的默认值为 1per_page 的默认值为 10

对于 React 前端,你可以在函数组件中使用 useStateuseEffect 钩子来处理分页。

让我们修改 ViewSpeakers 组件并为其添加分页功能:

import React, { useState, useEffect } from 'react';import { getSpeakers } from
    '../../../services/SpeakersAPI';
const ViewSpeakers = () => {
    const [speakers, setSpeakers] = useState([]);
    const [currentPage, setCurrentPage] = useState(1);
    const [speakersPerPage] = useState(10);
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(null);
    useEffect(() => {
        fetchSpeakers();
    }, []);
};
export default ViewSpeakers;

上述代码定义了一个组件,该组件使用分页显示演讲者列表。该组件利用 React 钩子来管理其状态。speakers 状态变量用于存储演讲者列表,而 pageperPage 状态变量分别用于存储当前页码和每页显示的项目数。

使用 useEffect 钩子在组件挂载时以及 pageperPage 状态变量更改时从服务器获取演讲者信息。fetchSpeakers 函数使用 Axios 库向 '/api/v1/speakers?page=${page}&per_page=${perPage}' 端点发起 GET 请求,并将当前页码和每页的项目数作为查询参数传递。

响应数据随后存储在 speakers 状态变量中。ViewSpeakers 组件随后遍历演讲者数组并显示每位演讲者的姓名和电子邮件。该组件还包括两个按钮,一个用于导航到上一页,另一个用于导航到下一页。

这些按钮的 onClick 处理程序相应地更新页面状态变量,并使用 1 防止用户导航到不存在的上一页。

摘要

在本章中,我们详细讨论了如何在 React-Flask 网络应用程序中获取和显示数据。我们考察了处理获取和显示数据的一种方法。您能够从后端开始,定义Speaker模型类,并实现各种端点来处理从数据库中获取数据,以及在该数据库上添加、更新和删除数据。

我们使用了 Axios 库向 Flask 后端发送请求,然后从数据库检索数据并将其以响应的形式返回到前端。React 前端随后处理响应并将数据显示给最终用户。最后,我们实现了分页作为一种高效展示大量数据集并提高 React-Flack 网络应用程序项目性能的方法。

接下来,我们将讨论在 React-Flask 应用程序中的身份验证和授权,并检查确保您的应用程序安全且准备就绪的最佳实践。

第十二章:身份验证和授权

在构建全栈 Web 应用程序时,你通常会希望实现一个系统,让用户信任你处理他们的敏感信息。作为一名全栈 Web 开发者,了解如何实现强大的身份验证和授权机制至关重要。你需要知道如何保护用户数据的安全和应用程序的完整性。想象一下,你正在构建一个允许用户在线购物的电子商务网站。

如果你未能正确地验证和授权用户,那么有人可能会未经授权访问网站并使用他人的个人信息下订单。这可能导致合法用户的财务损失,并损害在线业务或你的客户的声誉。

此外,如果你未能正确地验证和授权用户,这也可能使你的 Web 应用程序容易受到诸如 SQL 注入等攻击,攻击者可以访问存储在数据库中的敏感信息。这可能导致客户数据的丢失,并且可能面临法律后果。

在本章中,我们将深入 Web 安全的世界,探索保护 Flask Web 应用程序的最佳实践和技术。正如著名的计算机科学家布鲁斯·施奈尔(Bruce Schneier)曾经说过:“安全是一个过程,而不是一个产品”(www.schneier.com/essays/archives/2000/04/the_process_of_secur.html)。本章将为你提供了解信息安全的重要性以及如何在 Flask 应用程序中实施它的知识和技能。

从理解身份验证和授权的基本原理到管理用户会话和创建具有安全密码的账户,本章将涵盖 Web 应用程序安全的关键要素。我们将检查保护你的 Flask 应用程序的过程,并展示如何在实践中实现这些概念。

在本章中,你将学习以下主题:

  • 理解信息安全的基本原理

  • 定义 Web 应用程序中的身份验证和身份验证角色

  • 实现密码安全和散列密码

  • 理解 Web 应用程序开发中的访问和授权

  • 将身份验证添加到你的 Flask 应用程序中

  • 识别系统用户并管理他们的信息

  • 会话管理

  • 创建受密码保护的仪表板

  • 在 Flask 中实现闪存消息

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter12

理解信息安全的基本原理

信息安全是 Web 应用程序开发的关键方面。在当今数字时代,个人和敏感信息通常通过 Web 应用程序存储和传输,这使得它们容易受到各种类型的网络安全威胁。这些威胁的范围从简单的攻击,如SQL 注入跨站脚本XSS),到更复杂的攻击,如中间人攻击MITM)和分布式拒绝服务DDoS)。

让我们深入了解一些可能危害您的 Web 应用程序安全性的各种威胁类型:

  • 用户名密码详细信息。如果应用程序容易受到 SQL 注入攻击,攻击者可以在密码字段中输入类似' OR '1'='1的内容。

    SQL 查询可能变为SELECT * FROM users WHERE username = 'username' AND password = '' OR '1'='1';,这可能会让攻击者无需有效密码即可登录。

  • <script>malicious_scripts()</script>,其他查看评论部分的用户可能会无意中执行该脚本。

  • 跨站请求伪造CSRF):这是一种攻击,攻击者诱使用户不知情地向用户已认证的 Web 应用程序发出请求。这可能导致未经用户同意代表用户执行未经授权的操作。

    CSRF 攻击利用了网站对用户浏览器的信任。例如,一个毫无戒心的用户登录到在线银行网站并获取会话 cookie。攻击者创建了一个包含隐藏表单的恶意网页,该表单提交请求将用户的账户资金转移到攻击者的账户。

    用户访问攻击者的网页,并使用用户的会话 cookie 提交隐藏表单,导致未经授权的数据传输。这种攻击利用了网站对用户浏览器执行未经授权操作的信任。

  • 分布式拒绝服务DDoS)攻击:这种攻击涉及从多个来源向目标服务器、服务或网络发送大量流量,使其对合法用户不可访问。例如,攻击者可能使用僵尸网络(被破坏的计算机网络)向 Web 应用程序发送大量流量。这可能导致 Web 应用程序变慢或完全无法向用户提供服务。

然而,有一些方法可以减轻这些恶意威胁,它们能够破坏您的 Web 应用程序。现在,我们将强调一些保护 Web 应用程序的最佳实践。

  • 输入验证:您需要确保所有输入数据都经过适当的清理和验证,以防止 SQL 注入和 XSS 攻击。

  • 在 Flask 中使用SQLAlchemy,为您处理 SQL 查询的构建,并提供一种安全且高效的方式与数据库交互。

  • 密码存储:使用强大的哈希算法和为每个用户生成唯一的盐值来安全地存储密码。

  • 使用 HTTPS:使用 HTTPS 加密客户端和服务器之间的所有通信,以防止窃听和中间人攻击。

  • 会话管理:正确管理会话以防止会话劫持并修复 Web 应用程序中的会话固定漏洞。

  • 访问控制:使用基于角色的访问控制来限制对敏感资源和功能的访问。

  • 日志和监控:您需要持续记录所有应用程序活动的详细日志,并监控可疑活动。

  • 使用最新软件:您需要定期更新框架、库以及 Web 应用程序使用的所有依赖项,以确保已知漏洞得到修补。

  • 使用X-XSS-ProtectionX-Frame-OptionsContent-Security-Policy来防止某些类型的攻击。

  • 定期测试漏洞:定期进行渗透测试和漏洞扫描,以识别和修复任何安全漏洞。

在本章的剩余部分,我们将讨论和实现 Flask Web 应用程序中的身份验证和授权,以帮助您确保您的应用程序及其用户数据的安全。

接下来,我们将讨论 Web 应用程序中的身份验证和身份验证角色。这将提高您对如何验证用户身份以及各种身份验证类型的理解。

定义 Web 应用程序中的身份验证和身份验证角色

身份验证是验证用户身份并确保只有授权用户才能访问应用程序的资源和服务的过程。身份验证是任何 Web 应用程序的重要方面,包括使用 Flask 构建的应用程序。

这通常是通过提示用户提供一组凭证,例如用户名和密码,Web 应用程序可以使用这些凭证来确认用户身份来完成的。在 Web 应用程序开发中,身份验证的目的是确保只有授权用户可以访问敏感信息并在 Web 应用程序中执行某些操作。

在 Web 开发中,我们有几种可以在任何 Web 应用程序项目中使用的身份验证方法。以下是一些最常用的方法:

  • 基于密码的身份验证:这是我们日常使用中最常见的身份验证形式,涉及用户输入用户名/电子邮件和密码以获取对 Web 应用程序的访问权限。这种方法简单易行,但存在其弱点。基于密码的身份验证容易受到暴力破解和字典攻击等攻击。

  • 多因素认证(MFA):这种方法通过要求用户提供多种身份验证形式来增加一个额外的安全层。例如,用户可能需要输入密码,并提供发送到他们的手机或电子邮件的一次性代码。MFA 比基于密码的认证更安全,但可能会对用户体验产生负面影响。

  • 基于令牌的认证:这种方法涉及向用户发放一个令牌,他们必须向 Web 应用程序出示以获得访问权限。令牌可以是 JWT 或 OAuth 令牌的形式,通常存储在浏览器的 cookies 或本地存储中。令牌可以轻松撤销,这使得维护安全性变得更加容易。

  • 生物识别认证:这种方法涉及使用生物特征,如指纹、面部识别或语音识别来验证用户的身份。生物识别认证被认为比其他方法更安全,但实施成本可能更高。

当你决定使用哪种认证方法时,考虑 Web 应用程序所需的安全级别和用户体验至关重要。这些认证方法各有优缺点。选择适合你应用程序的正确方法是至关重要的。

例如,如果你正在构建一个需要高度安全性的网络应用程序,你可能想要考虑使用多因素认证(MFA)或生物识别认证。当然,生物识别认证很少在公共或通用网络应用程序中使用。如果你正在构建一个不需要高度安全性的简单网络应用程序,基于密码的认证可能是安全且足够的。

接下来,我们将讨论在 Flask Web 应用程序中实现密码安全和哈希密码的概念。

实现密码安全和哈希密码

在任何需要访问的 Web 应用程序中,密码通常是防止未经授权访问的第一道防线。作为开发者,你将想要确保在构建 Flask 应用程序时,密码被安全地管理。Web 应用程序中密码管理的关键组成部分是永远不要以明文形式存储密码。

相反,密码应该被哈希处理,这是一个单向加密过程,它产生一个固定长度的输出,无法被逆转。当用户输入他们的密码时,它会被哈希处理并与存储的哈希值进行比较。如果两个哈希值匹配,则密码正确。哈希密码可以帮助保护免受暴力攻击和字典攻击等攻击。

暴力攻击涉及尝试所有可能的字符组合以找到匹配项,而字典攻击则涉及尝试预计算的单词列表。哈希密码使得攻击者无法在计算上逆转哈希并发现原始密码变得不可行。

在 Flask 中,你可以使用 Flask-Bcrypt 这样的库来处理密码哈希。Flask-Bcrypt 是一个 Flask 扩展,为 Flask 提供了 bcrypt 密码哈希功能。Flask-Bcrypt 提供了简单的接口用于哈希和检查密码。你还可以使用 Flask-Bcrypt 生成用于密码哈希的随机盐。

让我们快速通过一个使用 Flask-Bcrypt 进行密码哈希的例子:

from flask import Flask, render_template, requestfrom flask_bcrypt import Bcrypt
app = Flask(__name__)
bcrypt = Bcrypt()
@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == "POST":
        password = request.form.get("password")
        password_hash =
            bcrypt.generate_password_hash(password)
                .decode('utf-8')
        return render_template("index.html",
            password_hash=password_hash)
    else:
        return render_template("index.html")
@app.route("/login", methods=["POST"])
def login():
    password = request.form.get("password")
    password_hash = request.form.get("password_hash")
//Check GitHub for the complete code
if __name__ == "__main__":
    app.run(debug=True)

上一段代码使用了 Flask Bcrypt 库来哈希和检查密码。它导入了 Bcrypt 类和 check_password_hash 函数,使用 Flask 应用程序创建了一个 Bcrypt 实例。当表单提交时,使用 flask_bcrypt 扩展对密码进行哈希处理,并将哈希后的密码在同一页面上显示给用户。render_template 函数用于渲染 HTML 模板,而 Bcrypt 扩展用于安全的密码哈希。

接下来,我们将讨论网络应用程序开发中的访问和授权。

理解网络应用程序开发中的访问和授权

网络应用程序开发中的访问和授权是控制谁可以访问 Web 应用程序中特定资源和操作的过程。作为一个开发者,你将希望设计和确保用户只能执行他们被授权执行的操作,并访问他们被授权访问的资源。

如前所述,认证是验证用户身份的过程。授权是确定用户在 Web 应用程序中可以做什么的过程。当你结合这两个机制时,你就有了一个系统,确保只有授权用户才能访问敏感信息并在 Web 应用程序中执行某些操作。

在 Web 应用程序开发中可以使用多种不同的访问控制方法。我们将讨论其中一些,并具体说明 Flask 如何处理访问和授权:

  • Flask-LoginFlask-Security

  • Flask-OAuthlib。此扩展提供了对 OAuth 1.0aOAuth 2.0 的支持。Flask-OAuthlib 使得开发者在 Flask 应用程序中实现 OAuth 变得容易。

  • Flask-JWTFlask-JWT-Extended

    这些扩展提供了令牌生成、验证和过期等功能,以及根据 JWT 中包含的声明来限制对某些资源和操作的访问,以确保它是由可信源生成的且未被篡改。

  • Flask-RBAC

    Flask-RBAC 扩展提供了角色管理、权限管理和基于用户角色的限制对某些资源和操作的访问的能力。

  • Flask-PoliciesFlask-Policies 提供了策略管理、执行以及根据策略中指定的条件限制对某些资源和操作的访问的能力。

通过使用这些库,你可以轻松处理用户角色和权限,并根据用户的角色限制对某些视图和路由的访问。接下来,我们将探讨如何在 Flask Web 应用程序中实现身份验证。

为你的 Flask 应用程序添加身份验证

JWT 是现代 Web 应用程序中流行的身份验证方法。JWT 是一个经过数字签名的 JSON 对象,可以通过在各方之间传输声明(例如授权服务器和资源服务器)来用于身份验证用户。在 Flask Web 应用程序中,你可以使用PyJWT库来编码和解码 JWT 以进行身份验证。

当用户登录 Flask 应用程序时,后端验证用户的凭据,如他们的邮箱和密码,如果它们有效,则生成 JWT 并发送给客户端。客户端将 JWT 存储在浏览器的本地存储或作为 cookie。对于后续请求受保护的路线和资源,客户端在请求头中发送 JWT。

后端解码 JWT 以验证用户的身份,授予或拒绝对请求资源的访问,并为后续请求生成新的 JWT。JWT 身份验证允许无状态身份验证。这意味着身份验证信息存储在 JWT 中,可以在不同的服务器之间传递,而不是存储在服务器的内存中。这使得扩展应用程序更容易,并降低了数据丢失或损坏的风险。

JWT 身份验证还通过使用数字签名来防止数据篡改,从而增强了安全性。签名使用服务器和客户端之间共享的秘密密钥生成。签名确保 JWT 中的数据在传输过程中未被更改。JWT 身份验证是 Flask 应用程序中安全且高效的用户身份验证方法。

通过在 Flask 应用程序中实现 JWT 身份验证,开发者可以简化用户身份验证的过程,并降低安全漏洞的风险。让我们来检查 JWT 的后端和前端实现。

Flask 后端

以下代码定义了两个 Flask 端点 – /api/v1/login/api/v1/dashboard

@app.route('/api/v1/login', methods=['POST'])def login():
    email = request.json.get('email', None)
    password = request.json.get('password', None)
    if email is None or password is None:
        return jsonify({'message': 'Missing email or
            password'}), 400
    user = User.query.filter_by(email=email).first()
    if user is None or not bcrypt.check_password_hash
        (user.password, password):
        return jsonify({'message': 'Invalid email or
            password'}), 401
    access_token = create_access_token(identity=user.id)
    return jsonify({'access_token': access_token}), 200
@app.route('/api/v1/dashboard', methods=['GET'])
@jwt_required
def dashboard():
    current_user = get_jwt_identity()
    user = User.query.filter_by(id=current_user).first()
    return jsonify({'email': user.email}), 200

/api/v1/login 端点是用于处理用户登录请求的。它接收一个包含两个属性的 JSON 请求:emailpassword。如果这两个属性中的任何一个缺失,函数将返回一个包含消息“缺少邮箱或密码”和状态码400(错误请求)的 JSON 响应。

接下来,该函数查询数据库以查找具有给定邮箱的用户。如果不存在这样的用户,或者提供的密码与数据库中存储的散列密码不匹配,则函数返回一个包含消息“无效的邮箱或密码”和状态码401(未授权)的 JSON 响应。

否则,函数会使用 create_access_token 函数生成一个 JWT,并将其作为 JSON 响应返回,状态码为 200(OK)。JWT 可以用于在后续请求中对用户进行认证。/api/v1/dashboard 端点是一个受保护的端点,只有拥有有效 JWT 的用户才能访问。

使用 jwt_required 装饰器来强制执行此限制。当访问此端点时,JWT 用于提取用户的身份,然后从数据库中检索用户的 email。然后,该电子邮件作为 JSON 响应返回,状态码为 200(OK)。

React 前端

以下代码展示了登录表单和仪表板。LoginForm 组件有三个状态 – emailpasswordaccessToken。当表单提交时,它会对 /api/v1/login 端点发送一个带有电子邮件和密码数据的 POST 请求,并将请求的响应存储在 accessToken 状态中:

import React, { useState } from 'react';import axios from 'axios';
const LoginForm = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [accessToken, setAccessToken] = useState('');
  const handleSubmit = async (event) => {
    event.preventDefault();
    try {
      const res = await axios.post('/api/v1/login', {
        email, password });
      setAccessToken(res.data.access_token);
    } catch (err) {
      console.error(err);
    }
  };
  return (
    <>
      {accessToken ? (
        <Dashboard accessToken={accessToken} />
      ) : (
        <form onSubmit={handleSubmit}>
          ....
          />
          <button type="submit">Login</button>
        </form>
      )}
    </>
  );
};
};
 export default LoginForm;

Dashboard 组件接受一个 accessToken 属性,并有一个状态,email。它会对 /api/v1/dashboard 端点发送一个带有设置为 accessToken 的授权头的 GET 请求,并将响应存储在 email 状态中。该组件显示一条消息,内容为 "欢迎来到 dashboard, [email]!"

LoginForm 组件根据 accessToken 是否为真返回 Dashboard 组件或登录表单。

接下来,我们将讨论如何识别网络应用程序用户并管理他们的信息。

识别系统用户和管理他们的信息

在大多数网络应用程序中,用户通过唯一的标识符(如用户名或电子邮件地址)进行识别。通常,在 Flask 应用程序中,你可以使用数据库来存储用户信息,例如用户名、电子邮件地址和散列密码。

当用户尝试登录时,输入的凭据(用户名和密码)将与数据库中存储的信息进行比较。如果输入的凭据匹配,则用户被认证,并为该用户创建一个会话。在 Flask 中,你可以使用内置的会话对象来存储和检索用户信息。

通过使用会话,你可以在 Flask 网络应用程序中轻松识别用户并检索他们的信息。然而,需要注意的是,会话容易受到会话劫持攻击。因此,使用诸如登录后重新生成会话 ID 和使用安全 cookie 等安全的会话管理技术是至关重要的。

让我们考察一个实现示例:

from flask import Flask, request, redirect, session, jsonifyapp = Flask(__name__)
app.secret_key = 'secret_key'
@app.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    email = data.get('email')
    password = data.get('password')
    session['email'] = email
    return jsonify({'message': 'Login successful'}), 201
@app.route('/dashboard', methods=['GET'])
def dashboard():
    email = session.get('email')
    user = User.query.filter_by(email=email).first()
    return jsonify({'email': email, 'user':
        user.to_dict()}), 200

在前面的代码中,第一行从 Flask 库中导入所需的模块。下一行创建了一个 Flask 类的实例,并将其分配给 app 变量。app.secret_key 属性被设置为 'secret_key',它用于安全地签名会话 cookie。

登录功能被定义为 api/v1/login 路径上的 POST 端点。此端点使用 request.get_json() 方法从请求体中获取 JSON 数据并提取 emailpassword 的值。然后使用 session['email'] = emailemail 存储在会话中。该函数返回一个包含消息 "Login successful" 和状态码 201 的 JSON 响应,表示成功创建资源。

然后,仪表板功能被定义为 api/v1/dashboard 路径上的 GET 端点。它使用 session.get('email') 从会话中检索 email。然后,该函数使用 User.query.filter_by(email=email).first() 查询数据库以获取具有指定电子邮件的用户。email 和用户数据(使用 to_dict() 转换为字典)以 JSON 响应的形式返回,状态码为 200,表示成功检索资源。

您还可以使用基于令牌的认证方法在 Flask 应用程序中识别用户。在此方法中,当用户登录时,会向用户颁发一个令牌,并将该令牌存储在用户的浏览器中作为 cookie 或放置在本地存储中。然后,此令牌随用户发出的每个后续请求一起发送,服务器使用此令牌来识别用户。JWT 是常用的令牌格式,Flask-JWTFlask-JWT-Extended 等库使得在 Flask 中实现基于 JWT 的认证变得简单。

接下来,我们将深入了解在 Web 应用程序中跟踪用户会话。

会话管理

Flask-Session;在前端 React 端,你可以使用 React 的 localStoragesessionStorage

Flask 作为 Python 的首选框架,以其简洁性而闻名,使得构建从小型到企业级大小的 Web 应用程序变得容易。Flask 可以使用内置的会话对象和一些社区成员提供的 Flask 扩展来管理用户会话。

会话对象是一个类似字典的对象,存储在服务器上,可以通过安全的会话 cookie 由客户端访问。要使用会话对象,必须在 Flask 应用程序中设置一个 密钥。此密钥用于加密和签名会话数据,这些数据存储在客户端浏览器的安全 cookie 中。当用户访问受保护的资源时,服务器验证会话 cookie,如果 cookie 有效,则授予访问权限。

让我们在 Flask 后端和 React 前端中实现会话管理。我们将创建一个计数器端点,用于跟踪用户访问仪表板页面的次数。

Flask 后端

我们将使用 Flask-Session 来存储会话数据并安全地管理会话。要使用 Flask-Session,您需要先安装它。您可以在终端中运行 pip install flask-session 命令来完成此操作。

安装 Flask-Session 后,您需要将以下代码添加到您的 Flask 应用程序中:

from flask import Flask, sessionfrom flask_session import Session
app = Flask(__name__)
app.config["SESSION_TYPE"] = "filesystem"
Session(app)
@app.route("/api/v1/couters")
def visit_couter():
    session["counter"] = session.get("counter", 0) + 1
    return "Hey , you have visited this page:
        {}".format(session["counter"])

上述代码展示了在 Flask 后端中实现会话管理的简单示例:

  1. 第一行导入 Flask 模块,而第二行导入 Flask-Session 扩展。

  2. 接下来的几行创建了一个 Flask 应用程序对象,并配置会话类型存储在文件系统中。

  3. 然后,使用 Flask 应用程序对象作为其参数初始化 Session 对象。

  4. @app.route 装饰器为 visit_counter 函数创建了一个路由 - 在这种情况下,是 /api/v1/counters 路由。

  5. visit_counter 函数检索会话中 counter 键的当前值,如果不存在则将其设置为 0,然后增加 1。然后,将更新后的值作为响应返回给用户。

让我们探索这个实现中的 React 前端部分。

React 前端

你可以使用 Axios 库向 Flask 服务器发送 HTTP 请求。如果尚未安装,可以使用 npm install axios 命令安装 Axios。

一旦安装了 Axios,你就可以使用它向 Flask 服务器发送 HTTP 请求来设置或获取会话数据:

import React, { useState } from "react";import axios from "axios";
function VisitCouter() {
    const [counter, setCounter] = useState(0);
    const getCounter = async () => {
        const response = await axios.get(
            "http://localhost:5000/api/v1/counters");
        setCounter(response.data.counter);
        };
        return (
          <div>
            <h1>You have visited this page: {counter}
              times!</h1>
            <button onClick={getCounter}>Get Counter
              </button>
          </div>
        );
}
export default VisitCounter;

上述代码演示了 React 前端的前端实现,它从 Flask 后端检索访问计数器。

  1. 第一行导入所需的库 - 即 Reactaxios

  2. 下一个部分声明了 VisitCounter 函数组件,它返回一个用户视图。

  3. 在组件内部,使用 useState 钩子初始化状态变量 counter

  4. getCounter 函数使用 axios 库向 Flask 后端的 /api/v1/counters 端点发送 GET 请求。然后,使用来自后端的响应(其中包含更新的计数器值)来更新计数器状态变量。

  5. 组件返回一个 div,显示计数器的值,以及一个按钮,当点击时,会触发 getCounter 函数从后端检索更新的计数器值。

接下来,我们将讨论如何在 Flask-React Web 应用程序中创建密码保护的仪表板。

创建密码保护的仪表板

在 Web 应用程序中保护页面对于维护安全和隐私至关重要。通过扩展,这有助于防止未经授权访问敏感信息。在本节中,你将在 Flask-React Web 应用程序中实现一个受保护的仪表板页面。

仪表板是一个用户友好的界面,提供了数据和信息的概览。仪表板上显示的数据可以来自各种来源,例如数据库、电子表格和 API。

Flask 后端

以下代码演示了一个实现,允许管理员用户登录并查看受保护的仪表板页面。我们将实现最小化的登录和注销端点,定义登录和注销功能并保护 dashboard 端点。应用程序使用 Flask-Session 库在文件系统中存储会话数据:

from flask import Flask, request, jsonify, sessionfrom flask_session import Session
app = Flask(__name__)
app.config["SESSION_TYPE"] = "filesystem"
Session(app)
@app.route("/api/v1/login", methods=["POST"])
def login():
    username = request.json.get("username")
    password = request.json.get("password")
    if username == "admin" and password == "secret":
        session["logged_in"] = True
        return jsonify({"message": "Login successful"})
    else:
        return jsonify({"message": "Login failed"}), 401
@app.route("/api/v1/logout")
def logout():
    session.pop("logged_in", None)
    return jsonify({"message": "Logout successful"})
@app.route("/api/v1/dashboard")
def dashboard():
    if "logged_in" not in session:
        return jsonify({"message": "Unauthorized access"}),
            401
    else:
        return jsonify({"message": "Welcome to the
            dashboard"})

login端点,应用程序接收一个包含请求体中 JSON 格式的usernamepassword参数的POST请求。代码检查usernamepassword参数是否与预定义的值匹配——即adminsecret。如果值匹配,代码将会话数据中的logged_in键设置为True,表示用户已登录。

它返回一个包含声明Login successful的消息的 JSON 响应。如果值不匹配,代码返回一个包含声明Login failed401 HTTP 状态代码的 JSON 响应,表示未授权访问。

logout端点从会话数据中删除logged_in键,表示用户已注销。它返回一个包含声明Logout successful的消息的 JSON 响应。

仪表板端点检查会话数据中是否存在logged_in键。如果不存在,代码返回一个包含声明Unauthorized access401 HTTP 状态代码的 JSON 响应。如果logged_in键存在,代码返回一个包含声明"Welcome to the dashboard"的 JSON 响应。

React 前端

以下代码片段是一个 React 组件,用于显示用户的仪表板。它使用 React 钩子,特别是useStateuseEffect,来管理其状态和更新用户界面:

import React, { useState, useEffect } from "react";import axios from "axios";
function Dashboard() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [message, setMessage] = useState("");
  const checkLogin = async () => {
    const response = await axios.get(
      "http://localhost:5000/api/v1/dashboard");
    if (response.status === 200) {
      setIsLoggedIn(true);
      setMessage(response.data.message);
    }
  };
  useEffect(() => {
  checkLogin();
  }, []);
  if (!isLoggedIn) {
    return <h1>Unauthorized access</h1>;
  }
  return <h1>{message}</h1>;
}
export default Dashboard;

当组件渲染时,它使用axios库向http://localhost:5000/api/v1/dashboard发出 HTTP GET请求。这是在checkLogin函数中完成的,该函数在组件挂载时由useEffect钩子调用。

如果服务器的响应是200 OK,这意味着用户有权访问仪表板。组件的状态通过将isLoggedIn设置为truemessage设置为从服务器返回的消息来更新,以反映这一点。如果响应不是200 OK,这意味着用户未授权,isLoggedIn保持为false

最后,组件返回一个消息,告诉用户他们是否有权访问仪表板。如果isLoggedInfalse,它返回Unauthorized access。如果isLoggedIntrue,它返回来自服务器的消息。

以这种方式,您可以使用 React 和 Flask 创建一个密码保护的仪表板,只有经过身份验证的用户才能访问,从而为您的应用程序增加安全性。

接下来,您将学习如何在 Flask 和 React Web 应用程序中实现 Flash 消息。

在 Flask 中实现 Flash 消息

Flash 消息增强了任何 Web 应用程序的用户体验,为用户提供及时和有用的反馈。Flash 用于在重定向后显示网页上的状态或错误消息。例如,在表单提交成功后,可以将消息存储在 Flash 中,以便在重定向页面上显示成功消息。

闪存信息存储在用户的会话中,这是一个类似于字典的对象,可以在请求之间存储信息。使用闪存信息,您可以在请求之间安全高效地传递信息。这对于显示不需要长时间持续或只需要显示一次的消息很有用,例如成功或错误消息。由于闪存信息存储在用户的会话中,它们只能由服务器访问,并且不会以纯文本形式发送到客户端,这使得它们更加安全。

让我们修改登录和注销端点以显示闪存信息。

Flask 后端

以下代码演示了带有登录和注销端点的闪存信息系统的实现。代码首先导入必要的模块并创建一个 Flask 应用程序。app.secret_key = "secret_key"这一行设置了密钥,该密钥用于加密存储在会话中的闪存信息:

from flask import Flask, request, jsonify, session, flashfrom flask_session import Session
app = Flask(__name__)
app.config["SESSION_TYPE"] = "filesystem"
app.secret_key = "secret_key"
Session(app)
@app.route("/api/v1/login", methods=["POST"])
def login():
    username = request.json.get("username")
    password = request.json.get("password")
    if username == "admin" and password == "secret":
        session["logged_in"] = True
        flash("Login successful")
        return jsonify({"message": "Login successful"})
    else:
        flash("Login failed")
        return jsonify({"message": "Login failed"}), 401
@app.route("/api/v1/logout")
def logout():
    session.pop("logged_in", None)
    flash("Logout successful")
    return jsonify({"message": "Logout successful"})

登录端点由login函数定义,该函数绑定到/api/v1/login URL。该函数从请求中的 JSON 数据中检索usernamepassword值,并检查它们是否与预定义的"admin""secret"值匹配。如果值匹配,用户的会话通过在会话中设置logged_in键标记为已登录,并设置一个表示登录成功的闪存信息。

函数随后返回一个 JSON 响应,指示登录成功。如果值不匹配,设置一个闪存信息,指示登录失败,并返回一个表示登录失败的 JSON 响应。注销端点由logout函数定义,该函数绑定到/api/v1/logout URL。

函数从会话中删除logged_in键,表示用户不再登录,并设置一个表示注销成功的闪存信息。随后返回一个表示注销成功的 JSON 响应。

React 前端

以下代码片段演示了一个表示从后端处理闪存信息的 Web 应用程序仪表板的 React 函数组件。Dashboard组件使用了useStateuseEffect钩子:

import React, { useState, useEffect } from "react";import axios from "axios";
function Dashboard() {
    const [isLoggedIn, setIsLoggedIn] = useState(false);
    const [message, setMessage] = useState("");
    const [flashMessage, setFlashMessage] = useState("");
    const checkLogin = async () => {
        const response = await axios.get(
            "http://localhost:5000/api/v1/dashboard");
        if (response.status === 200) {
            setIsLoggedIn(true);
            setMessage(response.data.message);
        }
    };
                    .....
        if (!isLoggedIn) {
            return (
                <div>
                    <h1>Unauthorized access</h1>
                    <h2>{flashMessage}</h2>
                    <button onClick={() =>
                        handleLogin("admin", "secret")}>
                        Login</button>

Dashboard组件跟踪以下状态变量:

  • isLoggedIn: 一个表示用户是否登录的布尔值。它最初设置为false

  • message: 一个表示在仪表板上显示的消息的字符串值。

  • flashMessage: 一个表示在页面上显示的闪存信息的字符串值。

Dashboard组件有三个功能:

  • checkLogin: 一个异步函数,它向/api/v1/dashboard端点发送GET请求以检查用户是否已登录。如果响应状态是200,它将isLoggedIn状态变量更新为true,并显示response.data.message的值。

  • handleLogin: 一个异步函数,它使用提供的usernamepassword值作为请求体,向/api/v1/login端点发送POST请求。如果响应状态是200,它将isLoggedIn状态变量更新为true并将flashMessage更新为response.data.message的值。如果响应状态不是200,它将flashMessage更新为response.data.message的值。

  • handleLogout: 一个异步函数,它向/api/v1/logout端点发送GET请求。如果响应状态是200,它将isLoggedIn状态变量更新为false并将flashMessage更新为response.data.message的值。

使用useEffect钩子在组件挂载时调用checkLogin函数。

最后,组件根据isLoggedIn:的值返回一个 UI。如果用户未登录,它将显示一条消息说“未经授权访问”和“``登录成功"”。

以这种方式,你可以在 React 应用程序的前端使用闪存消息向用户提供反馈,然后使用 Flask 后端来增强用户体验。总的来说,闪存消息使 Web 应用程序更加互动和用户友好。

摘要

本章提供了关于信息安全基础以及如何使用身份验证和授权来确保 Flask Web 应用程序的全面概述。你了解了最佳实践,并提供了在 Flask 应用程序中实现身份验证和授权的用例。我们还讨论了不同的身份验证方法和访问控制方法。

你探索了如何管理用户会话和实现密码保护的仪表板。此外,本章还展示了如何使用闪存消息向 Web 应用程序的用户提供反馈。你应已对如何确保 Flask 应用程序的安全以及如何在项目中实现身份验证和授权有了坚实的理解。

在下一章中,我们将讨论如何处理 Flask Web 应用程序中的错误,其中 React 处理前端部分。我们将深入研究内置的 Flask 调试功能,并学习如何在 React-Flask 应用程序中处理自定义错误消息。

第十三章:错误处理

错误处理是任何 Web 应用程序用户体验中的关键组件。Flask提供了几个内置的工具和选项,用于以干净和高效的方式处理错误。错误处理的目标是捕获和响应在应用程序执行过程中可能发生的错误,例如运行时错误、异常和无效的用户输入。

Flask 提供了一个内置的调试器,可以在开发过程中用于捕获和诊断错误。那么,为什么错误处理在任何 Web 应用程序中都是一个如此重要的概念呢?错误处理机制在预期向北发展却向南发展时,向用户提供有意义的错误消息,有助于维护用户体验的整体质量。此外,主动的错误处理使得调试变得容易。

如果错误处理实现得很好,那么调试问题和识别应用程序中问题的根本原因就会变得更容易。作为开发者,你也会希望通过预测和处理潜在的错误来提高应用程序的可靠性。这无疑使得你的应用程序更加可靠,并且不太可能在意外情况下崩溃。

在本章中,我们将探讨处理 Flask Web 应用程序中错误的不同策略和技术。你将了解并学习如何使用内置的Flask 调试器、实现错误处理器以及创建自定义的错误页面,以便向用户提供有意义的反馈。

在本章中,你将学习以下主题:

  • 使用 Flask 调试器

  • 创建错误处理器

  • 创建自定义错误页面

  • 跟踪应用程序中的事件

  • 向管理员发送错误邮件

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter13

使用 Flask 调试器

Flask 作为一个轻量级的 Python 网络框架,被广泛用于构建 Web 应用程序。使用 Flask 的一个即用即得的好处是其内置的调试器,它为识别和修复应用程序中的错误提供了一个强大的工具。

当你的 Flask 应用程序发生错误时,调试器会自动激活。调试器将提供关于错误的详细信息,包括堆栈跟踪、源代码上下文以及错误发生时在作用域内的任何变量。这些信息对于确定错误的根本原因和修复它的可能想法至关重要。

Flask 调试器还提供了一些交互式工具,可以用来检查应用程序的状态并理解正在发生什么。例如,你可以评估表达式并检查变量的值。你还可以在代码中设置断点,逐行执行代码以查看其执行情况。

让我们通过以下代码片段来进行分析:

import pdb@app.route("/api/v1/debugging")
def debug():
    a = 10
    b = 20
    pdb.set_trace()
    c = a + b
    return f"The result is: {c}"

在这种情况下,你可以在c = a + b之前的行设置一个断点,就像前面的代码中所做的那样,并运行应用程序。当断点被触发时,你可以进入调试器并检查abc的值。你还可以评估表达式并查看其结果。例如,要评估表达式a + b,你可以在调试器的命令提示符中输入a + b并按Enter。结果30将被显示。你还可以使用n命令逐行执行代码,使用c命令继续执行直到下一个断点。

通过这种方式,你可以使用 Flask 调试器的交互式工具来了解应用程序中发生的情况,并更有效地进行调试。这对于处理大型或复杂的代码库特别有用。当没有额外工具和信息时,Flask 调试器的交互式工具在难以理解导致错误的根本原因时非常有用。

除了交互式工具之外,Flask 还提供了一个可以启用的调试模式,可以提供更详细的错误信息。当启用调试模式时,Flask 将显示包含错误信息、堆栈跟踪和源代码上下文的详细错误页面。这些信息对于调试复杂问题非常有帮助。

要启用 Flask 调试器,只需在你的 Flask 应用程序中将debug配置值设置为True。在本项目书中,我们在.env文件中设置了此参数。你应该只在开发中使用它,因为它可能会向任何有权访问它的人透露你应用程序的敏感信息。

此外,Flask 还允许使用第三方扩展来增强调试体验。例如,Flask-DebugToolbar提供了一个工具栏,可以添加到你的应用程序中,以显示有关当前请求及其上下文的信息。

Flask 的内置调试器是一个强大的工具,可以帮助你快速识别和修复应用程序中的错误。无论你是在处理小型项目还是企业级应用程序,调试器都提供了有助于解决问题和提高应用程序可靠性和性能的有价值信息。

接下来,我们将讨论并实现 Flask Web 应用程序中的错误处理器。

创建错误处理器

Flask 还提供了一个处理错误的机制,称为错误处理器。错误处理器是在你的应用程序中发生特定错误时被调用的函数。这些函数可以用来返回自定义错误页面、记录有关错误的日志,或者执行任何适合错误的操作。要在 Flask Web 应用程序中定义错误处理器,你需要使用errorhandler装饰器。

装饰器接受错误代码作为其参数,并装饰的函数是当发生该错误时将被调用的错误处理器。错误处理器函数接受一个错误对象作为参数,该对象提供了关于发生错误的信息。这些信息可以用来向客户端提供更详细的错误响应,或者为了调试目的记录有关错误的附加信息。

在 Flask 后端和 React 前端应用程序中,错误处理是确保流畅用户体验的关键步骤。如前所述,错误处理器的目标是当出现问题时向用户提供有意义的反馈,而不仅仅是返回一个通用的错误消息。

例如,您可以定义错误处理器来处理错误 400404500

Flask 后端

以下代码展示了为 HTTP 错误代码 404(未找到)、400(错误请求)和 500(内部服务器错误)创建的错误处理器:

from flask import jsonify@app.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Not found'}), 404
@app.errorhandler(400)
def bad_request(error):
    return jsonify({'error': 'Bad request'}), 400
@app.errorhandler(500)
def internal_server_error(error):
    return jsonify({'error': 'internal server error'}), 500

not_foundbad_requestinternal_server_error 函数返回包含错误消息的 JSON 响应,以及相应的 HTTP 错误代码。

React 前端

在 React 前端,您可以通过向 Flask 后端发送 HTTP 请求并检查响应中的错误来处理这些错误。例如,您可以在 React 中使用 Axios

import React, { useState, useEffect } from 'react';import axios from 'axios';
const Speakers = () => {
    const [error, setError] = useState(null);
    useEffect(() => {
        axios.get('/api/v1/speakers')
        .then(response => {
            // handle success
        })
        .catch(error => {
            switch (error.response.status) {
                case 404:
                    setError('Resource not found.');
                    break;
                case 400:
                    setError('Bad request');
                    break;
                case 500:
                    setError('An internal server error
                        occurred.');
                    break;
                default:
                    setError('An unexpected error
                        occurred.');
                    break;
            }
        });
    }, []);
    return (
        <div>
            {error ? <p>{error}</p> : <p>No error</p>}
        </div>
    );
};
export default Speakers;

上述错误处理代码展示了 React 前端与 Flask 后端 API 的通信。代码导入了 ReactuseStateuseEffect 钩子,以及用于发送 API 请求的 axios 库。然后,代码定义了一个功能组件 Speakers,该组件向后端的 /api/v1/speakers 端点发送 API GET 请求。

useEffect 钩子用于管理 API 调用,响应在 .then() 块中处理成功,在 .catch() 块中处理错误。在 .catch() 块中,检查错误响应的状态并根据状态码设置特定的错误消息。例如,如果状态码是 404,则将“资源未找到”设置为错误。

错误信息随后通过条件渲染在 UI 中显示,如果没有错误,则显示“无错误”文本。错误信息使用 useState 钩子在状态中存储,初始值为 null

接下来,我们将讨论和实现 Flask 网络应用程序中的自定义错误页面。

创建自定义错误页面

除了 Flask 中的错误处理器外,您还可以创建自定义错误页面,以提供更好的用户体验。当您的应用程序发生错误时,错误处理器可以返回一个包含错误信息、解决问题说明或任何其他适当内容的自定义错误页面。

在 Flask 中创建自定义错误页面,只需创建一个如前文所述的错误处理器,并返回一个包含错误页面内容的 JSON 响应。

例如,让我们看一下以下代码中包含自定义错误消息的 JSON 响应:

@app.errorhandler(404)def not_found(error):
    return jsonify({'error': 'Not found'}), 404

以下代码在发生 404 错误时返回包含错误消息的 JSON 响应,以及相应的 HTTP 错误代码。让我们定义 React 前端以使用 ErrorPage 组件处理 UI:

import React from 'react';const ErrorPage = ({ error }) => (
    <div>
        <h1>An error has occurred</h1>
        <p>{error}</p>
    </div>
);
export default ErrorPage;

以下代码显示了 ErrorPage 组件,它接受一个错误属性并在错误消息中显示它。您可以在应用程序中使用此组件在发生错误时显示自定义错误页面。

您可以直接将 ErrorPage 组件添加到应用程序的其余部分。例如,使用以下代码将 ErrorPage 组件添加到 Speaker 组件:

import React, { useState, useEffect } from 'react';import axios from 'axios';
import ErrorPage from './ErrorPage';
const Speakers = () => {
    const [error, setError] = useState(null);
    useEffect(() => {
        axios.get('/api/v1/speakers')
            .then(response => {
                // handle success
            })
            .catch(error => {
                setError(error.response.data.error);
            });
    }, []);
    if (error) {
        return <ErrorPage error={error} />;
    }
    return (
        // rest of your application
    );
};
export default Speakers;

接下来,我们将讨论如何在 Flask 网络应用程序中跟踪和记录事件。

在您的应用程序中跟踪事件

Flask 允许您以优雅的方式跟踪应用程序中的事件。这对于识别潜在问题至关重要。通过跟踪事件,您可以更好地了解应用程序中正在发生的事情,并就如何改善情况做出明智的决策。

在 Flask 中跟踪事件有几种方法,包括使用内置的日志记录功能、第三方日志记录服务或自定义代码跟踪。例如,您可以使用 Python 的 logging 模块将有关应用程序活动的信息记录到文件或控制台。

使用日志记录模块很简单;只需将 logging 导入到您的 Flask 应用程序中,并配置它以适当的级别记录信息。例如,以下代码配置了日志记录模块以将信息记录到名为 error.log 的文件中:

import loggingfrom flask import Flask
app = Flask(__name__)
# Set up a logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Specify the log file
file_handler = logging.FileHandler('error.log')
file_handler.setLevel(logging.DEBUG)
# Add the handler to the logger
logger.addHandler(file_handler)
@app.route('/logger')
def logger():
    logger.debug('This is a debug message')
    logger.info('This is an info message')
    logger.warning('This is a warning message')
    logger.error('This is an error message')
    return 'Log messages have been written to the log file'
if __name__ == '__main__':
    app.run()

以下代码演示了在 Flask 网络应用程序中实现日志记录模块的方法。代码使用 logging.getLogger(__name__) 方法设置了一个日志记录器对象。日志记录器设置为调试级别,使用 logger.setLevel(logging.DEBUG)。使用 file_handler = logging.FileHandler('error.log') 创建了一个 FileHandler 对象,并将处理程序设置为调试级别,同样使用 file_handler.setLevel(logging.DEBUG)

使用 logger.addHandler(file_handler) 将处理程序添加到日志记录器对象。在 logger() 函数中,调用了四个日志记录方法:debug()info()warning()error()。这些方法将消息记录到日志文件中,并带有相应的日志级别(调试、信息、警告和错误)。记录的消息是简单的字符串消息。

此外,在跟踪 Flask 应用程序中的事件时,您可以使用第三方日志记录服务。使用 Flask 与第三方日志记录服务结合可以提供更高级的日志记录功能,如集中式日志管理、实时日志搜索和警报。

例如,您可以使用基于云的日志管理服务,如 AWS CloudWatchLogglyPapertrail

简单地考察一下 AWS CloudWatch 的实现。AWS CloudWatch 是一种日志服务,为 AWS 资源提供日志管理和监控。要使用 Flask 与 AWS CloudWatch,你可以使用 CloudWatch Logs API 直接将日志数据发送到 AWS CloudWatch。

以下步骤实现了使用 AWS CloudWatch 在 Flask 应用程序中进行日志记录:

  1. 设置 AWS 账户并创建一个 CloudWatch 日志组

  2. 安装 boto3 库,它提供了对 AWS CloudWatch API 的 Python 接口。使用 pip install boto3 安装 Boto2 并确保你的虚拟环境已激活。

  3. 在你的 Flask 应用程序中导入 boto3 库,并使用你的 AWS 凭据进行配置。

  4. 创建一个记录器并将其日志级别设置为所需的详细程度。

  5. 在你的应用程序代码中,使用记录器以各种级别(如 info、warning、error 等)记录消息。

  6. 配置记录器将日志发送到 AWS CloudWatch。这可以通过创建一个自定义处理程序来实现,该处理程序使用 boto3 库将日志消息发送到 CloudWatch。

  7. 将你的 Flask 应用程序部署并监控 AWS CloudWatch 中的日志。

让我们来看看代码实现:

import boto3import logging
from flask import Flask
app = Flask(__name__)
boto3.setup_default_session(
    aws_access_key_id='<your-access-key-id>',
    aws_secret_access_key='<your-secret-access-key>',
    region_name='<your-region>')
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
cloudwatch = boto3.client('logs')
log_group_name = '<your-log-group-name>'
class CloudWatchHandler(logging.Handler):
    def emit(self, record):
        log_message = self.format(record)
        cloudwatch.put_log_events(
            logGroupName=log_group_name,
            logStreamName='<your-log-stream-name>',)
if __name__ == '__main__':
    app.run()

完整的源代码可以在 GitHub 上找到。

上述代码展示了如何使用 boto3 库将 Flask 应用程序的日志发送到 AWS CloudWatch 的实现方式。它的工作原理如下:

  1. 导入 boto3 库并设置一个默认会话,指定 AWS 访问密钥 ID秘密访问密钥区域名称

  2. 使用 logging 模块创建一个记录器对象,并将日志级别设置为 DEBUG

  3. 使用 boto3 库创建一个 CloudWatch 客户端对象。

  4. 创建一个名为 CloudWatchHandler 的自定义处理程序类,它继承自 logging.Handler 类并重写了其 emit 方法。在 emit 方法中,将日志消息格式化并发送到 AWS CloudWatch,使用 CloudWatch 客户端的 put_log_events 方法。

  5. 创建一个 CloudWatchHandler 类的实例,并将其日志级别设置为 DEBUG。然后将此处理程序添加到记录器对象。

  6. 创建一个名为 /logging_with_aws_cloudwatch 的路由,该路由使用记录器对象生成不同级别(debuginfowarningerror)的日志消息。

在 Flask 应用程序中处理错误和跟踪事件对于确保其可靠性和健壮性至关重要。有了 Flask 的内置调试器、错误处理器、自定义错误页面、日志记录和第三方日志库,你可以轻松诊断和解决在 Flask 应用程序开发中出现的问题。

现在你已经能够实现 Flask 内置的调试器、错误处理器、自定义错误页面、日志记录和第三方日志库,如果管理员能够实时收到关于应用程序日志中错误的通知,那岂不是很好?

让我们来看看如何在 Flask 中实现这一点。

向管理员发送错误邮件

向管理员发送错误邮件提供了一种高效的通知方式,让他们了解 Flask 应用程序中的错误和问题。这允许你在问题升级成更大问题并负面影响用户体验之前快速识别和解决问题。其好处包括及时识别和解决错误、提高系统可靠性和减少停机时间。

让我们深入探讨一个向管理员发送错误邮件的示例:

import smtplibfrom email.mime.text import MIMEText
from flask import Flask, request
app = Flask(__name__)
def send_email(error):
    try:
        msg = MIMEText(error)
        msg['Subject'] = 'Error in Flask Application'
        msg['From'] = 'from@example.com'
        msg['To'] = 'to@example.com'
        s = smtplib.SMTP('localhost')
        s.send_message(msg)
        s.quit()
    except Exception as e:
        print(f'Error sending email: {e}')
@app.errorhandler(500)
def internal_server_error(error):
    send_email(str(error))
    return 'An error occurred and an email was sent to the
        administrator.', 500
if __name__ == '__main__':
    app.run()

上一段代码展示了在 Flask 应用程序中发送错误邮件以通知管理员错误实现的示例。它的工作原理如下:

  1. 该代码使用 smtplibemail.mime.text 库来创建和发送电子邮件消息。

  2. send_email(error) 函数接受一个错误消息作为参数,并使用 MIMEText 对象创建一个电子邮件消息。邮件的主题、发件人电子邮件地址、收件人电子邮件地址和错误消息被设置为邮件内容。然后,通过本地邮件服务器使用 smtplib 库发送邮件。

Flask 的 errorhandler 装饰器用于捕获应用程序中发生的任何 500 内部服务器错误。当发生错误 500 时,会调用 internal_server_error 函数,并使用错误消息作为参数调用 send_email 函数。该函数返回一个响应给用户,表明发生了错误,并向管理员发送了电子邮件。

摘要

错误处理自古以来就是软件开发的一个基本方面。确保你的 Flask Web 应用程序能够有效地处理错误至关重要。我们讨论了 Flask 调试器、错误处理程序和自定义错误页面。有了这些,你可以向用户提供有意义的反馈,并帮助维护应用程序的稳定性和可靠性。

作为全栈开发者,我们强调了持续关注错误处理的重要性。你应该定期审查和更新你的错误处理策略,以确保你的应用程序保持健壮和弹性。我们还考虑了记录错误并向管理员发送通知,以便你可以快速识别和解决可能出现的任何问题。

简而言之,无 bug 的开发体验对于任何专业开发者来说都只是一个幻象。你应该准备好有效地处理你的 Web 应用程序中的预期和意外错误。通过这样做,即使面对意外错误和故障,你的应用程序也能继续为用户提供价值。

接下来,我们将探讨在 Flask 中使用 Blueprints 进行模块化开发。通过 Blueprints 和模块化架构,你可以轻松维护和扩展你的 React-Flask Web 应用程序。

第十四章:模块化架构 – 利用蓝图的力量

在一个名为 Flaskland 的遥远王国里,住着一个名叫模块化的勇敢王子。他以热爱干净、有序的编程代码而闻名,他的梦想是创造一个所有代码片段都能和谐共处的王国。有一天,当他漫步在这片土地上时,他发现了一座混乱的城堡。代码片段散落在各处,找不到任何清晰的结构。

王子知道这是一个他必须承担的挑战。他召集了他的助手函数军队,并将它们组织成模块,每个模块都有特定的目的。然后他宣布这些模块是王国的基石,有了它们,他们可以征服混乱。

因此,王子和他的助手函数军队着手建立一个由结构良好、可重用代码构成的王国。他们日夜不停地工作,直到新组建的王国终于诞生。代码片段被组织起来,这个王国看起来非常美丽。这个故事捕捉了代码模块化的精髓,即把程序或系统分解成更小、自包含的模块或组件的实践。Flask 中的蓝图鼓励这种模块化的构建 Web 应用程序的方法。

模块化架构随着 Web 应用程序在规模和范围上的日益复杂而变得越来越重要。模块化架构是一种模块化编程范式,它强调将大型应用程序分解成更小、可重用的模块,这些模块可以独立开发和测试。

20 世纪 80 年代的面向对象编程(OOP)革命也对模块化架构的发展产生了重大影响。OOP 鼓励创建自包含、可重用的对象,这些对象可以组合成复杂的应用程序。这种方法非常适合开发模块化应用程序,并有助于推动模块化架构的广泛应用。

模块化的原则、关注点的分离和封装仍然是模块化架构的关键要素,这种模式持续演变和适应,以满足软件开发不断变化的需求。如今,模块化架构是一种被广泛接受和广泛使用的软件设计模式。

模块化架构在各种环境中得到应用,从大规模企业应用程序到小型单页 Web 应用程序。在 Flask Web 应用程序中,蓝图指的是将一组相关的视图和其他代码组织成一个单一模块的方法。蓝图类似于 React 中的组件:封装了一组函数和状态的 UI 可重用部件。但在 Flask 的上下文中,Flask 允许你将应用程序组织成更小、可重用的组件,称为蓝图。

本章我们将探讨网页开发中的模块化架构。在 Blueprints 的视角下,我们将讨论 Blueprints 如何帮助你构建解耦的、可重用的、可维护的和可测试的 Flask 网页应用。

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

  • 理解模块化架构在网页开发中的优势

  • 理解 Flask Blueprints

  • 使用 Blueprints 设置 Flask 应用

  • 使用 Flask Blueprints 处理 React 前端

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter14

由于页面数量的限制,大部分长代码块已被截断。请参阅 GitHub 以获取完整代码。

理解模块化架构在网页开发中的优势

模块化架构是一种软件开发方法,它涉及将一个大型、复杂的系统分解成更小、独立且可重用的模块。在网页开发的历史中,模块化架构变得更加明显。传统的单体架构涉及将网络应用的各个组件紧密耦合在一起,导致代码库庞大、难以操控,难以维护和扩展。

随着网络应用变得越来越复杂,对可扩展性的需求增加,开发者开始寻求替代方法,以便将网络应用分解成更小、更独立的组件。

模块化架构作为解决这些限制的方案出现,因为它允许开发者创建更小、可重用的组件,这些组件可以组合成一个完整的网络应用。这种方法提供了包括提高可维护性、更容易的可扩展性和更好的关注点分离在内的几个好处。

使用模块化架构,开发者可以在隔离的情况下对单个组件进行工作,这降低了破坏整个应用的风险,并使得独立测试和部署更改变得更加容易。因此,模块化架构迅速在网页开发者中获得了流行,许多现代网页开发框架,如 Flask、Django、Ruby on Rails 和 Angular,都采用了这种架构风格。模块化架构的流行度在过去几年中持续增长,并且仍然是现代网页开发实践中的一个关键组成部分。

让我们探索一些模块化架构在网页开发中的优势:

  • 可扩展性:在传统的单体架构中,随着应用的成长,管理和维护变得越来越困难。使用模块化架构,每个模块都是独立的,可以独立开发、测试和部署,这使得根据需要扩展单个组件变得更加容易。

  • 可重用性:模块化架构鼓励代码重用,这导致开发过程更加高效。模块可以在不同的项目中重用,从而减少开发新应用程序所需的时间和精力。此外,模块化架构使得更新和维护现有代码变得更加容易,因为对单个模块的更改不会影响应用程序的其他部分。

  • 可维护性:采用模块化架构,应用程序被划分为更小、更易于管理的组件,这使得识别和解决问题变得更加容易。模块化设计使得隔离问题和调试问题更加容易,从而减少了解决问题所需的时间和精力。此外,模块化架构使得测试单个组件变得更加容易,确保应用程序在长时间内保持可靠和可维护。

  • 灵活性:模块化架构允许开发者轻松修改或扩展应用程序的功能,而不会影响整个系统。这使得添加新功能、进行更改或集成新技术到应用程序中变得更加容易。采用模块化架构,开发者可以专注于单个模块,确保应用程序在长时间内保持灵活和适应性强。

  • 改进协作:模块化架构使得开发者能够并行工作于应用程序的不同部分,从而提高协作效率并减少完成项目所需的时间。模块化设计允许团队将工作分解成更小、更易于管理的组件,这使得协调和整合他们的工作变得更加容易。

  • 更好的性能:模块化架构可以通过减少单个组件的大小和提高应用程序的加载时间来提高 Web 应用程序的性能。通过更小、更专注的组件,应用程序可以更快地加载,从而改善用户体验。此外,模块化架构允许更好的资源分配,确保应用程序高效、有效地使用资源。

总结来说,模块化架构在 Web 开发中变得越来越重要,因为它在传统单体架构之上提供了许多优势。凭借其提高可扩展性、可重用性、可维护性、灵活性、协作和性能的能力,模块化架构为开发者在其项目中采用这种方法提供了强有力的理由。

通过采用模块化架构,开发者可以创建更好、更高效的程序,这些程序在长时间内更容易管理和维护。

接下来,我们将讨论 Flask 社区中的大问题——蓝图。蓝图是一个强大的组织工具,它促进了将 Web 应用程序结构化为模块化和可重用组件。

理解 Flask 蓝图

如你所知,Flask 是一个简单且轻量级的框架,允许开发者快速轻松地创建网络应用程序。Flask 蓝图是 Flask 的一个重要特性,有助于开发者将应用程序组织成可重用组件。

Flask 蓝图是将 Flask 应用程序组织成更小、可重用组件的一种方式。本质上,蓝图是一组路由、模板和静态文件,可以在多个 Flask 应用程序中注册和使用。蓝图允许你将 Flask 应用程序拆分成更小、模块化的组件,这些组件可以轻松维护和扩展。这种模块化的构建网络应用程序的方法使得管理代码库和与其他开发者协作变得更加容易。

让我们快速浏览一下在 Flask 应用程序开发中使用蓝图的一些好处:

  • 模块化设计:Flask 蓝图允许开发者将应用程序拆分成更小、可重用的组件。这使得随着时间的推移维护和扩展代码库变得更加容易。

  • 可重用性:一旦创建了一个蓝图,你就可以在不同的 Flask 应用程序中重用它。这为你节省了时间和精力。实际上,使用 Flask 蓝图可以极大地简化构建复杂网络应用程序的过程,让开发者只需点击几下鼠标就能快速轻松地创建可重用组件。

  • 灵活性:Flask 蓝图可以根据应用程序的需求进行定制。你可以为蓝图定义自己的 URL 前缀,这允许你自定义应用程序的 URL 结构。这让你能够更多地控制网络应用程序的结构和访问方式。

  • 模板继承:蓝图可以从主应用程序继承模板,这允许你在多个蓝图之间重用模板。这使得创建一致且设计良好的网络应用程序变得更加容易。

  • 命名空间:蓝图可以定义自己的视图函数,并且这些函数在蓝图内部命名空间化。这有助于防止应用程序不同部分之间的命名冲突。

Flask 蓝图无疑促进了应用程序代码库中关注点的清晰分离。通过将代码组织成独立的蓝图,你可以确保应用程序的每个组件都负责特定的功能区域。这可以使代码更容易理解和调试,同时确保应用程序随着时间的推移更容易维护。

在下一节中,我们将深入探讨在考虑蓝图的情况下设置 Flask 应用程序。

使用蓝图设置 Flask 应用程序

Flask 中的蓝图是一种将 Flask 应用程序组织成更小、可重用组件的方式。要在 Flask 应用程序中使用蓝图,你通常在一个单独的 Python 文件中定义你的蓝图,在那里你可以定义你的路由、模板以及任何其他特定于该蓝图的必要逻辑。一旦定义,你就可以将蓝图注册到你的 Flask 应用程序中,这允许你在主 Flask 应用程序中使用蓝图功能。

使用蓝图,你可以轻松地在应用程序的不同部分之间分离关注点,使得随着时间的推移更容易维护和更新。

现在,让我们深入探讨如何使用蓝图设置 Flask 应用程序。

结构化蓝图 Flask 应用程序

在网络应用开发中,代码库的高效组织和模块化对于构建健壮和可维护的项目至关重要。Flask 中的一个关键结构元素是蓝图的概念。这些蓝图提供了一种结构化的方式来隔离和封装网络应用程序的各个组件。

这种方法始终促进清晰性、可重用性和可扩展性。我们将要检查attendees蓝图的结构——这是一个精心设计的组织结构,旨在简化我们网络应用中与参会者相关的功能开发。

attendees蓝图位于bizza\backend\blueprints\attendees目录中。在bizza/backend项目目录内创建一个新的目录用于 Flask 应用程序,并将其命名为blueprints。添加到项目中的蓝图使得目录结构如下所示:

参会者蓝图

bizza\backend\blueprints\attendees-models
-templates
-static
-attendee_blueprint.py

详细结构

bizza\backend\blueprints\attendees-models
- __init__.py
- attendee.py
-templates
- attendees/
- base.html
- attendee_form.html
- attendee_list.html
- attendee_profile.html
- attendee_profile_edit.html
-static
- css/
- attendees.css
- js/
- attendees.js
attendee_blueprint.py

前面的attendees蓝图包含以下组件:

  • models:这是一个包含名为attendee.py的 Python 模块的子目录,该模块定义了参会者的数据模型。__init__.py文件是一个空的 Python 模块,它指示 Python 将此目录视为一个包。

  • 模板:这是一个包含用于参会者视图的 HTML 模板的子目录。base.html模板是一个基础模板,其他模板都继承自它。attendee_form.html模板用于创建或编辑参会者资料。attendee_list.html模板用于显示所有参会者的列表。attendee_profile.html模板用于显示单个参会者的资料。attendee_profile_edit.html模板用于编辑参会者的资料。

  • static:这是一个包含模板使用的静态文件的子目录。css目录包含一个attendees.css文件,用于美化 HTML 模板。js目录包含一个attendees.js文件,用于客户端脚本。

  • attendee_blueprint.py:这是一个包含蓝图定义和参会者视图路由的 Python 模块。此蓝图定义了显示参会者列表、显示单个参会者资料、创建新的参会者资料和更新现有参会者资料的路由。蓝图还包含处理参会者数据的数据库相关函数,例如添加新参会者和更新参会者信息。

定义模型和蓝图模块

模型是网络应用程序数据结构的基础。模型代表网络应用程序中的基本实体和关系。它们封装数据属性、业务逻辑和交互,为现实世界概念提供一致的表现。

在蓝图模块中定义模型时,你创建了一个封装数据相关逻辑的自包含单元。通过将模型集成到蓝图模块中,你实现了和谐的协同作用,并带来了以下好处:

  • 清晰的分离:蓝图模块隔离各种功能,而模型封装数据处理。这种分离简化了代码库维护并提高了可读性。

  • 结构清晰:蓝图模块为模型提供逻辑上下文,使其更容易导航和理解数据相关操作。

  • 可重用性:在蓝图内定义的模型可以通过蓝图集成在其他应用部分重用,促进不要重复自己(DRY)的编码方法。

现在,让我们深入探讨蓝图模块中参会者模型的属性:

参会者蓝图

-models- __init__.py
- attendee.py

attendee.py模型定义如下:

from bizza.backend.blueprints import dbclass Attendee(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    email = db.Column(db.String(120), unique=True,
        nullable=False)
    registration_date = db.Column(db.DateTime,
        nullable=False)
    def __repr__(self):
        return f'<Attendee {self.name}>'

前面的Attendee模型代表会议的参会者。它有idnameemailregistration_date列。__repr__方法指定了模型实例应如何表示为字符串。

参会者蓝图定义如下:

from bizza.backend.blueprints.attendees.models.attendee import Attendeefrom bizza.backend.blueprints.attendees.forms import AttendeeForm, EditAttendeeForm
from bizza.backend.blueprints.attendees import db
attendee_bp = Blueprint('attendee', __name__, template_folder='templates', static_folder='static')
@attendee_bp.route('/attendees')
def attendees():
    attendees = Attendee.query.all()
    return render_template('attendees/attendee_list.html',
        attendees=attendees)
@attendee_bp.route('/attendee/add', methods=['GET',
    'POST'])
def add_attendee():
    form = AttendeeForm()
    if form.validate_on_submit():
        attendee = Attendee(name=form.name.data,
                            email=form.email.data,
                            phone=form.phone.data,
            ...
        return redirect(url_for('attendee.attendees'))
    return render_template('attendees/attendee_form.html',
        form=form, action='Add')
...

前面的代码片段定义了一个用于管理参会者的 Flask 蓝图。它从attendees包中导入必要的模块,包括Attendee模型、AttendeeFormEditAttendeeForm,以及从bizza.backend.blueprints包中的db

蓝图有一个需要用户登录的参会者列表路由。它使用Attendee.query.all()方法从数据库中检索所有参会者,并使用参会者列表渲染attendee_list.html模板。

蓝图还有一个通过GETPOST请求可访问的添加参会者路由。它创建一个AttendeeForm实例,如果表单验证通过,则使用通过表单提交的数据创建一个新的参会者对象,将其添加到数据库中,并提交更改。如果成功,它会显示一条消息并重定向到参会者列表页面。如果表单无效,它会重新渲染带有表单和添加操作的attendee_form.html模板。

注册 Blueprint

当你创建一个 Blueprint 时,你定义其路由、视图、模型、模板和静态文件。一旦你定义了你的 Blueprint,你需要使用 register_blueprint 方法将其注册到你的 Flask 应用程序中。此方法告诉 Flask 将 Blueprint 的视图、模板和静态文件包含到应用程序中。

因此,当调用 app.register_blueprint 方法时,它将 Blueprint 中定义的路由和视图添加到应用程序中。这使得 Blueprint 提供的功能对应用程序的其他部分可用。

让我们使用一个基本的 Flask 应用程序工厂函数来创建和配置一个 Flask 应用程序:

from flask import Flaskfrom flask_sqlalchemy import SQLAlchemy
# initialize the db object
db = SQLAlchemy()
def create_app():
    app = Flask(__name__)
    # load the config
    app.config.from_object('config.Config')
    # initialize the db
    db.init_app(app)
    # import the blueprints
    from .blueprints.speaker_blueprint import speaker_bp
    from .blueprints.presentation_blueprint import
        presentation_bp
    from .blueprints.attendee_blueprint import attendee_bp
    # register the blueprints
    app.register_blueprint(speaker_bp)
    app.register_blueprint(presentation_bp)
    app.register_blueprint(attendee_bp)
    return app

上述代码执行以下操作:

  1. 导入 FlaskSQLAlchemy 模块。

  2. 创建 Flask 应用程序的实例。

  3. 从配置文件中加载配置。

  4. 使用应用程序初始化 SQLAlchemy 对象。

  5. 从应用程序的不同部分导入 Blueprint。

  6. 将 Blueprint 注册到 Flask 应用程序中。

  7. 返回 Flask 应用程序对象。

接下来,我们将关注如何无缝地将 Blueprint 和 React 前端集成。我们需要发挥创意,发现将 Blueprint 与 React 前端融合的激动人心的方法,将我们的开发提升到新的水平。

使用 Flask Blueprint 处理 React 前端

在 React 前端和 Flask 后端的情况下,可以使用 Blueprint 来组织前端需要与后端通信的不同 API 路由和视图。前端可以向 Blueprint 中定义的后端 API 端点发出请求,后端可以返回适当的数据。

此外,使用 Flask 作为后端和 React 作为前端提供了一个灵活且强大的开发环境。Flask 是一个轻量级且 易于使用 的 Web 框架,非常适合构建 RESTful API,而 React 是一个流行且强大的前端库,允许创建复杂和动态的用户界面。使用这些技术,你可以创建高性能、可扩展的 Web 应用程序,易于维护和更新。

是时候发挥我们的想象力,探索将 Blueprint 与 React 前端结合的无限潜力了。将 Flask 后端与 React 前端集成涉及设置两者之间的通信,使用 API 端点。例如,我们设置了一个典型的 Flask Blueprint,比如 attendees Blueprint 结构,如下所示:

bizza\backend\blueprints\attendees-models
-attendee_blueprint.py

此路由应作为 React 应用的入口点。修改 attendees_blueprint.py 中现有的 Flask 路由,使其返回 JSON 数据而不是 HTML。

在 React 前端中,我们将创建一个 attendee 组件,并使用类似 axios 的库调用 Flask 路由进行 API 调用,以检索 JSON 数据并在 UI 中渲染。

更新的 attendee_blueprint.py 文件如下:

from flask import Blueprint, jsonify, requestfrom bizza.backend.blueprints.attendees.models.attendee import Attendee
from bizza.backend.blueprints.attendees.forms import AttendeeForm, EditAttendeeForm
from bizza.backend.blueprints.attendees import db
attendee_bp = Blueprint('attendee', __name__, url_prefix='/api/attendees')
@attendee_bp.route('/', methods=['GET'])
def get_attendees():
    attendees = Attendee.query.all()
    return jsonify([a.to_dict() for a in attendees])
@attendee_bp.route('/<int:attendee_id>',
    methods=['DELETE'])
def delete_attendee(attendee_id):
    attendee = Attendee.query.get_or_404(attendee_id)
    db.session.delete(attendee)
    db.session.commit()
    return jsonify(success=True)

上述代码定义了一个 Flask 蓝图,用于在应用程序中管理参会者。该蓝图在 /api/v1/attendees URL 前缀下注册。它包括获取所有参会者、添加新参会者、获取特定参会者、更新现有参会者和删除参会者的路由。

get_attendees() 函数被装饰为 @attendee_bp.route('/', methods=['GET']),这意味着它将处理对 /api/v1/attendees/ URL 的 GET 请求。它查询数据库以获取所有参会者,使用在 Attendee 模型中定义的 to_dict() 方法将它们转换为字典,并返回参会者列表的 JSON 表示。

add_attendee() 函数被装饰为 @attendee_bp.route('/', methods=['POST']),这意味着它将处理对 /api/v1/attendees/ URL 的 POST 请求。它首先从 POST 请求数据中创建一个 AttendeeForm 对象。如果表单数据有效,则使用表单数据创建一个新的参会者并将其添加到数据库中。

然后将新的参会者使用 to_dict() 方法转换为字典,并作为 JSON 响应返回。如果表单数据无效,则错误以 JSON 响应返回。

get_attendee() 函数被装饰为 @attendee_bp.route('/<int:attendee_id>', methods=['GET']),这意味着它将处理对 /api/v1/attendees/<attendee_id> URL 的 GET 请求,其中 attendee_id 是请求的特定参会者的 ID。它查询数据库以获取具有指定 ID 的参会者,使用 to_dict() 方法将其转换为字典,并返回参会者的 JSON 表示。

update_attendee() 函数被装饰为 @attendee_bp.route('/<int:attendee_id>', methods=['PUT']),这意味着它将处理对 /api/v1/attendees/<attendee_id> URL 的 PUT 请求。它首先查询数据库以获取具有指定 ID 的参会者。然后,它从 PUT 请求数据中创建一个 EditAttendeeForm 对象,使用当前参会者对象作为默认值。

如果表单数据有效,则使用新数据更新参会者对象并将其保存到数据库中。然后,使用 to_dict() 方法将更新后的参会者对象转换为字典,并作为 JSON 响应返回。如果表单数据无效,则错误以 JSON 响应返回。

delete_attendee() 函数被装饰为 @attendee_bp.route('/<int:attendee_id>', methods=['DELETE']),这意味着它将处理对 /api/v1/attendees/<attendee_id> URL 的 DELETE 请求。它查询数据库以获取具有指定 ID 的参会者,将其从数据库中删除,并返回表示成功的 JSON 响应。

利用 Flask Blueprints 来处理 React 前端与 Flask 后端的集成提供了许多好处,包括代码组织、模块化、可扩展性和可维护性。这种结构化的开发方法促进了无缝的全栈开发,同时保持了关注点的清晰分离。

摘要

随着我们即将结束本章内容,让我们花一点时间回顾一下我们所经历的激动人心的旅程。本章探讨了网络开发中的模块化架构以及 Flask Blueprints 如何帮助构建解耦的、可重用的、可维护的和可测试的 Flask 网络应用程序。

模块化、关注点分离和封装的好处仍然是模块化架构的关键要素。在 Flask 中,Blueprints 将一组相关的视图和其他代码组织成一个单独的模块。本章还涵盖了使用 Blueprints 设置 Flask 应用程序的内容。最后,我们讨论了一种非常灵活的方法,即使用 React 前端和 Flask Blueprints 构建大规模的全栈网络应用程序。

接下来,我们将探讨 Flask 中的单元测试。系好安全带,让我们深入探索 Flask 后端开发中令人兴奋的测试世界。

第十五章:Flask 单元测试

单元测试是软件开发的一个关键阶段,它保证了应用程序每个组件的正确运行。在第七章“React 单元测试”中,我们讨论了与 React 组件相关的单元测试,这是构建可靠用户界面以构建网络应用程序前端部分的过程。在后台开发中,单元测试的原则相似,只是你使用的是不同的编程语言——或者更确切地说,你仍然在与后台技术栈一起工作。

单元测试确保软件应用程序的每个组件或模块在与其他应用程序部分隔离的情况下正确工作。通过单独和彻底地测试每个单元,开发人员可以在开发周期的早期识别和修复问题,这可以从长远来看节省时间和精力。

单元测试有助于早期发现缺陷,并为重构代码提供安全网,使得随着时间的推移维护和演进应用程序变得更加容易。最终,单元测试的目标是产生符合用户需求和期望的高质量软件。

在本章中,我们将简要讨论单元测试在 Flask 中的重要性,并探讨使用 pytest 作为 Flask 应用程序的测试框架的好处。我们还将涵盖 pytest 的安装和设置过程,以及测试驱动开发(TDD)的基本原理。

此外,我们还将深入探讨编写基本测试和断言以及处理异常。在本章结束时,你将能够理解单元测试在 Flask 应用程序中的重要性,描述 pytest 是什么以及它与其他测试框架的区别,以及如何将 pytest 集成到现有的项目中。

你还将学习如何使用 pytest 测试 JSON API,了解如何向 API 端点发送请求并验证响应数据。最后,你将能够应用测试驱动开发(TDD)原则,在编写实际代码之前编写测试,并使用测试来指导开发过程。

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

  • Flask 应用程序中的单元测试

  • 介绍 pytest

  • pytest 的设置

  • pytest 的基本语法、结构和功能

  • 编写单元测试

  • 测试 JSON API

  • 使用 Flask 进行测试驱动开发

  • 处理异常

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter15

Flask 应用程序中的单元测试

Flask就像厨师的刀一样,对于网络开发者来说——它是一个多才多艺的工具,可以帮助你迅速制作出可扩展和灵活的应用程序。然而,随着 Flask 应用程序复杂性的增加,确保应用程序的所有组件正确协同工作变得越来越困难。这就是单元测试发挥作用的地方。

单元测试是一种软件测试技术,它涉及在隔离于应用程序其余部分的情况下测试应用程序的每个组件或模块。通过单独和彻底地测试每个单元,开发人员可以在开发过程的早期阶段识别和修复问题。单元测试的实践有助于快速发现缺陷,并在进行更改或修改代码时提供保障,从而使得随着时间的推移维护和演进应用程序变得更加容易。

使用 Flask 应用程序,单元测试有助于确保所有路由、视图和其他组件按预期工作。单元测试还可以帮助捕捉与数据库交互、外部 API 调用和其他外部依赖项的问题。

测试启发式方法或原则如下:

  • 首次:快速、独立、可重复、自我验证和及时

  • RITE:可读性、隔离性、全面性和明确性

  • 3A:安排、行动、断言

这些原则可以作为开发者的指南和最佳实践,确保他们的单元测试工作有效。这些测试原则可以提高代码质量,最小化错误和缺陷,并最终向应用程序用户提供更优质的软件产品。通过遵守这些原则,开发人员和测试人员可以提高代码库的整体可靠性和可维护性。

让我们简要地审视这些测试原则,以了解它们如何指导你编写出色的单元测试。

FIRST

FIRST 强调单元测试运行快速、不依赖于外部因素、可重复运行而不产生副作用、自我检查和及时编写的重要性:

  • pytest_mock插件。

  • 独立:单元测试应该设计为相互独立运行,以便一个测试的失败不会影响其他测试的执行。在 Flask 中,我们可以通过在每个测试之前使用 Flask 测试客户端重置应用程序状态来实现测试之间的独立性。

  • 可重复:单元测试应该设计为每次运行时都能产生相同的结果,无论它们在哪个环境中执行。这意味着正在测试的单元不应该依赖于外部因素,如系统时间或随机数生成器,这些因素可能会引入测试结果的可变性。

  • 自检性:单元测试应该设计成能够检查其结果并报告失败,而不需要人工干预。这意味着单元测试应该包含断言,比较预期的结果与测试的实际结果。在 Flask 中,我们可以使用内置的断言语句来检查测试结果。

  • 及时性:单元测试应该设计成能够及时编写,理想情况下在它们所测试的代码编写之前。这意味着它们应该是开发过程的一部分,而不是事后考虑。在 Flask 中,我们可以遵循 TDD(测试驱动开发)方法来确保测试在代码编写之前完成。

接下来,我们将探讨 RITE(可重复、隔离、全面和可扩展)这一测试原则,它可以提高单元测试的有效性并提升代码质量。

RITE

RITE 强调单元测试易于阅读和理解的重要性,它们应该与其它组件隔离,覆盖所有可能的场景,并且在断言中明确:

  • 可重复性:测试应该能够在不同的系统和环境中重复。这意味着测试不应该依赖于外部因素,如网络连接、时间或其他系统资源。通过确保测试可以在不同的环境中一致运行,开发者可以确信他们的代码按预期工作。

  • 隔离性:测试应该相互独立,不共享任何状态。这意味着每个测试都应该从一个干净的状态开始,不依赖于任何之前的测试结果或全局状态。通过隔离测试,开发者可以确保每个测试都在测试特定的功能部分,并且不受系统其他部分的影响。

  • 全面性:测试应该测试系统的所有方面,包括边缘情况和错误条件。这意味着开发者应该努力创建尽可能覆盖代码库的测试,包括所有可能的输入和输出。

  • 可扩展性:测试应该易于扩展和维护,随着系统的演变而发展。这意味着测试应该设计成能够适应代码库的变化,例如新功能或系统架构的变化。

简而言之,RITE 原则是有益的,因为它们可以帮助你提高代码的质量、可靠性和可维护性。

在前进的过程中,我们将探讨 3A(安排、行动和断言)这一单元测试方法,它可以使你的单元测试更易于阅读和维护。

3A

3A 是一个简单的单元测试结构指南,包括三个步骤——安排(Arrange)、行动(Act)和断言(Assert)。安排阶段设置测试场景,行动阶段执行被测试的操作,断言阶段检查预期的结果。3A 原则是设计和编写有效单元测试的最佳实践:

  • 安排(Arrange):在这个步骤中,你通过初始化对象、设置变量和其他必要操作来设置测试的条件。这确保了测试环境得到正确配置,并且被测试的系统处于预期的状态。

  • 行动(Act):在这个步骤中,你执行被测试的动作或方法调用。这可能包括向函数传递参数、在对象上调用方法或向 API 端点发出请求。关键是确保所采取的行动是具体且针对被测试的功能的。

  • 断言(Assert):在这个步骤中,你验证动作的结果是否与预期结果相符。这通常涉及到检查函数返回的值、比较方法调用前后对象的状态,或者确保 API 端点返回正确的响应状态码和数据。

接下来,我们将探讨 Pytest 作为一个广泛使用的测试框架,它能够无缝地与 Flask 集成。Pytest 赋予开发者高效创建和执行单元测试、集成测试等能力,确保 Flask Web 应用的健壮性和可靠性。

介绍 Pytest

Pytest是一个开源的 Python 测试框架,它简化了编写和执行简洁、易读测试的过程。Pytest 提供了一种简单灵活的方式来编写测试,并自带广泛的支持测试选项,包括功能测试、单元测试和集成测试。

由于其易用性、强大的固定系统以及与其他 Python 测试工具的集成,Pytest 在 Python 开发者中得到了广泛的应用。Pytest 具有自动发现并运行项目中所有测试的-test发现能力。Pytest 生成详细的报告,为开发者提供了对测试结果的宝贵见解。

这些报告包括关于执行测试的数量、每个测试的运行时间以及发生的任何失败或错误的信息。这些信息可以帮助开发者迅速定位并解决问题,从而提高代码库的整体质量。Pytest 拥有一个庞大的用户和贡献者社区,他们积极开发和维护扩展 Pytest 功能的插件。

有趣的是,Pytest 与其他测试框架(如unittestnosedoctesttoxhypothesis libraryrobot framework)相比,以其简洁和强大、多功能性和社区支持而不同,提供了易于使用的测试功能以及详细的报告。Pytest 无疑是 Python 开发者进行单元测试和其他测试需求的热门选择。

接下来,我们将逐步介绍如何设置 Pytest 并创建我们的第一个测试。

设置 Pytest

测试 Python 代码是开发过程中的一个重要部分,Pytest 是实现强大测试环境的有力工具。在本节中,我们将向您介绍设置 Pytest 的步骤,并将您的 Python 代码测试体验从业余水平提升到专业水平,提供高级功能和能力,使测试更快、更简单、更有效。

要设置 Pytest,您可以按照以下步骤操作:

  1. pip是 Python 的包安装器。在bizza/backend/项目目录中打开您的终端或命令提示符,并运行以下命令:

    pip install pytest
    

    前一行安装了 Pytest 及其所有依赖项。

  2. 在您的项目目录中的test_addition.py – 即bizza/backend/tests/test_addition.py。这是一个简单的示例测试文件,用于热身。

  3. test_addition.py中,使用以下格式编写一个简单的测试函数:

    def test_function_name():    assert expression
    

    让我们讨论前面的简短格式片段:

    • test_function_name代表测试函数的名称。

    • expression代表您想要测试的代码。

    • assert语句检查表达式是否为真,如果表达式为假,则引发错误。

注意

在 Pytest 中,测试函数通过其名称识别,并且应该以test_前缀开头。使用这种命名约定,Pytest 可以识别您的函数作为测试并自动运行它们。当您在终端中运行 Pytest 时,Pytest 会在您的代码库中搜索任何以test_开头的函数。然后,Pytest 执行这些函数并报告测试结果。

现在,让我们描述一个测试函数,该函数测试添加两个数是否产生预期的结果:

def test_addition():    assert 1 + 1 == 2

前面的代码显示了一个简单的 Pytest 测试函数,该函数测试两个数的相加。函数的名称以test_开头,这告诉 Pytest 它是一个测试函数。

函数体包含一个断言,检查1 + 1是否等于2。如果断言为true,则测试通过。如果断言为false,则测试失败,Pytest 会报告错误。

  1. bizza/backend/。运行以下命令以运行您的测试:

    pytest
    (venv) C:\bizza\backend>pytest========================================================================= test session starts =========================================================================platform win32 -- Python 3.10.1, pytest-7.3.1, pluggy-1.0.0rootdir: C:\bizza\backendplugins: Faker-16.6.0collected 1 itemtests\test_addition.py [100%]========================================================================= 1 passed in 21.61s ==========================================================================
    

    让我们看一下前面的输出:

    1. 上一段代码的第一行显示了有关平台和 Python、Pytest 以及其他相关插件的版本信息。

    2. 第二行指示测试的根目录。在这种情况下,它是C:\bizza\backend

    3. 第三行显示 Pytest 已收集一个测试项,该测试项存储在tests\test_addition.py文件中。

    4. 第四行显示了测试结果:一个单独的点表示测试通过。如果测试失败,这将显示为"F"

    5. 第五行显示了一些摘要信息,包括通过测试的数量和运行测试所需的时间。

    6. 最后,命令提示符返回,表示测试已运行完成。

假设test_addition.py函数的输出已更改为5而不是2。我们应该期待测试失败吗?当然,是的!测试应该失败。以下为失败的测试输出:

(venv) C:\bizza\backend>pytest================================================= test session starts =================================================
collected 1 item
tests\test_addition.py F                              [100%]
====================================================== FAILURES =======================================================
____________________________________________________ test_addition ____________________________________________________
    def test_addition():
>      assert 1 + 1 == 5
E      assert (1 + 1) == 5
tests\test_addition.py:3: AssertionError

前面的输出表明名为test_addition.py的测试失败了。断言1 + 1 == 5失败,因为 1 + 1 的实际结果是 2,而不是 5。

准备好下一步了吗?让我们来检查 Pytest 的基本语法和结构。然后,我们将深入探讨使用 Pytest 进行单元测试。

Pytest 的基本语法、结构和功能

Pytest 测试函数的基本语法和结构可以表示如下:

def test_function_name():    # Arrange: set up the necessary test data or
      environment
    # Act: execute the code being tested
    result = some_function()
    # Assert: check that the expected behavior is observed
    assert result == expected_result

test_function_name应该是一个描述性的名称,传达测试的目的:

  • Arrange部分设置必要的测试数据或环境,例如初始化对象或连接到数据库

  • Act部分执行被测试的代码,例如调用一个函数或执行特定的操作

  • Assert部分检查是否观察到了预期的行为,使用断言来验证代码的输出或行为是否符合预期

Pytest 支持广泛的断言,包括assert x == y, assert x != y, assert x in y,等等。Pytest 还支持使用 fixtures,可以用来管理测试依赖关系和设置测试数据和环境。

Pytest 测试函数的基本语法和结构旨在使编写清晰、简洁的测试变得容易,以验证代码按预期工作。使用 Pytest 的结构和 fixtures 的使用,你可以编写可靠、可重复且易于维护的测试。

接下来,我们将探讨 Pytest 的一个关键特性:** fixtures**。

使用 fixtures

在软件测试中,fixture是为测试运行所需定义的状态或数据集。本质上,fixtures 是帮助管理和提供一致资源(如数据、配置或对象)的函数,这些资源用于测试套件中的不同测试用例。Fixtures 使你能够为测试建立稳定且受控的环境。

它们确保每个测试用例都能访问所需的资源,而不会在多个测试中重复设置和清理方法。你可能想知道设置和清理方法是什么。让我们暂停一下,更详细地了解一下测试 Flask 应用程序中的这对组合。

在单元测试中,设置和清理方法的概念是准备和清理测试环境的关键技术,用于在每个测试用例执行前后。在深入测试用例之前,设置过程开始发挥作用。设置方法在每个测试用例之前执行,其目的是建立测试所需的条件。

例如,让我们考虑一个 Flask 单元测试场景;设置方法可以被设计成模拟 Flask 应用程序实例并配置测试客户端,从而为模拟 HTTP 请求和响应提供必要的测试基础设施。

相反,还有拆卸阶段。拆卸过程在每个测试用例执行后进行,涉及清理在设置操作期间最初建立的资源。回到 Flask 单元测试的例子,拆卸方法可能被编程为优雅地终止测试客户端并关闭 Flask 应用程序实例。这确保了没有残留的资源保持活跃,可能会干扰后续的测试。

这对设置和拆卸通常位于封装测试用例套件的类的范围内。为了更好地理解,考虑以下代码片段,它展示了如何将设置和拆卸方法结合到一个类中,以验证 Flask 应用程序:

class FlaskTestCase:    def setup(self):
        self.app = create_app()
        self.client = app.test_client()
    def teardown(self):
        self.app = None
        self.client = None
    def test_index_page(self):
        response = self.client.get("/")
        assert response.status_code == 200
        assert response.content == b"Bizza Web Application"

在前面的代码中,设置方法创建了一个 Flask 应用程序实例和一个测试客户端。另一方面,拆卸方法优雅地结束测试客户端并处理 Flask 应用程序实例。结果是,一旦测试结束,资源就得到了整洁有序的关闭。

然而,在 pytest 中,可以使用固定装置来模拟设置和拆卸范式。固定装置充当为多个测试用例提供共享资源的函数。固定装置允许你定义和管理测试依赖项。这就是 pytest 中固定装置的工作方式。你使用@pytest.fixture装饰器定义一个固定装置。然后,这个函数可以作为测试函数的参数使用,允许测试函数访问固定装置的数据或环境。

当运行测试函数时,pytest 会自动检测任何定义为参数的固定装置,并首先运行这些固定装置函数,将它们的返回值作为参数传递给测试函数。这确保了测试函数可以访问它运行正确所需的数据或环境。

以下代码片段展示了可以用来生成 Flask 应用程序实例和测试客户端的固定装置:

import pytest@pytest.fixture()
def app():
    app = create_app()
    return app
@pytest.fixture()
def client(app):
    client = app.test_client()
    return client

上述代码显示,app固定装置创建了一个 Flask 应用程序实例,而client固定装置创建了一个测试客户端。这些固定装置然后可以在测试套件中的测试用例中使用,以获取对 Flask 应用程序和测试客户端的访问。

值得注意的是,采用固定装置进行设置和拆卸的一个明显优势是它们的可重用性。通过使用固定装置,设置和拆卸逻辑可以高效地在多个测试用例之间共享。这无疑将确保测试代码更加易于维护,并且通过扩展,提高测试用例的重用性。

测试中的固定装置可以提供以下明确的好处:

  • 可重用性:你可以定义一个 fixture 一次,并在多个测试中使用它。这可以节省时间并减少重复。

  • 可读性:通过将设置代码分离到 fixture 函数中,你的测试函数可以更加专注且易于阅读。

  • 可维护性:Fixtures 确保即使你的代码库在演变过程中,你的测试也是一致的和可重复的。

pytest 中的 Fixtures 提供了一个强大且灵活的机制来管理测试依赖关系并简化你的测试工作流程。

现在,让我们深入探讨 pytest 中的参数化。使用 pytest 中的参数化测试可以让你用更少的代码重复来更彻底地测试你的代码。

pytest 中的参数化

在 pytest 中参数化测试是一个功能,它允许你编写一个可以执行不同输入参数集的单个测试函数。当你想用各种输入或配置测试一个函数或方法时,这非常有用。

要在 pytest 中参数化一个测试函数,你可以使用@pytest.mark.parametrize装饰器。这个装饰器接受两个参数:参数的名称和表示要测试的不同参数集的值或元组列表。

让我们探索 pytest 中的参数化测试函数:

import pytestdef add(a, b):
    return a + b
@pytest.mark.parametrize("a, b, expected_result", [
    (1, 2, 3),
    (10, 20, 30),
    (0, 0, 0),
    (-1, 1, 0), ids=["1+2=3", "10+20=30", "0+0=0",
        "-1+1=0"]
])
def test_addition(a, b, expected_result):
    assert add(a, b) == expected_result

上述代码是 pytest 中参数化测试的演示,用于测试具有多个输入值的函数。

被测试的函数是add(a, b),它接受两个参数ab,并返回它们的和。@pytest.mark.parametrize装饰器用于提供输入值列表及其对应的预期结果。

装饰器接受三个参数:

  • 一个以逗号分隔的参数名称字符串——在本例中为"a, b, expected_result"

  • 表示参数集及其预期结果的元组列表。在本例中,我们有四个参数集:(1, 2, 3)(10, 20, 30)(0, 0, 0)(-1, 1, 0)

  • 一个可选的ids参数,它为测试用例提供自定义名称。

对于列表中的每个参数集,pytest 将使用相应的abexpected_result值执行test_addition()函数。测试函数中的assert语句检查add(a, b)的实际结果是否与预期结果匹配。

当测试函数执行时,pytest 将为每个参数集生成一个单独的报告,这样你可以确切地看到哪些案例通过了,哪些失败了:

  • 第一个参数集(1, 2, 3)测试add()函数是否正确地将12相加,结果为3

  • 第二个参数集(10, 20, 30)测试add()是否正确地将1020相加,结果为30

  • 第三个参数集(0, 0, 0)测试add()是否正确地将两个零相加,结果为0

  • 第四个参数集(-1, 1, 0)测试add()是否正确地将-11相加,结果为0

参数化测试可以通过减少测试函数中的重复代码量以及更容易测试广泛的输入和配置,帮助你编写更简洁和有效的测试代码。

这还不是 pytest 功能的全部。接下来,我们将探索 pytest 中的外部依赖模拟。

pytest 中的外部依赖模拟

模拟外部依赖是一种测试技术,它涉及创建外部依赖的模拟版本,如 API 或数据库,以隔离你的测试代码从这些依赖中。当你编写单元测试时,你通常只想测试测试范围内的代码,而不是它所依赖的任何外部服务或库。

这种做法有助于保持你的测试集中且快速,同时避免由于依赖可能不可用或行为不可预测的外部依赖而产生的不正确或错误的测试结果。

要创建一个模拟对象,你必须使用一个模拟框架,例如 unittest.mockpytest-mock,来创建一个模仿真实对象行为的假对象。然后,你可以使用这个模拟对象在你的测试中代替真实对象,这允许你在受控环境中测试你的代码。

例如,假设你正在测试一个从外部 API 获取数据的函数。你可以使用模拟框架来创建一个模仿 API 行为的模拟对象,然后在测试中使用这个模拟对象而不是实际调用 API。这允许你在受控环境中测试你的函数行为,而不必担心网络连接或外部 API 的行为。

在你的测试中使用模拟策略可以帮助你编写更全面的测试,因为它允许你模拟错误条件或难以或无法通过真实外部依赖复制的边缘情况。例如,你可以使用模拟对象来模拟网络超时或数据库错误,然后验证你的测试代码是否正确处理了这些条件。

假设在我们的项目中有一个 Speaker 类,它依赖于外部的 email_service 模块来向演讲者发送电子邮件通知。我们想要为 Speaker 类编写一个测试,以验证当添加新演讲者时,Speaker 类会发送预期的电子邮件通知。为了实现这一点,我们可以使用 pytest-mock 插件来模拟 email_service 模块并检查是否执行了预期的调用。

让我们深入到代码实现中。

bizza/backend/tests 目录下添加 test_speaker.py 文件:

# test_speaker.pyfrom bizza.backend.speaker import Speaker
def test_speaker_notification(mocker):
    # Arrange
    email_mock = mocker.patch(
        "bizza.backend.email_service.send_email")
    speaker = Speaker("John Darwin", "john@example.com")
    # Act
    speaker.register()
    # Assert
    email_mock.assert_called_once_with(
        "john@example.com",
        "Thank you for registering as a speaker",
        "Hello John, \n\nThank you for registering as a
        speaker. We look forward to your talk!\n\nBest
        regards,\nThe Conference Team"
    )

在前面的代码中,我们使用 mocker.patchemail_service.send_email 函数创建了一个模拟对象。然后,我们创建了一个新的 Speaker 对象并调用了 Speaker 对象的 register() 方法,这应该会触发发送电子邮件通知。

然后,我们使用了模拟对象的assert_called_once_with方法来检查预期的电子邮件是否以正确的参数发送。如果send_email函数以不同的参数被调用,测试将失败。

通过使用pytest-mock来模拟外部依赖项,我们可以将我们的测试从任何潜在的网络问题或其他email_service模块的依赖项中隔离出来。这使得我们的测试更加可靠,并且随着时间的推移更容易维护。

模拟外部依赖项是一种强大的技术,可以将测试代码从外部服务或库中隔离出来,并创建受控环境,允许你编写全面、可靠的测试。

编写单元测试

使用 pytest 编写测试涉及创建验证代码功能的测试函数。这些测试函数由 pytest 执行,可以组织成测试模块和测试包。除了测试函数之外,pytest 还提供了其他测试功能,如 fixtures、参数化和模拟,这些可以帮助你编写更健壮和高效的测试。

在本节中,我们将介绍使用 pytest 编写测试的基础知识,包括创建测试函数,使用断言来检查预期行为,以及将测试组织成测试套件。

现在,让我们集中精力编写一个应用程序用户注册组件的单元测试。

单元测试用户注册

单元测试是软件开发过程中的关键部分。正如之前所述,单元测试无疑允许开发者验证他们的代码是否正确且可靠地工作。单元测试特别重要的一个领域是用户注册,这是许多应用的一个关键部分。

用户注册功能通常涉及收集用户输入,验证输入,将其存储在数据库中,并向用户发送确认电子邮件。彻底测试这些功能对于确保其按预期工作以及用户可以成功且安全地注册非常重要。

在这个上下文中,单元测试可以用来验证注册功能是否正确处理各种场景,例如有效和无效的输入、重复的用户名和电子邮件确认。

让我们检查一个用户注册的单元测试实现。

用户创建单元测试

让我们来测试新用户是否可以被创建并保存到数据库中。在tests目录下创建test_user_login_creation.py

def test_create_user(db):    # Create a new user
    user = User(username='testuser',
        password='testpassword',
            email='test@example.com')
    #Add the user to the database
    db.session.add(user)
    db.session.commit()
    # Retrieve the user from the database
    retrieved_user = db.session.query(User)
        .filter_by(username='testuser').first()
    # Assert that the retrieved user matches the original
      user
    assert retrieved_user is not None
    assert retrieved_user.username == 'testuser'
    assert retrieved_user.email == 'test@example.com'

在前面的测试片段中,我们创建了一个具有特定usernamepasswordemail address的新用户。然后,我们将用户添加到数据库中并提交更改。最后,我们使用查询从数据库中检索用户,并断言检索到的用户在所有字段上与原始用户匹配。这个测试确保新用户可以成功创建并保存到数据库中。

输入验证单元测试

让我们来测试注册表单是否正确验证用户输入并返回适当的错误消息:

def test_user_registration_input_validation(client, db):    # Attempt to register a new user with an invalid
      username
    response = client.post('/register',
        data={'username': 'a'*51,
            'password': 'testpassword',
                'email': 'test@example.com'})
    # Assert that the response status code is 200 OK
    assert response.status_code == 200
    # Assert that an error message is displayed for the
      invalid username
    assert b'Invalid username. Must be between 1 and 50
        characters.' in response.data
    # Attempt to register a new user with an invalid email
      address
    response = client.post('/register',
        data={'username': 'testuser',
            'password': 'testpassword',
                'email': 'invalid-email'})
    # Assert that the response status code is 200 OK
    assert response.status_code == 200
    # Assert that an error message is displayed for the
      invalid email address
    assert b'Invalid email address.' in response.data
    # Attempt to register a new user with a password that
      is too short
    response = client.post('/register',
        data={'username': 'testuser',
            'password': 'short',
                'email': 'test@example.com'})
    # Assert that the response status code is 200 OK
    assert response.status_code == 200
    # Assert that an error message is displayed for the
      short password
    assert b'Password must be at least 8 characters long.'
        in response.data

在前面的测试中,我们模拟了使用各种无效输入尝试注册新用户的情况,例如无效的usernameemail addresspassword属性过短。我们使用无效输入数据向'/register'端点发送POST请求,并断言响应状态码为200 OK,表示注册表单已成功提交,但存在错误。

然后,我们断言页面为每个无效输入显示了适当的错误消息。这个测试确保注册表单正确验证用户输入,并为无效输入返回适当的错误消息。

接下来,我们将检查login组件的单元测试。

单元测试用户登录

单元测试用户登录涉及测试负责验证尝试登录应用程序的用户代码的功能。这通常涉及验证用户凭证是否正确,并根据认证是否成功返回适当的响应。

在这种情况下,单元测试可以帮助确保登录过程可靠且安全,对无效登录尝试进行适当的错误处理。此外,单元测试还可以帮助识别登录过程中的潜在漏洞,例如注入攻击或密码猜测尝试。

有效凭证用户单元测试

让我们来测试一个使用有效凭证的用户可以成功登录并访问应用程序:

def test_user_login(client, user):    # Login with valid credentials
    response = client.post('/login',
        data={'username': user.username,
            'password': user.password},
        follow_redirects=True)
    # Check that the response status code is 200 OK
    assert response.status_code == 200
    # Check that the user is redirected to the home page
      after successful login
    assert b'Welcome to the application!' in response.data

在前面的测试中,我们使用客户端固定值模拟用户通过向登录端点发送带有有效凭证的POST请求来登录。我们还使用用户固定值创建一个具有有效凭证的测试用户。在发送登录请求后,我们检查响应状态码是否为200 OK,以及用户是否被重定向到主页,这表明登录成功。

无效凭证用户单元测试

让我们来测试一个使用无效凭证的用户无法登录,并收到适当的错误信息:

def test_login_invalid_credentials(client):    # Try to log in with invalid credentials
    response = client.post('/login',
        data={'username': 'nonexistentuser',
        'password': 'wrongpassword'})
    # Check that the response status code is 401
      Unauthorized
    assert response.status_code == 401
    # Check that the response contains the expected error
      message
    assert b'Invalid username or password' in response.data

在前面的测试中,我们尝试使用无效的用户名和密码登录,并期望服务器响应401 Unauthorized状态码和指示凭证无效的错误消息。

测试 SQL 注入攻击

让我们来测试代码是否正确验证用户输入以防止 SQL 注入攻击:

def test_sql_injection_attack_login(client):    # Attempt to login with a username that contains SQL
      injection attack code
    response = client.post('/login',
        data={'username': "'; DROP TABLE users; --",
            'password': 'password'})
    # Check that the response status code is 401
      Unauthorized
    assert response.status_code == 401
    # Check that the user was not actually logged in
    assert current_user.is_authenticated == False

在前面的测试中,我们尝试使用 SQL 注入攻击代码作为登录表单中的username输入。测试检查响应状态码是否为401 Unauthorized,这表明攻击未成功,用户未登录。

它还检查current_user.is_authenticated属性是否为False,确认用户未认证。这个测试有助于确保代码正确验证用户输入,以防止 SQL 注入攻击。

测试密码强度

让我们测试代码是否正确验证用户密码以确保它们满足最小复杂度要求(例如,最小长度、特殊字符的要求等):

def test_password_strength():    # Test that a password with valid length and characters
      is accepted
    assert check_password_strength("abc123XYZ!") == True
    # Test that a password with an invalid length is rejected
    assert check_password_strength("abc") == False
    # Test that a password without any special characters
      is rejected
    assert check_password_strength("abc123XYZ") == False
    # Test that a password without any lowercase letters is
      rejected
    assert check_password_strength("ABC123!") == False
    # Test that a password without any uppercase letters is
      rejected
    assert check_password_strength("abc123!") == False
    # Test that a password without any numbers is rejected
    assert check_password_strength("abcXYZ!") == False

在前面的测试中,check_password_strength() 是一个接受密码字符串作为输入的函数,如果它满足最小复杂度要求则返回 True,否则返回 False。这个单元测试通过测试各种场景来验证该函数按预期工作。

使用测试框架 Pytest 和编写有效的单元测试,开发者可以尽早捕捉到错误和缺陷,降低生产中的错误风险,并提高代码库的整体质量和可靠性。

注意

前面的测试假设你已经设置了一个 Flask 应用程序,其中包含用户注册和登录的路由,以及一个带有用户模型的 SQLAlchemy 数据库。我们还假设你已经配置了一个带有 Pytest 的 Flask 测试客户端固定装置(client)的测试客户端。

接下来,我们将查看测试 JSON API 以确保 API 端点按预期工作。

测试 JSON API

测试 JSON API 是开发任何与外部客户端通信的 Web 应用程序的重要部分。API 提供了一种简单灵活的方式在服务器和客户端之间交换数据。在将 API 暴露给外部用户之前,确保 API 按预期工作至关重要。

单元测试 JSON API 涉及验证 API 端点对于不同类型的输入数据返回预期的结果,并处理错误情况。此外,确保 API 遵循行业标准协议并对常见 Web 漏洞具有安全性也是至关重要的。这样,开发者可以确保 Web 应用程序的可靠性和安全性,并最大限度地减少错误或安全漏洞的风险。

让我们通过一个包含四个测试的测试套件来过一遍——test_get_all_speakerstest_create_speakertest_update_speakertest_delete_speaker

import pytestimport requests
# Define the base URL for the speakers API
BASE_URL = 'https://localhost:5000/v1/api/speakers/'
def test_get_all_speakers():
    # Send a GET request to the speakers API to retrieve
      all speakers
    response = requests.get(BASE_URL)
    # Check that the response has a status code of 200 OK
    assert response.status_code == 200
    # Check that the response contains a JSON object with a
      list of speakers
    assert isinstance(response.json(), list)

前面的测试,test_get_all_speakers,向演讲者 API 发送一个 GET 请求检索所有演讲者,然后检查响应状态码为 200 OK 并包含一个包含演讲者列表的 JSON 对象。

测试创建演讲者数据

以下测试,test_create_speaker,定义了一个要创建的演讲者数据对象,向演讲者 API 发送一个 POST 请求使用这些数据创建一个新的演讲者,然后检查响应状态码为 201 CREATED 并包含一个包含新创建的演讲者数据的 JSON 对象:

def test_create_speaker():    # Define the speaker data to be created
    speaker_data = {
        'name': 'John Darwin',
        'topic': 'Python',
        'email': 'john@example.com',
        'phone': '555-555-5555'
    }
    # Send a POST request to the speakers API to create a
      new speaker
    response = requests.post(BASE_URL, json=speaker_data)
    # Check that the response has a status code of 201
      CREATED
    assert response.status_code == 201
    # Check that the response contains a JSON object with
      the newly created speaker data
    assert response.json()['name'] == 'John Darwin'
    assert response.json()['topic'] == 'Python'
    assert response.json()['email'] == 'john@example.com'
    assert response.json()['phone'] == '555-555-5555'

更新演讲者数据对象

以下测试代码,test_update_speaker,定义了一个要更新的演讲者数据对象,向演讲者 API 发送一个 PUT 请求使用这些数据更新 id 1 的演讲者,然后检查响应状态码为 200 表示更新成功:

def test_update_speaker():    # Define the speaker data to be updated
    speaker_data = {
        'name': 'John Doe',
        'topic': 'Python for Data Science',
        'email': 'johndoe@example.com',
        'phone': '555-555-5555'
    }
    # Send a PUT request to the speakers API to update the
      speaker data
    response = requests.put(BASE_URL + '1',
        json=speaker_data)
    # Check that the response has a status code of 200 OK
    assert response.status_code == 200
    # Check that the response contains a JSON object with
      the updated speaker data
    assert response.json()['name'] == 'John Darwin'
    assert response.json()['topic'] == 'Python for Data
        Science'
    assert response.json()['email'] == 'john@example.com'
    assert response.json()['phone'] == '555-555-5555'

测试删除演讲者数据对象

以下代码片段向 Speakers API 发送一个DELETE请求来删除ID 1的演讲者。测试函数检查响应的状态码为204 NO CONTENT。如果成功从 API 中删除了ID 1的演讲者,API 的响应应该有状态码204 NO CONTENT。如果找不到演讲者或删除请求中存在错误,响应状态码将不同,测试将失败:

def test_delete_speaker():    # Send a DELETE request to the speakers API to delete
      the speaker with ID 1
    response = requests.delete(BASE_URL + '1')
    # Check that the response has a status code of 204 NO
      CONTENT
    assert response.status_code == 204

在这一点上,你可能想知道,为什么在我们应用程序中一旦出现错误,我们就需要投入时间和资源来纠正它们,而完全有可能从一开始就主动预防它们的发生?

接下来,我们将讨论使用 Flask 作为软件开发的重要主动方法!

测试驱动开发与 Flask

TDD 是一种软件开发方法,你需要在编写实际代码之前编写自动化测试。这个过程包括为特定的功能或功能编写测试用例,然后编写必要的最少代码以使测试通过。一旦测试通过,你将编写额外的测试来覆盖不同的边缘情况和功能,直到你完全实现了所需的功能。

以使用 Flask 的参与者端点作为案例研究,TDD 过程可能看起来是这样的:

  1. 定义功能:第一步是定义你想要实现的功能。在这种情况下,功能是一个允许用户查看活动参与者列表的端点。

  2. 编写测试用例:接下来,你必须编写一个测试用例来定义端点的预期行为。例如,你可能编写一个测试来检查端点返回包含参与者列表的 JSON 响应。

  3. 运行测试:然后你运行测试,由于你还没有实现端点,所以测试将失败。

  4. 编写最少代码:你编写必要的最少代码以使测试通过。在这种情况下,你会编写参与者端点的代码。

  5. 再次运行测试:然后,你必须再次运行测试,这次应该会通过,因为你已经实现了端点。

  6. 如果活动不存在,将出现404错误。

现在,让我们使用 TDD 方法实现参与者端点,从编写失败的测试用例开始,因为我们还没有实现端点。

定义功能

第一步是定义你想要实现的功能。在这种情况下,功能是一个允许用户查看活动参与者列表的端点。

编写失败的测试用例

下一步是编写一个测试用例,检查参与者端点是否返回预期的数据。这个测试最初应该会失败,因为我们还没有实现端点。

tests目录内创建test_attendees.py,并将以下代码添加到bizza/backend/tests/test_attendees.py中:

from flask import Flask, jsonifyimport pytest
app = Flask(__name__)
@pytest.fixture
def client():
    with app.test_client() as client:
        yield client
def test_attendees_endpoint_returns_correct_data(client):
    response = client.get('/events/123/attendees')
    expected_data = [{'name': 'John Darwin',
        'email': 'john@example.com'},
            {'name': 'Jane Smith',
                'email': 'jane@example.com'}]
    assert response.json == expected_data

实现通过测试的最少代码

现在,我们可以实现参会者端点函数以返回硬编码的数据。这是使测试通过所需的最小代码量:

# Define the attendee endpoint@app.route('/events/<int:event_id>/attendees')
def get_attendees(event_id):
    # Return a hardcoded list of attendees as a JSON
      response
    attendees = [{'name': 'John Darwin',
        'email': 'john@example.com'},
            {'name': 'Jane Smith',
                'email': 'jane@example.com'}]
    return jsonify(attendees)

运行测试并确保其通过

再次运行测试以确保它现在可以通过:

$ pytest test_attendees.py----------------------------------------------------------------------
Ran 1 test in 0.001s
OK

代码重构

现在我们有了通过测试,我们可以重构代码以提高其可维护性、效率和可读性。例如,我们可以用从数据库或外部 API 检索的数据替换硬编码的数据。

编写额外的测试用例

最后,我们可以编写额外的测试用例以确保端点在不同场景下的行为正确。例如,我们可能会编写测试以确保端点正确处理无效输入,或者在没有找到给定活动的参会者时返回空列表。

使用 TDD(测试驱动开发)流程,你可以确保你的代码经过彻底测试,并且你已经实现了所有期望的功能。这种方法可以帮助你在开发早期阶段捕捉到错误,并使未来维护和重构代码变得更加容易。

到目前为止,我们已经讨论了 TDD 作为一种软件开发方法,其中测试是在实际代码实现之前创建的。这种方法鼓励开发者编写定义代码预期行为的测试,然后编写代码本身以使测试通过。接下来,我们将深入探讨 Flask 测试套件中的异常处理。

处理异常

使用单元测试处理异常是一种软件开发技术,它涉及测试代码在运行时可能遇到的不同类型的异常的处理方式。异常可能由各种因素触发,例如无效输入、意外输入或代码运行环境中的问题。

单元测试是编写小型、自动化的测试以确保单个代码单元按预期工作的实践。在处理异常方面,单元测试可以帮助确保代码能够适当地响应各种错误条件。作为开发者,你需要测试你的代码能够优雅地处理异常。你可以在受控环境中模拟这些错误条件,以便你对代码处理可能发生的异常的能力更有信心。

例如,在一个具有attendees端点的 Flask 应用程序中,你可能想测试应用程序如何处理没有参会者的活动请求。通过编写一个单元测试,向端点发送一个没有参会者的活动的请求,我们可以确保应用程序返回适当的错误响应代码和消息,而不是崩溃或提供不准确响应。

让我们深入探讨如何处理参会者端点的异常的代码实现:

from flask import Flask, jsonifyapp = Flask(__name__)
class Event:
    def __init__(self, name):
        self.name = name
        self.attendees = []
    def add_attendee(self, name):
        self.attendees.append(name)
    def get_attendees(self):
        if not self.attendees:
            raise Exception("No attendees found for event")
        return self.attendees
@app.route('/event/<event_name>/attendees')
def get_attendees(event_name):
    try:
        event = Event(event_name)
        attendees = event.get_attendees()
    except Exception as e:
        return jsonify({'error': str(e)}), 404
    return jsonify(attendees)

在先前的实现中,我们向Event类添加了一个自定义异常,名为Exception("No attendees found for event")。在get_attendees方法中,如果没有参与者,我们将抛出此异常。在 Flask 端点函数中,我们将Event实例化和get_attendees调用包裹在try/except块中。

如果抛出异常,我们将返回一个包含错误信息和404状态码的 JSON 响应,以指示请求的资源未找到。

让我们检查测试函数:

def test_get_attendees_empty():    event_name = 'test_event'
    app = create_app()
    with app.test_client() as client:
        response =
            client.get(f'/event/{event_name}/attendees')
        assert response.status_code == 404
        assert response.json == {'error': 'No attendees
            found for event'}
def test_get_attendees():
    event_name = 'test_event'
    attendee_name = 'John Doe'
    event = Event(event_name)
    event.add_attendee(attendee_name)
    app = create_app()
    with app.test_client() as client:
        response =
            client.get(f'/event/{event_name}/attendees')
        assert response.status_code == 200
        assert response.json == [attendee_name]

在第一个测试函数test_get_attendees_empty()中,我们期望端点返回404状态码和错误信息 JSON 响应,因为没有参与者参加该活动。在第二个测试test_get_attendees()中,我们向活动添加一个参与者,并期望端点返回200状态码和包含参与者姓名的 JSON 响应。

当你在代码中对预期的异常进行测试并优雅地处理它们时,你可以确保你的应用程序按预期行为,并在需要时向用户提供有用的错误信息。

摘要

单元测试作为 Flask 应用程序开发的关键方面,确保了应用软件的可靠性和功能性。在本章中,我们学习了如何为 Flask 应用程序的各个组件构建和实施有效的单元测试。我们探讨了 Pytest 如何简化测试过程并提高开发者的生产力。

本章涵盖了 Pytest 的基础知识,包括其介绍、设置过程、基本语法和功能。我们发现了设置和清理方法的重要性,这些方法有助于创建受控的测试环境,并在每个测试用例之后确保资源的适当处置。

通过应用这些技术,我们能够创建更健壮和隔离的单元测试,这些测试反映了现实世界的场景。此外,我们提供了如何编写单元测试、测试 JSON API、应用 TDD 以及处理 Flask 应用程序中的异常的指南。通过采用这些实践,开发者可以提高其 Flask 应用程序的整体质量,并最大限度地减少错误和缺陷的风险。

随着我们继续前进并结束构建健壮且可扩展的 Flask 应用程序的旅程,下一章将深入探讨容器化和部署的世界。我们将探讨如何容器化 Flask 应用程序,使我们能够复制开发环境,并轻松地将我们的应用程序部署到各种平台。

我们还将深入研究将 Flask 应用程序部署到云服务,利用 Docker 和 AWS 等平台的力量进行高效和可扩展的部署。

第十六章:容器化与 Flask 应用程序部署

经过漫长的旅程,我们终于到达了最后一章!我们兴奋得无法用言语表达!现在,我们即将开始展示我们的全栈 Web 应用程序给世界的最后一圈。在当今的软件开发生态中,容器化采用的步伐正在迅速加快。

根据 Gartner 的预测,生产中容器化应用程序的采用将显著增加,预计到 2022 年,全球超过 75%的组织将使用它们,这一比例从 2020 年报告的不到 30%有显著增长 (www.gartner.com/en/documents/3985796)。

容器化与软件应用程序的部署已成为开发者保持现代性和需求所必需的技能。那些拥有容器化和部署软件应用程序技能和知识的开发者,将更好地满足现代软件开发实践的需求,跟上行业趋势,并在就业市场上保持竞争力。

容器化允许开发者将应用程序及其所需依赖打包成一个标准化的、可移植的容器,该容器可以在不同的计算环境中一致运行。当然,部署确保了你的应用程序能够到达生产环境,在那里最终用户可以使用它。

在本章中,我们将讨论容器化作为改变信息技术行业的革命。我们将探讨容器化在软件开发中的重要性及其带来的好处,以及它所解决的问题。

此外,我们将深入探讨软件开发行业中的一种容器化平台,称为Docker。我们将介绍 Docker,并使用它来容器化 React 前端和 Flask 后端。我们将讨论 Docker 的优势以及为什么它在开发者中如此受欢迎。

到本章结束时,你将了解容器化在现代软件开发中的重要性,你将能够将 React 和 Flask 应用程序打包成可以运输和共享的容器。

最后,你将学习如何使用亚马逊网络服务AWS)Elastic Beanstalk 来部署利用 AWS 完全托管云平台的 React-Flask 应用程序,这使得开发者可以轻松地部署、管理和扩展他们的 Web 应用程序和服务。

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

  • 什么是容器化?

  • 介绍 Docker

  • 容器化 React 和 Flask 应用程序

  • 理解 AWS 弹性容器注册

  • 使用docker-compose

  • 将 React 和 Flask 应用程序部署到 AWS Elastic Beanstalk

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter16

由于页面数量限制,一些较长的代码块已被缩短。请参考 GitHub 获取完整代码。

什么是容器化?

容器化是一种软件开发实践,涉及将应用程序及其所需依赖项打包到一个称为容器的自包含单元中。容器是一个隔离和轻量级的运行时环境,提供了一种在不同计算环境中一致和可重复运行应用程序的方式。

假设你在运行 MacOS 的本地机器上使用 Flask 框架开发了一个 Web 应用程序。你希望将这个应用程序部署到运行 Ubuntu Linux 的服务器上,以生产环境运行。然而,操作系统版本、依赖项或其他系统配置可能存在差异,这些差异可能会影响应用程序的行为。

通过将你的 Flask 应用程序及其所有必需的依赖项打包到容器中,你可以确保应用程序在不同计算环境中的一致性和可靠性运行。容器将提供一个隔离和轻量级的运行时环境,封装应用程序和相关依赖项,确保它在底层系统配置变化的情况下也能一致运行。

容器类似于虚拟机(VMs),因为它们提供了一种将应用程序与底层主机操作系统隔离的方法。然而,虽然 VMs 需要主机操作系统的完整副本才能运行,但容器只需要运行应用程序所需的最小运行时组件。

容器利用一种称为容器化的技术,该技术使用操作系统的虚拟化。容器化允许在同一主机操作系统上运行多个容器,每个容器都有自己的独立文件系统、网络和进程空间。通过容器化,开发者可以降低部署时间和成本。

让我们来看看容器化的其他一些好处:

  • 容器提供了一种标准化的打包应用程序及其所需依赖项的方式,这减少了配置和设置所需的时间和精力。

  • 容器可以在不同的计算环境中移动,允许在任何具有相同容器运行时系统的系统上部署。这种可移植性方法消除了为不同环境创建和维护单独部署配置的需要。

  • 容器共享相同的操作系统内核,与传统的虚拟化相比,这能更有效地使用系统资源。

  • 容器在应用程序及其伴随的依赖项之间提供隔离,使得在共享基础设施上运行应用程序时可能出现的冲突和错误变得过时。

简而言之,容器是轻量级的、自包含的运行应用程序,可移植的,高效的,并且可以根据需要轻松复制和扩展。

尽管有几种容器化技术可供选择,我们将在下一节具体讨论 Docker。在深入探讨 Docker 之前,让我们简要地看看软件行业中可用的其他容器化工具和平台:

  • Kubernetes:一个开源的容器编排系统,用于自动化部署、扩展和管理容器化应用程序

  • Apache Mesos:一个开源平台,用于管理和部署容器化应用程序和大数据服务

  • LXC/LXD:一种使用轻量级虚拟机提供隔离和资源管理的容器化解决方案

  • CoreOS rkt:一个为容器环境提供安全、简单和速度的容器运行时

  • OpenVZ:一个开源的容器化平台,为 Linux 提供基于容器的虚拟化

  • AWS 弹性容器服务 (ECS): AWS 提供的一项完全管理的容器编排服务

  • Google Kubernetes Engine (GKE):由 Google Cloud Platform 提供的一项完全管理的 Kubernetes 服务

随着对可扩展和高效软件部署的需求不断增长,越来越多的开发者将转向 Docker 作为解决方案。在下一节中,我们将探讨 Docker 的基础知识以及它如何帮助您简化开发工作流程。

介绍 Docker

Docker是一个流行的平台,用于在容器中开发、打包和部署应用程序。在 Docker 发明之前,软件开发者必须处理软件依赖性问题,这意味着软件在一个电脑上运行良好,但在另一个系统上可能无法工作。

软件开发者会在他们的电脑上创建程序,但当他们试图与他人分享时,事情常常出错。在某一台电脑上运行完美的程序可能在另一台电脑上无法运行,这是因为操作系统、软件版本、配置文件或其他系统相关因素的不同。

为了解决这个问题,2013 年一群开发者发布了一个名为 Docker 的工具。Docker 允许开发者将程序及其所有必要的依赖项打包成一个称为Docker 镜像的东西。Docker 镜像是一个只读模板,包含创建 Docker 容器的指令。Docker 镜像包括运行应用程序所需的应用程序代码、运行时、库、依赖项和配置。

使用 Docker,开发者可以为他们的程序创建 Docker 镜像并与他人共享。Docker 容器是 Docker 镜像的可运行实例。Docker 容器是一个轻量级、隔离和可移植的环境,可以在支持 Docker 的任何系统上运行。这意味着程序将在每台电脑上以相同的方式运行,这使得共享和部署变得更加容易。

开发者可以通过编写Dockerfile来创建 Docker 镜像,这是一个包含构建 Docker 镜像指令的文本文件。Dockerfile 指定了基础镜像,添加了必要的包和文件,并设置了应用程序的配置选项。

一旦你构建了你的应用程序 Docker 镜像,你可能希望将其部署到生产环境或发送给其他开发者。为了实现这一点,你可以使用 Docker 仓库,这是一个用于存储和分发 Docker 镜像的中心仓库。Docker Hub 是最受欢迎的公共仓库,但你也可以为你的组织设置自己的私有仓库。在本章的过程中,我们将存储本书项目的 Docker 镜像到 AWS 弹性容器 注册 (ECR)。

Docker Compose 是 Docker 生态系统中的另一个有趣工具。Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。Docker Compose 使用 YAML 文件来定义运行应用程序所需的服务、网络和卷。在下一节中,我们将详细讨论 Docker Compose。接下来,我们将探讨如何将一个简单的 Flask 应用程序容器化。

创建 Flask 应用程序

现在,让我们通过一个简单的 Flask 应用程序演示使用 Docker 进行容器化的过程:

  1. docs.docker.com/get-docker/下载 Docker 并在你的系统上安装 Docker。

图 16.1 – Docker 的下载页面

图 16.1 – Docker 的下载页面

  1. 选择适合你的 Docker 平台的适当计算机操作系统。

  2. Docker 安装完成后,使用以下命令在终端中测试它:

    Docker version and you will get the following output:
    

图 16.2 – 验证 Docker 安装的命令

图 16.2 – 验证 Docker 安装的命令

  1. 现在 Docker 已安装在你的计算机上,运行mkdir bizza-docker命令来创建一个新的工作目录,用于使用 Docker 容器部署 Flask 应用程序。然后,输入cd bizza-docker

图 16.3 – 创建 Docker 工作目录

图 16.3 – 创建 Docker 工作目录

让我们为新的 Flask Docker 应用程序创建一个虚拟环境。

  1. 在终端中运行python –m venv venv来安装虚拟环境。

  2. 使用以下命令激活虚拟环境:

    • venv\Scripts\activate

    • source venv/bin/activate

  3. 在 Docker 项目目录bizza-docker/内部,创建一个app.py文件,并添加以下代码片段以运行一个简单的 Flask 应用程序:

    from flask import Flaskapp = Flask(__name__)@app.route('/')def index():    return "Bizza Web App Dockerization!"if __name__ == "__main__":    app.run(host='0.0.0.0', port=5001, debug=True)
    

    前面的代码运行了一个简单的 Flask 应用程序,在浏览器中显示Bizza Web App Docker 化

  4. bizza-docker/内部创建一个.flaskenv文件,并添加以下代码片段:

    FLASK_APP = app.pyFLASK_DEBUG = True
    
  5. 现在,在终端中使用flask run运行 Flask 应用程序,你将得到以下输出:

图 16.4 – 测试 Flask 应用程序

图 16.4 – 测试 Flask 应用程序

现在 Flask 应用已经运行正常,让我们创建一个 Flask 应用程序requirements.txt文件,以便能够重现此简单应用程序的依赖关系。

  1. 运行pip freeze > requirements.txt命令,你将得到以下输出:

图 16.5 – Flask 依赖的 requirements.txt 文件

图 16.5 – Flask 依赖的 requirements.txt 文件

下面的块显示了requirements.txt文件的内容:

blinker==1.6.2click==8.1.3
colorama==0.4.6
Flask==2.3.2
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.2
python-dotenv==1.0.0
Werkzeug==2.3.3

我们现在拥有了构建 Docker 镜像所需的所有资源。

创建 Dockerfile

Dockerfile 定义了 Flask 应用程序的容器镜像。我们将创建一个 Dockerfile,它使用官方的 Python 3.8 镜像作为基础镜像,安装 Flask 及其依赖项,并将 Flask 应用程序代码复制到容器中。

bizza-docker目录中,创建一个 Dockerfile 文件 – 确保在创建 Dockerfile 文件时使用大写字母D。不用担心为什么;这是一个约定:

FROM python:3.8.2-alpineWORKDIR /packt-bizza-docker
ADD . /packt-bizza-docker
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip3 install -r requirements.txt
COPY . .
ENV FLASK_APP=app.py
ENV FLASK_ENV=development
EXPOSE 5001
CMD ["python3", "app.py"]

为了简化部署过程并确保在不同开发、测试和生产阶段之间保持环境的一致性,让我们检查前面 Dockerfile 的结构。

上述代码是一个用于构建 Python Flask Web 应用程序 Docker 镜像的 Dockerfile:

  • FROM python:3.8.2-alpine: 这指定了构建 Docker 镜像时要使用的基镜像。在这种情况下,基镜像为python:3.8.2-alpine,这是 Python 3.8.2 的一个轻量级版本,针对在 Alpine Linux 上运行进行了优化。

  • WORKDIR /packt-bizza-docker: 这将 Docker 容器的当前工作目录设置为/packt-bizza-docker。Dockerfile 中的所有后续命令都将相对于此目录执行。

  • ADD . /packt-bizza-docker: 这一行将当前目录中的所有文件和目录复制到 Docker 容器中的/packt-bizza-docker目录。

  • COPY requirements.txt .: 这将当前目录中的requirements.txt文件复制到 Docker 容器的根目录。

  • RUN pip install --no-cache-dir -r requirements.txt: 这使用pip安装requirements.txt文件中指定的 Python 依赖项。--no-cache-dir选项确保pip不会缓存下载的包。

  • RUN pip install -r requirements.txt: 这一行使用pip3安装requirements.txt文件中指定的 Python 依赖项。

  • COPY . .: 这将当前目录中的所有文件和目录复制到 Docker 容器的根目录。这包括 Flask 应用程序代码。

  • ENV FLASK_APP=app.py, ENV FLASK_ENV=development: 这设置了 Flask 应用程序的环境变量。FLASK_APP指定了主 Flask 应用程序文件的名称(在本例中为app.py)。FLASK_ENV将 Flask 环境设置为开发模式。

  • EXPOSE 5001: 这将 Docker 容器的5001端口暴露出来。

  • CMD ["python3", "app.py"]:这指定了在 Docker 容器启动时要运行的命令。在这种情况下,它使用 Python 3 运行 app.py 文件。

接下来,我们将从先前定义的 Dockerfile 构建 Docker 镜像。

构建 Docker 镜像

定义了 Dockerfile 后,您可以构建 Flask 应用程序的 Docker 镜像。此镜像包含运行应用程序所需的全部依赖项和配置文件。

要构建 Docker 镜像,请在包含 Dockerfile 的 bizza-docker 目录中从终端执行以下命令:

docker build -t packt-bizza-docker .

我们将得到以下输出:

图 16.6 –  命令的输出

图 16.6 – docker build 命令的输出

上述命令将使用当前目录中存在的 Dockerfile 构建 Docker 镜像。生成的镜像将被标记为 packt-bizza-docker。现在,让我们继续下一步并启动容器以使简单的 Flask 应用程序生效。

运行 Docker 容器

构建 Docker 镜像后,您可以从镜像运行 Docker 容器。此容器为运行 Flask 应用程序提供了一个轻量级、隔离和可移植的环境。

要运行 Docker 容器,请使用以下命令:

docker run -d -p 5001:5001 packt-bizza-docker .

我们将得到以下输出:

图 16.7 – 分离模式下的  输出

图 16.7 – 分离模式下的 docker run 输出

上述命令将以分离模式(-d)运行容器,并通过将主机端口 5001 映射到容器端口 5001 来执行端口映射(-p)。容器将基于 packt-bizza-docker 镜像。或者,您也可以不使用 -d 标志来以非分离模式启动容器,如下面的图所示:

图 16.8 – 非分离模式下的  输出

图 16.8 – 非分离模式下的 docker run 输出

上述 docker run 命令使我们能够访问 Docker 容器内运行的 Flask 应用程序。您需要使用 -p 5001:5001 将容器上的端口暴露给主机机器。

现在我们已经运行了 Docker 容器,我们可以通过网页浏览器或使用 curl 命令行工具(如 http://127.0.0.1:5001)来测试 Flask 应用程序。请确保应用程序按预期运行,并且所有依赖项都正常工作。

最后,您可以将 Docker 镜像推送到 Docker 仓库,如 Docker Hub 或 AWS ECS。这使得与其他开发者共享镜像或部署到生产环境变得容易。

要停止正在运行的 Docker 容器,您可以使用 docker stop 命令后跟 容器 ID名称

例如,如果容器 ID 是 c2d8f8a4b5e3,您可以使用 docker stop c2d8f8a4b5e3 命令停止容器,如下面的图所示:

图 16.9 – docker stop 命令的输出

图 16.9 – docker stop 命令的输出

如果你不知道容器 ID 或名称,你可以使用 docker ps 命令列出所有正在运行的容器及其详细信息,包括 ID 和名称。一旦你确定了想要停止的容器,你可以使用前面描述的 docker stop 命令。

让我们快速看一下另一个重要的 Docker 命令:docker container prune

docker container prune 命令用于删除已停止的容器并释放磁盘空间。当你运行 Docker 容器时,容器会消耗系统资源,如内存和 CPU 周期。当你停止容器时,这些资源会被释放,但容器仍然存在于你的系统中。随着时间的推移,如果你运行多个容器,你可能会拥有许多已停止的容器,这可能会占用你系统上大量的磁盘空间。

运行 docker container prune 命令是一种简单的方法来删除所有已停止的容器并回收磁盘空间。在继续之前,此 docker container prune 命令将提示你确认是否要删除容器,因此请在确认之前仔细检查容器列表。

重要的是要注意,docker container prune 命令只会删除已停止的容器。如果你有任何正在运行的容器,它们将不会受到此命令的影响。

接下来,我们将讨论 Docker 化 React 和 Flask 应用程序的过程。我们将使用全栈 Bizza 网络应用程序作为案例研究。

Docker 化 React 和 Flask 应用程序

Docker 化网络应用程序允许开发者在不同的机器上设置一致的开发环境。Docker 化工具减少了设置新开发环境所需的时间和精力。使用 Docker,开发者可以轻松地在本地机器上复制生产环境,测试他们的代码,并在部署之前调试任何问题。

在本节中,我们将对 React 和 Flask 的工作应用程序进行 Docker 化,并使其准备好用于生产。

让我们从 React 开始。

使用 React 的 Bizza 前端应用程序

一旦你创建了你的 React 应用程序,使其对互联网用户可访问的第一步就是构建该应用程序。构建 React 应用程序是开发过程中的一个关键步骤,以确保应用程序针对生产进行了优化,并按预期运行。

构建过程将 React 项目的源代码转换成可用于部署和向用户提供的生产就绪格式:

  1. 让我们从 GitHub 仓库下载 Bizza 应用程序目录 – github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter16/bizza/frontend

  2. 要安装应用程序所需的依赖项,请导航到 bizza/frontend 目录,并在终端中执行 npm install 命令。

  3. 要运行前端应用程序,请在终端中执行 npm start 命令。

  4. 现在,让我们使用 npm run build 命令构建应用程序。

现在,随着 bizza React 应用程序的构建完成,生成的文件可以部署到 Web 服务器或云平台,并向用户提供服务。最终的构建目录位于 bizza/frontend/src/build 内。

在构建过程中,采取了以下步骤:

  1. 转换 JavaScript 和 JSX 代码:React 应用程序通常用 JavaScript 和 JSX 编写,JSX 是 JavaScript 的语法扩展。然而,现代网络浏览器只能执行 JavaScript 代码。因此,在部署 React 应用程序之前,需要使用像 Babel 这样的工具将 JavaScript 和 JSX 代码转换为纯 JavaScript。

  2. 打包代码和资源:React 应用程序通常由多个组件、模块以及如图像、CSS 文件和字体等资源组成。打包涉及将所有必需的代码和资源组合成一个文件或一组文件,以便向用户提供服务。

  3. 优化代码和资源:为了提高性能,打包的代码和资源可以通过压缩、压缩或删除不必要的代码进行优化。

  4. builddist

现在,通常在这个阶段,build 目录的内容会被部署到 Web 服务器或云平台,供最终用户使用。然而,对于本书中概述的部署过程,您将利用 Docker 的一项功能,称为 多阶段构建。多阶段构建是 Docker 的一项功能,允许您创建一个由多个阶段组成的 Docker 镜像,其中每个阶段都是一个具有特定用途的自包含 Docker 镜像。

多阶段构建的目的是优化 Docker 镜像的大小和效率。使用多阶段构建,您可以通过仅包含必要的文件和依赖项来减小最终 Docker 镜像的大小。这导致构建更快、镜像尺寸更小,以及资源使用更高效。

多阶段构建过程涉及创建多个 Docker 镜像,每个镜像都有特定的用途。构建的第一阶段通常包含源代码、依赖项、库和其他必要的文件。

构建的最后阶段通常只包含运行应用程序所需的必要文件和依赖项,从而生成一个更小、更高效的 Docker 镜像。多阶段构建的本质是确保中间阶段用于构建和编译应用程序,但不包含在最终镜像中。

现在,让我们检查一个使用多阶段构建的 React 前端应用的 Dockerfile

# Build stageFROM node:14.17.0-alpine3.13 as build-stage
WORKDIR /frontend
COPY package*.json ./
RUN npm install --production
COPY . .
RUN npm run build
# Production stage
FROM nginx:1.21.0-alpine
COPY --from=build-stage /frontend/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# Clean up unnecessary files
RUN rm -rf /var/cache/apk/* \
          /tmp/* \
          /var/tmp/* \
          /frontend/node_modules \
          /frontend/.npm \
          /frontend/.npmrc \
          /frontend/package*.json \
          /frontend/tsconfig*.json \
          /frontend/yarn.lock

让我们分解前面的 Dockerfile 镜像构建指令:

  • Dockerfile 使用 Node.js 14.17.0-alpine3.13 镜像作为基础创建一个构建阶段。Dockerfile 设置工作目录为 /frontend,并将本地目录中的 package*.json 文件复制到镜像中。然后运行 npm install --production 命令来安装生产依赖项。接下来,Dockerfile 将整个项目目录复制到镜像中,并运行 npm run build 命令来构建 React 应用程序。

  • Dockerfile 使用较小的 nginx:1.21.0-alpine 镜像作为基础创建一个生产阶段。Dockerfile 将构建阶段位于 /frontend/build 的构建好的 React 应用程序复制到 nginx 的 HTML 目录 /usr/share/nginx/html

  • EXPOSE 命令暴露端口 80 以允许与容器进行通信。

  • CMD 命令设置容器启动时默认运行的命令。在这种情况下,Dockerfile 使用 nginx -g 'daemon off;' 命令在前台启动 nginx 服务器。

  • Dockerfile 使用 rm 命令和 RUN 命令清理不必要的文件,例如 node_modules 目录和其他配置文件,从而减少镜像的整体大小,使其部署更快。

现在我们已经有了 bizza 前端 React 应用的 Docker 镜像。让我们创建 Flask 后端 Docker 镜像。

使用 Flask 的 Bizza 后端应用程序

在 Flask 后端,我们将创建两个 Docker 镜像。在此处下载完整的 Flask 后端应用程序:github.com/PacktPublishing/Full-Stack-Flask-and-React/tree/main/Chapter16/bizza/backend

我们将为 Flask 应用程序创建一个 Docker 镜像,并为 PostgreSQL 创建另一个 Docker 镜像。虽然将这两个镜像融合成一个单独的 Docker 镜像是可能的,但为了可扩展性和减小镜像大小,将它们分开是最佳实践。

让我们回顾 Flask 应用程序多阶段构建 Dockerfile 的定义。

Flask 应用程序的 Dockerfile 将存储在项目 root 目录中,而名为 postgres 的子目录将包含 PostgreSQL 的 Dockerfile:

# Build StageFROM python:3.8.12-slim-buster AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -U pip==21.3.1 && \
    pip install --no-cache-dir --user -r requirements.txt
COPY . .
# Run Stage
FROM python:3.8.12-slim-buster AS run
WORKDIR /app
COPY --from=build /root/.local /root/.local
COPY --from=build /app .
ENV PATH=/root/.local/bin:$PATH
ENV FLASK_APP=app.py
ENV FLASK_ENV=production
EXPOSE 5001
CMD ["python3", "app.py"]

让我们分解前面的 Dockerfile。

这个 Dockerfile 定义了一个 Flask 应用程序的多阶段构建。Dockerfile 有两个阶段:buildrun

第一个阶段 build 负责构建应用程序并安装所需的依赖项。现在,让我们检查构建阶段每一行的作用:

  • FROM python:3.8.12-slim-buster AS build:这一行将构建阶段的基镜像设置为 python:3.8.12-slim-buster

  • WORKDIR /app:这一行设置工作目录为 /app

  • COPY requirements.txt .:这一行将主机机器上的 requirements.txt 文件复制到容器中的 /app 目录。

  • RUN pip install --no-cache-dir -U pip==21.3.1 && \ pip install --no-cache-dir --user -r requirements.txt: 这些行将 pip 更新到版本 21.3.1 并安装 requirements.txt 文件中指定的 Python 包。

    使用 --no-cache-dir 选项是为了防止安装过程中使用之前运行中缓存的任何数据,这有助于确保安装的包是最新的,并且与 requirements.txt 中指定的版本相匹配。使用 --user 选项将包安装到用户的家目录中,这有助于避免权限问题。

  • COPY . .: 这行将整个应用程序目录从主机机器复制到容器的 /app 目录。

  • FROM python:3.8.12-slim-buster AS run: 这表示第二阶段的开始。

第二阶段,run,负责在生产环境中运行应用程序。这行将 run 阶段的基镜像设置为 python:3.8.12-slim-buster

  • WORKDIR /app: 这行设置工作目录为 /app

  • COPY --from=build /root/.local /root/.localCOPY --from=build /app .: 这两行将应用程序目录和已安装的包从构建阶段复制到 run 阶段。第一行将已安装的包从构建阶段复制到运行阶段的 /root/.local 目录。第二行将应用程序目录从构建阶段复制到运行阶段的 /app 目录。

  • ENV PATH=/root/.local/bin:$PATH, ENV FLASK_APP=app.py, 和 ENV FLASK_ENV=production: 这三行设置了应用程序的环境变量。PATH 环境变量被更新以包含 /root/.local/bin 目录,该目录包含已安装的包。

    这确保了安装的包在系统的 PATH 中可用。FLASK_APP 环境变量被设置为 app.py,这指定了 Flask 运行的主应用程序文件。FLASK_ENV 环境变量被设置为 production,这启用了诸如更好的错误处理和改进的性能等特性。

  • EXPOSE 5001: 这行暴露端口 5001,这是 Flask 应用程序将监听的端口。

  • CMD ["python3", "app.py"]: 这行指定了容器启动时默认运行的命令。它使用 python3 命令运行 app.py 文件。

在讨论了主 Flask 应用程序的 Dockerfile 之后,让我们检查 PostgreSQL 的 Dockerfile。

这里是用于创建数据库镜像的 Postgres Dockerfile:

FROM postgres:13-alpineENV POSTGRES_DB=<databse_name>
ENV POSTGRES_USER= <databse_user>
ENV POSTGRES_PASSWORD= <databse_password>
RUN apk add --no-cache --update bash
COPY init.sql /docker-entrypoint-initdb.d/
EXPOSE 5432

让我们通过 Postgres Dockerfile 来了解:

  • FROM postgres:13-alpine: 这行指定了我们的 Docker 容器的基镜像,即 postgres:13-alpine。这个镜像基于 Alpine Linux 发行版,并包含 PostgreSQL 版本 13。

  • ENV POSTGRES_DB=<database_name>, ENV POSTGRES_USER=<database_user>, 和 ENV POSTGRES_PASSWORD=<database_password>:这三行设置了 Postgres 容器的环境变量。POSTGRES_DB变量指定要创建的数据库的名称。POSTGRES_USER变量指定为数据库创建的用户名,而POSTGRES_PASSWORD变量指定该用户的密码。

  • RUN apk add --no-cache --update bash:这一行将init.sql文件复制到容器中的/docker-entrypoint-initdb.d/目录。这个目录用于 Postgres 镜像在容器首次启动时运行初始化脚本。在这种情况下,init.sql文件是一个脚本,将创建数据库和任何必要的表。

  • EXPOSE 5432:这一行暴露了默认由 PostgreSQL 使用的端口5432,以便允许从容器外部建立连接。然而,这实际上并没有发布端口,因为这需要在运行时使用docker rundocker-compose命令来完成。

这个 Postgres Dockerfile 可以用来构建 Postgres 数据库的 Docker 镜像,它可以与 React 和 Flask 应用的 Docker 容器一起使用,以构建一个完整的 Web 应用堆栈。

在 Flask 应用和 Postgres 镜像定义良好后,我们将把创建的 Docker 镜像推送到 AWS ECR 以进行在线存储。

理解 AWS ECR

Amazon ECR 是一个完全托管的 Docker 注册服务,它使得存储、管理和部署 Docker 镜像变得容易。Amazon ECR 与 Amazon ECS 集成,以提供构建、部署和管理大规模容器化应用的无缝体验。Amazon ECR 旨在扩展以满足甚至最苛刻的容器化应用的需求。Amazon ECR 具有安全功能来保护您的容器镜像,包括静态和传输中的加密,以及基于角色的访问控制(RBAC)。

要开始使用 Amazon ECR,第一步是创建一个 ECR 仓库。请参考以下 Amazon ECR 界面的截图。

点击开始使用按钮以启动仓库创建过程。这将允许您为存储 Docker 镜像创建一个专用位置。

图 16.10 – AWS ECR

图 16.10 – AWS ECR

接下来,我们有一个截图展示了在 Amazon ECR 中名为packt-bizza-web-app的公共仓库:

图 16.11 – 公共仓库

图 16.11 – 公共仓库

仓库是存储您的 Docker 镜像的逻辑容器。一旦创建了仓库,您就可以将 Docker 镜像推送到仓库。然后,您可以从仓库拉取镜像以部署到您的 ECS 集群。

Amazon ECR 是一个强大的工具,可以帮助您简化容器镜像的管理。有趣的是,Amazon ECR 在存储和管理容器镜像方面非常经济高效。

使用 ECR 是免费的;您只需为使用的存储和带宽付费。接下来,我们将使用 Docker Compose 来定义和运行 React、Flask 和 Postgres 容器。

使用 Docker Compose

Docker Compose 是一个用于定义和运行多容器 Docker 应用的工具。Docker Compose 提供了一个工具来定义一组容器及其相互之间的关系,然后通过单个命令运行它们所有。

使用 Docker Compose,开发者可以定义应用程序容器的确切配置,包括镜像、环境变量和网络设置。这确保了应用程序在不同环境中的一致运行,并且可以轻松地复制。

在深入了解配置定义的细节之前,我们需要了解 Docker Compose 的以下几个组件:

  • YAML 文件: YAML 文件用于定义应用程序容器的配置。YAML 文件指定了要使用的镜像、要公开的端口、环境变量以及任何其他所需的设置。

  • 服务(Services): 您应用程序中的每个容器都在 YAML 文件中定义为服务。服务可以相互依赖,并且可以一起启动和停止。

  • 网络(Networks): Docker Compose 为您的应用程序创建一个网络,允许容器之间相互通信。

  • 卷(Volumes): 卷用于在容器运行之间持久化数据。

  • 命令(Commands): Docker Compose 提供了一套命令来启动、停止和管理您的应用程序。

现在,让我们创建一个 Docker Compose 文件来管理 React 前端、Flask 后端和 PostgreSQL 数据库容器之间的关系:

  1. 在主项目目录 bizza/ 内,创建 docker-compose.yaml

  2. 为每个容器定义服务。在 docker-compose.yaml 文件中,为每个容器定义一个独立的服务:

    version: '3'services:  frontend:    image: <your-ecr-repository>/bizza-frontend-react-      app    ports:      - "3000:3000"  backend:    image: <your-ecr-repository>/bizza-backend-flask-      app    ports:      - "5000:5000"    depends_on:      - db  db:    image: <your-ecr-repository>/bizza-postgresql    environment:      POSTGRES_USER: <your-db-username>      POSTGRES_PASSWORD: <your-db-password>      POSTGRES_DB: <your-db-name>    ports:      - "5432:5432"
    

在前面的代码中,我们定义了三个服务:frontendbackenddbfrontend 服务运行 Bizza 前端 React 应用,backend 服务运行 Bizza 后端 Flask 应用,而 db 服务运行 PostgreSQL 数据库。

现在,让我们配置网络和依赖关系。使用 portsdepend_on 选项配置服务之间的网络连接。例如,前端服务在端口 3000 上公开,后端服务在端口 5000 上公开,而 db 服务在端口 5432 上公开。后端服务还依赖于 db 服务,因此后端将在 db 服务启动后启动。

一旦我们在 docker-compose.yaml 文件中定义了服务,我们就可以使用 docker-compose up 命令启动容器。这将启动容器并将它们连接到适当的网络。

通过 Docker Compose 管理应用的容器,我们可以简化启动和停止应用的过程,同时确保所有必需的组件都在正确运行并相互通信。

有趣的是,Docker Compose 是一个用于管理容器的有用工具;然而,Docker Compose 更适合小规模部署和开发环境。Docker Compose 适用于 bizza 项目,这是一个用于学习目的的小规模应用。

然而,AWS Elastic Beanstalk 是设计来处理生产级工作负载的,并提供许多功能和好处,可以帮助简化 Web 应用管理和扩展。无论如何,我们将把 bizza 应用的最终部署转向 AWS Elastic Beanstalk。

在下一节中,我们将探讨 AWS Elastic Beanstalk,这是一个用于在云端部署和管理应用的服务。

将 React 和 Flask 应用部署到 AWS Elastic Beanstalk

AWS Elastic Beanstalk 是一种完全托管的 AWS 云服务,允许开发者轻松地在 AWS 上部署和管理 Web 应用和服务。AWS Elastic Beanstalk 提供了一个平台,通过自动处理基础设施配置、负载均衡和应用扩展,简化了在 AWS 上部署和管理 Web 应用的过程。

你可以在包括 Node.js、Python、Ruby 和 Go 在内的多种编程语言和 Web 框架上部署 Elastic Beanstalk。Elastic Beanstalk 还与 Amazon RDS、Amazon DynamoDB 和 Amazon SNS 等其他 AWS 服务集成,以提供构建和扩展 Web 应用的完整解决方案。

使用 Elastic Beanstalk,开发者可以轻松地专注于编码。一旦你准备好部署你的应用,你可以简单地上传你的应用包或链接到一个仓库,然后选择适合你应用的适当平台和环境。Elastic Beanstalk 会自动配置所需资源并设置环境,还可以根据需求自动扩展应用。

此外,AWS Elastic Beanstalk 还提供了一系列功能和工具,帮助开发者简化他们的开发工作流程,例如 持续集成和持续交付CI/CD)管道、监控和日志工具,以及与流行的开发工具如 Git 和 Jenkins 的集成。

现在,让我们开始使用 Elastic Beanstalk 部署我们的应用。本指南假设你已经创建了一个 AWS 账户。如果没有,请访问 aws.amazon.com/free/ 并按照说明创建一个 AWS 账户。AWS 免费层足以部署这个项目:

  1. 登录到你的 AWS 账户,并访问 Amazon ECR 控制台 console.aws.amazon.com/ecr/

  2. 要创建一个 Amazon ECR 仓库,你可以按照以下步骤操作:

    1. 访问 Amazon ECR 控制台。

    2. 在导航面板中,选择存储库

    3. 选择创建存储库

    4. 存储库名称字段中,输入您的存储库名称。

    5. 存储库类型字段中,选择公共私有

    6. 选择创建

  3. 或者,您可以使用以下 AWS CLI 命令创建 Amazon ECR 存储库:

    aws ecr create-repository --repository-name nameofyourrepository
    

    然而,要成功运行前面的命令,您需要以下内容已排序:

    • 拥有一个 AWS 账户和一个具有创建 ECR 存储库权限的 IAM 用户。您可以在 GitHub 上找到权限 JSON 文件的链接:github.com/PacktPublishing/Full-Stack-Flask-and-React/blob/main/Chapter16/bizza/Deployment/ecr-permissions.json

    • 已安装并配置 AWS CLI,并使用您的 AWS 凭证。

  4. 接下来,我们需要将 Docker 镜像推送到 Amazon ECR 存储库。要将 bizza 应用程序的 Docker 镜像推送到 Amazon ECR 存储库,请按照以下步骤操作:

    1. 在命令行中,导航到包含每个应用程序 Dockerfile 的目录。使用以下命令构建 Docker 镜像:

      docker build -t <image-name> .
      
    2. 然后,使用以下命令标记您的镜像:

      docker tag <docker_image_name>:<tag_name> <AWS_ACCOUNT_ID>.dkr.ecr.<region>.amazonaws.com/<AWS_REPOSITORY_NAME>:<tag_name>
      
    3. 将每个 Docker 镜像推送到 Amazon ECR 存储库。在您的项目目录中,运行docker login并输入 docker 登录凭证。完成后,运行aws configure命令以登录 AWS。

    4. 一旦您在终端中登录了 Docker 和 AWS,请运行以下命令:

      aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin AWS_ACCOUNT_ID.dkr.ecr.<region>.amazonaws.com
      

    让我们回顾一下前面命令的各个方面:

    • aws ecr get-login-password:此命令从 ECR 检索认证令牌。

    • --region <region>:这指定了 ECR 注册表所在的区域。如果您不知道您的 ECR 存储库在哪里,请运行以下命令:aws ecr describe-repositories –``repository-names nameofyourrepository

    • |:这是管道操作符。它告诉 shell 将第一个命令的输出作为输入传递给第二个命令。

    • docker login:此命令将您登录到 Docker 注册表。

    • --username AWS:这指定了登录注册表时使用的用户名。

    • --password-stdin:这告诉 Docker CLI 从标准输入读取密码。

    • <AWS_ACCOUNT_ID>.dkr.ecr.<region>.amazonaws.com:这是您想要登录的 ECR 注册表的注册 ID。

    1. 在每个项目组件目录中输入docker push <account-id>.dkr.ecr.<region>.amazonaws.com/<nameof yourrepository:<tag_name>>
  5. 要创建 Elastic Beanstalk 环境,您可以按照以下步骤操作:

    1. 前往 Elastic Beanstalk 控制台:console.aws.amazon.com/elasticbeanstalk

    2. 在导航面板中,选择创建环境

    3. 平台部分,选择Docker

    4. 应用程序代码部分,选择使用现有的应用程序

    5. 应用程序代码存储库字段中,输入您的 Docker 镜像存储库的 URL。

    6. 应用程序名称字段中,输入您环境的名称。

    7. 选择创建环境

  6. 要配置 Elastic Beanstalk 环境以使用 Amazon ECR 存储库,您可以按照以下步骤操作:

    1. 在 Elastic Beanstalk 控制台中,选择您环境的名称。

    2. 在导航面板中,选择配置

    3. 软件部分,选择Docker

    4. 存储库 URL字段中,输入您的 Amazon ECR 存储库的 URL。

    5. 选择保存

  7. 要将应用程序部署到 Elastic Beanstalk 环境,您可以按照以下步骤操作:

    1. 在 Elastic Beanstalk 控制台中,选择您环境的名称。

    2. 在导航面板中,选择部署

    3. 部署方法部分,选择一键部署

    4. 选择部署

    现在,应用程序已部署到 Elastic Beanstalk 环境。您可以通过 Elastic Beanstalk 控制台中显示的 URL 访问应用程序。

AWS Elastic Beanstalk 无疑是那些希望专注于构建应用程序和服务而不是管理基础设施的开发者的绝佳选择。AWS Elastic Beanstalk 提供了一个简单、可扩展和灵活的平台,可以帮助开发者快速轻松地将应用程序部署到 AWS 云平台。

摘要

在本章中,我们探讨了容器化和部署的世界。我们首先讨论了什么是容器化以及为什么它对现代软件开发有用。然后,我们介绍了最受欢迎的容器化技术 Docker,并学习了如何使用它来打包和部署 React 和 Flask 应用程序。

接下来,我们探讨了 Docker Compose 的使用,这是一个用于定义和运行多容器 Docker 应用程序的工具。我们学习了如何使用 Docker Compose 在多个容器中编排应用程序的部署。

我们还深入探讨了 AWS ECR,这是一个完全托管的容器注册服务,允许开发者安全、可靠地存储、管理和部署 Docker 容器镜像。最后,我们了解了 AWS Elastic Beanstalk,这是一个简化部署、管理和扩展 Web 应用程序流程的服务。我们学习了如何将我们的 Docker 化 React 和 Flask 应用程序部署到 Elastic Beanstalk,并具备所有安全性和可扩展性功能。

简而言之,容器化和部署是现代软件开发的关键组成部分,而像 Docker 这样的工具以及 AWS 的 Elastic Container Registry 和 Elastic Beanstalk 这样的服务对于管理和扩展基于容器的应用程序至关重要。

我们衷心感谢您选择这本书作为您掌握全栈开发艺术的指南。您的选择反映了您踏上结合现代网络技术力量的变革性旅程的决心。能陪伴您在这条探索和学习之路上,我们深感荣幸。

在这本书的每一页中,我们都精心制作了一份全面的路线图,旨在为你提供征服前端和后端开发领域所需的所有技能集。我们深入 React 的深处,揭示了其基于组件的架构、状态管理和动态用户界面。同时,我们也探讨了 Flask 的复杂性,赋予你构建强大的 API、管理数据库以及优雅地处理服务器端操作的能力。

当你翻到这本书的最后一页时,请花一点时间来欣赏你所获得的知识和所磨练的技能。你现在拥有了打造令人惊叹的用户界面、利用服务器端应用的力量以及无缝连接前端和后端功能所需的工具。作为全栈开发者的旅程已经开启,可能性无限。

但是等等,你的探险还没有结束!当你关闭这一章时,新的天地在等待着你。技术世界是不断演变的,你对掌握全栈开发的执着完美地与未来的机遇相吻合。无论你选择构建复杂的 Web 应用、设计直观的用户体验,还是为创新项目做出贡献,你的专业知识都将成为成功的基石。

因此,随着你在 React 和 Flask 方面的新技能,接下来是什么?也许你会探索高级的 React 框架,如Next.js,深入微服务领域使用 Flask,甚至开始创建你自己的开创性应用。前方的道路铺满了无限的可能,你塑造数字体验的能力从未如此重要。

再次感谢您选择使用 Flask 和 React 的全栈开发作为您的指南。您对学习和成长的承诺令人鼓舞,我们热切期待您为不断发展的技术世界做出的卓越贡献。

posted @ 2025-09-18 14:34  绝不原创的飞龙  阅读(6)  评论(0)    收藏  举报