无服务器全栈开发-全-

无服务器全栈开发(全)

原文:zh.annas-archive.org/md5/161462738f8907491ed6159ae08deabc

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

为什么我要写这本书

当我第一次学编程时,根本不知道软件开发的范围有多广。我只想做一个应用程序。哦,我一开始就发现自己是多么的天真,一旦开始深入挖掘和拼凑完成我想要做的事情所需要的所有东西。

我学到的其中一件主要的事情是,应用程序通常由两个主要部分组成:前端(或客户端代码)和后端 API 及服务。当时,云技术还处于起步阶段,学习如何构建全栈应用程序简直让人不知所措!这更加困难,因为我想要开发原生移动应用程序,而我发现,开发移动应用程序的门槛比开发 Web 应用程序要高得多。

快进到十年后,环境开始大不相同。曾经需要一大团队的开发人员才能完成的事情,现在有时只需要一个开发人员就能完成。像 React Native、Flutter 和 Cordova 这样的工具使开发者能够使用单一代码库构建和发布跨平台移动应用程序。像 AWS Amplify、Firebase 和其他云技术也使得同样的开发者能够更快地利用云来构建后端。

我认为我们正进入一个新范式,现在比以往任何时候都更容易成为一名全栈开发者,全栈开发者的定义也开始发生变化。我写这本书是为了阐述我对这一新范式在实践中是什么样子的愿景,并展示一种专门利用最前沿的前端和云技术的技术。我在这本书中描述的,是我认为软件工程的未来。

本书适合谁

本书适合任何想要构建全栈应用程序的软件工程师,特别是对云计算感兴趣的工程师。它还针对前端开发人员,旨在教他们如何利用现有技能集,使用云技术构建全栈应用程序。

它也是 CTO 和创业公司的创始人们的好资源,旨在最大限度提高效率,并在使用最少资源的同时,尽可能提高开发速度。本书中概述的技术非常适合快速原型设计和快速实验,使开发者和创始人能够迅速将他们的想法推向市场,并拥有一个在成功的情况下也具有可扩展性和耐用性的产品。

本书的组织结构

本书的目标是向你介绍构建使用 React 和无服务器技术的真实世界和可扩展全栈应用程序所需的所有组成部分。它逐步介绍了特性(如身份验证、API 和数据库)和一些实现这些特性的技术,无论是在前端还是后端,通过在每一章中构建不同的应用程序。

您创建的每个应用程序将基于前一章节学到的知识。在最后一章中,您将构建一个复杂的应用程序,利用许多在工作或创业中构建实际应用程序所需的云服务。完成本书学习后,您应该具备在使用 React 和 AWS 云技术构建自己的无服务器应用程序时所需的知识和理解。

第一章,无服务器计算时代的全栈开发

在本章中,我将描述无服务器哲学,介绍无服务器应用程序的特性和优势,并向您介绍 AWS 和 AWS Amplify CLI。

第二章,使用 AWS Amplify 入门

在本章中,我们将使用 AWS Amplify 创建和部署一个无服务器函数。我们将创建函数,然后添加 API 并与之交互。

第三章,创建您的第一个应用程序

在这里,我们将通过构建一个笔记应用程序,从头开始介绍创建新的全栈应用程序的基本过程。我们将创建一个新的 React 应用程序,初始化一个新的 Amplify 项目,添加一个 GraphQL API,然后从客户端(React 应用程序)连接到并与 API 进行交互。

第四章,认证简介

在本章中,我们将演示如何向应用程序添加认证。我们将从创建一个新的 React 应用程序开始,并使用 AWS Amplify React 库中的 withAuthenticator 高阶组件(HOC)添加基本的认证。我们将读取用户的元数据,并创建一个个人资料屏幕,让用户查看其信息。

第五章,自定义认证策略

在本章中,我们将通过使用 React、React Router 和 AWS Amplify 创建自定义认证流程来更仔细地研究认证。该应用程序将包括注册界面、登录界面和忘记密码界面。登录后,将显示一个主菜单,用户可以导航到其个人资料页面、地图页面和欢迎界面,这将作为应用程序的主视图。

第六章,深入探讨无服务器函数:第一部分 和 第七章,深入探讨无服务器函数:第二部分

在这里,我们将介绍无服务器函数以及如何在 React 应用程序中与其交互。我们将演示如何通过创建一个应用程序从一个受 CORS 保护的 API 获取柴犬图片,使用我们的代码存储在 AWS Lambda 函数中,并使用 AWS Amplify CLI 创建和配置该函数。

第八章,深入了解 AWS AppSync

在本章中,我们将基于我们在 第三章 中学到的内容构建一个更复杂的 API,该 API 包括多对多关系和多种授权类型。我们将构建一个允许管理员创建舞台和表演的事件应用程序。我们将允许所有用户能够阅读事件信息,无论他们是否已登录,但我们将只允许已登录的管理员用户创建、更新或删除事件和舞台。

第九章,使用 Amplify DataStore 构建离线应用

在本章中,我们将介绍如何使用 Amplify DataStore 添加离线功能。

第十章,处理图像和存储

在这里,我们将学习如何创建一个照片分享应用程序,允许用户上传和查看图像。

第十一章,主机:使用 CI 和 CD 将您的应用程序部署到 Amplify Console

在这一最后一章中,我们将把我们在 第十章 创建的照片分享应用程序部署到一个真实域上,使用 Amplify Console。我们将学习如何通过在更新合并到主分支时启动新的构建来添加持续集成(CI)和持续部署(CD)。最后,我们将学习如何添加自定义域,使您的应用程序可以在您拥有的真实 URL 上运行。

本书使用的约定

本书中使用了以下排版约定:

斜体

指示新术语、网址、电子邮件地址、文件名和文件扩展名。

等宽

用于程序清单,以及段落中用来指代程序元素,如变量或函数名,数据库,数据类型,环境变量,语句和关键字。

等宽粗体

显示用户应按字面意义键入的命令或其他文本。

等宽斜体

显示应替换为用户提供的值或上下文确定的值的文本。

提示

此元素表示提示或建议。

注意

此元素表示一般注释。

使用代码示例

补充材料(例如代码示例、练习等)可在 https://github.com/dabit3/full-stack-serverless-code 下载。

如果您有技术问题或在使用代码示例时遇到问题,请发送电子邮件至 bookquestions@oreilly.com

本书旨在帮助您完成工作。一般情况下,如果本书提供示例代码,您可以在您的程序和文档中使用它。除非您复制了代码的大部分内容,否则您无需联系我们以获取许可。例如,编写一个使用本书多个代码片段的程序不需要许可。出售或分发 O’Reilly 书籍中的示例需要许可。引用本书并引用示例代码来回答问题不需要许可。将本书大量示例代码整合到您产品的文档中需要许可。

我们感谢您,但通常不需要署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Full Stack Serverless by Nader Dabit (O’Reilly)。2020 年版权所有 Nader Dabit,978-1-492-05989-9。”

如果您认为您对示例代码的使用超出了合理使用范围或上述许可,请随时联系我们:permissions@oreilly.com

O’Reilly 在线学习

超过 40 年以来,O’Reilly Media 提供技术和业务培训、知识和见解,帮助公司取得成功。

我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专业知识。O’Reilly 的在线学习平台提供即时访问实时培训课程、深度学习路径、交互式编码环境,以及来自 O’Reilly 和其他 200 多家出版商的广泛文本和视频。欲了解更多信息,请访问:http://oreilly.com

如何联系我们

请将有关本书的评论和问题发送至出版社:

  • O’Reilly Media, Inc.

  • 1005 Gravenstein Highway North

  • 加利福尼亚州塞巴斯托波尔,95472

  • 800-998-9938(美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104(传真)

我们为这本书准备了一个网页,上面列出了勘误、示例和任何额外信息。您可以访问这个页面:https://oreil.ly/Full_Stack_Serverless

发送电子邮件至 bookquestions@oreilly.com 提出对本书的评论或技术问题。

欲了解有关我们的书籍和课程的新闻和信息,请访问:http://oreilly.com

在 Facebook 上找到我们:http://facebook.com/oreilly

在 Twitter 上关注我们:http://twitter.com/oreillymedia

在 YouTube 上观看我们:http://www.youtube.com/oreillymedia

致谢

感谢我的妻子莉莉,她在我职业生涯中始终支持我,并且在我在办公室和家里加班写书时,超越了一切来维护我们的生活秩序。

感谢我的孩子们维克多和伊莱,你们太棒了,是我的灵感之源;我非常爱你们两个。感谢我的父母,让我有机会学习和在生活中得到第二、第三、第四次的机会。

感谢许多团队和个人:感谢整个 AWS 移动团队,他们在我刚结束动荡的咨询生涯时给了我一个机会加入他们的团队,并让我有机会与我所见过的最聪明的人一起工作。感谢迈克尔·帕里斯、莫希特·斯里瓦斯塔瓦、丹尼斯·希尔斯、阿德里安·霍尔、理查德·瑟雷克尔德、迈克尔·拉比埃尼克、罗汉·德什潘德、阿米特·帕特尔以及所有其他教会我门道、帮助我学会一切必要技能以开始新工作的队友们。感谢 Russ Davis、Lee Johnson 和 SchoolStatus 给予我在工作中学习最前沿技术的机会,这最终推动了我进入咨询行业的职业生涯。感谢 Brian Noah、Nate Lubeck 以及我在 Egood 的团队,那是我第一个“真正”的技术工作,他们让我接触到 meetup 和会议的世界,以及成为优秀开发者所需的一切。

第一章:无服务器计算时代的全栈开发

人们通常将云计算与后端开发和 DevOps 联系起来。然而,在过去几年中,这一情况已经开始发生变化。随着函数即服务(FaaS)的兴起,以及托管服务形式的强大抽象,云提供商已经降低了对云计算新手以及传统前端开发者的准入门槛。

使用现代工具、框架和服务,如亚马逊网络服务(AWS)Amplify 和 Firebase(等等),单个开发者可以利用其现有的技能和对单一框架及生态系统(如 JavaScript)的知识,构建可扩展的全栈应用程序,其中包括以往需要高技能后端工程师和 DevOps 工程师团队才能构建和维护的所有特性。

本书专注于通过利用 Amplify 框架来缩小前端和后端开发之间的差距。在这里,您将学习如何使用 Amplify 命令行界面(CLI)直接从前端环境中构建可扩展的云应用程序。您将创建和与各种 API 和 AWS 服务进行交互,如使用 Amazon Cognito 进行身份验证,使用 Amazon S3 进行云存储,使用 Amazon API Gateway 和 AWS AppSync 进行 API 以及使用 Amazon DynamoDB 进行数据库操作。

到最后一章时,你将理解如何在云中利用 AWS 服务(后端)和 React(前端)构建真实的全栈应用程序。你还将学习如何使用 React 的现代 API,如 hooks 和函数式组件,以及 React Context 用于全局状态管理。

现代无服务器哲学

“无服务器”这个术语通常与 FaaS 联系在一起。尽管关于它的定义各有不同,但这个术语最近已经演变成一种哲学,而不仅仅是一个共享的定义。

当人们谈论无服务器时,他们实际上是在描述如何以最有效的方式交付业务价值,重点放在编写业务逻辑上,而不是为业务逻辑编写支持基础设施。通过采用无服务器思维方式,您可以通过有意识地努力寻找和利用 FaaS、托管服务和智能抽象来做到这一点,只有在现有服务尚不存在时才构建定制解决方案。

越来越多的公司和开发者采用这种方法,因为重新发明轮子是没有意义的。随着这一理念的普及,初创公司和云提供商也提供了大量简化后端复杂性的服务和工具。

对于学术界对于 无服务器 含义的看法,你可以阅读由加州大学伯克利分校的一个小组于 2019 年撰写的论文,“简化云编程:伯克利对无服务器计算的视角”,^(1)。在这篇论文中,作者扩展了 无服务器 的定义:

尽管云函数——作为 FaaS(函数即服务)的打包提供——代表了无服务器计算的核心,云平台也提供了专门的无服务器框架,以满足特定应用需求,作为 BaaS(后端即服务)的提供。简单来说,无服务器计算 = FaaS + BaaS。

后端即服务(BaaS)通常指像数据库(Firestore、Amazon DynamoDB)、认证服务(Auth0、Amazon Cognito)和人工智能服务(Amazon Rekognition、Amazon Comprehend)等托管服务。伯克利重新定义了无服务器的含义,强调了在这场讨论的更广泛范围内正在发生的事情,因为云提供商开始构建更多和更好的托管服务,并将它们放入无服务器的范畴中。

无服务器应用的特征

现在你对无服务器哲学有了一些了解,那么无服务器应用的一些特征是什么呢?虽然对于无服务器的定义可能会有不同的答案,但以下特征和特点通常是行业普遍认同的。

减少运维责任

无服务器架构通常允许你将更多的运维责任转移给云提供商或第三方。

当你决定实施 FaaS 时,你唯一需要关心的是函数中运行的代码。所有服务器的修补、更新、维护和升级工作都不再是你的责任。这回归到了云计算的核心,以及无服务器试图提供的东西:一种花更少时间管理基础设施,更多时间构建功能并提供业务价值的方式。

大量使用托管服务

托管服务通常承担提供一组定义好的功能的责任。它们在无服务器的意义上能够无缝扩展,不需要任何服务器操作或管理运行时间,并且最重要的是,本质上是无需编写代码的。

无服务器架构的优势

如今有许多构建应用程序的方式。早期所做的决策不仅会影响应用程序的生命周期,还会影响开发团队,最终影响公司或组织。在本书中,我主张使用无服务器技术和方法构建你的应用程序,并提出了一些可以实现这一点的方式。但是,采用这种方式构建应用程序的优势是什么?为什么无服务器变得如此流行?

可扩展性

无服务器的主要优势之一是即插即用的可扩展性。在构建应用程序时,您不必担心应用程序变得极其流行,快速吸纳大量新用户会发生什么事情——云服务提供商会为您处理这些事情。

云服务提供商会自动扩展您的应用程序,根据每次交互运行代码。在无服务器函数中,您的代码并行运行并逐个处理每个触发器(相应地,随着工作负载的增加而扩展)。

无需担心扩展您的服务器和数据库是一个巨大的优势。在设计应用程序时,这是您无需担心的一件事。

成本

无服务器架构和传统基于云或本地基础设施的定价模型有很大不同。

在传统方法中,您通常不管是否使用计算资源都需要支付费用。这意味着,如果您希望确保应用程序可以扩展,您需要准备好应对您可能看到的最大工作负载,而不管您实际上是否达到了这一点。这种方法意味着在应用程序生命周期的大部分时间里,您都在为未使用的资源付费。

使用无服务器技术,您只支付所使用的费用。使用函数即服务(FaaS),您将根据对函数的请求次数、函数代码执行所需的时间以及每个函数的保留内存计费。使用像 Amazon Rekognition 这样的托管服务,您只需支付处理的图像和视频分钟数等费用——再次,仅支付所用之物。

这使您可以构建功能和应用程序,基本上无需预先投资基础设施成本。只有当您的应用程序开始受到更多采用并扩展时,您才需要支付服务费用。

从云服务提供商收到的账单只是云基础设施总成本的一部分——还有运营人员的薪资。如果操作资源较少,这些成本会减少。

此外,以这种方式构建应用程序通常有助于更快地进入市场,减少总体开发时间,从而降低开发成本。

开发速度

构建功能较少,开发速度加快。能够快速启动大多数应用程序的常见功能(例如数据库、身份验证、存储和 API)使您能够迅速专注于编写您希望交付的核心功能和业务逻辑。

实验

如果您不投入大量时间来构建重复的功能,您可以更轻松地进行实验,风险更小。

在推出新功能时,您经常需要评估风险(构建功能涉及的时间和金钱成本)与可能的投资回报(ROI)之间的关系。随着尝试新事物所涉及的风险减少,您可以自由地测试过去可能无法实现的想法。

A/B 测试(也称为分桶测试分割测试)是一种比较应用程序多个版本以确定哪个性能最佳的方法。由于开发速度的增加,无服务器应用程序通常使您能够更快更轻松地对不同的想法进行 A/B 测试。

安全性和稳定性

因为你订阅的服务是服务提供商的核心竞争力所在,通常你得到的东西比你自己建立的要更加精细和安全。想象一下,一家公司多年来的核心业务模型一直是提供完善的认证服务,解决了成千上万家公司和客户的问题和边缘情况。

现在,想象一下在您自己的团队或组织内部复制这样的服务。虽然这完全可行,但选择使用由专门负责构建和维护该特定事物的人构建和维护的服务,是一种保险,最终将为您节省时间和金钱。

使用这些服务提供商的另一个优势是,它们将努力使停机时间最少。这意味着它们不仅承担了构建、部署和维护这些服务的负担,还尽一切可能确保它们的稳定性。

较少的代码

大多数工程师都会同意,到头来,代码是一种负担。有价值的是代码提供的功能,而不是代码本身。当你找到方法在限制需要维护的代码量的同时交付这些功能,甚至完全摒弃代码时,你正在减少应用程序的整体复杂性。

较少的复杂性意味着较少的错误,更容易让新工程师上手,并且对于维护和添加新功能的人员来说,认知负荷也会减少。开发人员可以接入这些服务并实现功能,而无需了解实际后端实现的细节,甚至可以几乎不编写任何后端代码。

无服务器的不同实现方式

让我们来看看构建无服务器应用程序的不同方法,以及它们之间的一些区别。

无服务器框架

第一个无服务器实现之一,Serverless Framework,是最受欢迎的。它是一个免费且开源的框架,于 2015 年 10 月以 JAWS 的名义发布,并使用 Node.js 编写。最初,Serverless Framework 仅支持 AWS,但随后还添加了对 Google 和 Microsoft Azure 等云提供商的支持。

Serverless Framework 利用配置文件(serverless.yml)、CLI 和函数代码的组合,为希望将无服务器函数和其他 AWS 服务从本地环境部署到云中的人提供了良好的体验。使用 Serverless Framework 进行快速上手可能对于新手来说有一定的学习曲线,特别是对于对云计算不熟悉的开发人员来说。需要学习很多术语,以及理解如何构建除了“Hello World”应用程序之外的其他内容。

总体而言,Serverless Framework 是一个不错的选择,如果您在某种程度上理解云基础设施的工作原理,并且希望寻找能够与 AWS 以外的其他云提供商一起使用的解决方案。

AWS 无服务器应用模型

AWS 无服务器应用模型(AWS SAM)是一个开源框架,由 AWS 和社区共同开发和维护,于 2016 年 11 月 18 日发布。该框架仅支持 AWS。

SAM 允许您通过在 YAML 文件中定义 API Gateway API、AWS Lambda 函数和无服务器应用程序所需的 Amazon DynamoDB 表来构建无服务器应用程序。它使用 YAML 配置、函数代码和 CLI 的组合来创建、管理和部署无服务器应用程序。

SAM 的一个优势是它是 AWS CloudFormation 的扩展,后者非常强大,允许您在 AWS 中几乎做任何事情。这也可能是对于云计算新手而言的一个缺点,如果不熟悉 AWS 服务、权限、角色和术语,则必须事先了解服务的工作方式、设置它们的命名约定以及如何将它们全部连接在一起。

如果您熟悉 AWS 并且只将您的无服务器应用部署到 AWS,SAM 是一个不错的选择。

Amplify Framework

Amplify Framework 是 CLI、客户端库、工具链和 Web 托管平台的组合。Amplify 的目的是为开发人员提供一种简单的方式来构建和部署利用云的全栈 Web 和移动应用程序。它不仅支持无服务器函数和认证功能,还支持 GraphQL API、机器学习(ML)、存储、分析、推送通知等。

Amplify 通过摒弃对于 AWS 新手可能不熟悉的术语和缩写,并使用类别名称方法来引用服务,为云计算提供了一个简单的入门点。例如,认证服务不再称为 Amazon Cognito,而是简称为 auth,框架在内部使用 Amazon Cognito。

其他选择

越来越多的公司开始提供对无服务器函数的抽象,通常旨在改善直接与 AWS Lambda 一起工作时传统上关联的负面用户体验。在这些选项中,一些流行的选择包括 Apex、Vercel、Cloudflare Workers 和 Netlify Functions。

许多这些工具和框架实际上仍然使用 AWS 或其他云提供商的基础设施,因此你基本上会因为他们声称提供更好的用户体验而支付更多费用。大多数这些工具并不提供 AWS 或其他云提供商可用的其他服务套件;像身份验证、AI 和 ML 服务、复杂对象存储和分析可能会或可能不会包含在它们的服务中。

如果你有兴趣学习其他开发无服务器应用程序的方式,我建议你查看这些选项。

AWS 简介

在本节中,我将概述 AWS,并讨论为什么像 Amplify 框架这样的东西存在。

关于 AWS

AWS,亚马逊的子公司,是第一家为开发者提供按需云计算平台的公司。它于 2004 年首次推出,只有一个服务:Amazon Simple Queue Service(Amazon SQS)。2006 年,他们正式重新推出,总共有三个服务:Amazon SQS、Amazon S3 和 Amazon EC2。自 2006 年以来,AWS 不断增长,并仍然是全球最大的云计算提供商,每年继续增加服务。AWS 现在提供超过两百种服务。

随着当前云计算技术向无服务器技术发展,入门门槛正在降低。然而,对于前端开发人员或云计算新手来说,入门仍然往往是困难的。

随着这种新的无服务器范式,AWS 看到了一个机会,创建一个框架,专注于帮助那些传统前端开发人员和新手开发者开始构建云应用程序。

AWS 上的全栈无服务器

全栈无服务器是为了给开发者提供堆栈两端所需的一切,以尽可能快速地构建可扩展的应用程序。在这里,我们将看看如何使用 AWS 工具和服务以这种方式构建应用程序。

Amplify CLI

如果你刚开始使用 AWS,服务的数量可能会让你感到不知所措。除了要在许多服务中进行选择外,每个服务通常都有自己的陡峭学习曲线。为了帮助缓解这一点,AWS 创建了Amplify CLI

Amplify CLI 为希望在 AWS 上构建应用程序的开发者提供了一个简单的入门点。CLI 允许开发者直接从他们的前端环境创建、配置、更新和删除云服务。

相比于 AWS 控制台和许多其他工具(如 CloudFormation)使用的服务名方法,CLI 采用了分类名方法。AWS 有许多服务名称(例如,Amazon S3,Amazon Cognito 和 Amazon Pinpoint),这对新开发者来说可能会有些混淆。CLI 不再使用服务名称来创建和配置这些服务,而是使用类似storage(Amazon S3)、auth(Amazon Cognito)和analytics(Amazon Pinpoint)这样的名称,帮助你理解服务的实际功能,而不仅仅是服务名称。

CLI 有大量命令,允许您在不离开前端环境的情况下创建、更新、配置和删除服务。您还可以使用 CLI 快速启动和部署新环境,以便测试新功能而不影响主环境。

一旦您使用 CLI 创建和部署了功能,您就可以使用 Amplify 客户端库开始与客户端应用程序中的服务进行交互。

Amplify 客户端

构建全栈应用程序需要结合客户端工具和后端服务。过去,与 AWS 服务交互的主要方式是使用 AWS 软件开发工具包(SDK),如 Java、.NET、Node.js 和 Python。这些 SDK 工作得很好,但对于客户端开发来说并不特别适用。在 Amplify 出现之前,没有简单的方法可以使用 AWS 构建客户端应用程序。如果您查看 AWS Node.js SDK 的文档,您还会注意到对于新接触 AWS 的开发者来说,它具有陡峭的学习曲线。

Amplify 客户端是一个专门为需要与 AWS 服务交互的 JavaScript 应用程序提供易于使用的 API 的库。Amplify 还为 React Native、本机 iOS 和本机 Android 提供了客户端 SDK。

Amplify 客户端采取的方法是提供更高层次的抽象,并内置最佳实践,以提供声明式、易于使用的 API。同时,它允许您完全控制与后端的交互。它还特别针对客户端构建,具有诸如 WebSocket 和 GraphQL 订阅支持的功能。它利用 localStorage 存储浏览器中的安全令牌,而在 React Native 中则使用 AsyncStorage 来持久化用户认证的IdTokensAccessTokens

Amplify 还为流行的前端和移动框架提供 UI 组件,包括 React、React Native、Vue、Angular、Ionic、本机 Android 和本机 iOS。这些特定于框架的组件允许您快速启动常见功能,如身份验证和复杂对象的存储和检索,而无需构建前端 UI 和处理状态。

Amplify Framework 不支持 AWS 服务的整套功能;相反,它支持其中的一个子集,几乎所有这些功能都属于无服务器类别。使用 Amplify,支持与 EC2 互动不是很有意义,但支持与表述性状态传输(REST)和 GraphQL API 互动非常合理。

Amplify 被创建为一种端到端解决方案,以填补以前未填补的空白,但它还涵盖了构建全栈云应用程序的新方法。

AWS AppSync

AWS AppSync 是一个管理的API 层,使用 GraphQL 使应用程序能够与任何数据源、REST API 或微服务进行交互。

API 层是应用程序中最重要的部分之一。现代应用程序通常与大量的后端服务和 API 进行交互,例如数据库、托管服务、第三方 API 和存储解决方案等。微服务架构是指使用模块化组件或服务组合构建的大型应用程序的常用术语。

大多数服务和 API 将具有不同的实现细节,这在使用微服务架构时会带来挑战。这导致前端开发人员在向这些 API 发出请求时,代码不一致,有时混乱,并增加了认知负荷。

处理微服务架构的一个良好方法是提供一致的 API 网关层,然后将所有请求转发到后端服务。这使得客户端与之交互的交互层保持一致,从而简化了前端的开发工作。

GraphQL 是 Facebook 创建并开源的技术,特别适合创建 API 网关。GraphQL 引入了一种定义和一致的规范,以三种操作的形式与 API 进行交互:查询(读取)、变更(写入/更新)和订阅(实时数据)。这些操作作为主要模式的一部分定义,并提供了客户端与服务器之间的合同形式,即 GraphQL 类型。GraphQL 操作不绑定于任何特定的数据源,因此作为开发人员,您可以自由地使用它们与数据库、HTTP 端点、微服务甚至无服务器函数进行交互。

通常,在构建 GraphQL API 时,您需要处理构建、部署、维护和配置自己的 API。使用 AWS AppSync,您可以将服务器和 API 管理以及安全性外包给 AWS。

现代应用程序通常还涉及实时和离线支持等问题。AppSync 的另一个好处是它内置了对离线(Amplify 客户端 SDK)和实时(GraphQL 订阅)的支持,使开发人员能够构建这些类型的应用程序。

在本书中,您将使用 AWS AppSync 以及各种数据源(如 DynamoDB 用于 NoSQL 和 AWS Lambda 用于无服务器函数)作为主要的 API 层。

AWS Amplify CLI 介绍

您将在本书中始终使用 Amplify CLI 来创建和管理您的云服务。为了了解它的工作原理,在本节中您将使用 CLI 创建和部署一个服务。服务部署完成后,您还将学习如何删除它以及删除与部署相关的任何后端资源。让我们看看如何创建您的第一个服务。

安装和配置 Amplify CLI

要开始使用,您首先需要安装并配置 Amplify CLI:

~ npm install -g @aws-amplify/cli
注意

要使用 CLI,您首先需要在计算机上安装 Node.js 版本 10.x 或更高版本以及 npm 版本 5.x 或更高版本。要安装 Node.js,我建议访问 Node.js 安装页面 并按照安装说明操作,或者使用 Node Version Manager (NVM)。

安装完成 CLI 后,您需要使用 AWS 帐户中的身份和访问管理(IAM)用户配置它。为此,您将使用一组用户凭据(访问密钥 ID 和秘密访问密钥)配置 CLI。使用这些凭据,您将能够直接从 CLI 代表此用户创建 AWS 服务。

要创建新用户并配置 CLI,您将运行 configure 命令:

~ amplify configure

这将引导您完成以下步骤:

  1. 指定 AWS 区域。

    这将允许您选择要创建用户的区域(以及由此用户关联的服务)。选择距离您最近或首选的区域。

  2. 指定用户名。

    此名称将成为您在 AWS 帐户中创建的用户的本地引用。我建议使用一个您以后在引用时能够识别的名称,例如 amplify-cli-us-east-1-usermycompany-cli-admin

输入您的名称后,CLI 将打开 AWS IAM 仪表板。从这里,您可以通过点击“下一步:权限”、“下一步:标签”、“下一步:审核”和“创建用户”接受默认设置来创建 IAM 用户。

在下一个屏幕中,您将获得 IAM 用户凭据:访问密钥 ID 和秘密访问密钥。请参见 图 1-1。

AWS IAM 仪表板 IAM 用户

图 1-1. AWS IAM 仪表板

回到 CLI,粘贴访问密钥 ID 和秘密访问密钥的值。现在您已成功配置 CLI,可以开始创建新服务。

初始化您的第一个 Amplify 项目

现在 CLI 已安装并配置完成,您可以创建您的第一个项目。这一步通常在客户端应用程序的根目录下完成。由于本书大部分时间您将使用 React,我们将从初始化一个新的 React 项目开始:

~ npx create-react-app amplify-app

# after creating the React app, change into the new directory
~ cd amplify-app

现在您需要安装 Amplify,这将在客户端上使用。您将使用的库是 AWS Amplify 和 AWS Amplify React,用于 React 特定的 UI 组件:

~ npm install aws-amplify @aws-amplify/ui-react

接下来,您可以创建一个 Amplify 项目。为此,您将运行 init 命令:

~ amplify init

这将引导您完成以下步骤:

  1. 输入项目名称。

    这将是项目的本地名称,通常描述项目或其功能。

  2. 输入环境名称。

    这将是您将要工作的初始环境的参考。在此工作流程中,典型的环境可能是 devlocalprod,但可以是任何对您有意义的内容。

  3. 选择您的默认编辑器。

    这将设置您的编辑器偏好。CLI 稍后将使用此偏好打开当前项目的文件。

  4. 选择您要构建的应用类型。

    这将确定 CLI 是否应配置、构建和运行命令(如果使用 JavaScript)。在本例中,请选择 javascript

  5. 您使用的 JavaScript 框架是什么?

    这将确定一些基本的构建和启动命令。在本例中,请选择 react

  6. 选择您的源代码目录路径。

    这允许您设置源代码所在的目录。在本例中,请选择 src

  7. 选择您的分发目录路径。

    对于 Web 项目,这将是包含编译后的 JavaScript 源代码以及 favicon、HTML 和 CSS 文件的文件夹。在本例中,请选择 build

  8. 选择您的构建命令。

    这指定了编译和捆绑 JavaScript 代码的命令。在本例中,请使用 npm run-script build

  9. 选择您的启动命令。

    这指定了在本地服务器上提供应用程序的命令。在本例中,请使用 npm run-script start

  10. 是否要使用 AWS 配置文件?

    在这里,选择 Y,然后选择您运行 amplify configure 时创建的 AWS 配置文件。

现在,Amplify CLI 将初始化您的新 Amplify 项目。初始化完成后,在您的项目中会创建两个额外的资源:一个位于 src 目录下名为 aws-exports 的文件,以及一个位于根目录下名为 amplify 的文件夹。

aws-exports 文件

aws-exports 文件是 CLI 为您创建的资源类别及其凭据的键值对。

amplify 文件夹

此文件夹包含 Amplify 项目的所有代码和配置文件。在此文件夹中,您将看到两个子文件夹:backend#current-cloud-backend 文件夹。

backend 文件夹

此文件包含项目中的所有本地代码,例如用于 AppSync API 的 GraphQL 模式、任何无服务器函数的源代码,以及代表 Amplify 项目当前本地状态的基础设施即代码。

#current-cloud-backend 文件夹

此文件夹保存反映最后一次 Amplify push 命令部署的云中资源的代码和配置。它帮助 CLI 区分云中已经提供的资源配置和当前在本地 backend 目录中的配置(反映了您的本地更改)。

现在您已初始化项目,可以添加第一个云服务:authentication

创建并部署您的第一个服务

要创建新服务,可以使用 Amplify 的 add 命令:

~ amplify add auth

这将引导您完成以下步骤:

  1. 您是否想使用默认的认证和安全配置?

    这为您提供了使用默认配置(注册时 MFA,登录时密码)、使用社交提供商创建认证配置或创建完全自定义认证配置的选项。对于本示例,请选择 Default configuration

  2. 您希望用户如何进行登录?

    这将允许您指定所需的登录属性。对于此示例,请选择默认选项 Username

  3. 您是否要配置高级设置?

    这将允许您进行进一步的高级设置,如额外的注册属性和 Lambda 触发器。对于本示例,您不需要这些设置,因此选择默认选项 No, I am done

    现在,您已成功配置了认证服务,现在可以准备部署。要部署认证服务,可以运行 push 命令:

    ~ amplify push
    
  4. 您确定要继续吗?

    选择 Y

部署完成后,您的认证服务已成功创建。恭喜,您已部署了第一个功能。现在,让我们来测试一下。

在 React 应用程序中与认证服务交互有几种方法。您可以使用 Amplify 的 Auth 类,它提供了超过 30 个可用的方法(如 signUpsignInsignOut 等),或者您可以使用框架特定的组件,如 withAuthenticator,它将为您提供一个完整的认证流程,包括预配置的用户界面。让我们试试 withAuthenticator 高阶组件。

首先,配置 React 应用程序以与 Amplify 协作。为此,请打开 src/index.js 并在最后一个 import 语句下添加以下代码:

import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)

现在,应用程序已配置完成,您可以开始与认证服务进行交互。接下来,打开 src/App.js 并使用以下代码更新文件:

import React from 'react'
import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react'

function App() {
  return (
    <div>
      <h1>Hello from AWS Amplify</h1>
      <AmplifySignOut />
    </div>
  )
}

export default withAuthenticator(App)

此时,您可以通过启动应用程序来测试它:

~ npm start

现在,你的应用程序应该使用预配置的认证流程启动。参见 图 1-2。

AWS Amplify withAuthenticator 组件

图 1-2. withAuthenticator 高阶组件

删除资源

一旦您不再需要某个功能或项目,可以使用 CLI 删除它。

若要移除单个功能,您可以运行remove命令:

~ amplify remove auth

若要删除整个 Amplify 项目以及在您的帐户中部署的所有相应资源,您可以运行delete命令:

~ amplify delete

摘要

云计算正在快速增长,越来越多的公司开始依赖云来处理大部分工作负载。随着使用量的增加,云计算的知识正在成为技能组合中的宝贵补充。

无服务器范式,作为云计算的一个子集,也在商业用户中迅速流行起来,因为它提供了云计算的所有优势,并具有自动扩展的特性,几乎不需要维护。

类似 Amplify 框架这样的工具正在帮助所有背景的开发人员轻松上手云和无服务器计算。在接下来的章节中,您将学习如何在云中构建真实的全栈无服务器应用程序,利用云服务和 Amplify 框架。

^(1) Eric Jonas, Johann Schleier-Smith 等人,《云编程简化:关于无服务器计算的伯克利视角》(2019 年 2 月 10 日),http://www2.eecs.berkeley.edu/Pubs/TechRpts/2019/EECS-2019-3.html

第二章:使用 AWS Amplify 入门

大多数应用程序的核心是数据/API 层。这一层可能有很多形式。在无服务器世界中,这通常将由一组 API 端点和无服务器函数组成。这些无服务器函数可以执行一些逻辑并返回数据,与某种数据库交互,甚至与另一个 API 端点交互。

使用 Amplify 创建 API 的两种主要方式有:

  • Amazon API Gateway 和 Lambda 函数的组合

  • 与某种数据源(数据库、Lambda 函数或 HTTP 端点)连接的 GraphQL API

API 网关是 AWS 的一个服务,允许您创建 API 端点并将其路由到不同的服务,通常通过 Lambda 函数。当您发起 API 调用时,它将通过 API 网关路由请求,调用函数,并返回响应。使用 Amplify CLI,您可以创建 API 网关端点以及 Lambda 函数;CLI 将自动配置 API,以便通过 HTTP 请求调用 Lambda 函数。

创建 API 后,您需要一种与其交互的方式。使用 Amplify 客户端,您将能够使用 Amplify API 类向端点发送请求。API 类允许您与 GraphQL API 和 API 网关端点进行交互,如 图 2-1 所示。

在本章中,您将创建您的第一个全栈无服务器应用程序,该应用程序将通过 API 网关端点与无服务器函数进行交互。您将使用 CLI 创建一个 API 端点以及一个无服务器函数,然后使用 Amplify 客户端库与 API 进行交互。

带 Lambda 的 API

图 2-1. 带 Lambda 的 API

首先,应用程序将从函数中获取一个硬编码的项目数组。然后,您将学习如何更新函数,以便发起异步 HTTP 请求到另一个 API 以检索数据并将其返回给客户端。

创建和部署无服务器函数

许多无服务器应用程序的核心是无服务器函数。无服务器函数在无状态计算容器中运行您的代码,这些容器是事件驱动的,短暂的(可能只持续一个调用),并由您选择的云提供商完全管理。这些函数可以无缝扩展,并且不需要任何服务器操作。

尽管大多数人认为无服务器函数是通过 API 调用触发的,但这些函数也可以通过各种不同的事件触发。除了 HTTP 请求之外,触发无服务器函数的几种常见方式包括通过向存储服务上传图像,数据库操作(如创建、更新或删除),甚至来自另一个无服务器函数。

无服务器函数会自动扩展,因此如果您的应用程序遇到大量流量,无需担心。当您首次调用函数时,服务提供商将创建函数实例并运行其处理程序方法来处理事件。函数完成并返回响应后,它将继续存在并处理额外的事件。如果第一个事件仍在处理时发生另一个调用,服务将创建另一个实例。

无服务器函数还有一个与传统基础设施不同的付款模型。使用像 AWS Lambda 这样的服务,您只需按实际使用付费,按照函数的请求次数和代码执行的时间计费。这与预配并支付服务器等基础设施形成对比,后者无论是否被使用都会收费。

现在您已了解了无服务器函数,让我们看看如何创建一个无服务器函数,并将其连接到一个 API,该 API 将从 HTTP 请求中调用它。

创建 React 应用程序并安装依赖项

要开始,您首先需要创建 React 应用程序。为此,您可以使用 npx

~ npx create-react-app amplify-react-app
~ cd amplify-react-app

接下来,您需要安装依赖项。对于此应用程序,您只需要 AWS Amplify 库:

~ npm install aws-amplify

安装完依赖项后,您现在可以在 React 应用程序的根目录中初始化一个新的 Amplify 项目:

~ amplify init

? Enter a name for the project: cryptoapp
? Enter a name for the environment: local
? Choose your default editor: <your-preferred-editor>
? Choose the type of app that you're building: javascript
? What javascript framework are you using: react
? Source Directory Path: src
? Distribution Directory Path: build
? Build Command: npm run-script build
? Start Command: npm run-script start
? Do you want to use an AWS profile? Here, choose *Y* and pick the AWS
  profile you created when you ran `amplify configure`.

现在,Amplify 项目和 React 应用程序均已成功创建,您可以开始添加新功能。

使用 Amplify CLI 创建新的无服务器函数

在下一步中,我们将创建一个无服务器函数,您将在此应用程序中使用它。本章中构建的应用程序是一个加密货币应用程序。首先,您将在函数中硬编码一个加密货币信息数组,并将其返回给客户端。稍后在本章中,您将更新此函数以调用另一个 API(CoinLore),并异步获取和返回数据。

要创建函数,请运行以下命令:

~ amplify add function

? Select which capability you want to add: Lambda function
? Provide a friendly name for your resource to be used as a label for
  this category in the project: cryptofunction
? Provide the AWS Lambda function name: cryptofunction
? Choose the function runtime that you want to use: NodeJS
? Choose the function template that you want to use: Serverless express
  function (Integration with Amazon API Gateway)
? Do you want to access other resources created in this project from
  your Lambda function? No
? Do you want to invoke this function on a recurring schedule? No
? Do you want to configure Lambda layers for this function? No
? Do you want to edit the local Lambda function now? No
提示

如果函数成功创建,您将看到一条消息:“已在本地成功添加资源 cryptofunction。”

您现在应该看到一个位于 amplify 目录中的新子文件夹 amplify/backend/function/cryptofunction

代码解析

当您创建此资源时,将在 amplify/backend 中创建一个名为 function 的新文件夹。CLI 创建的所有函数都将存储在此文件夹中。目前,您只有一个单独的函数 cryptofunction。在 cryptofunction 文件夹中,您将看到一些配置文件以及一个 src 目录,其中包含主要函数代码。

无服务器函数本质上只是独立运行的封装应用程序。因为您创建的函数是 JavaScript,所以您会看到在任何 JavaScript 应用程序中通常看到的所有内容,包括package.jsonindex.js文件。

接下来,请查看位于src/index.js中的函数入口点,在cryptofunction文件夹中。在这个文件中,您将看到一个名为exports.handler的函数。这是函数调用的入口点。当调用函数时,此代码将被运行。

如果您愿意,您可以直接在此函数中处理事件,但由于您将使用 API,更有用的方法是将路径代理到具有路由的 express 应用程序中(即http://yourapi/)。这样做可以为您提供单个函数中的多个路由以及每个路由的多个 HTTP 请求方法,如getputpostdeleteserverless express框架为您提供了一种简便的方法来实现这一点,并已内置到函数的样板中。

index.js中,您将看到一个类似于这样的代码行:

awsServerlessExpress.proxy(server, event, context);

此代码是将事件、上下文和路径代理到运行在app.js中的 express 服务器的地方。

app.js中,然后您将能够针对您为 API 创建的任何路由创建 HTTP 请求(本示例为获取加密货币的/coins路由)。

创建/coins 路由

现在您已经了解了应用程序的结构,让我们在app.js中创建一个新的路由,并从中返回一些数据。您将创建的路由是/coins路由。此路由将返回一个包含 coins 数组的对象。

让我们添加新路由。在第一个app.get('/items')路由之前,添加以下代码:

/* amplify/backend/function/cryptofunction/src/app.js /*

app.get('/coins', function(req, res) {
  const coins = [
    { name: 'Bitcoin', symbol: 'BTC', price_usd: "10000" },
    { name: 'Ethereum', symbol: 'ETH', price_usd: "400" },
    { name: 'Litecoin', symbol: 'LTC', price_usd: "150" }
  ]
  res.json({
    coins
  })
})

这个新路由有一个硬编码的加密货币信息数组。当使用这个路由调用函数时,它将响应一个包含名为coins的单一属性的对象,该属性将包含 coins 数组。

添加 API

现在函数已经创建并配置好了,让我们在其前面放置一个 API,以便您可以使用 HTTP 请求触发它。

要做到这一点,您将使用 Amazon API Gateway。API Gateway 是一种全管理的服务,使开发人员能够创建、发布、维护、监控和安全地管理 REST 和 WebSocket API。API Gateway 是 Amplify CLI 和 Amplify 客户端库都支持的服务之一。

在本节中,您将创建一个新的 API Gateway 端点,并配置它来调用上一节中创建的 Lambda 函数。

创建新的 API

要创建 API,您可以使用 Amplify add命令。从项目的根目录,在您的终端中运行以下命令:

~ amplify add api

? Please select from one of the below mentioned services: REST
? Provide a friendly name for your resource to be used as a label for
  this category in the project: cryptoapi
? Provide a path: /coins
? Choose a Lambda source: Use a Lambda function already added in the
  current Amplify project
? Choose the Lambda function to invoke by this path: cryptofunction
? Restrict API access: N
? Do you want to add another path? N

部署 API 和 Lambda 函数

现在函数和 API 都已创建好,您需要将它们部署到您的账户以使其生效。为此,您可以运行 Amplify push命令:

~ amplify push

? Are you sure you want to continue? Y

部署成功后,服务即已启动并准备就绪。

您可以随时使用 Amplify CLI 的 status 命令查看项目的当前状态。status 命令将列出项目中当前配置的所有服务,并为每个服务提供状态:

 ~ amplify status

Current Environment: local

| Category | Resource name  | Operation | Provider plugin   |
| -------- | -------------- | --------- | ----------------- |
| Function | cryptofunction | No Change | awscloudformation |
| Api      | cryptoapi      | No Change | awscloudformation |

在此状态输出中需要注意的主要内容是 OperationOperation 告诉您下次在项目中运行 push 时将发生什么。Operation 属性将设置为 CreateUpdateDeleteNo Change

与新 API 交互

现在资源已经部署完成,您可以开始从 React 应用程序与 API 进行交互。

配置客户端应用以与 Amplify 配合使用

要在任何应用程序中使用 Amplify 客户端库,通常需要设置基本配置,通常在根级别。创建资源时,CLI 会使用关于资源的信息填充 aws-exports.js 文件。您将使用此文件来配置客户端应用程序以与 Amplify 配合使用。

要配置应用程序,请打开 src/index.js 并在最后一个导入语句下添加以下内容:

import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)

Amplify 客户端 API 类别

客户端应用程序配置完成后,您可以开始与资源进行交互。

Amplify 客户端库有各种 API 类别,可以导入并用于各种功能,包括用于身份验证的 Auth、用于在 S3 中存储项目的 Storage,以及用于与 REST 和 GraphQL API 交互的 API

在此部分,您将使用 API 类别。API 具有各种可用的方法,包括 API.getAPI.postAPI.putAPI.del,用于与 REST API 交互,以及用于与 GraphQL API 交互的 API.graphql

在使用 REST API 时,API 接受三个参数:

API.get(apiName: String, path: String, data?: Object)

apiName

在通过命令行创建 API 时给定的名称。在我们的示例中,该值将是 cryptoapi

path

您希望与之交互的路径。在我们的示例中,我们创建了 /coins,因此路径将是 /coins

data

这是一个可选对象,其中包含您希望传递给 API 的任何属性,包括头部、查询字符串参数或主体。

在我们的示例中,API 调用将如下所示:

API.get('cryptoapi', '/coins')

API 返回一个 promise,这意味着您可以使用 promise 或 async 函数来处理调用:

// promise
API.get('cryptoapi', '/coins')
  .then(data => console.log(data))
  .catch(error => console.log(error))

// async await
const data = await API.get('cryptoapi', '/coins')

在本书的示例中,我们将使用 async 函数来处理 promise。

调用 API 并在 React 中渲染数据

接下来,让我们调用 API 并渲染数据。在 src/App.js 中更新如下:

// Import useState and useEffect hooks from React
import React, { useState, useEffect } from 'react'

// Import the API category from AWS Amplify
import { API } from 'aws-amplify'

import './App.css';

function App() {
  // Create coins variable and set to empty array
  const [coins, updateCoins] = useState([])

  // Define function to all API
  async function fetchCoins() {
    const data = await API.get('cryptoapi', '/coins')
    updateCoins(data.coins)
  }

  // Call fetchCoins function when component loads
  useEffect(() => {
    fetchCoins()
  }, [])

  return (
    <div className="App">
      {
        coins.map((coin, index) => (
          <div key={index}>
            <h2>{coin.name} - {coin.symbol}</h2>
            <h5>${coin.price_usd}</h5>
          </div>
        ))
      }
    </div>
  );
}

export default App

然后,运行应用程序:

~ npm start

当应用程序加载时,您应该看到一个包含名称、符号和价格的硬币列表,如图 2-2 所示。

从 API 获取数据

图 2-2. 从 API 获取数据

更新调用另一个 API 的功能

接下来,您将更新函数以调用另一个 API,即 CoinLore API,该 API 将从 CoinLore 服务返回动态数据。用户将能够添加类似 limitstart 的过滤器,以限制从 API 返回的项目数。

要开始,您首先需要一种方法与 Lambda 函数中的 HTTP 端点进行交互。本课程中您将使用的库是 Axios 库。Axios 是一个基于承诺的浏览器和 Node.js 的 HTTP 客户端。

安装 Axios

您需要做的第一件事是在函数文件夹中安装 Axios 包,以便从函数中发送 HTTP 请求。导航到 amplify/backend/function/cryptofunction/src,安装 Axios,然后返回应用程序的根目录:

~ cd amplify/backend/function/cryptofunction/src
~ npm install axios
~ cd ../../../../../

更新函数

接下来,更新位于 amplify/backend/function/cryptofunction/src/app.js 中的 /coins 路由如下:

// Import axios
const axios = require('axios')

app.get('/coins', function(req, res) {
  // Define base url
  let apiUrl = `https://api.coinlore.com/api/tickers?start=0&limit=10`

  // Check if there are any query string parameters
  // If so, reset the base url to include them
  if (req.apiGateway && req.apiGateway.event.queryStringParameters) {
   const { start = 0, limit = 10 } = req.apiGateway.event.queryStringParameters
   apiUrl = `https://api.coinlore.com/api/tickers/?start=${start}&limit=${limit}`
  }

  // Call API and return response
  axios.get(apiUrl)
    .then(response => {
      res.json({  coins: response.data.data })
    })
    .catch(err => res.json({ error: err }))
})

在前述函数中,我们导入了 Axios 库,然后使用它向 CoinLore API 发送了 API 调用。在 API 调用中,您可以传递 startlimit 参数到请求中,以定义返回的硬币数量,并定义起始点。

req 参数中,有一个 apiGateway 属性,其中包含 eventcontext 变量。在刚刚定义的函数中,有一个检查以查看是否存在此 event,以及 event 上的 queryStringParameters 属性。如果 queryStringParameters 属性存在,我们将使用这些值来更新基本 URL,并使用 queryStringParameters,用户可以在查询 CoinLore API 时指定 startlimit 值。

一旦函数更新完成,您可以通过在终端中运行 push 命令来部署更新:

~ amplify push

Current Environment: local

| Category | Resource name  | Operation | Provider plugin   |
| -------- | -------------- | --------- | ----------------- |
| Function | cryptofunction | Update    | awscloudformation |
| Api      | cryptoapi      | No Change | awscloudformation |

? Are you sure you want to continue? Y

更新客户端应用程序

现在,您已经更新了函数,让我们更新 React 应用程序,以便用户可以指定 limitstart 参数。

为此,您需要添加用户输入字段,并为用户提供一个按钮来触发新的 API 请求。

更新 src/App.js 如下所示:

// Create additional state to hold user input for limit and start properties
const [input, updateInput] = useState({ limit: 5, start: 0 })

// Create a new function to allow users to update the input values
function updateInputValues(type, value) {
  updateInput({ ...input, [type]: value })
}

// Update fetchCoins function to use limit and start properties
async function fetchCoins() {
  const { limit, start } = input
  const data = await API.get('cryptoapi', `/coins?limit=${limit}&start=${start}`)
  updateCoins(data.coins)
}

// Add input fields to the UI for user input
<input
  onChange={e => updateInputValues('limit', e.target.value)}
  placeholder="limit"
/>
<input
  placeholder="start"
  onChange={e => updateInputValues('start', e.target.value)}
/>

// Add button to the UI to give user the option to call the API
<button onClick={fetchCoins}>Fetch Coins</button>

接下来,运行应用程序:

~ npm start

摘要

恭喜!您已部署了您的第一个无服务器 API!

从本章中需要记住的几点是:

  • Lambda 函数可以通过各种事件触发。在本章中,我们通过 API Gateway 的 API 调用触发了该函数。

  • 可以使用 Amplify CLI 通过 amplify add function 命令创建 Lambda 函数,并通过 amplify add api 命令创建 API。

  • 单个 API Gateway 端点可以配置为与多个 Lambda 函数配合使用。在本章的示例中,我们仅将其连接到了单个函数。

  • Lambda 函数本质上是独立的 Node.js 应用程序。在本章的示例中,我们选择运行一个 express 应用程序以处理 getpostdelete 等 REST 方法,尽管到目前为止我们只使用了 get 调用。

  • Amplify 客户端库中的API类别可用于 GraphQL 和 REST API。

第三章:创建您的第一个应用程序

在 第二章 中,您使用 API Gateway 和无服务器函数创建了一个基本的 API 层。这种组合非常强大,但您尚未与真实数据库进行交互。

在本章中,您将创建一个与 DynamoDB NoSQL 数据库交互以执行 CRUD+L(创建、读取、更新、删除和列表)操作的 GraphQL API。您将了解 GraphQL 的定义、开发者为何采用它以及其工作原理。

我们将构建一个笔记应用程序,允许用户创建、更新和删除笔记。它还将启用 GraphQL 订阅功能,以便实时查看更新。如果另一个用户正在与应用程序交互并创建新的笔记,我们的应用程序将实时更新新值。

GraphQL 简介

GraphQL 是 REST 的一种替代实现。让我们看看 GraphQL 是什么,GraphQL API 由什么组成,以及 GraphQL 的工作原理。

GraphQL 是什么?

GraphQL 是一个 API 规范。它是一个用于 API 的查询语言,以及用于用您的数据来实现这些查询的运行时。它可以用作 REST 的替代方案,并与 REST 有一些相似之处。

GraphQL 是 Facebook 在 2015 年引入的,尽管在 2012 年就在内部使用。GraphQL 允许客户端定义 API 调用所需的数据结构,以便他们可以准确地知道服务器将返回的数据结构。以这种方式请求数据使得客户端应用程序与后端 API 和服务的交互方式更加高效,减少了数据的不足提取、防止数据的过度提取和类型错误。

GraphQL API 的组成部分是什么?

一个 GraphQL API 主要由三个部分组成:模式、解析器和数据源,如 图 3-1 所示。

GraphQL API 设计

图 3-1. GraphQL API 设计

用 GraphQL 模式定义语言(SDL)编写的模式定义了可以针对 API 执行的数据模型(类型)和操作。模式由基本类型(数据模型)和像查询(用于获取数据)、突变(用于创建、更新和删除数据)以及订阅(用于订阅实时数据更改)这样的 GraphQL 操作组成。

这是一个 GraphQL 模式的示例:

# base type
type Todo {
  id: ID
  name: String
  completed: Boolean
}

# Query definitions
type Query {
  getTodo(id: ID): Todo
  listTodos: [Todo]
}

# Mutation definitions
type Mutation {
  createTodo(input: Todo): Todo
}

# Subscription definitions
type Subscription {
  onCreateTodo: Todo
}

一旦模式被创建,您可以开始为在模式中定义的 GraphQL 操作编写解析器(查询、突变、订阅)。GraphQL 解析器告诉 GraphQL 操作在执行时该做什么,并通常会与某些数据源或另一个 API 进行交互,如 图 3-2 所示。

GraphQL 工作原理

图 3-2. GraphQL 工作原理

GraphQL 操作

GraphQL 操作是您与 API 数据源进行交互的方式。GraphQL 操作可以类似地映射到 RESTful API 的 HTTP 方法:

GET -> Query
PUT -> Mutation
POST -> Mutation
DELETE -> Mutation
PATCH -> Mutation

GraphQL 请求操作看起来类似于一个 JavaScript 对象,只有键没有值。键和值在 GraphQL 操作响应中返回。以下是典型的 GraphQL 查询示例,获取一个项目数组:

query {
  listTodos {
    id
    name
    completed
  }
}

此请求将返回以下响应:

{
  "data": {
    "listTodos": [
      { "id": "0", "name": "buy groceries", "completed": false },
      { "id": "1", "name": "exercise", "completed": true }
    ]
  }
}

您还可以将参数传递给 GraphQL 操作。以下操作是一个查询 Todo 的示例,传入我们想要获取的 Todo 的 ID:

query {
  getTodo(id: "0") {
    name
    completed
  }
}

此请求将返回以下响应:

{
  "data": {
    "getTodo": {
      "name": "buy groceries"
      "completed": false
    }
  }
}

虽然有许多实现 GraphQL 服务器的方法,但在本书中我们将使用 AWS AppSync。AppSync 是一种托管服务,允许我们使用 Amplify CLI 快速轻松地部署 GraphQL API、解析器和数据源。

创建 GraphQL API

现在您已经基本了解了 GraphQL 是什么,让我们继续使用它来构建 Notes 应用程序。

您需要做的第一件事是创建一个新的 React 应用程序并安装必要的依赖项。该应用程序将使用 AWS Amplify 库与 API 交互,uuid 用于创建唯一的 id,Ant Design 库用于样式:

~ npx create-react-app notesapp
~ cd notesapp
~ npm install aws-amplify antd uuid

现在,在新应用的根目录中,您可以创建 Amplify 项目:

~ amplify init

? Enter a name for the project: notesapp
? Enter a name for the environment: dev
? Choose your default editor: <your editor of choice>
? Choose the type of app that you're building: javascript
? What javascript framework are you using: react
? Source Directory Path: src
? Distribution Directory Path: build
? Build Command: npm run-script build
? Start Command: npm run-script start
? Do you want to use an AWS profile? Y

初始化 Amplify 项目后,我们可以添加 GraphQL API:

~ amplify add api

? Please select from one of the below mentioned services: GraphQL
? Provide API name: notesapi
? Choose the default authorization type for the API: API Key
? Enter a description for the API key: public (or some description)
? After how many days from now the API key should expire: 365 (or your
  preferred expiration)
? Do you want to configure advanced settings for the GraphQL API: N
? Do you have an annotated GraphQL schema? N
? Do you want a guided schema creation? Y
? What best describes your project: Single object with fields
? Do you want to edit the schema now? Y

接下来,在您的文本编辑器中打开基础 GraphQL 模式(由 CLI 生成),位于 notesapp/amplify/backend/api/notesapi/schema.graphql,更新模式如下,并保存:

type Note @model {
  id: ID!
  clientId: ID
  name: String!
  description: String
  completed: Boolean
}

此模式包含一个主要的 Note 类型,包含五个字段。字段可以是可空的(非必需的)或非可空的(必需的)。非可空字段用 ! 字符指定。

此模式中的 Note 类型带有 @model 指令。该指令不是 GraphQL SDL 的一部分,而是 AWS Amplify GraphQL Transform 库的一部分。

GraphQL Transform 库允许您使用不同的指令(如 @model@connection@auth 等)对 GraphQL 模式进行注释。

在此模式中使用的 @model 指令将基础的 Note 类型转换为一个完整的 AWS AppSync GraphQL API,包括:

  1. 针对查询和变异的额外模式定义(CreateReadUpdateDeleteList 操作)

  2. 为 GraphQL 订阅添加额外的模式定义

  3. DynamoDB 数据库

  4. 所有 GraphQL 操作的解析器代码映射到 DynamoDB 数据库

要部署 API,可以运行 push 命令:

~ amplify push

? Are you sure you want to continue? Yes
? Do you want to generate code for your newly created GraphQL API: Yes
? Choose the code generation language target: javascript
? Enter the file name pattern of graphql queries, mutations and
  subscriptions: src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations -
  queries, mutations and subscriptions: Y
? Enter maximum statement depth [increase from default if your schema is
  deeply nested]: 2

一旦部署完成,API 和数据库已成功创建在您的账户中。接下来,让我们在 AWS 控制台中打开新创建的 AppSync API,并测试几个 GraphQL 操作。

查看和与 GraphQL API 交互

要随时在 AWS 控制台中打开 API,您可以使用以下命令:

- amplify console api

> Choose GraphQL

一旦打开了 AppSync 控制台,点击左侧菜单中的 Queries 打开查询编辑器。在这里,您可以使用您的 API 测试 GraphQL 查询、变异和订阅。

我们首先尝试的操作是执行变更以创建新的笔记。在查询编辑器中,执行以下变更(参见 图 3-3):

mutation createNote {
  createNote(input: {
    name: "Book flight"
    description: "Flying to Paris on June 1 returning June 10"
    completed: false
  }) {
    id name description completed
  }
}

GraphQL 变更

图 3-3. GraphQL 变更

现在,您已经创建了一个项目,可以尝试查询它。让我们尝试查询应用程序中的所有笔记:

query listNotes {
  listNotes {
    items {
      id
      name
      description
      completed
    }
  }
}

你也可以尝试使用其中一个笔记的 ID 查询单个笔记:

query getNote {
  getNote(id: "<NOTE_ID>") {
    id
    name
    description
    completed
  }
}

现在我们知道 GraphQL API 已部署并正常运行,让我们开始编写一些前端代码。

构建 React 应用程序

您需要做的第一件事是配置 React 应用程序以识别位于 src/aws-exports.js 的 Amplify 资源。为此,请打开 src/index.js 并在最后一个导入后添加以下内容:

import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)

列出笔记(GraphQL 查询)

现在应用程序已配置完成,您可以开始对 GraphQL API 进行调用。我们将要实现的第一个操作是查询以列出所有笔记。

查询将返回一个数组,我们将对数组中的所有项进行映射,显示笔记名称、描述以及是否已完成。

src/App.js 文件中,首先在文件顶部导入以下内容:

import React, {useEffect, useReducer} from 'react'
import { API } from 'aws-amplify'
import { List } from 'antd'
import 'antd/dist/antd.css'
import { listNotes } from './graphql/queries'

让我们来看一下上述内容中使用的一些术语:

useEffectuseReducer

React hooks

API

这是我们将用来与 AppSync 端点交互的 GraphQL 客户端(类似于 fetchaxios

列表

使用 Ant Design 库的 UI 组件来渲染列表

listNotes

用于获取笔记数组的 GraphQL 查询操作

接下来,我们需要创建一个变量来保存我们的初始应用程序状态。因为我们的应用程序将持有并处理多个状态变量,我们将使用 React 中的 useReducer 钩子来管理状态。

useReducer 的 API 如下:

const [state, dispatch] = useReducer(reducer <function>, initialState <any>)

useReducer 接受一个形如 (state, action) => newState 的 reducer 函数和一个 initialState 作为参数:

/* Example of some basic state */
const initialState = { notes: [] }

/* Example of a basic reducer */
function reducer(state, action) {
  switch(action.type) {
    case 'SET_NOTES':
      return { ...state, notes: action.notes }
    default:
      return state
  }
}

/* Implementing useReducer */
const [state, dispatch] = useReducer(reducer: <function>, initialState: <any>)

/* Sending an update to the reducer */
const notes = [{ name: 'Hello World' }]
dispatch({ type: 'SET_NOTES', notes: notes })

/* Using the state in your app */
{
  state.notes.map(note => <p>{note.name}</p>)
}

调用 useReducer 钩子时,将返回一个包含两个项目的数组:

  • 应用程序状态

  • dispatch 函数(此函数允许您更新应用程序状态)

Notes 应用程序的初始状态将包含笔记、表单值、错误和加载状态的数组。

src/App.js 文件中,在最后一个导入后添加以下 initialState 对象:

const initialState = {
  notes: [],
  loading: true,
  error: false,
  form: { name: '', description: '' }
}

然后创建 reducer。目前,reducer 只有用于设置笔记数组或设置错误状态的 case:

function reducer(state, action) {
  switch(action.type) {
    case 'SET_NOTES':
      return { ...state, notes: action.notes, loading: false }
    case 'ERROR':
      return { ...state, loading: false, error: true }
    default:
      return state
  }
}

接下来,更新主 App 函数以通过调用 useReducer 并传入 reducerinitialState 来创建状态和 dispatch 变量:

export default function App() {
  const [state, dispatch] = useReducer(reducer, initialState)
}

要获取笔记,请在主 App 函数中创建一个 fetchNotes 函数,该函数将调用 AppSync API,并在 API 调用成功后设置笔记数组:

async function fetchNotes() {
  try {
    const notesData = await API.graphql({
      query: listNotes
    })
    dispatch({ type: 'SET_NOTES', notes: notesData.data.listNotes.items })
  } catch (err) {
    console.log('error: ', err)
    dispatch({ type: 'ERROR' })
  }
}

现在,在主 App 函数中通过调用 useEffect 钩子来调用 fetchNotes 函数:

useEffect(() => {
  fetchNotes()
}, [])
注意

useEffect 类似于 componentDidMountuseEffect 将在组件的初始渲染提交到屏幕后运行。useEffect 的第二个参数是一个值数组,其效果取决于是否在重新渲染期间再次调用它。如果数组为空,则不会在额外的渲染中调用它。如果数组包含值并且这些值发生变化,则组件将重新渲染。

您需要做的下一件事是在组件的主 UI 中返回主 UI。在主 App 函数中,添加以下内容:

return (
  <div style={styles.container}>
    <List
      loading={state.loading}
      dataSource={state.notes}
      renderItem={renderItem}
    />
  </div>
)

这里我们使用 Ant Design 中的 List 组件。该组件将遍历一个数组(dataSource),并通过调用 renderItem 函数为数组中的每个项目返回一个项目。接下来,在主 App 函数中定义 renderItem

function renderItem(item) {
  return (
    <List.Item style={styles.item}>
      <List.Item.Meta
        title={item.name}
        description={item.description}
      />
    </List.Item>
  )
}

最后,创建应用程序将要使用的组件的样式:

const styles = {
  container: {padding: 20},
  input: {marginBottom: 10},
  item: { textAlign: 'left' },
  p: { color: '#1890ff' }
}

现在我们准备运行应用程序!在终端中运行 start 命令:

~ npm start

当应用程序加载时,您应该看到当前笔记列表呈现在屏幕上,如图 3-4 所示。

笔记列表

图 3-4. 笔记列表

创建笔记(GraphQL 变更)

现在您知道如何查询笔记列表了,接下来让我们看看如何创建一个新的笔记。为此,您需要以下内容:

  1. 创建一个新笔记的表单

  2. 一个函数,当用户在表单中输入时更新状态

  3. 一个函数,将新笔记添加到 UI 并发送 API 调用以创建新笔记

首先,导入 UUID 库,以便为客户端创建唯一标识符。我们现在这样做是为了在以后实现订阅时能够识别创建笔记的客户端。我们还将从 Ant Design 导入 InputButton 组件:

import { v4 as uuid } from 'uuid'
import { List, Input, Button } from 'antd'

接下来,您需要导入 createNote 变更定义:

import { createNote as CreateNote } from './graphql/mutations'

然后,在最后一个导入下面创建一个新的 CLIENT_ID 变量:

const CLIENT_ID = uuid()

更新 reducer 中的 switch 语句以添加三个新情况。我们将需要新情况来处理以下三个操作:

  1. 将新笔记添加到本地状态

  2. 重置表单状态以清除表单内容

  3. 当用户输入时更新表单状态

case 'ADD_NOTE':
  return { ...state, notes: [action.note, ...state.notes]}
case 'RESET_FORM':
  return { ...state, form: initialState.form }
case 'SET_INPUT':
  return { ...state, form: { ...state.form, [action.name]: action.value } }

接下来,在主 App 函数中创建 createNote 函数:

async function createNote() {
  const { form } = state
  if (!form.name || !form.description) {
     return alert('please enter a name and description')
  }
  const note = { ...form, clientId: CLIENT_ID, completed: false, id: uuid() }
  dispatch({ type: 'ADD_NOTE', note })
  dispatch({ type: 'RESET_FORM' })
  try {
    await API.graphql({
      query: CreateNote,
      variables: { input: note }
    })
    console.log('successfully created note!')
  } catch (err) {
    console.log("error: ", err)
  }
}

在此函数中,在 API 调用成功之前更新本地状态。这称为乐观响应。这样做是因为我们希望 UI 反应迅速,并在用户添加新笔记后立即更新。如果 API 调用失败,您可以在 catch 块中实现一些功能来通知用户错误。

现在,在主 App 函数中创建一个 onChange 处理程序,以在用户与输入交互时更新表单状态:

function onChange(e) {
  dispatch({ type: 'SET_INPUT', name: e.target.name, value: e.target.value })
}

最后,我们将更新 UI 添加表单组件。在 List 组件之前,添加以下两个输入和一个按钮:

<Input
  onChange={onChange}
  value={state.form.name}
  placeholder="Note Name"
  name='name'
  style={styles.input}
/>
<Input
  onChange={onChange}
  value={state.form.description}
  placeholder="Note description"
  name='description'
  style={styles.input}
/>
<Button
  onClick={createNote}
  type="primary"
>Create Note</Button>

现在,我们应该能够使用表单创建新的笔记,如图 3-5 所示。

创建笔记

图 3-5. 创建笔记

删除笔记(GraphQL Mutation)

接下来,让我们看看如何删除笔记。要做到这一点,我们将需要以下内容:

  1. 一个 deleteNote 函数,从 UI 和 GraphQL API 中删除笔记

  2. 每个笔记中的一个按钮来调用 deleteNote 函数

首先,导入 deleteNote mutation:

import {
  createNote as CreateNote,
  deleteNote as DeleteNote
} from './graphql/mutations'

接着,在主 App 函数中创建一个 deleteNote 函数:

async function deleteNote({ id }) {
  const index = state.notes.findIndex(n => n.id === id)
  const notes = [
    ...state.notes.slice(0, index),
    ...state.notes.slice(index + 1)];
  dispatch({ type: 'SET_NOTES', notes })
  try {
    await API.graphql({
      query: DeleteNote,
      variables: { input: { id } }
    })
    console.log('successfully deleted note!')
    } catch (err) {
      console.log({ err })
  }
}

在这个函数中,我们正在查找笔记的索引,并创建一个新的不包含已删除笔记的笔记数组。然后,我们分发 SET_NOTES 动作,传入新的笔记数组来更新本地状态并显示一个乐观的响应。接下来,我们调用 GraphQL API 来删除 AppSync API 中的笔记。

现在,在 renderItem 函数中更新 List.Item 组件,以添加一个删除按钮到 actions 属性,该按钮将调用 deleteNote 函数,传入该项:

<List.Item
  style={styles.item}
  actions={[
    <p style={styles.p} onClick={() => deleteNote(item)}>Delete</p>
  ]}
>
  <List.Item.Meta
   title={item.name}
   description={item.description}
  />
</List.Item>

现在,我们应该能够删除笔记(见 图 3-6)。

删除笔记

图 3-6. 删除笔记

更新笔记(GraphQL Mutation)

我们接下来想要添加的下一个功能是能够更新笔记为已完成。要做到这一点,您将需要以下内容:

  1. 一个 updateNote 函数,在 UI 和 GraphQL API 中更新笔记

  2. 每个笔记中的一个按钮来调用 updateNote 函数

首先,导入 updateNote mutation:

import {
  updateNote as UpdateNote,
  createNote as CreateNote,
  deleteNote as DeleteNote
} from './graphql/mutations'

接下来,在主 App 函数中创建一个 updateNote 函数:

async function updateNote(note) {
  const index = state.notes.findIndex(n => n.id === note.id)
  const notes = [...state.notes]
  notes[index].completed = !note.completed
  dispatch({ type: 'SET_NOTES', notes})
  try {
    await API.graphql({
      query: UpdateNote,
      variables: { input: { id: note.id, completed: notes[index].completed } }
    })
    console.log('note successfully updated!')
  } catch (err) {
    console.log('error: ', err)
  }
}

在这个函数中,我们首先找到所选笔记的索引,然后创建一个笔记数组的副本。然后,我们将所选笔记的完成值更新为当前值的相反值。然后,我们用新版本的笔记更新笔记数组,设置本地状态中的笔记数组,并调用 GraphQL API,传入需要在 API 中更新的笔记。

最后,更新 List.Item 组件以添加一个更新按钮,调用 updateNote 函数,传入该项。该组件将根据项的 completed 布尔值(根据 completed 是 true 还是 false)渲染为 completedmark complete

<List.Item
  style={styles.item}
  actions={[
    <p style={styles.p} onClick={() => deleteNote(item)}>Delete</p>,
    <p style={styles.p} onClick={() => updateNote(item)}>
      {item.completed ? 'completed' : 'mark completed'}
    </p>
  ]}
>

现在,我们应该能够更新笔记为已完成未完成(见 图 3-7)。

更新笔记

图 3-7. 更新笔记

实时数据(GraphQL 订阅)

我们将要实现的最后一个功能是能够实时订阅更新。我们希望订阅的更新是当新的笔记被添加时。当这种情况发生时,我们希望实现的功能是让我们的应用接收到新的笔记,用新的笔记更新笔记数组,并将更新后的笔记数组渲染到我们的屏幕上。

为此,您将实现一个 GraphQL 订阅。使用 GraphQL 订阅,您可以订阅不同的事件。这些事件通常是某种类型的变更操作(创建、更新、删除)。当其中一个事件发生时,事件数据将发送到初始化订阅的客户端。然后您需要处理客户端收到的数据。

要使其工作,您只需要在useEffect钩子中初始化订阅,并在订阅触发时与笔记数据一起调度ADD_NOTE类型。

首先,导入onCreateNote订阅:

import { onCreateNote } from './graphql/subscriptions'

接下来,使用以下代码更新useEffect钩子:

useEffect(() => {
  fetchNotes()
  const subscription = API.graphql({
    query: onCreateNote
  })
    .subscribe({
      next: noteData => {
        const note = noteData.value.data.onCreateNote
        if (CLIENT_ID === note.clientId) return
        dispatch({ type: 'ADD_NOTE', note })
      }
    })
    return () => subscription.unsubscribe()
}, [])

在这个订阅中,我们订阅了onCreateNote事件。当创建新的笔记时,此事件将被触发,并调用next函数,将note数据作为参数传入。

我们获取笔记数据,并检查我们的客户端是否是创建笔记的应用程序。如果我们的客户端创建了笔记,则直接返回而不进行进一步操作。如果我们不是创建笔记的客户端,则会分发ADD_NOTE动作,并传入订阅中的笔记数据。

摘要

祝贺你,你已经部署了你的第一个无服务器 GraphQL 应用程序!

这一章节需要记住的几个要点:

  • useEffect钩子类似于 React 生命周期方法中的componentDidMount,在组件首次渲染后运行。

  • useReducer钩子允许您管理应用程序状态,并且在具有更复杂应用逻辑时优于useState

  • GraphQL 查询 用于在 GraphQL API 中获取数据。

  • GraphQL 变更 用于在 GraphQL API 中创建、更新或删除数据。

  • 您可以通过使用 GraphQL 订阅 在 GraphQL API 中订阅 API 实时事件。

第四章:认证简介

认证身份几乎是任何应用程序的重要组成部分。了解用户是谁、他们拥有哪些权限、是否已登录以及为当前已登录用户渲染正确视图和返回正确数据提供唯一标识符,这使得您的应用程序能够处理这些信息。

大多数应用程序需要机制来处理用户注册、用户登录、密码加密和更新,以及围绕身份管理的无数其他任务。现代应用程序通常需要像开放认证(OAUTH)、多因素认证(MFA)和基于时间的一次性密码(TOTP)等功能。

在过去,开发人员必须从头开始手动编写所有这些认证功能。单单完成这项任务可能需要一个开发团队数周,甚至数月的时间,并且必须确保安全性。今天有完全托管的认证服务,如 Auth0、Okta 和 Amazon Cognito,它们为我们处理了所有这些工作。

在本章中,您将学习如何在 React 应用程序中使用 Amazon Cognito 和 AWS Amplify 正确且安全地实现认证。

你将要构建的应用程序是一个基本应用程序,需要认证才能查看,并且还有一个显示登录用户个人资料信息的个人资料页面。如果用户已登录,则可以在公共路由、带有认证表单的个人资料路由以及仅对已登录用户可见的受保护路由之间导航。

如果用户未登录,则只能查看个人资料路由上的公共路由和认证表单。如果用户尝试在未登录时访问受保护路由,则我们希望将其重定向到认证表单,以便他们可以登录,然后在认证后访问路由。

该应用程序还将持续保留用户状态,因此如果他们刷新应用程序或离开并回到应用程序,他们将保持登录状态。

介绍 Amazon Cognito

Amazon Cognito 是 AWS 提供的一个完全托管的身份服务。Cognito 支持简单和安全的用户注册、登录、访问控制用户身份管理。Cognito 可以扩展到数百万用户,并支持使用社交身份提供者(如 Facebook、Google 和 Amazon)进行登录。对于任何应用程序的前 50000 个用户,它还是免费的。

Amazon Cognito 的工作原理

Cognito 主要由用户池身份池两个组成部分:

用户池

这些提供了一个安全的用户目录,存储所有用户,并能够扩展到数亿用户。它是一个完全托管的服务。作为无服务器技术,用户池易于设置,无需担心搭建任何基础设施。用户池负责管理所有注册并登录账户的用户,这也是我们本章将重点关注的内容。

身份池

这些允许您授权已登录到应用程序的用户访问各种其他 AWS 服务。假设您希望允许用户访问 Lambda 函数以从另一个 API 获取数据;在创建身份池时,您可以指定这一点。用户池的作用在于,这些身份的来源可以是 Cognito 用户池,甚至是 Facebook 或 Google。

Cognito 用户池允许您的应用程序调用各种方法来管理用户身份的所有方面,包括以下项目:

  • 注册用户

  • 登录用户

  • 登出用户

  • 更改用户密码

  • 重置用户密码

  • 确认 MFA 验证码

Amazon Cognito 与 AWS Amplify 集成

AWS Amplify 以多种方式支持 Amazon Cognito。首先,您可以直接从 Amplify CLI 创建和配置 Amazon Cognito 服务。一旦通过 CLI 创建了认证服务,您可以使用 Amplify JavaScript 客户端库从 JavaScript 应用程序中调用各种方法(如signUpsignInsignOut)。

Amplify 还具有预配置的 UI 组件,可以让您仅需几行代码即可快速创建整个认证流程,适用于 React、React Native、Vue 和 Angular 等框架。

在本章中,您将使用 Amplify CLI、Amplify JavaScript 客户端和 Amplify React UI 组件的组合来构建一个演示路由认证受保护路由的应用程序。您还将使用 React Router 进行路由,并使用 Ant Design 为应用程序添加基本样式(参见图 4-1)。

带有路由和认证的 React

图 4-1. 带有路由和认证的 React

创建 React 应用程序并添加 Amplify

您要做的第一件事是创建 React 应用程序,安装必要的依赖项,并创建 Amplify 项目。

要开始,请打开终端并创建一个新的 React 应用程序:

~ npx create-react-app basic-authentication
~ cd basic-authentication

然后安装 AWS Amplify、AWS Amplify React、React Router 和 Ant Design 库:

~ npm install aws-amplify @aws-amplify/ui-react antd react-router-dom

初始化一个新的 Amplify 项目:

~ amplify init

# Follow the steps to give the project a name, environment name, and set
  the default text editor.
# Accept defaults for everything else and choose your AWS Profile.

初始化了 Amplify 项目后,我们可以创建认证服务。要做到这一点,请运行以下命令:

~ amplify add auth

? Do you want to use the default authentication and security
  configuration? Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings? No, I am done.

现在认证服务已配置完成,您可以使用amplify push命令部署它:

~ amplify push

? Are you sure you want to continue? Yes

认证服务已部署,现在让我们开始测试它。

客户端身份验证概述

使用 Amplify,在客户端实现身份验证的两种主要方式如下:

Auth

Amplify 客户端库公开了一个Auth类,拥有超过 30 种不同的方法,允许您处理与用户管理相关的所有事务。一些可用方法的示例包括Auth.signUpAuth.signInAuth.signOut

使用此类,您可以根据应用程序的需求创建完全自定义的身份验证流程。为此,您必须自行管理所有的样式和应用程序状态。

特定于框架的身份验证组件

Amplify 中特定于框架的库为 React、React Native、Vue 和 Angular 暴露了用于管理身份验证的更高级抽象。这些组件将使用几行代码呈现整个(可定制的)身份验证流程。

在第一章中,您有机会尝试 AWS Amplify React 库中名为withAuthenticator的高阶组件(HOC)。在这里,您将使用此 HOC 以及路由来创建受保护的路由和仅在用户登录后才能查看的个人资料视图。

构建应用程序

下一步将是继续为应用程序创建文件和文件夹结构。

创建文件和文件夹结构

在您的应用程序中,在src目录中创建以下文件:

Container.js
Nav.js
Profile.js
Protected.js
Public.js
Router.js

这些文件执行以下操作:

Container.js

此文件将包含一个组件,您将使用它来为其他组件应用可重用的样式。

Nav.js

在这个组件中,您将创建一个导航界面。

Profile.js

此组件将呈现已登录用户的个人资料信息。这也是您将添加用于注册和登录的身份验证组件的组件。

Protected.js

这是我们将用作创建受保护路由示例的组件。如果用户已登录,则他们将能够查看此路由。如果他们未登录,则将被重定向到登录表单。

Public.js

这是一个基本路由,无论用户是否已登录,都可以查看。

Router.js

此文件将包含路由器和一些逻辑以确定当前路由名称。

现在这些文件已经创建好,您已经可以开始编写一些代码了。

创建第一个组件

首先,让我们创建用于应用程序的最简单的组件——Container组件。此组件将用于包装所有其他组件,以便我们可以在组件之间应用一些可重用的样式:

/* src/Container.js */
import React from 'react'

const Container = ({ children }) => (
  <div style={styles.container}>
    { children }
  </div>
)

const styles = {
  container: {
    margin: '0 auto',
    padding: '50px 100px'
  }
}

export default Container

使用此组件,您现在可以在整个应用程序中应用一致的样式,而无需重写样式。然后可以像这样使用它:

<Container>
  <h1>Hello World</h1>
</Container>

任何作为Container组件的子组件的内容都将使用Container组件中设置的样式进行渲染。这样做可以让您在一个地方控制样式。如果以后想要进行样式更改,只需调整一个组件即可。

公共组件

此组件简单地将路由名称渲染到 UI 中,并且无论用户是否已登录,都可以访问。在此组件中,您将使用Container组件添加一些填充和边距:

/* src/Public.js */
import React from 'react'
import Container from './Container'

function Public() {
  return (
    <Container>
      <h1>Public route</h1>
    </Container>
  )
}

export default Public

导航组件

Nav(导航)组件将利用 Ant Design 库和 React Router。Ant Design 将提供MenuIcon组件以创建一个漂亮的菜单,而 React Router 将提供Link组件,以便我们可以链接并导航到应用程序的不同部分。

你还会注意到有一个传递给组件的current属性。该属性表示当前路由的名称。对于这个应用程序,该值将是homeprofileprotected中的一个。current值将在Router组件中计算并作为属性传递给这个组件的Menu组件的selectedKeys数组中,用于突出显示导航栏中的当前路由。

/* src/Nav.js */
import React from 'react'
import { Link } from 'react-router-dom'
import { Menu } from 'antd'
import { HomeOutlined, ProfileOutlined, FileProtectOutlined } from
         '@ant-design/icons'

const Nav = (props) => {
  const { current } = props
  return (
    <div>
      <Menu selectedKeys={[current]} mode="horizontal">
        <Menu.Item key='home'>
          <Link to={`/`}>
            <HomeOutlined />Home
          </Link>
        </Menu.Item>
        <Menu.Item key='profile'>
          <Link to='/profile'>
          <ProfileOutlined />Profile
          </Link>
        </Menu.Item>
        <Menu.Item key='protected'>
          <Link to='/protected'>
            <FileProtectOutlined />Protected
          </Link>
        </Menu.Item>
      </Menu>
    </div>
  )
}

export default Nav

Protected 组件

Protected组件将是受保护或私有的路由。如果尝试访问此路由的用户已登录,则他们将能够查看此路由。如果他们未登录,则将重定向到配置文件页面以注册或登录。

在这个组件中,你将会使用来自 React 的useEffect钩子和来自 AWS Amplify 的Auth类:

useEffect

这是一个 React 钩子,允许你在函数组件中执行副作用。该钩子接受一个函数,在函数第一次渲染时调用,可选择在每次额外渲染时再次调用。通过将空数组作为第二个参数传入,我们选择仅在组件加载时触发该函数。如果你曾在 React 类中使用过componentDidMountuseEffect具有类似的特性和用法。

Auth

这个 AWS Amplify 类处理用户身份管理。你可以使用这个类来完成从用户注册和登录到重置密码等所有操作。在这个组件中,我们将调用一个方法,Auth.currentAuthenticatedUser,来检查用户当前是否已登录,如果是,则返回有关已登录用户的数据。

/* src/Protected.js */
import React, { useEffect } from 'react';
import { Auth } from 'aws-amplify'
import Container from './Container'

function Protected(props) {
  useEffect(() => {
    Auth.currentAuthenticatedUser()
      .catch(() => {
        props.history.push('/profile')
      })
  }, [])
  return (
    <Container>
      <h1>Protected route</h1>
    </Container>
  );
}

export default Protected

当组件渲染时,我们通过在useEffect钩子中调用Auth.currentAuthenticatedUser来检查用户是否已登录应用程序。如果此 API 调用不成功,则意味着用户未登录,我们需要重定向他们。我们通过调用props.history.push('/profile')来重定向他们。

如果用户已登录,则我们不采取任何操作,并允许他们查看路由。

Router 组件

Router组件将定义我们在应用程序中希望使用的组件和路由。

这个组件还将设置当前路由名称,该名称将根据window.location.href属性在Nav组件中使用,以突出显示当前路由。

你将从 React Router 中使用的组件是HashRouterSwitchRoute

HashRouter

这是一个路由器,它使用 URL 的哈希部分(即window.location.hash)来保持 UI 与 URL 的同步。

Switch

Switch渲染与位置匹配的第一个子路由。这与仅使用路由器的默认功能不同,后者可能会渲染与位置匹配的多个路由。

Route

此组件允许您根据路径参数定义要渲染的组件:

/* src/Router.js */
import React, { useState, useEffect } from 'react'
import { HashRouter, Switch, Route } from 'react-router-dom'

import Nav from './Nav'
import Public from './Public'
import Profile from './Profile'
import Protected from './Protected'

const Router = () => {
  const [current, setCurrent] = useState('home')
  useEffect(() => {
    setRoute()
    window.addEventListener('hashchange', setRoute)
    return () =>  window.removeEventListener('hashchange', setRoute)
  }, [])
  function setRoute() {
    const location = window.location.href.split('/')
    const pathname = location[location.length-1]
    setCurrent(pathname ? pathname : 'home')
  }
  return (
    <HashRouter>
      <Nav current={current} />
      <Switch>
        <Route exact path="/" component={Public}/>
        <Route exact path="/protected" component={Protected} />
        <Route exact path="/profile" component={Profile}/>
        <Route component={Public}/>
      </Switch>
    </HashRouter>
  )
}

export default Router

在此组件的useEffect钩子内部,我们通过调用setRoute来设置路由名称。我们还设置了一个事件侦听器,以便在路由更改时调用setRoute

在声明Route组件时,您可以将要渲染的组件作为component属性传递。

配置文件组件

完成应用程序的最后一个组件是Profile组件。此组件将执行以下几项任务:

  • 如果用户未登录,则呈现身份验证表单。

  • 提供注销按钮。

  • 将用户的配置文件信息呈现到用户界面。

就像在第一章中一样,我们使用withAuthenticator高阶组件来通过包装默认导出的Profile组件来呈现认证流程。如果用户未登录,此流程将显示注册/登录表单;如果用户已登录,将显示带有用户配置文件详细信息的 UI。

要注销用户,我们使用AmplifySignOut UI 组件。此组件将注销用户并重新呈现 UI 以显示身份验证表单。

要显示用户配置文件数据,我们使用Auth.currentAuthenticatedUser方法。如果用户已登录,此方法将返回用户配置文件数据以及有关会话的信息。我们感兴趣的配置文件信息包括用户名和用户属性,例如电话号码、电子邮件以及用户注册时收集的其他信息:

/* src/Profile.js */
import React, { useState, useEffect } from 'react'
import { Auth } from 'aws-amplify'
import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react'
import Container from './Container'

function Profile() {
  useEffect(() => {
    checkUser()
  }, [])
  const [user, setUser] = useState({})
  async function checkUser() {
    try {
      const data = await Auth.currentUserPoolUser()
      const userInfo = { username: data.username, ...data.attributes, }
      setUser(userInfo)
    } catch (err) { console.log('error: ', err) }
  }
  return (
    <Container>
      <h1>Profile</h1>
      <h2>Username: {user.username}</h2>
      <h3>Email: {user.email}</h3>
      <h4>Phone: {user.phone_number}</h4>
      <AmplifySignOut />
    </Container>
  );
}

export default withAuthenticator(Profile)

样式化 UI 组件

在幕后,Amplify UI 组件是使用 Web 组件实现的。这意味着我们可以将它们作为一流的 HTML 元素进行 CSS 样式设置。我们希望我们的 UI 组件与应用程序中其余部分的蓝色匹配。为此,我们可以将以下 CSS 属性添加到index.css底部来定义我们想要使用的颜色:

/* src/index.css */

:root {
  --amplify-primary-color: #1890ff;
  --amplify-primary-tint: #1890ff;
  --amplify-primary-shade: #1890ff;
}

配置应用程序

现在应用程序已构建完成。我们唯一需要做的是更新index.js来导入路由器并添加 Amplify 配置。我们还要导入 Ant Design 库的必要 CSS:

/* src/index.js */
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Router from './Router';
import 'antd/dist/antd.css';

import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)

ReactDOM.render(<Router />, document.getElementById('root'));

测试应用程序

要测试应用程序,我们现在可以运行start命令:

~ npm start

概述

恭喜,您已经使用路由和受保护路由构建了认证流程!

从本章中需要记住的一些事项如下:

  • 使用withAuthenticator高阶组件快速启动和运行预配置的认证流程。

  • 使用Auth类更精细地控制身份验证并获取有关当前已登录用户的数据。

  • Ant Design 帮助您使用预配置的设计快速启动,而无需编写任何特定于样式的代码。

第五章:自定义认证策略

在本章中,我们将构建并改进我们在第四章中完成的应用程序,您将在该章中学习如何使用withAuthenticator高阶组件来创建预配置的认证表单。您还将学习如何使用 React Router 和Auth类根据用户的登录状态创建公共和受保护的路由。

虽然这为我们可以使用 Amplify 和关于认证和路由的基础奠定了基础,但我们希望进一步进行一步,构建一个完全定制的认证流程,以便我们完全了解底层发生的事情并理解管理自定义认证表单所需的逻辑和状态。这意味着我们需要更新我们的应用程序,以便为注册、登录和重置密码创建自定义表单,而不是使用withAuthenticator高阶组件。

我们还将通过创建一个可以重复使用的钩子来进一步实现受保护路由的概念,以包装我们想要保护的任何组件(而不是在每个组件中重写逻辑)。

Auth类拥有超过 30 种不同的方法,非常强大,并允许您处理大多数应用程序所需的所有认证逻辑。到本章结束时,您将了解如何使用Auth类和 React 状态来构建和管理自定义认证表单。

创建受保护的路由钩子

首先要做的是创建自定义的protectedRoute钩子,我们将使用它来保护需要身份验证的任何路由。此钩子将检查已登录用户的信息,如果用户未登录,将重定向到登录页面或其他指定的路由。如果用户已登录,则钩子将返回并呈现传入的组件作为参数。通过使用此钩子,我们可以消除跨多个组件可能需要的任何重复身份验证逻辑。

src目录中,创建一个名为protectedRoute.js的新文件,并添加以下代码:

import React, { useEffect } from 'react'
import { Auth } from 'aws-amplify'

const protectedRoute = (Comp, route = '/profile') => (props) => {
  async function checkAuthState() {
    try {
      await Auth.currentAuthenticatedUser()
    } catch (err) {
      props.history.push(route)
    }
  }
  useEffect(() => {
    checkAuthState()
  }, [])
  return <Comp {...props} />
}

export default protectedRoute

此组件在加载时使用useEffect钩子来检查用户是否已登录。如果用户已登录,则不会发生任何操作,并且会呈现传入的组件作为参数。如果用户未登录,则进行重定向。

可以将重定向路由作为第二个参数传递给钩子,或者如果没有传入重定向路由,则将默认设置为/profile。现在,我们可以使用此钩子来保护任何组件,如下所示:

// Default redirect route
export default protectedRoute(App)

// Custom redirect route
export default protectedRoute(App, '/about-us')

现在已经创建了受保护的路由钩子,我们可以开始重构我们的应用程序。我们可能希望的下一步是更新我们应用程序中的Protected组件,以使用这个新的protectedRoute钩子。为此,请打开Protected.js并使用以下代码更新组件:

import React from 'react';
import Container from './Container'
import protectedRoute from './protectedRoute'

function Protected() {
  return (
    <Container>
      <h1>Protected route</h1>
    </Container>
  );
}

export default protectedRoute(Protected)

现在此组件受到保护,如果未经过身份验证的用户尝试访问它,将继续进行重定向。

创建表单

接下来我们要做的是创建主要的Form组件。该组件将包含以下操作的所有逻辑和 UI:

  • 注册

  • 确认注册

  • 登录

  • 重置密码

在第四章中,我们使用了 withAuthenticator 组件,该组件为我们封装了大部分这些逻辑,但现在我们将从头开始重写我们自己的版本。了解如何创建和处理自定义表单非常重要,因为您可能会使用自定义设计和业务逻辑,这可能与 withAuthenticator 组件等抽象不兼容。

首先,我们将创建我们需要的新组件文件。在 src 目录中创建以下文件:

Button.js
Form.js
SignUp.js
ConfirmSignUp.js
SignIn.js
ForgotPassword.js
ForgotPasswordSubmit.js

现在您已经创建了这些组件,让我们继续创建一个可重用的按钮,该按钮将在所有表单中用作提交按钮。在 Button.js 中,添加以下代码:

import React from 'react'

export default function Button({ onClick, title }) {
  return (
    <button style={styles.button} onClick={onClick}>
      {title}
    </button>
  )
}

const styles = {
  button: {
    backgroundColor: '#006bfc',
    color: 'white',
    width: 316,
    height: 45,
    fontWeight: '600',
    fontSize: 14,
    cursor: 'pointer',
    border:'none',
    outline: 'none',
    borderRadius: 3,
    marginTop: '25px',
    boxShadow: '0px 1px 3px rgba(0, 0, 0, .3)',
  },
}

Button 组件是一个基本组件,接受两个 props:titleonClickonClick 处理程序将调用与按钮关联的函数,而 title 组件将渲染按钮的文本。

接下来,打开 Form.js 并添加以下代码:

/* src/Form.js */
import React, { useState } from 'react'
import { Auth } from 'aws-amplify'
import SignIn from './SignIn'
import SignUp from './SignUp'
import ConfirmSignUp from './ConfirmSignUp'
import ForgotPassword from './ForgotPassword'
import ForgotPasswordSubmit from './ForgotPasswordSubmit'

const initialFormState = {
  username: '', password: '', email: '', confirmationCode: ''
}

function Form(props) {
  const [formType, updateFormType] = useState('signIn')
  const [formState, updateFormState] = useState(initialFormState)
  function renderForm() {}
  return (
    <div>
      {renderForm()}
    </div>
  )
}

在这里,我们已经导入了将要编写的单独表单组件,并创建了一些初始的表单状态。我们将在表单状态中跟踪的项目是认证流程中的输入字段(usernamepasswordemailconfirmationCode)。

还有一个组件状态的部分,它跟踪要呈现的表单类型:formType。因为表单组件将在一个路由中全部显示,所以我们需要检查当前的表单状态,然后呈现注册表单、登录表单或重置密码表单。

updateFormType 将是切换不同表单类型的函数。例如,一旦用户成功注册,我们将调用 updateFormType('signIn') 来渲染 SignIn 组件,以便他们可以进行登录。

renderForm 函数稍后将通过一些自定义逻辑进行更新,但目前什么也不做。

然后,在 Form.js 中添加以下样式和默认导出。一些元素的样式将在多个组件之间共享,因此我们将导出组件以及样式:

const styles = {
  container: {
    display: 'flex',
    flexDirection: 'column',
    marginTop: 150,
    justifyContent: 'center',
    alignItems: 'center'
  },
  input: {
    height: 45,
    marginTop: 8,
    width: 300,
    maxWidth: 300,
    padding: '0px 8px',
    fontSize: 16,
    outline: 'none',
    border: 'none',
    borderBottom: '2px solid rgba(0, 0, 0, .3)'
  },
  toggleForm: {
    fontWeight: '600',
    padding: '0px 25px',
    marginTop: '15px',
    marginBottom: 0,
    textAlign: 'center',
    color: 'rgba(0, 0, 0, 0.6)'
  },
  resetPassword: {
    marginTop: '5px',
  },
  anchor: {
    color: '#006bfc',
    cursor: 'pointer'
  }
}

export { styles, Form as default }

接下来,让我们继续创建各个表单组件。

SignIn 组件

SignIn 组件将呈现登录表单。该组件将接受两个 props,一个用于更新表单状态 (updateFormState),另一个用于调用 signIn 函数:

/* src/SignIn.js */
import React from 'react'
import Button from './Button'
import { styles } from './Form'

function SignIn({ signIn, updateFormState }) {
  return (
    <div style={styles.container}>
      <input
        name='username'
        onChange={e => {e.persist();updateFormState(e)}}
        style={styles.input}
        placeholder='username'
      />
      <input
        type='password'
        name='password'
        onChange={e => {e.persist();updateFormState(e)}}
        style={styles.input}
        placeholder='password'
      />
      <Button onClick={signIn} title="Sign In" />
    </div>
  )
}

export default SignIn

注册组件

SignUp 组件将呈现注册表单。该组件将接受两个 props,一个用于更新表单状态 (updateFormState),另一个用于调用 signUp 函数:

/* src/SignUp.js */
import React from 'react'
import Button from './Button'
import { styles } from './Form'

function SignUp({ updateFormState, signUp }) {
  return (
    <div style={styles.container}>
      <input
        name='username'
        onChange={e => {e.persist();updateFormState(e)}}
        style={styles.input}
        placeholder='username'
      />
      <input
        type='password'
        name='password'
        onChange={e => {e.persist();updateFormState(e)}}
        style={styles.input}
        placeholder='password'
      />
      <input
        name='email'
        onChange={e => {e.persist();updateFormState(e)}}
        style={styles.input}
        placeholder='email'
      />
      <Button onClick={signUp} title="Sign Up" />
    </div>
  )
}

export default SignUp

ConfirmSignUp 组件

用户注册后,他们将收到用于 MFA 的确认代码。ConfirmSignUp 组件包含将处理和提交此 MFA 代码的表单。

此组件将接受两个 props(在 React 中,props 意味着“属性”,用于在组件之间传递数据),一个用于更新表单状态 (updateFormState),另一个用于调用 confirmSignUp 函数:

/* src/ConfirmSignUp.js */
import React from 'react'
import Button from './Button'
import { styles } from './Form'

function ConfirmSignUp(props) {
  return (
    <div style={styles.container}>
      <input
        name='confirmationCode'
        placeholder='Confirmation Code'
        onChange={e => {e.persist();props.updateFormState(e)}}
        style={styles.input}
      />
      <Button onClick={props.confirmSignUp} title="Confirm Sign Up" />
    </div>
  )
}

export default ConfirmSignUp

接下来的两个表单将用于处理忘记密码的重置。第一个表单 (ForgotPassword) 将接受用户的用户名作为输入并发送确认码。然后用户可以使用该确认码和新密码在第二个表单 (ForgotPasswordSubmit) 中重置密码。

忘记密码组件

ForgotPassword 组件将接受两个 props,一个用于更新表单状态 (updateFormState),另一个用于调用 forgotPassword 函数:

/* src/ForgotPassword.js */
import React from 'react'
import Button from './Button'
import { styles } from './Form'

function ForgotPassword(props) {
  return (
    <div style={styles.container}>
      <input
        name='username'
        placeholder='Username'
        onChange={e => {e.persist();props.updateFormState(e)}}
        style={styles.input}
      />
      <Button onClick={props.forgotPassword} title="Reset password" />
    </div>
  )
}

export default ForgotPassword

忘记密码提交组件

ForgotPasswordSubmit 组件将接受两个 props,一个用于更新表单状态 (updateFormState),另一个用于调用 forgotPassword 函数:

/* src/ForgotPasswordSubmit.js */
import React from 'react'
import Button from './Button'
import { styles } from './Form'

function ForgotPasswordSubmit(props) {
  return (
    <div style={styles.container}>
      <input
        name='confirmationCode'
        placeholder='Confirmation code'
        onChange={e => {e.persist();props.updateFormState(e)}}
        style={styles.input}
      />
      <input
        name='password'
        placeholder='New password'
        type='password'
        onChange={e => {e.persist();props.updateFormState(e)}}
        style={styles.input}
      />
      <Button onClick={props.forgotPasswordSubmit} title="Save new password" />
    </div>
  )
}

export default ForgotPasswordSubmit

完成 Form.js

现在所有的单独表单组件都已创建,我们可以更新 Form.js 来使用这些新组件。

接下来要做的是打开 Form.js 并创建将与身份验证服务交互的函数。这些函数——signInsignUpconfirmSignUpforgotPasswordforgotPasswordSubmit——将作为 props 传递给各个表单组件。

在最后一个 import 下面,添加以下代码:

/* src/Form.js */
async function signIn({ username, password }, setUser) {
  try {
    const user = await Auth.signIn(username, password)
    const userInfo = { username: user.username, ...user.attributes }
    setUser(userInfo)
  } catch (err) {
    console.log('error signing up..', err)
  }
}

async function signUp({ username, password, email }, updateFormType) {
  try {
    await Auth.signUp({
      username, password, attributes: { email }
    })
    console.log('sign up success!')
    updateFormType('confirmSignUp')
  } catch (err) {
    console.log('error signing up..', err)
  }
}

async function confirmSignUp({ username, confirmationCode }, updateFormType) {
  try {
    await Auth.confirmSignUp(username, confirmationCode)
    updateFormType('signIn')
  } catch (err) {
    console.log('error signing up..', err)
  }
}

async function forgotPassword({ username }, updateFormType) {
  try {
    await Auth.forgotPassword(username)
    updateFormType('forgotPasswordSubmit')
  } catch (err) {
    console.log('error submitting username to reset password...', err)
  }
}

async function forgotPasswordSubmit(
    { username, confirmationCode, password }, updateFormType
  ) {
  try {
    await Auth.forgotPasswordSubmit(username, confirmationCode, password)
    updateFormType('signIn')
  } catch (err) {
    console.log('error updating password... :', err)
  }
}

signUpconfirmSignUpforgotPasswordforgotPasswordSubmit 函数将接受相同的参数,即表单状态对象和 updateFormType 函数,以更新显示的表单类型。

signIn 函数与其他函数不同,它接受一个 setUser 函数作为参数。这个 setUser 函数将从 Profile 组件作为 prop 传递给 Form 组件。这个 setUser 函数允许我们重新渲染 Profile 组件,以便在用户成功登录后显示或隐藏表单。

在 第四章 中,Profile.js 组件使用 withAuthenticator 组件来渲染表单,因此我们无需自己渲染适当的 UI。现在我们正在处理自己的表单状态,我们需要根据用户是否已验证决定是渲染 Profile 组件还是 Form 组件。

你会注意到在这些函数中,我们使用了 AWS Amplify 的 Auth 类上的不同方法。这些方法与我们创建的函数命名相对应,以便我们确切地知道每个函数在做什么。

updateForm 辅助函数

接下来,让我们为更新表单状态创建一个 辅助函数。我们在 Form.js 中创建的初始表单状态变量如下:

const initialFormState = {
  username: '', password: '', email: '', confirmationCode: ''
}

此状态是一个对象,包含我们将要使用的每个表单的值。

然后,我们使用 initialFormState 变量来创建组件状态(以及用于更新组件状态的函数),使用 useState 钩子:

const [formState, updateFormState] = useState(initialFormState)

我们现在遇到的问题是updateFormState期望一个包含所有这些字段的新对象来更新表单状态,但表单处理程序只会给我们正在输入的单个表单事件。我们如何将这个输入事件转换为状态的新对象呢?我们将通过创建一个辅助函数来做到这一点,我们将在Form函数内部使用这个函数。

Form.js中,在useState钩子的下方和Form函数内部添加以下代码:

function updateForm(event) {
  const newFormState = {
    ...formState, [event.target.name]: event.target.value
  }
  updateFormState(newFormState)
}

updateForm函数将使用现有状态以及来自事件的新值创建一个新的state对象,然后使用这个新的表单对象调用updateFormState。我们随后可以在所有组件中重复使用这个函数。

renderForm 函数

现在我们已经创建了所有的表单组件、设置了表单状态,并创建了认证功能,让我们更新renderForm函数以渲染当前的表单。在Form.js中,更新renderForm函数以使用以下代码:

function renderForm() {
  switch(formType) {
    case 'signUp':
      return (
        <SignUp
          signUp={() => signUp(formState, updateFormType)}
          updateFormState={e => updateForm(e)}
        />
      )
    case 'confirmSignUp':
      return (
        <ConfirmSignUp
          confirmSignUp={() => confirmSignUp(formState, updateFormType)}
          updateFormState={e => updateForm(e)}
        />
      )
    case 'signIn':
      return (
        <SignIn
          signIn={() => signIn(formState, props.setUser)}
          updateFormState={e => updateForm(e)}
        />
      )
    case 'forgotPassword':
      return (
        <ForgotPassword
        forgotPassword={() => forgotPassword(formState, updateFormType)}
        updateFormState={e => updateForm(e)}
        />
      )
    case 'forgotPasswordSubmit':
      return (
        <ForgotPasswordSubmit
          forgotPasswordSubmit={
            () => forgotPasswordSubmit(formState, updateFormType)}
          updateFormState={e => updateForm(e)}
        />
      )
    default:
      return null
  }
}

renderForm函数将检查在状态中设置的当前formType并渲染适当的表单。随着formType的更改,将调用renderForm并据此重新渲染正确的表单。

表单类型切换

在这个组件中,我们最后需要做的一件事是渲染按钮,允许我们手动在不同的表单状态之间切换。我们希望在登录、注册和忘记密码之间切换这三个主要的表单状态。

要做到这一点,让我们更新Form函数的返回语句,以便还返回一些按钮,允许用户切换表单类型:

return (
  <div>
    {renderForm()}
    {
      formType === 'signUp' && (
        <p style={styles.toggleForm}>
          Already have an account? <span
            style={styles.anchor}
            onClick={() => updateFormType('signIn')}
          >Sign In</span>
        </p>
      )
    }
    {
      formType === 'signIn' && (
        <>
          <p style={styles.toggleForm}>
            Need an account? <span
              style={styles.anchor}
              onClick={() => updateFormType('signUp')}
            >Sign Up</span>
          </p>
          <p style={{ ...styles.toggleForm, ...styles.resetPassword}}>
            Forget your password? <span
              style={styles.anchor}
              onClick={() => updateFormType('forgotPassword')}
            >Reset Password</span>
          </p>
        </>
      )
    }
  </div>
)

根据当前的表单类型,Form组件现在将显示不同的按钮,允许用户在登录、注册和重置密码之间切换。

更新 Profile 组件

现在我们需要更新Profile组件以使用新的Form组件。主要更改是根据当前是否有已登录用户来渲染Form组件或用户配置文件信息。

Amplify 有一个称为Hub的本地事件系统。Amplify 使用Hub来处理不同类别的事件,例如身份验证事件(如用户登录)或文件下载通知。

在此组件中,我们还将设置一个Hub监听器来监听signOut认证事件,以便我们可以从状态中移除用户,并重新渲染Profile组件以显示认证表单。

使用以下代码更新Profile.js

import React, { useState, useEffect } from 'react'
import { Button } from 'antd'
import { Auth, Hub } from 'aws-amplify'
import Container from './Container'
import Form from './Form'

function Profile() {
  useEffect(() => {
    checkUser()
    Hub.listen('auth', (data) => {
      const { payload } = data
      if (payload.event === 'signOut') {
        setUser(null)
      }
    })
  }, [])
  const [user, setUser] = useState(null)
  async function checkUser() {
    try {
      const data = await Auth.currentUserPoolUser()
      const userInfo = { username: data.username, ...data.attributes, }
      setUser(userInfo)
    } catch (err) { console.log('error: ', err) }
  }
  function signOut() {
    Auth.signOut()
      .catch(err => console.log('error signing out: ', err))
  }
  if (user) {
    return (
      <Container>
        <h1>Profile</h1>
        <h2>Username: {user.username}</h2>
        <h3>Email: {user.email}</h3>
        <h4>Phone: {user.phone_number}</h4>
        <Button onClick={signOut}>Sign Out</Button>
      </Container>
    );
  }
  return <Form setUser={setUser} />
}

export default Profile

在此组件中,我们检查是否存在用户,如果存在用户,则返回用户的配置文件信息。如果没有用户,则返回认证表单(Form)。我们将setUser作为一个属性传递给Form组件,这样当用户登录时,我们可以更新表单状态以重新渲染组件,并显示该用户的配置文件信息。

测试应用

要测试应用程序,现在可以运行start命令:

npm start

摘要

祝贺你,你已经完全构建了一个自定义的认证流程!

从本章中需要记住的几件事:

  • 使用Auth类处理对 Amazon Cognito 认证服务的直接 API 调用。

  • 正如你所见,处理自定义表单状态可能会变得冗长。试着理解自己编写认证流程与使用像withAuthenticator HOC 这样的东西之间的权衡。

  • 认证是复杂的。通过使用像 Amazon Cognito 这样的托管身份服务,我们已经抽象出所有后端代码和逻辑。我们唯一需要知道或理解的是如何与认证 API 进行交互,然后管理本地状态。

第六章:无服务器函数深入解析:第一部分

在第二章中,您学会了如何使用 API Gateway 和 AWS Lambda 创建和交互无服务器 API。在这里,您将继续学习如何通过创建两种新类型的函数来使用无服务器函数。本章中的函数将不同,因为您将使用它们与其他 AWS 服务进行交互,以帮助应用程序开发过程。

在本章中,您将创建以下两种类型的函数:

一个根据用户电子邮件地址动态将用户添加到组的函数

在一些应用程序中,您将需要执行“粗粒度”访问控制,通常意味着根据用户关联的角色或组的类型广泛授予某些权限。在我们的示例中,我们将有一个由其电子邮件地址标识的管理员用户组。如果用户使用这些电子邮件地址之一注册,我们将将他们放入名为Admin的组中。

一个在图像上传到 Amazon S3 后自动调整图像大小的函数

许多应用程序在用户上传图像后需要在服务器上进行动态图像调整。这样做的原因有很多,从需要通过压缩图像使 Web 应用程序更具性能到需要动态创建头像或缩略图等较小尺寸的图像。

在第七章中,我们将继续学习有关无服务器函数的知识,通过创建一个与数据库交互的电子商务应用程序,并允许用户通过调用 API 来创建、读取、更新和删除数据库中的项目。

事件源和数据结构

在第二章中,我们简要讨论了作为事件驱动架构一部分的无服务器函数的事件源。到目前为止,我们实现的唯一事件源来自 API Gateway:触发函数的 HTTP 请求,并从 API 获取数据并在响应中返回。在本章中,我们将使用另外两种事件类型和来源,一种来自 Amazon S3,另一种来自 Amazon Cognito。

要理解从事件源传入 Lambda 的事件,重要的一点是强调以下内容:不同事件类型之间的事件数据形状将不同。例如,来自 API Gateway 的 HTTP 事件数据结构将不同于 Amazon S3 事件数据结构,而 Amazon S3 事件数据结构将与 Amazon Cognito 数据结构不同。

了解事件数据的结构,以及了解事件中可用的数据,将帮助您理解 Lambda 函数中可以执行的功能的能力。为了更好地理解这一点,让我们来看看来自不同事件的各种数据结构。目前,您不需要理解这些数据结构中的每个字段和值。我将在下面的示例中概述对我们重要的值。

API 网关事件

API 网关事件数据是在从 API 网关 HTTP 事件(如 GET、PUT、POST 或 DELETE)调用时传递给 Lambda 函数的数据结构。此数据结构包含调用函数的 HTTP 方法、调用的路径、如果传入的话还有请求体,以及调用 API 的用户的身份信息(在 requestContext.identity 字段中)(如果用户已经通过身份验证):

{
    "resource": "/items",
    "path": "/items",
    "httpMethod": "GET",
    "headers": { /* header info */ },
    "multiValueHeaders": { /* multi value header info */ },
    "queryStringParameters": null,
    "multiValueQueryStringParameters": null,
    "pathParameters": null,
    "stageVariables": null,
    "requestContext": {
        "resourceId": "b16tgj",
        "resourcePath": "/items",
        "httpMethod": "GET",
        "extendedRequestId": "CzuJMEDMoAMF_MQ=",
        "requestTime": "07/Nov/2019:21:46:09 +0000",
        "path": "/dev/items",
        "accountId": "557458351015",
        "protocol": "HTTP/1.1",
        "stage": "dev",
        "domainPrefix": "eq4ttnl94k",
        "requestTimeEpoch": 1573163169162,
        "requestId": "1ac70afe-d366-4a52-9329-5fcbcc3809d8",
        "identity": {
          "cognitoIdentityPoolId": "",
          "accountId": "",
          "cognitoIdentityId": "",
          "caller": "",
          "apiKey": "",
          "sourceIp": "192.168.100.1",
          "cognitoAuthenticationType": "",
          "cognitoAuthenticationProvider": "",
          "userArn": "",
          "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)
          AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82
          Safari/537.36 OPR/39.0.2256.48",
          "user": ""
        },
        "domainName": "eq4ttnl94k.execute-api.us-east-1.amazonaws.com",
        "apiId": "eq4ttnl94k"
    },
    "body": null,
    "isBase64Encoded": false
}

Amazon S3 事件

Amazon S3 事件是在从文件上传或更新到 Amazon S3 时调用 Lambda 函数时将收到的数据结构。此数据结构包含来自 S3 的记录数组。在此事件数据中,您通常将使用的主要信息是 s3 字段。此属性包含存储桶名称、键和正在存储的项目的大小等信息:

{
  "Records": [
    {
      "eventVersion": "2.1",
      "eventSource": "aws:s3",
      "awsRegion": "us-east-2",
      "eventTime": "2019-09-03T19:37:27.192Z",
      "eventName": "ObjectCreated:Put",
      "userIdentity": {
        "principalId": "AWS:AIDAINPONIXQXHT3IKHL2"
      },
      "requestParameters": {
        "sourceIPAddress": "205.255.255.255"
      },
      "responseElements": {
        "x-amz-request-id": "D82B88E5F771F645",
        "x-amz-id-2": "vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj
                       5NeC6qMa0emYBDXOo6QBU0Wo="
      },
      "s3": {
        "s3SchemaVersion": "1.0",
        "configurationId": "828aa6fc-f7b5-4305-8584-487c791949c1",
        "bucket": {
          "name": "lambda-artifacts-deafc19498e3f2df",
          "ownerIdentity": {
            "principalId": "A3I5XTEXAMAI3E"
          },
          "arn": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df"
        },
        "object": {
          "key": "b21b84d653bb07b05b1e6b33684dc11b",
          "size": 1305107,
          "eTag": "b21b84d653bb07b05b1e6b33684dc11b",
          "sequencer": "0C0F6F405D6ED209E1"
        }
      }
    }
  ]
}

Amazon Cognito 事件

Amazon Cognito 事件数据是在从 Amazon Cognito 操作调用时传递给函数的数据结构。这些操作可以是用户注册、用户确认其帐户或用户登录等事件:

{
    "version": "1",
    "region": "us-east-1",
    "userPoolId": "us-east-1_uVWAMpQuY",
    "userName": "dabit3",
    "callerContext": {
        "awsSdkVersion": "aws-sdk-unknown-unknown",
        "clientId": "2ects9inqraapp43ejve80pv12"
    },
    "triggerSource": "PostConfirmation_ConfirmSignUp",
    "request": {
        "userAttributes": {
            "sub": "164961f8-13f7-40ed-a8ca-d441d8ec4724",
            "cognito:user_status": "CONFIRMED",
            "email_verified": "true",
            "phone_number_verified": "false",
            "phone_number": "+16018127241",
            "email": "dabit3@gmail.com"
        }
    },
    "response": {}
}

你将使用这些事件和其中包含的信息来执行不同类型的操作,从而在函数内部执行这些操作。

IAM 权限和触发器配置

在使用 CLI 设置这些触发器时,幕后发生了几件事:

  • CLI 正在启用 Lambda 配置中的触发器本身。启用触发器后,每次发生互动(API 事件、S3 上传等)时,事件将发送到函数。

  • CLI 为函数本身提供了与其他服务交互的额外权限。例如,在本章中启用 S3 触发器时,我们希望 Lambda 函数能够读取和存储该存储桶中的图像。

    为了实现这一点,CLI 将在幕后为函数添加额外的身份和访问管理(IAM)策略,为其提供诸如读取和写入访问权限以处理与 S3 的工作,或者与我们其他示例中的 Cognito 用户池交互的权限。

创建基础项目

我们要做的第一件事是创建一个新的 React 应用程序并安装本章所需的依赖项:

~ npx create-react-app lambda-trigger-example
~ cd lambda-trigger-example
~ npm install aws-amplify @aws-amplify/ui-react uuid

接下来,我们将创建一个新的 Amplify 项目:

~ amplify init
# walk through the steps like we've done in the previous projects

现在项目已经初始化完成,我们可以开始添加服务。本章我们需要的服务将包括 Amazon Cognito、Amazon S3 和 AWS Lambda。我们将从添加 Amazon Cognito 开始,并测试一下后确认 Lambda 触发器。

添加后确认 Lambda 触发器

我们接下来要做的是创建一个认证服务。然后,我们将创建和配置一个后确认 Lambda 触发器。这意味着我们希望每当有人成功注册并确认其帐户时,就会调用一个 Lambda 函数。此后确认触发器每个已确认的用户只触发一次:

~ amplify add auth

? Do you want to use the default authentication and security configuration?
  Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings? Yes
? What attributes are required for signing up? Email
? Do you want to enable any of the following capabilities? Add User to Group
? Enter the name of the group to which users will be added. Admin
? Do you want to edit your add-to-group function now? Y

现在,使用以下代码更新函数:

// amplify/backend/function/<function_name>/src/add-to-group.js

const aws = require('aws-sdk');

exports.handler = async (event, context, callback) => {
  const cognitoProvider = new
  aws.CognitoIdentityServiceProvider({
    apiVersion: '2016-04-18'
  });

  let isAdmin = false
  const adminEmails = ['dabit3@gmail.com']

  // If the user is one of the admins, set the isAdmin variable to true
  if (adminEmails.indexOf(event.request.userAttributes.email) !== -1) {
    isAdmin = true
  }

  const groupParams = {
    UserPoolId: event.userPoolId,
  }

  const userParams = {
    UserPoolId: event.userPoolId,
    Username: event.userName,
  }

  if (isAdmin) {
    groupParams.GroupName = 'Admin',
    userParams.GroupName = 'Admin'

    // First check to see if the group exists, and if not create the group
    try {
      await cognitoProvider.getGroup(groupParams).promise();
    } catch (e) {
      await cognitoProvider.createGroup(groupParams).promise();
    }

    // If the user is an administrator, place them in the Admin group
    try {
      await cognitoProvider.adminAddUserToGroup(userParams).promise();
      callback(null, event);
    } catch (e) {
      callback(e);
    }
  } else {
    // If the user is in neither group, proceed with no action
    callback(null, event)
  }
}

在这个函数中,有一个主要的功能部分。如果用户是admins电子邮件数组中指定的管理员之一,我们会自动将他们放入名为Admins的组中。请更改adminEmails数组中的值,以包括您的电子邮件地址。

要部署服务,请运行push命令:

~ amplify push

现在后端已经设置好,我们可以进行测试。为此,我们首先需要配置 React 项目以识别 Amplify 依赖项。打开src/index.js文件,并在最后一个导入语句下面添加以下内容:

import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)

接下来,我们将注册一个新用户,并在他们是管理员时显示问候语。为此,打开src/App.js文件并添加以下内容:

import React, { useEffect, useState } from 'react'
import { Auth } from 'aws-amplify'
import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react'
import './App.css'

function App() {
  const [user, updateUser] = useState(null)
  useEffect(() => {
    Auth.currentAuthenticatedUser()
      .then(user => updateUser(user))
      .catch(err => console.log(err));
  }, [])
  let isAdmin = false
  if (user) {
    const { signInUserSession: { idToken: { payload }} }  = user
    console.log('payload: ', payload)
    if (
      payload['cognito:groups'] &&
    payload['cognito:groups'].includes('Admin')
    ) {
      isAdmin = true
    }
  }
  return (
    <div className="App">
      <header>
      <h1>Hello World</h1>
      { isAdmin && <p>Welcome, Admin</p> }
      </header>
      <AmplifySignOut />
    </div>
  );
}

export default withAuthenticator(App)

运行应用程序:

~ npm start

现在,使用管理员用户注册。如果用户确实是管理员之一,您应该会看到Welcome, Admin的问候语。

您还可以通过运行以下命令查看 Amazon Cognito 认证服务以及所有用户和组:

~ amplify console auth

? Which console: User Pool

> In the left hand menu, click on "Users and Groups"

使用 AWS Lambda 和 Amazon S3 进行动态图像调整大小

在下一个示例中,我们将添加功能,允许用户将图像上传到 Amazon S3。我们还将配置一个 S3 触发器,每当文件上传到存储桶时就调用 Lambda 函数。在此函数中,我们将检查图像的尺寸,如果超过某个宽度阈值,则将其调整大小为低于该宽度阈值。

要使此功能正常工作,我们需要在项目中配置 S3 以在文件上传时触发 Lambda 函数。我们可以通过 Amplify CLI 来实现这一点,只需创建 S3 存储桶并选择正确的配置即可。从 CLI 中运行以下命令:

~ amplify add storage

? Please select from one of the below mentioned services: Content
? Please provide a friendly name for your resource that will be used to label
  this category in the project: <your_resource_name>
? Please provide bucket name: <your_globally_unique_bucket_name>
? Who should have access: Auth and Guest users
? What kind of access do you want for Authenticated users? Choose all
  (create / update, read, & delete)
? What kind of access do you want for Guest users? Choose all
  (create / update, read, & delete)
? Do you want to add a Lambda Trigger for your S3 Bucket? Y
? Select from the following options: Create a new function
? Do you want to edit the local S3Trigger18399e19 lambda function now? Y

这将在您的文本编辑器中打开该函数。

添加自定义逻辑以调整图像大小

现在,我们可以更新函数来实现图像调整大小。

在这个函数中,当事件发生时,我们将从 S3 获取图像,并检查其是否大于 1000 像素宽。如果是这样,则将其调整为 1000 像素宽并保存回 S3 存储桶。如果图像未超过 1000 像素宽,则退出函数而不执行任何操作:

// amplify/backend/function/<functionname>/src/index.js

// Import the sharp library
const sharp = require('sharp')
const aws = require('aws-sdk')
const s3 = new aws.S3()

exports.handler = async function (event, context) { //eslint-disable-line
  // If the event type is delete, return from the function
  if (event.Records[0].eventName === 'ObjectRemoved:Delete') return

  // Next, we get the bucket name and the key from the event.
  const BUCKET = event.Records[0].s3.bucket.name
  const KEY = event.Records[0].s3.object.key
  try {
    // Fetch the image data from S3
    let image = await s3.getObject({ Bucket: BUCKET, Key: KEY }).promise()
    image = await sharp(image.Body)

    // Get the metadata from the image, including the width and the height
    const metadata = await image.metadata()
    if (metadata.width > 1000) {
      // If the width is greater than 1000, the image is resized
      const resizedImage = await image.resize({ width: 1000 }).toBuffer()
      await s3.putObject({
        Bucket: BUCKET,
        Body: resizedImage,
        Key: KEY
      }).promise()
      return
    } else {
      return
    }
  }
  catch(err) {
    context.fail(`Error getting files: ${err}`);
  }
};

为了使我们的函数正常工作,我们需要再做一件事情。在我们的 Lambda 函数中需要引入 Sharp 库,但目前我们还没有安装这个依赖项。为了确保这个模块被安装,更新函数的 package.json 文件,添加这个包的依赖项以及我们需要的安装脚本,以便 Sharp 在 Lambda 环境中正确运行。我们将要添加的两个字段是 scriptsdependencies

// amplify/backend/function/<functionname>/src/package.json
{
  "name": "your-function-name",
  "version": "2.0.0",
  "description": "Lambda function generated by Amplify",
  "main": "index.js",
  "license": "Apache-2.0",
  "scripts": {
    "install": "npm install --arch=x64 --platform=linux --target=10.15.0 sharp"
  },
  "dependencies": {
    "sharp": "⁰.23.2"
  }
}

现在,服务已准备部署:

~ amplify push

从 React 应用程序上传图像

接下来,打开 src/App.js 文件,并添加以下代码以渲染一个图像选择器和照片列表:

import React, { useState, useEffect} from 'react'
import { Storage } from 'aws-amplify'
import { v4 as uuid } from 'uuid'
import './App.css'

function App() {
  const [images, setImages] = useState([])
  useEffect(() => {
    fetchImages()
  }, [])
  async function onChange(e) {
    /* When a file is uploaded, create a unique name and save it using
       the Storage API */
    const file = e.target.files[0];
    const filetype = file.name.split('.')[file.name.split.length - 1]
    await Storage.put(`${uuid()}.${filetype}`, file)
    /* Once the file is uploaded, fetch the list of images */
    fetchImages()
  }
  async function fetchImages() {
    /* This function fetches the list of image keys from your S3 bucket */
    const files = await Storage.list('')
    /* Once we have the image keys, the images must be signed in order
       for them to be displayed */
    const signedFiles = await Promise.all(files.map(async file => {
      /* To sign the images, we map over the image key array and get a
         signed url for each image */
      const signedFile = await Storage.get(file.key)
      return signedFile
    }))
    setImages(signedFiles)
  }

  return (
    <div className="App">
      <header className="App-header">
        <input
          type="file"
          onChange={onChange}
        />
        {
          images.map(image => (
            <img
              src={image}
              key={image}
              style={{ width: 500 }}
            />
          ))
        }
      </header>
    </div>
  );
}

export default App

接下来,运行应用程序:

~ npm start

当您上传的图像宽度超过 1,000 像素时,您会注意到它最初会加载为原始尺寸,但如果重新加载应用程序,您会看到图像已调整为正确的 1,000 像素宽度。

总结

恭喜,您现在已成功实现了两种类型的 Lambda 触发器!

从本章中要记住的几点是:

  • Lambda 函数可以从许多不同的事件类型中调用,包括 API 调用、图像上传、数据库操作和身份验证事件。

  • 根据调用 Lambda 函数的事件类型,event 数据结构有所不同。

  • 理解事件变量中的数据可以更好地评估在函数内可以完成的事情。

  • 当 Amplify CLI 启用 Lambda 触发器时,函数会获得额外的 IAM 权限,允许它直接与其他服务交互。

第七章:无服务器函数深入解析:第二部分

到目前为止,我们已经涵盖了使用 Lambda 函数可以实现的相当多的功能。在本章中,我们将继续学习如何以不同的方式使用 Lambda 函数来实现构建应用程序时会有用的常见功能。我们将深入了解如何创建并集成一个完全功能的后端应用程序,包括 API、身份验证、数据库和授权规则。

使用 Amplify,有两种主要方法来创建 API:GraphQL 和 REST。我们将继续在第 Chapter 8 章中介绍 GraphQL,但在这里,我们将学习如何在 Lambda 函数中运行的 REST API 中实现此功能。

我们将使用 Amazon DynamoDB,一个 NoSQL 数据库。我们将通过 API 网关端点将 HTTP 请求调用 Lambda 函数。Lambda 函数将接收 HTTP 请求,然后根据不同的路径进行路由,因为函数将运行一个 Express web 服务器。

这将允许我们在单个函数内部有不同的路由可用。然后,我们将映射不同的 HTTP 方法,如postdelete,到路由上以执行数据库上的不同操作。

我们将构建什么

我们将构建一个基本的电子商务应用程序,允许用户查看产品,并允许管理员创建和删除产品。此应用程序的构建块将为构建几乎任何类型的 CRUD+L(创建、读取、更新、删除和列表)应用程序打下基础,这是许多真实项目的支柱。

我们将使用第二章和第六章中学到的内容,并在本章中进一步构建这些思想。

我们需要以下服务和功能:

Lambda 函数

主应用程序逻辑将驻留在一个 Lambda 函数中,该函数将运行一个 Express 服务器。服务器将具有我们需要处理的不同 HTTP 方法的路由:getpostdelete

API

为了与主 Lambda 函数交互,我们需要能够使用 HTTP 请求调用它,发送getpostdelete请求以与 API 和数据库进行交互。

DynamoDB NoSQL 数据库

这是将为应用程序存储所有数据的数据库。

认证

我们需要一种方法来验证用户,以便配置和启用管理员访问。

另一个 Lambda 函数

我们将需要一个 Lambda 触发器将管理员放置到管理员组中,因此将与身份验证流程关联的另一个 Lambda 函数(后确认触发器)。

与之前的章节一样,我们需要在客户端应用程序中集成导航以便在路由之间进行链接。当用户登录时,我们将访问用户的群组,以确定基于用户权限的应用程序状态。这些权限可能包括确定是否显示管理导航链接或允许用户根据其是否为管理员来查看删除项目按钮。

我们还将在服务器上设置一些授权保护,以确保用户执行操作时确实被授权执行该操作。

入门指南

要开始的第一件事是创建一个新的 React 应用程序并安装必要的依赖项:

~ npx create-react-app ecommerceapp

~ cd ecommerceapp

~ npm install aws-amplify @aws-amplify/ui-react react-router-dom antd

接下来,我们将初始化一个新的 Amplify 项目,并开始添加我们应用程序所需的服务:

~ amplify init

# Follow the steps to give the project a name, environment name, and
  set the default text editor.
# Accept defaults for everything else and choose your AWS Profile.

添加认证和群组权限

我们将首先创建认证服务。我们还需要确保创建 Lambda 触发器,以便将用户添加到我们将创建的 Admin 组中:

~ amplify add auth

? Do you want to use the default authentication and security configuration?
  Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings? Yes
? What attributes are required for signing up? Email
? Do you want to enable any of the following capabilities? Add User to Group
? Enter the name of the group to which users will be added. Admin
? Do you want to edit your add-to-group function now? Y

使用以下代码更新函数并配置adminEmails数组:

// amplify/backend/function/<function_name>/src/add-to-group.js
const aws = require('aws-sdk');

exports.handler = async (event, context, callback) => {
  const cognitoProvider = new
  aws.CognitoIdentityServiceProvider({
    apiVersion: '2016-04-18'
  });

  let isAdmin = false
  // Update this array to include any admin emails you would like to enable
  const adminEmails = ['dabit3@gmail.com']

  // If the user is one of the admins, set the isAdmin variable to true
  if (adminEmails.indexOf(event.request.userAttributes.email) !== -1) {
    isAdmin = true
  }

  if (isAdmin) {
    const groupParams = {
      UserPoolId: event.userPoolId,
      GroupName: 'Admin'
    }
    const userParams = {
      UserPoolId: event.userPoolId,
      Username: event.userName,
      GroupName: 'Admin'
    }

    // First check to see if the group exists, and if not create the group
    try {
      await cognitoProvider.getGroup(groupParams).promise();
    } catch (e) {
      await cognitoProvider.createGroup(groupParams).promise();
    }
    // The user is an administrator, place them in the Admin group
    try {
      await cognitoProvider.adminAddUserToGroup(userParams).promise();
      callback(null, event);
    } catch (e) { callback(e); }
  } else {
    // If the user is in neither group, proceed with no action
    callback(null, event)
  }
}

在此函数中,我们设置了一个管理员电子邮件数组和一个isAdmin变量。如果确认的用户是管理员,我们首先检查服务中是否已经创建了管理员组。如果尚未创建,我们将创建它。

然后,通过调用cognitoProvider.adminAddUserToGroup将用户添加到组中,传递参数。

添加数据库

接下来,我们将为项目创建 DynamoDB NoSQL 数据库。要添加数据库,我们可以使用Storage类别:

~ amplify add storage

? Please select from one of the below mentioned services: NoSQL Database
? Please provide a friendly name for your resource that will be used to label
  this category in the project: producttable
? Please provide table name: producttable
? What would you like to name this column: id
? Please choose the data type: string
? Would you like to add another column? N
? Please choose partition key for the table: id
? Do you want to add a sort key to your table? N
? Do you want to add global secondary indexes to your table? N
? Do you want to add a Lambda Trigger for your Table? N

在使用 DynamoDB 时,必须有一个唯一的主键或主键和排序键的唯一组合,以唯一标识数据库中的各个项目。在我们的数据库中,我们有一个id主键,将作为数据库中项目的唯一标识符。

表上还有一个选项可以创建全局二级索引(GSI)。这些允许我们添加额外的索引,可用于查询我们的表格并启用额外的数据访问模式。DynamoDB 和 NoSQL 数据库一般具有的最强大的功能之一是拥有其他索引的概念(DynamoDB 最多可以有 20 个 GSI),这些索引使我们能够实现多种访问模式。在这里我们不会使用任何二级索引,但我鼓励你深入了解如何使用这些来增强 DynamoDB 的功能和灵活性。

添加 API

现在数据库已创建,我们将创建一个 API 和另一个与数据库交互的 Lambda 函数:

~ amplify add api

? Please select from one of the below mentioned services: REST
? Provide a friendly name for your resource to be used as a label for this
  category in the project: ecommerceapi
? Provide a path: /products
? Choose a Lambda source: Create a new Lambda function
? Provide a friendly name for your resource to be used as a label for this
  category in the project: ecommercefunction
? Provide the AWS Lambda function name: ecommercefunction
? Choose the function runtime that you want to use: NodeJS
? Choose the function template that you want to use: Serverless express
  function (Integration with Amazon API Gateway)
? Do you want to access other resources created in this project from your
  Lambda function? Y
? Select the category: storage, auth
? Select the operations you want to permit for <app_name>: create, read, update,
  delete
? Select the operations you want to permit for producttable: create, read,
  update, delete
? Do you want to invoke this function on a recurring schedule? N
? Do you want to configure Lambda layers for this function? N
? Do you want to edit the local Lambda function now? N
? Restrict API access: Y
? Who should have access? Authenticated and Guest users
? What kind of access do you want for Authenticated users? create, read,
  update, delete
? What kind of access do you want for Guest users? read
? Do you want to add another path? N

现在我们已经创建了一个 API Gateway 端点以及一个新的 Lambda 函数,并集成了从 API Gateway 事件触发该函数的功能。CLI 会指导我们完成设置,并允许我们根据用户是否已经认证来设置一些基本的 API 授权规则。我们还设置了一个路径,现在我们将能够使用它:/products

Lambda 函数包含一个 Express 服务器作为 CLI 为我们创建的样板的一部分。如果您之前没有使用过 Express,它是一个轻量级的 Node.js Web 框架,提供了一组很好的内置功能来开发 Web 和移动应用程序。对于我们的目的,我们将使用它更轻松地提供路由,以映射到 API Gateway 中创建的端点。现在,我们可以调用/products端点上的getputpostdelete方法。

如果我们想要添加额外的端点,我们可以通过运行amplify update api来更新api类别,然后将我们创建的任何新端点直接添加到 Express 服务器代码中。

接下来,我们将更新正在运行 Express 服务器的 Lambda 函数中的代码,以处理我们希望启用的与数据库的交互。

我们需要做的第一件事是更新函数的导入:

/* amplify/backend/function/ecommercefunction/src/app.js */

/* Below the last existing `require` import, add the following
   imports variables */
const AWS = require('aws-sdk')
const { v4: uuid } = require('uuid')

/* Cognito SDK */
const cognito = new
AWS.CognitoIdentityServiceProvider({
  apiVersion: '2016-04-18'
})

/* Cognito User Pool ID
*  This User Pool ID variable will be given to you by the CLI output after
   adding the category
*  This will also be available in the file itself, commented out at the top
*/
var userpoolId = process.env.<your_app_id>

// DynamoDB configuration
const region = process.env.REGION
const ddb_table_name = process.env.STORAGE_PRODUCTTABLE_NAME
const docClient = new AWS.DynamoDB.DocumentClient({region})

接下来,我们将创建几个函数,允许我们对 API 调用执行授权检查。我们希望只有 Admin 组中的用户能够执行某些操作(同时留下未来允许其他组的潜力)。

为此,我们将创建两个函数:getGroupsForUsercanPerformAction

getGroupsForUser

这将允许我们传入来自 API 调用的事件,以确定调用者当前关联的群组。

canPerformAction

这首先检查用户是否已经认证,如果没有,将拒绝请求。然后它将检查用户是否属于作为第二个参数传入的组,并且如果是,则允许执行操作。如果不是,则拒绝操作。

使用以下代码创建函数:

// amplify/backend/function/ecommercefunction/src/app.js
async function getGroupsForUser(event) {
  let userSub =
    event
      .requestContext
      .identity
      .cognitoAuthenticationProvider
      .split(':CognitoSignIn:')[1]
  let userParams = {
    UserPoolId: userpoolId,
    Filter: `sub = "${userSub}"`,
  }
  let userData = await cognito.listUsers(userParams).promise()
  const user = userData.Users[0]
  var groupParams = {
    UserPoolId: userpoolId,
    Username: user.Username
  }
  const groupData = await cognito.adminListGroupsForUser(groupParams).promise()
  return groupData
}

async function canPerformAction(event, group) {
  return new Promise(async (resolve, reject) => {
    if (!event.requestContext.identity.cognitoAuthenticationProvider) {
      return reject()
    }
    const groupData = await getGroupsForUser(event)
    const groupsForUser = groupData.Groups.map(group => group.GroupName)
    if (groupsForUser.includes(group)) {
      resolve()
    } else {
      reject('user not in group, cannot perform action..')
    }
  })
}

接下来,我们将更新getpostdelete的 HTTP 方法以与数据库交互。

首先让我们更新app.get用于/products

// amplify/backend/function/ecommercefunction/src/app.js
app.get('/products', async function(req, res) {
  try {
    const data = await getItems()
    res.json({ data: data })
  } catch (err) {
    res.json({ error: err })
  }
})

async function getItems(){
  var params = { TableName: ddb_table_name }
  try {
    const data = await docClient.scan(params).promise()
    return data
  } catch (err) {
    return err
  }
}

这个方法调用一个我们创建的名为getItems的新函数,它使用扫描操作(docClient.scan)从 DynamoDB 表中获取项目。如果扫描操作成功,则在响应中返回项目。如果操作失败,则返回错误消息。

接下来,让我们更新app.post用于/products,看看如何在 DynamoDB 中创建一个新条目:

// amplify/backend/function/ecommercefunction/src/app.js
app.post('/products', async function(req, res) {
  const { body } = req
  const { event } = req.apiGateway
  try {
    await canPerformAction(event, 'Admin')
    const input = { ...body, id: uuid() }
    var params = {
      TableName: ddb_table_name,
      Item: input
    }
    await docClient.put(params).promise()
    res.json({ success: 'item saved to database..' })
  } catch (err) {
    res.json({ error: err })
  }
});

这个调用与get调用有些不同。您可以看到我们使用req对象从事件中检索body,然后从req.apiGateway对象获取事件数据。

我们首先调用canPerformAction来查看调用者是否为管理员。如果成功,我们将继续使用body参数创建输入对象,并追加一个唯一的 ID 到该对象。

然后,我们创建一个新的params变量,其中包含输入以及表名。最后,我们使用 DynamoDB Document Client 调用put方法来创建一个新项目。

接下来,让我们看看如何通过更新app.delete方法来删除一个项目,用于/products

// amplify/backend/function/ecommercefunction/src/app.js
app.delete('/products', async function(req, res) {
  const { event } = req.apiGateway
  try {
    await canPerformAction(event, 'Admin')
    var params = {
      TableName : ddb_table_name,
      Key: { id: req.body.id }
    }
    await docClient.delete(params).promise()
    res.json({ success: 'successfully deleted item' })
  } catch (err) {
    res.json({ error: err })
  }
});

delete方法和post方法一样,需要管理员来执行操作。为了实现这一点,我们首先调用canPerformAction来检查是否为管理员。然后,我们使用 DynamoDB Document Client 调用delete方法,通过传入id的主键来删除项目。

最后,因为我们在函数中使用了uuid库,所以我们需要将其添加为函数的依赖项到package.json文件中。在amplify/backend/function/ecommercefunction/src/package.json中,添加uuid作为依赖项:

{
  ...
  "dependencies": {
    "aws-serverless-express": "³.3.5",
    "body-parser": "¹.17.1",
    "express": "⁴.15.2",
    "uuid": "⁸.0.0" <- New dependency
  },
  ...
}

现在,后端已设置好,我们可以将其部署到 AWS:

~ amplify push

创建前端

在前端上,我们将首先创建我们需要处理的文件:

Admin.js

这个组件将持有管理员仪表板以创建新项目。

Container.js

这将是一个可重复使用的布局组件。

Main.js

这持有应用程序的主视图,将列出从 API 和数据库拉取的待售商品。

Nav.js

这将持有导航组件。

Profile.js

这将是一个基本的个人资料组件,允许用户注销。

Router.js

这个组件将持有路由器。

checkUser.js

这将持有一个函数,用于检索用户的个人资料并确定用户是否为管理员。

接下来,让我们切换到src目录,并创建这些组件:

~ cd src
~ touch Admin.js Container.js Main.js Nav.js Profile.js Router.js checkUser.js
~ cd ..

接下来,打开src/index.js并更新它,导入 Router、Amplify 库和 Ant Design 的 CSS:

import React from 'react'
import ReactDOM from 'react-dom'
import Router from './Router'

import 'antd/dist/antd.css'
import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)

ReactDOM.render(<Router />, document.getElementById('root'))

容器组件

Container组件将提供一个基本布局,固定宽度并以一致的方式将组件居中。

import React from 'react'

export default function Container({ children }) {
  return (
    <div style={containerStyle}>
      {children}
    </div>
  )
}

const containerStyle = {
  width: 900,
  margin: '0 auto',
  padding: '20px 0px'
}

checkUser 函数

这个函数将检查当前用户的信息,然后调用updateUser回调函数来更新用户。如果没有用户,则返回一个空对象。

如果有用户,则会检查用户是否有与之关联的 Cognito 组,如果有,则检查用户是否在Admin组中。如果用户在Admin组中,则isAuthorized布尔值将设置为true;如果不是,则设置为false

/* src/checkUser.js */
import { Auth } from 'aws-amplify'

async function checkUser(updateUser) {
  const userData = await Auth
    .currentSession()
    .catch(err => console.log('error: ', err)
  )
  if (!userData) {
    console.log('userData: ', userData)
    updateUser({})
    return
  }
  const { idToken: { payload }} = userData
  const isAuthorized =
    payload['cognito:groups'] &&
  payload['cognito:groups'].includes('Admin')
  updateUser({
    username: payload['cognito:username'],
    isAuthorized
  })
}

export default checkUser

导航组件

Nav组件将持有主要链接(首页个人资料),以及另一个管理员链接,仅在您以管理员用户身份登录时可见:

/* src/Nav.js */
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { Menu } from 'antd'
import { HomeOutlined, UserOutlined, ProfileOutlined } from '@ant-design/icons'
import { Hub } from 'aws-amplify'
import checkUser from './checkUser'

const Nav = (props) => {
  const { current } = props
  const [user, updateUser] = useState({})
  useEffect(() => {
    checkUser(updateUser)
    Hub.listen('auth', (data) => {
      const { payload: { event } } = data;
      console.log('event: ', event)
      if (event === 'signIn' || event === 'signOut') checkUser(updateUser)
    })
  }, [])

  return (
    <div>
      <Menu selectedKeys={[current]} mode="horizontal">
        <Menu.Item key='home'>
          <Link to={`/`}>
            <HomeOutlined />Home
          </Link>
        </Menu.Item>
        <Menu.Item key='profile'>
          <Link to='/profile'>
            <UserOutlined />Profile
          </Link>
        </Menu.Item>
        {
          user.isAuthorized && (
            <Menu.Item key='admin'>
              <Link to='/admin'>
                <ProfileOutlined />Admin
              </Link>
            </Menu.Item>
          )
        }
      </Menu>
    </div>
  )
}

export default Nav

在这个组件中,我们使用useEffect钩子在组件加载时调用checkUser函数。如果有已登录用户,这将使用用户信息设置组件状态。

我们还设置了一个监听器,使用Hub组件来监听auth事件(如注册、登录和登出)。当用户登录或登出时,我们再次调用checkUser函数以保持导航状态的更新。

在用户界面中,我们决定只在用户是授权管理员时显示Admin链接。

个人资料组件

此组件非常基础。如果用户已登录,我们将呈现组件和登出按钮。如果他们没有登录,则withAuthenticator组件将呈现用户的注册和登录流程:

/* src/Profile.js */
import React from 'react'
import './App.css'

import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react'

function Profile() {
  return (
    <div style={containerStyle}>
      <AmplifySignOut />
    </div>
  );
}

const containerStyle = {
  width: 400,
  margin: '20px auto'
}

export default withAuthenticator(Profile)

路由器组件

此组件配置了三个主要组件和路由:Main/)、Admin/admin)和Profile/profile)。

useEffect钩子中,我们首先调用setRoute函数。该函数将获取当前窗口位置,并设置要传递给Nav组件的当前路由信息:

/* src/Router.js */
import React, {useState, useEffect} from 'react'
import { HashRouter, Route, Switch } from 'react-router-dom'

import Nav from './Nav'
import Admin from './Admin'
import Main from './Main'
import Profile from './Profile'

export default function Router() {
  const [current, setCurrent] = useState('home')
  useEffect(() => {
    setRoute()
    window.addEventListener('hashchange', setRoute)
    return () =>  window.removeEventListener('hashchange', setRoute)
  }, [])
  function setRoute() {
    const location = window.location.href.split('/')
    const pathname = location[location.length-1]
    console.log('pathname: ', pathname)
    setCurrent(pathname ? pathname : 'home')
  }
  return (
    <HashRouter>
      <Nav current={current} />
      <Switch>
        <Route exact path='/' component={Main} />
        <Route path='/admin' component={Admin} />
        <Route path='/profile' component={Profile} />
        <Route component={Main} />
      </Switch>
    </HashRouter>
  )
}

我们还设置了一个监听器来监听路由变化(hashchange),当路由变化时,我们将调用setRoute函数,以设置当前路由信息传递给Nav组件。

管理组件

Admin组件包含一个表单,允许我们在库存中创建新项目:

/* src/Admin.js */
import React, { useState } from 'react'
import './App.css'
import { Input, Button } from 'antd'

import { API } from 'aws-amplify'
import { withAuthenticator } from '@aws-amplify/ui-react'

const initialState = {
  name: '', price: ''
}

function Admin() {
  const [itemInfo, updateItemInfo] = useState(initialState)
  function updateForm(e) {
    const formData = {
      ...itemInfo, [e.target.name]: e.target.value
    }
    updateItemInfo(formData)
  }
  async function addItem() {
    try {
      const data = {
        body: { ...itemInfo, price: parseInt(itemInfo.price) }
      }
      updateItemInfo(initialState)
      await API.post('ecommerceapi', '/products', data)
    } catch (err) {
      console.log('error adding item...')
    }
  }
  return (
    <div style={containerStyle}>
      <Input
        name='name'
        onChange={updateForm}
        value={itemInfo.name}
        placeholder='Item name'
        style={inputStyle}
      />
      <Input
        name='price'
        onChange={updateForm}
        value={itemInfo.price}
        style={inputStyle}
        placeholder='item price'
      />
      <Button
        style={buttonStyle}
        onClick={addItem}
      >Add Product</Button>
    </div>
  )
}

const containerStyle = { width: 400, margin: '20px auto' }
const inputStyle = { marginTop: 10 }
const buttonStyle = { marginTop: 10 }

export default withAuthenticator(Admin)

此组件中发生的主要事情是addItem函数。

此功能使用API类别与我们创建的 REST API 进行交互。当我们设置这个 API 时,我们命名为ecommerceapi。使用 API 名称以及路径(/products),我们可以对其进行请求,如getputpostdelete

在我们的组件中,我们调用了API.post,传入一个包含要发送到主体中的数据的对象:

/* Create the object to send with the request */
const data = {
  body: { ...itemInfo, price: parseInt(itemInfo.price) }
}
/* Update the local state with the initial state to clear the form */
updateItemInfo(initialState)
/* Post to the API */
await API.post('ecommerceapi', '/products', data)

主组件

最后一个组件是Main组件,它是渲染库存项目列表的主视图。

此组件中有两个主要功能,getProductsdeleteItem

getProducts

在 API 上调用get方法。当数据返回时,状态将更新,将产品数组设置为从 API 返回的数据。

deleteItem

  1. 要删除的项目的id用于创建产品数组的过滤列表,通过删除要删除的项目来实现。

  2. 过滤后的产品数组用于更新本地状态,在 UI 中通过删除视图中的项目并立即显示新产品列表来创建乐观的响应。

  3. 我们使用API类别进行delete请求,传入产品的id

/* src/Main.js */
import React, { useState, useEffect } from 'react'
import Container from './Container'
import { API } from 'aws-amplify'
import { List } from 'antd'
import checkUser from './checkUser'

function Main() {
  const [state, setState] = useState({products: [], loading: true})
  const [user, updateUser] = useState({})
  let didCancel = false
  useEffect(() => {
    getProducts()
    checkUser(updateUser)
    return () => didCancel = true
  }, [])
  async function getProducts() {
    const data = await API.get('ecommerceapi', '/products')
    console.log('data: ', data)
    if (didCancel) return
    setState({
      products: data.data.Items, loading: false
    })
  }
  async function deleteItem(id) {
    try {
      const products = state.products.filter(p => p.id !== id)
      setState({ ...state, products })
      await API.del('ecommerceapi', '/products', { body: { id } })
      console.log('successfully deleted item')
    } catch (err) {
      console.log('error: ', err)
    }
  }
  return (
    <Container>
      <List
        itemLayout="horizontal"
        dataSource={state.products}
        loading={state.loading}
        renderItem={item => (
          <List.Item
            actions={user.isAuthorized ?
              [<p onClick={() => deleteItem(item.id)}
              key={item.id}>delete</p>] : null}
          >
            <List.Item.Meta
              title={item.name}
              description={item.price}
            />
          </List.Item>
        )}
      />
    </Container>
  )
}

export default Main

测试它

现在,我们应该能够运行应用程序并进行测试:

~ npm start

摘要

恭喜,您现在已成功部署了一个全栈无服务器 CRUD+列表应用程序。

从本章中需要记住的几件事:

  • Lambda 函数可以从许多不同的事件类型中调用,包括 API 调用、图像上传、数据库操作和身份验证事件。在本章中,我们已经启用了从 HTTP 事件以及身份验证事件中调用的Function调用。

  • 在 Lambda 函数中运行 Express 服务器是扩展单个函数功能的一个好方法。

  • 在处理 REST API 时,API 类别需要两个必需的参数:API 名称和路径。它还可以接受一个可选的第三个参数,一个对象,可以包含您可能想要在 POST 请求中发送的任何参数。

  • 当在 Node.js Lambda 函数中与 DynamoDB 交互时,请使用 DynamoDB 文档客户端,因为它提供了一个易于使用的 API,用于在 DynamoDB 数据库中创建、更新、删除和查询项目。

第八章:AWS AppSync 深入解析

在第三章中,我们学习了 GraphQL,并创建了一个基本的 GraphQL API。在本章中,我们将进一步扩展这些概念,使用本书 GitHub 存储库中的AWS AppSync创建一个音乐节应用程序。

此应用程序将需要以下内容:

  • Amazon DynamoDB 表将用于演出和舞台。

  • GraphQL API 将用于创建、读取、更新、删除和列出演出和舞台。

  • 只有管理员才能创建、更新或删除演出或舞台。

  • 所有用户都应能够查看演出和舞台。

  • 应启用演出和舞台之间的关系。

  • 用户应能够查看所有演出,并导航到查看演出详细信息。

为 GraphQL、AppSync API 和 React Router 构建技能

在本节中,我们将介绍如何在 GraphQL 类型之间建立关系,如何在 GraphQL 类型和字段上实施授权规则,如何为 AppSync API 启用多种授权模式,以及如何使用 React Router 启用路由参数。

首先,我们将简要介绍每个主题,当我们开始构建应用程序时,我们将更深入地研究它们。

GraphQL 类型之间的关系

在创建 GraphQL API 或任何 API 时,建模数据之间的关系变得非常重要。例如,我们正在构建的应用程序将包含以下两种类型:

舞台

此类型将保存个别表演的舞台信息,包括舞台名称和舞台 ID。每个舞台将有多个与之相关联的表演。

表演

此类型将包含个别表演信息,包括表演者、描述、表演的舞台和表演的时间。

对于这种类型的 API,理想情况下,您至少希望具有以下访问模式:

  • 查询单个舞台和舞台的表演

  • 查询所有舞台和每个舞台的所有表演

  • 查询个别表演和相应的舞台信息

  • 查询所有表演和相应的舞台信息

现在的问题通常是:如何使这些不同的关系和访问模式变得可用?而在我们的情况下,如何使用像 DynamoDB 这样的 NoSQL 数据库来实现这一点?有两种方法可以做到这一点:

  • 在 DynamoDB 中设计数据,使得可以利用主键、排序键和本地二级索引的组合来执行所有这些访问模式,从而在单个表中完成。为了在 AppSync 中实现这一点,我们需要手动编写并维护所有解析器逻辑。

  • 直接在解析器级别启用这些关系。因为我们使用 GraphQL,并且 GraphQL 可以启用每个字段的解析器,所以这可以实现。为了更好地理解这一点,让我们来看看我们将要处理的一种类型。

GraphQL 中的舞台类型

为了更好地理解这些概念,让我们来看看我们将要处理的一个类型:

type Stage {
  id: ID!
  name: String!
  performances: [Performance]
}

当为此类型创建解析器或解析器时,这里是一个示例操作链,您可以假设在请求阶段和相应表演时会发生的操作:

  1. Stage GraphQL 解析器将使用阶段 ID 从数据库中的 Stage 表检索阶段信息。

  2. Stage 类型上的 performances 字段将有其自己的 GraphQL 解析器。此解析器应使用阶段 ID 通过查询数据库使用 GSI 检索相关表演,仅返回该 阶段 ID 的表演。

GraphQL Transform: @connection

在 第三章 中,我们使用 GraphQL Transform 库的 @model 指令来搭建整个后端,包括解析器、数据库和额外的 GraphQL 模式。回顾一下,GraphQL Transform 是一个指令库,允许我们“装饰”一个 GraphQL 模式并添加额外功能。

在这里,我们将介绍一些新的指令,包括 @connection,它使我们能够建模这些关系并仅使用几行代码生成必要的解析器。

多种身份验证类型

在 第三章 中,我们创建了一个 GraphQL API,使用 API 密钥作为身份验证类型。这在某些情况下是可以接受的,例如当您希望将 GraphQL 查询提供给您应用程序的所有用户时。

AppSync 支持四种主要的身份验证方法:

API 密钥

API 密钥要求在发出 HTTP 请求时,以某种形式在头部发送 API 密钥,例如 x-api-key。如果您像我们在本书中所做的那样使用 Amplify 客户端,则这将自动发送。

Amazon Cognito 用户池

Amazon Cognito,我们在本书中一直使用的托管身份验证服务,是我们本章将使用的机制之一。使用 Amazon Cognito,我们可以配置对 API 本身以及 GraphQL 类型和字段的私有和组访问。

OpenID Connect

OpenID Connect 允许您使用自己的身份验证提供者,因此,如果您更喜欢像 Auth0 这样的其他身份验证服务,或者您的公司有自己的身份验证实现,您仍然可以使用它来对 AppSync API 进行身份验证。

IAM

AWS IAM 类型强制在 GraphQL API 上执行 AWS Signature Version 4 签名过程。您可以使用 Cognito 身份池中的 AWS IAM 未认证角色来实现公共访问,从而以比 API 密钥更安全的方式启用针对您的 AppSync API 的公共访问。

在这里,我们将结合使用 API 密钥和 Amazon Cognito 为 API 提供多种身份验证类型,从而实现公共读访问以及私有读写访问。

授权

使用 GraphQL Transform 库,我们还可以使用 @auth 指令为 API 定义不同的授权规则。

使用 @auth,我们可以定义不同类型的规则,包括(但不限于)以下内容:

  • 允许所有用户创建和读取,但只有创建项目的所有者能够更新和删除。

  • 仅允许特定组用户能够创建、更新或删除。

  • 允许所有用户读取,但不能执行任何其他操作。

  • 前述规则的组合。

在本示例中,我们将构建的应用程序将支持私有和公共访问,但我们还需要更多控制这些规则。我们需要支持以下内容:

  • 属于名为 Admin 的 Amazon Cognito 组的经过身份验证的用户将能够执行所有操作:创建、读取、更新和删除。

  • 未经身份验证的用户将可以访问,但只能读取。

使用 GSIs 自定义数据访问模式。

DynamoDB 最强大的之一是(截至本文写作时)每个表允许 20 个额外的 GSIs。使用 GSI 或 GSI + 排序键(也可以看作是过滤键)之一,您可以为数据创建极其灵活和强大的数据访问模式。GraphQL Transform 库还有一个指令,@key,使得为 @model 类型配置自定义索引结构变得简单。

我们将使用 @key 指令来创建一个访问模式,通过将舞台 ID 设置为 Performance 表上的 GSI 来允许我们按舞台 ID 查询表演。这样做将使我们能够在单个 GraphQL 查询中请求舞台及其相应的表演。

这样我们就完成了技能概述,让我们开始构建应用程序吧。

开始构建应用程序

要开始,我们将再次逐步进行创建新的 React 项目、安装依赖项、初始化新的 Amplify 应用程序以及通过 CLI 添加功能的步骤。

切换到您希望应用程序位于的目录,并创建一个新的 React 项目:

~ npx create-react-app festivalapp
~ cd festivalapp

接下来,安装依赖项:

~ npm install aws-amplify antd @aws-amplify/ui-react react-router-dom

创建 Amplify 应用程序并添加功能

接下来,在项目目录的根目录中初始化一个新的 Amplify 项目:

~ amplify init

# Follow the steps to give the project a name, environment name, and set the
  default text editor.
# Accept defaults for everything else and choose your AWS Profile.

现在,Amplify 项目已初始化完成,我们可以继续添加功能了。

构建后端

我们将首先添加的功能是身份验证。该应用程序将需要基本身份验证,但还需要能够通过 Lambda 后确认触发器动态添加管理用户,就像我们在第六章中所做的那样。为了实现这一点,我们将创建认证服务以及一个 Lambda 触发器,允许我们在注册时将预定义的一组用户添加到管理组中。

身份验证

要使用 Cognito 添加身份验证,我们将再次使用 auth 类别:

~ amplify add auth

? Do you want to use the default authentication and security configuration?
  Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings? Yes
? What attributes are required for signing up? Email
? Do you want to enable any of the following capabilities? Add User to Group
? Enter the name of the group to which users will be added. Admin
? Do you want to edit your add-to-group function now? Y

使用以下代码更新函数,并配置 adminEmails 数组:

// amplify/backend/function/<function_name>/src/add-to-group.js

const aws = require('aws-sdk');

exports.handler = async (event, context, callback) => {
  const cognitoProvider = new
  aws.CognitoIdentityServiceProvider({
    apiVersion: '2016-04-18'
  });

  let isAdmin = false
  /* set your admin emails here */
  const adminEmails = ['user1@somedomain.com', 'user2@somedomain.com']

  // If the user is one of the admins, set the isAdmin variable to true
  if (adminEmails.indexOf(event.request.userAttributes.email) !== -1) {
    isAdmin = true
  }

  const groupParams = {
    UserPoolId: event.userPoolId,
  }

  const userParams = {
    UserPoolId: event.userPoolId,
    Username: event.userName,
  }

  if (isAdmin) {
    groupParams.GroupName = 'Admin',
    userParams.GroupName = 'Admin'

    // First check to see if the groups exists, and if not create the group
    try {
      await cognitoProvider.getGroup(groupParams).promise();
    } catch (e) {
      await cognitoProvider.createGroup(groupParams).promise();
    }

    // If the user is an administrator, place them in the Admin group
    try {
      await cognitoProvider.adminAddUserToGroup(userParams).promise();
      callback(null, event);
    } catch (e) {
      callback(e);
    }
  } else {
    // If the user is in neither group, proceed with no action
    callback(null, event)
  }
}

现在,身份验证服务已设置好,我们可以继续下一步:创建 AppSync API。

AppSync API

接下来,我们将创建 AppSync GraphQL API。请记住,对于此 API,我们需要为公共和受保护访问启用多种身份验证类型。这可以通过 CLI 全部启用。

要添加 AppSync API,我们将使用 api 类别:

~ amplify add api

? Please select from one of the below mentioned services: GraphQL
? Provide API name: festivalapi
? Choose an authorization type for the API: Amazon Cognito User Pool
Do you want to configure advanced settings for the GraphQL API: Yes
? Configure additional auth types? Y
? Choose the additional authorization types you want to configure for the API:
  API key
? Enter a description for the API key: public (or a custom description)
? After how many days from now the API key should expire: 365 (or a custom
  expiration date)
? Configure conflict detection? N
? Do you have an annotated GraphQL schema? N
? Do you want a guided schema creation? Y
? What best describes your project: Single object with fields
? Do you want to edit the schema now? Y

这将在您的文本编辑器中打开 GraphQL 模式,位于 amplify/backend/api/festivalapi/schema.graphql

我们将使用的架构有两种主要类型,一个是 Stage,另一个是 Performance。使用以下架构并继续(我们将在下一步中详细介绍其工作原理):

type Stage @model
  @auth(rules: [
  { allow: public, operations: [read] },
  { allow: groups, groups: ["Admin"] }
]) {
  id: ID!
  name: String!
  performances: [Performance] @connection(keyName: "byStageId", fields: ["id"])
}

type Performance @model
  @key(name: "byStageId", fields: ["performanceStageId"])
  @auth(rules: [
  { allow: public, operations: [read] },
  { allow: groups, groups: ["Admin"] }
]) {
  id: ID!
  performanceStageId: ID!
  productID: ID
  performer: String!
  imageUrl: String
  description: String!
  time: String
  stage: Stage @connection
}

让我们看看我们使用的指令及其工作原理。

@auth

首先,@auth 指令允许我们传入一个授权规则数组。每个规则都有一个 allow 字段(必填),以及其他元数据(可选),包括指定提供者(如果与默认授权类型不同)。

StagePerformance 类型中,我们使用了两种授权类型,一种是组访问 (groups),另一种是公共访问 (public)。您会注意到,对于公共访问,我们还设置了一个操作数组。此数组应包含我们希望在 API 上启用的操作列表。如果没有列出操作,则默认情况下将启用所有操作。

@key

@key 指令使我们能够为自定义数据访问模式向 DynamoDB 表中添加 GSI 和排序键。在上述架构中,我们创建了一个名为 byStageIdkey,它允许我们使用名为 performanceStageId 的字段(在 Performance 表上)查询舞台 ID 对应的表演。然后 performances 字段的解析器将使用舞台的 ID 查询对应的表演。

@connection

@connection 指令允许我们建立类型之间的关系模型。可以创建的关系类型有属于、一对多、多对一或多对多。在此示例中,我们创建了两种关系:

  • 一个舞台和表演之间的关系(一个舞台有多个表演)

  • 表演和舞台之间的关系(一个表演属于一个舞台)

部署服务

所有服务配置完成后,我们可以部署后端:

~ amplify push

服务已部署,我们可以开始编写客户端代码了。

构建前端

现在项目已创建和配置,后端已部署,我们可以开始设置客户端!

我们要做的第一件事是创建应用程序所需的文件:

~ cd src
~ touch Container.js Footer.js Nav.js Admin.js Router.js Performance.js Home.js

接下来我们需要打开 src/index.js 文件,添加 Amplify 配置,导入 Ant Design 样式,并用我们即将创建的路由器替换主组件。使用以下代码更新文件:

/* src/index.js */
import React from 'react';
import ReactDOM from 'react-dom';
import Router from './Router';
import 'antd/dist/antd.css';

import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)

ReactDOM.render(<Router />, document.getElementById('root'));

容器

现在,让我们创建 Container 组件,作为一个可重用的组件,为我们的视图添加填充和样式:

/* src/Container.js */
import React from 'react'

export default function Container({ children }) {
  return (
    <div style={container}>
      {children}
    </div>
  )
}

const container = {
  padding: '30px 40px',
  minHeight: 'calc(100vh - 120px)'
}

底部

在这里,我们将创建Footer组件,它将作为可重用组件添加基本页脚,以及一个链接供管理员注册和登录:

/* src/Footer.js */
import React from 'react'
import { Link } from 'react-router-dom'

function Footer() {
  return (
    <div style={footerStyle}>
      <Link to="/admin">
        Admins
      </Link>
    </div>
  )
}

const footerStyle = {
  borderTop: '1px solid #ddd',
  display: 'flex',
  alignItems: 'center',
  padding: 20
}

export default Footer

导航

现在,打开src/Nav.js来创建基本导航。只会有一个链接:一个链接回到主视图,其中将包含所有的演出和表演:

/* src/Nav.js */
import React from 'react'
import { Link } from 'react-router-dom'
import { Menu } from 'antd'
import { HomeOutlined } from '@ant-design/icons'

const Nav = (props) => {
  const { current } = props
  return (
    <div>
      <Menu selectedKeys={[current]} mode="horizontal">
        <Menu.Item key='home'>
          <Link to={`/`}>
            <HomeOutlined />Home
          </Link>
        </Menu.Item>
      </Menu>
    </div>
  )
}

export default Nav

管理员

我们将创建的管理组件现在只做三件事:允许用户注册、登录和退出。此组件的理念是为管理员提供一种注册的方式,以便他们可以作为管理员创建和管理 API。

提示

记住,当有人注册时,如果他们的电子邮件在 Lambda 触发器中启用,他们将在注册后被放置在管理员组中。然后他们将能够执行变更以创建、更新和删除阶段和表演。

如果您需要更新后端代码(如 GraphQL 模式或 Lambda 函数),您可以在本地进行更改,然后运行amplify push以部署更改到后端:

/* src/Admin.js */
import React from 'react'
import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react'
import { Auth } from 'aws-amplify'
import { Button } from 'antd'

function Admin() {
  return (
    <div>
      <h1 style={titleStyle}>Admin</h1>
      <AmplifySignOut />
    </div>
  )
}

const titleStyle = {
  fontWeight: 'normal',
  margin: '0px 0px 10px 0px'
}

export default withAuthenticator(Admin)

路由器

现在让我们创建路由器:

/* src/Router.js */
import React, { useState, useEffect } from 'react'
import { HashRouter, Switch, Route } from 'react-router-dom'

import Home from './Home'
import Admin from './Admin'
import Nav from './Nav'
import Footer from './Footer'
import Container from './Container'
import Performance from './Performance'

const Router = () => {
  const [current, setCurrent] = useState('home')
  useEffect(() => {
    setRoute()
    window.addEventListener('hashchange', setRoute)
    return () =>  window.removeEventListener('hashchange', setRoute)
  }, [])
  function setRoute() {
    const location = window.location.href.split('/')
    const pathname = location[location.length-1]
    setCurrent(pathname ? pathname : 'home')
  }
  return (
    <HashRouter>
      <Nav current={current} />
      <Container>
        <Switch>
          <Route exact path="/" component={Home}/>
          <Route exact path="/performance/:id" component={Performance} />
          <Route exact path="/admin" component={Admin}/>
        </Switch>
      </Container>
      <Footer />
    </HashRouter>
  )
}

export default Router

在此组件中,我们将路由器与持久 UI 组件(如容器和页脚)结合起来。

应用程序有三个路由:

主页

这是渲染阶段和表演的主要路由。

性能

这是将渲染单个表演和围绕表演的详细信息的路由。

管理员

这是将为管理员渲染注册/登录页面的路由。

在性能路由中,您将看到我们使用类似以下路径的路径:

/performance/:id

这样做可以使我们拥有 URL 参数,因此如果我们命中这样的路由,我们将能够轻松从 URL 中提取 ID:

/performance/100

命中带有 URL 参数的路由将允许我们在组件本身访问它们。这非常有用,因为我们将使用性能的 ID 来获取性能详情,并且在路由参数中轻松访问它们使此功能变得可能。它还使您能够轻松构建支持深链接的应用程序。

性能

接下来,让我们创建Performance组件:

/* src/Performance.js */
import React, { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
import { getPerformance } from './graphql/queries'
import { API } from 'aws-amplify'

function Performance() {
  const [performance, setPerformance] = useState(null)
  const [loading, setLoading] = useState(true)

  let { id } = useParams()
  useEffect(() => {
    fetchPerformanceInfo()
  }, [])
  async function fetchPerformanceInfo() {
    try {
      const talkInfo = await API.graphql({
        query: getPerformance,
        variables: { id },
        authMode: 'API_KEY'
      })
      setPerformance(talkInfo.data.getPerformance)
      setLoading(false)
    } catch (err) {
      console.log('error fetching talk info...', err)
      setLoading(false)
    }
  }

  return (
    <div>
      <p>Performance</p>
      { loading && <h3>Loading...</h3>}
      {
        performance && (
          <div>
            <h1>{performance.performer}</h1>
            <h3>{performance.time}</h3>
            <p>{performance.description}</p>
          </div>
        )
      }
    </div>
  )
}

export default Performance

此组件的渲染方法非常基础;它只是呈现性能performertimedescription。这个组件有趣的地方在于我们如何获取这些信息。我们通过以下流程来实现:

  1. 我们使用useState钩子创建了两个状态变量:loading(设置为 true)和performance(设置为 null)。我们还创建了一个名为id的变量,该变量使用 React Router 的useParams助手获取id的路由参数。

  2. 当组件加载时,我们使用useEffect钩子立即调用fetchPerformanceInfo函数。

  3. fetchPerformanceInfo函数将使用路由参数中的id来调用 AppSync API。这里的 API 调用使用API.graphql,传入variablesqueryauthMode。默认情况下,我们的 API 使用 Cognito 用户池作为认证模式。每当我们需要覆盖这一设置,例如在本例中进行公共 API 调用时,我们需要在 API 调用中指定authMode

  4. 从 API 返回数据后,我们调用setLoadingsetPerformance来更新 UI 并渲染从 API 返回的数据。

主页

现在,让我们创建最后一个组件,Home组件:

/* src/Home.js */
import React, { useEffect, useState } from 'react'
import { API } from 'aws-amplify'
import { listStages } from './graphql/queries'
import { Link } from 'react-router-dom'
import { List } from 'antd';

function Home() {
  const [stages, setStages] = useState([])
  const [loading, setLoading] = useState(true)
  useEffect(() => {
    getStages()
  }, [])
  async function getStages() {
    const apiData = await API.graphql({
      query: listStages,
      authMode: 'API_KEY'
    })
    const { data: { listStages: { items }}} = apiData
    setLoading(false)
    setStages(items)
  }

  return (
    <div>
     <h1 style={heading}>Stages</h1>
      { loading && <h2>Loading...</h2>}
      {
        stages.map(stage => (
          <div key={stage.id} style={stageInfo}>
            <p style={infoHeading}>{stage.name}</p>
            <p style={infoTitle}>Performances</p>
            <List
              itemLayout="horizontal"
              dataSource={stage.performances.items}
              renderItem={performance => (
                <List.Item>
                  <List.Item.Meta
                   title={<Link style={performerInfo}
                   to={`/performance/${
                         performance.id}`}>{
                         performance.performer}</Link>
                   }
                   description={performance.time}
                  />
                </List.Item>
              )}
            />
          </div>
        ))
      }
    </div>
  )
}

const heading = { fontSize: 44, fontWeight: 300, marginBottom: 5 }
const stageInfo = { padding: '20px 0px 10px', borderBottom: '2px solid #ddd' }
const infoTitle = { fontWeight: 'bold' , fontSize: 18 }
const infoHeading = { fontSize: 30, marginBottom: 5 }
const performerInfo = { fontSize: 24 }

export default Home

这个组件中的逻辑实际上与我们在Performance组件中所做的非常相似:

  1. 使用useState钩子创建两个主要的状态变量:stages(设置为空数组)和loading(设置为true)。

  2. 当应用程序加载时,我们使用具有自定义authModeAPI_KEYAPI类调用 AppSync API。

  3. 当数据从 API 返回时,设置阶段状态并将 loading 设置为 false。

现在,应用程序已完成,但还有一件事。因为我们已为性能解析器创建了自定义访问模式,所以我们需要更新listStages查询定义,以便也返回性能。为此,请使用以下内容更新listStages查询:

/* src/graphql/queries.js */

export const listStages = /* GraphQL */ `
  query ListStages(
    $filter: ModelStageFilterInput
    $nextToken: String
  ) {
    listStages(filter: $filter, limit: 500, nextToken: $nextToken) {
      items {
        id
        name
        performances {
          items {
            id
            time
            performer
            description
          }
        }
      }
      nextToken
    }
  }
`;

现在,应用程序已完成,我们可以填充一些数据。启动应用程序并使用管理员用户注册:

~ npm start

在页脚点击Admins链接进行注册。注册完成后,打开 AppSync 控制台:

~ amplify console api

> Choose GraphQL

在控制台的查询面板中,您需要点击使用用户池登录,使用刚创建的用户的用户名和密码进行登录。当要求输入 ClientID 时,请使用位于本地项目的aws-exports.js文件中的aws_user_pools_web_client_id

接下来,创建至少一个阶段和一个性能:

mutation createStage {
  createStage(input: {
    id: "stage-1"
    name: "Stage 1"
  }) {
    id name
  }
}

mutation createPerformance {
  createPerformance(input: {
    performanceStageId: "stage-1"
    performer: "Dreek"
    description: "Dreek LIVE in NYC! Don't miss out, performing
                  all of the hits with a few surprise performances!"
    time: "Monday, May 4 2022"
  }) {
    id performer description
  }
}

现在,我们的数据库中有一些数据,我们应该能够在应用程序中查看它,并在每个性能的主视图和详细视图之间进行导航!

摘要

从本章节中需要记住的几点如下:

  • GraphQL Transform 指令使您能够为 GraphQL API 添加强大的功能,如授权规则、关系和自定义索引,以支持额外的数据访问模式。

  • @auth指令允许您传入一系列规则来定义类型和字段的授权规则。

  • @connection指令使您能够建模 GraphQL 类型之间的关系。

  • @key指令使您能够为自定义数据访问模式定义自定义索引,并增强现有关系。

  • 当创建具有多种授权类型的 API 时,您将有一个Primary授权类型,这将是在进行 API 调用时的默认类型。每当您需要覆盖Primary授权类型时,必须将authMode参数传递给定义您想要使用的授权类型的API类。

第九章:使用 Amplify DataStore 构建离线应用程序

到目前为止,在本书中,我们已经使用过 REST API 和 GraphQL API。在使用 GraphQL API 时,我们使用 API 类直接调用突变和查询。

Amplify 还支持与 AppSync 交互的另一种 API 类型:Amplify DataStore。DataStore 与传统的 GraphQL API 有所不同的方法。

与直接使用查询和突变与 GraphQL API 本身进行交互不同,DataStore 引入了一个客户端 SDK,允许您写入和从本地存储读取,并使用平台的本地存储引擎(例如,Web 使用 IndexDB,本地 iOS 和 Android 使用 SQLite)持久化此数据。随着本地和远程数据的更新,DataStore 会自动将本地数据同步到 GraphQL 后端。

使用 DataStore SDK,然后只需执行保存、更新和删除等操作,直接写入 DataStore 本身。当您连接到互联网时,DataStore 会同步您的数据到云端;如果您没有联网,则会将数据排队,等待下次连接时处理。

DataStore 还为您处理冲突检测和解决冲突,提供三种内置的冲突解决策略之一:

自动合并

在运行时检查对象上的 GraphQL 类型信息以执行合并操作(建议选项)。

乐观并发性

将最新写入的项目与传入记录的版本进行检查。

自定义

使用 Lambda 函数并写入任何自定义业务逻辑,以便在合并或拒绝更新时进行处理。

关于 Amplify DataStore

Amplify DataStore 是以下内容的结合:

  • AppSync GraphQL API

  • 本地存储库和同步引擎,还可以离线持久化数据

  • 用于与本地存储库交互的客户端 SDK

  • 由 Amplify CLI 生成的特殊启用同步的 GraphQL 解析器,使服务器上的复杂冲突检测和冲突解决成为可能

Amplify DataStore 概述

在开始使用 DataStore 时,您仍然会像在过去的章节中一样创建 API。主要区别在于,在创建 API 时,您将在 CLI 流程的高级设置中启用冲突检测

从那里,要在客户端上启用 DataStore,我们需要为 DataStore 创建模型,以便与存储库进行交互。这可以通过使用您已有的 GraphQL 模式,并从 CLI 运行构建命令amplify codegen models轻松完成。

现在,您已经设置好了,可以开始与 DataStore 交互。

Amplify DataStore 操作

要与 Store 进行交互,首先从 Amplify 导入DataStore API 和您想要使用的模型。从那里,您可以执行与存储相关的操作。

请参见表 9-1 了解一些可用操作。

表 9-1。Amplify DataStore 操作

操作 命令
导入模型和 DataStore API import { DataStore } from '@aws-amplify/datastore' import { Message} from './models'
保存数据 await DataStore.save( new Message({ title: 'Hello World', sender: 'Chris' }) ))
读取数据 const posts = await DataStore.query(Post)
删除数据 const message = await DataStore.query(Message, '123') DataStore.delete(message)
更新数据 const message = await DataStore.query(Message, '123') await DataStore.save( Post.copyOf(message, updated => { updated.title = 'My new title' }) )
观察/订阅数据变更以实现实时功能 const subscription = DataStore.observe(Message).subscribe(msg => { console.log(message.model, message.opType, message.element) });

DataStore 谓词

您可以对 DataStore 应用谓词过滤器,使用在您的 GraphQL 类型上定义的字段,以及 DynamoDB 支持的以下条件:

Strings: eq | ne | le | lt | ge | gt | contains | notContains | beginsWith
            | between
Numbers: eq | ne | le | lt | ge | gt | between
Lists: contains | notContains

例如,如果您想要所有标题中包含“Hello”的消息列表:

const messages = await DataStore
  .query(Message, m =>
m.title('contains', 'Hello'))

您还可以将多个谓词链接为单个操作:

const message = await DataStore
  .query(Message, m => m.title('contains', 'Hello').sender('eq', 'Chris'))

这些谓词使您能够以许多方式从本地数据中检索不同的选择集。而不是在客户端检索整个集合并进行过滤,您可以精确地从存储中查询您需要的数据。

使用 Amplify DataStore 构建离线和实时应用程序

我们将构建一个实时和离线优先的消息板应用程序,如图 Figure 9-1 所示。

实时消息板

图 9-1. 实时消息板

应用程序的用户可以创建新消息,所有其他用户都将实时接收消息。如果用户离线,他们将继续能够创建消息。一旦上线,消息将与后端同步,并且其他用户创建的所有其他消息也将被获取并在本地同步。

我们的应用程序将针对 DataStore API 执行三种类型的操作:

save

在 DataStore 中创建新项目;在本地保存项目并在后台执行 GraphQL 变更。

query

从 DataStore 中读取;返回单个项目或列表(数组)并在后台执行 GraphQL 查询。

observe

监听数据变更(创建、更新、删除),并在后台执行 GraphQL 订阅。

让我们开始吧。

创建基础项目

要开始,我们将创建一个新的 React 项目,初始化一个 Amplify 应用程序并安装依赖项。

我们将首先创建 React 项目:

~ npx create-react-app rtmessageboard
~ cd rtmessageboard

接下来,我们将安装本地依赖项。

Amplify 支持全面安装 Amplify 和 范围(模块化)安装特定 API 的安装。范围化的包可以减少包大小,因为我们只安装正在使用的代码。由于我们只使用 DataStore API,我们可以安装范围化的 DataStore 包。

我们还将安装 Ant Design(antd)进行样式设计,React Color(react-color)用于易于使用的颜色选择器,并为 Amplify Core 配置创建作用域依赖项,以便仍然使用aws-exports.js配置 Amplify 应用:

~ npm install @aws-amplify/core @aws-amplify/datastore antd react-color

接下来,初始化一个新的 Amplify 项目:

~ amplify init

# Follow the steps to give the project a name, environment name, and set the
  default text editor.
# Accept defaults for everything else and choose your AWS Profile.

创建 API

现在我们将创建 AppSync GraphQL API:

~ amplify add api

? Please select from one of the below mentioned services: GraphQL
? Provide API name: rtmessageboard
? Choose the default authorization type for the API: API key
? Enter a description for the API key: public
? After how many days from now the API key should expire (1-365): 365 (or your
  preferred expiration)
? Do you want to configure advanced settings for the GraphQL API: Yes
? Configure additional auth types: N
? Configure conflict detection: Y
? Select the default resolution strategy: Auto Merge
? Do you have an annotated GraphQL schema: N
? Do you want a guided schema creation: Y
? What best describes your project: Single object with fields
? Do you want to edit the schema now: Y

使用以下类型更新模式:

type Message @model {
  id: ID!
  title: String!
  color: String
  image: String
  createdAt: String
}

现在我们已经创建了 GraphQL API,并且有一个 GraphQL 模式可以使用,我们可以为使用本地 DataStore API(基于 GraphQL 模式)的模型创建所需的模型:

~ amplify codegen models

这将在项目中创建一个名为models的新文件夹。使用此文件夹中的模型,我们可以开始与 DataStore API 交互。部署 API:

~ amplify push --y

后端部署完成后,我们可以开始编写客户端代码。

编写客户端代码

首先,在src/index.js中打开并在最后一个导入下方添加以下代码配置 Amplify 应用:

import 'antd/dist/antd.css'
import Amplify from '@aws-amplify/core'
import config from './aws-exports'
Amplify.configure(config)

注意,我们从@aws-amplify/core而不是aws-amplify进行导入。

接下来,在App.js中打开并使用以下代码更新它:

/* src/App.js */
import React, { useState, useEffect } from 'react'
import { SketchPicker } from 'react-color'
import { Input, Button } from 'antd'
import { DataStore } from '@aws-amplify/datastore'
import { Message} from './models'

const initialState = { color: '#000000', title: '' }
function App() {
  const [formState, updateFormState] = useState(initialState)
  const [messages, updateMessages] = useState([])
  const [showPicker, updateShowPicker] = useState(false)
  useEffect(() => {
    fetchMessages()
    const subscription = DataStore
      .observe(Message)
      .subscribe(() => fetchMessages())
    return () => subscription.unsubscribe()
  }, [])
  async function fetchMessages() {
    const messages = await DataStore.query(Message)
    updateMessages(messages)
  }
  function onChange(e) {
    if (e.hex) {
      updateFormState({ ...formState, color: e.hex})
    } else { updateFormState({ ...formState, [e.target.name]: e.target.value}) }
  }
  async function createMessage() {
    if (!formState.title) return
    await DataStore.save(new Message({ ...formState }))
    updateFormState(initialState)
  }
  return (
    <div style={container}>
      <h1 style={heading}>Real Time Message Board</h1>
      <Input
        onChange={onChange}
        name="title"
        placeholder="Message title"
        value={formState.title}
        style={input}
      />
      <div>
        <Button
        onClick={() => updateShowPicker(!showPicker)}
        style={button}
        >Toggle Color Picker</Button>
        <p>Color:
          <span
           style={{fontWeight: 'bold', color: formState.color}}>{formState.color}
          </span>
        </p>
      </div>
      {
        showPicker && (
          <SketchPicker
           color={formState.color}
           onChange={onChange} /
          >
        )
      }
      <Button type="primary" onClick={createMessage}>Create Message</Button>
      {
        messages.map(message => (
          <div
            key={message.id}
            style={{...messageStyle, backgroundColor: message.color}}
          >
            <div style={messageBg}>
              <p style={messageTitle}>{message.title}</p>
            </div>
          </div>
        ))
      }
    </div>
  );
}

const container = { width: '100%', padding: 40, maxWidth: 900 }
const input = { marginBottom: 10 }
const button = { marginBottom: 10 }
const heading = { fontWeight: 'normal', fontSize: 40 }
const messageBg = { backgroundColor: 'white' }
const messageStyle = { padding: '20px', marginTop: 7, borderRadius: 4 }
const messageTitle = { margin: 0, padding: 9, fontSize: 20  }

export default App

让我们逐步了解此组件中正在进行的最重要部分:

  1. 我们还从 Amplify 导入DataStore API 以及Message模型。

  2. 我们使用useState钩子创建三个组件状态的部分:

    formState

    此对象管理表单的状态,包括用于显示消息背景颜色的titlecolor

    messages

    一旦从 DataStore 获取了消息数组,这将管理这些消息数组。

    showPicker

    这将管理一个布尔值,该值将被切换以显示和隐藏用于填充消息的color值的颜色选择器(默认情况下,颜色设置为黑色并保存在formState中)。

  3. 当组件加载(在useEffect中)时,我们通过调用fetchMessages函数获取所有消息,并创建订阅(DataStore.observe)以监听消息更新。订阅触发时,我们再次调用fetchMessages函数,因为我们知道已经有更新,并且我们希望使用从 API 返回的最新数据更新应用。

  4. fetchMessages函数调用DataStore.query,然后使用返回的消息数组更新组件状态。

  5. onChange处理程序处理表单输入的更新以及更改颜色选择器。

  6. createMessage中,我们首先检查确保标题字段已填写。如果填写了,我们使用DataStore.save保存消息,然后重置表单状态。

让我们来测试一下:

~ npm start

测试离线功能

尝试离线,创建新的突变,然后再次在线。您应该注意到,当再次在线时,应用程序将在数据库中创建在离线时创建的所有消息。

要验证此内容,请在 AWS 控制台中打开 AppSync API:

~ amplify console api

? Please select from one of the below mentioned services: GraphQL

接下来,点击数据源,然后打开消息表资源。现在您应该能看到消息表中的项目。

测试实时功能

为了测试实时功能,打开另一个浏览器窗口,这样你就可以在两个窗口中同时运行同一个应用程序。然后在一个窗口中创建一个新项目,看看更新是否会自动在另一个窗口中显示出来。

摘要

从本章中请记住以下几点:

  • Amplify 使两个不同的 API 能够与 AppSync 交互:API 类别以及 DataStore。

  • 使用 DataStore 时,你不再直接向 API 发送 HTTP 请求。相反,你是在写入本地存储引擎,然后 DataStore 负责同步到云端。

  • Amplify DataStore 默认离线工作。

第十章:使用图像和存储工作

许多应用程序需要一种管理文件、图像和视频存储的方法。虽然可以将这些对象转换为二进制数据并直接存储在数据库中,但通常最好不要这样做。相反,使用像 Amazon S3 这样的托管文件托管服务更好,因为它更便宜、更快速,同样安全。

在本章中,我们将看看如何创建一个照片分享应用程序,实时以流方式呈现带有图像和图像说明的帖子,允许您分享图像。

使用 Amazon S3

Amazon S3 允许您拥有随需扩展的安全文件托管。Amplify 使用 S3 作为处理文件(如图像、视频、PDF 等)存储的 Storage 类别。

在与 S3 一起工作时,通常会有三种类型的文件访问可用:

公共

具有公共访问权限的项目将可供应用程序所有用户访问。这些文件存储在您的 S3 存储桶中的public/路径下。但是,公共并不意味着任何人都可以使用资源的 URL 查看它。为了查看,您必须使用 Amplify SDK 检索资源的临时签名 URL。该签名 URL 将在一段时间后(默认为 15 分钟)过期。

私人

项目对所有用户可读,但仅创建用户可写。在 S3 中,文件存储在路径private/{user_identity_id}下,其中user_identity_id对应于该用户的唯一 Amazon Cognito ID。

受保护

这些文件仅对个人用户可访问。文件存储在路径private/{user_identity_id}下,其中user_identity_id对应于该用户的唯一 Amazon Cognito ID。

默认情况下,存储文件时将设置为public,除非另有指定:

await Storage.put('test.txt', 'Hello')

如果希望指定privateprotected访问权限,则在保存时需要指定级别:

/* Private level access */
await Storage.put('test.txt', 'Private Content', {
  level: 'private',
  contentType: 'text/plain'
})

/* Protected level access */
await Storage.put('test.txt', 'Protected Content', {
  level: 'protected',
  contentType: 'text/plain'
})

存储类别使用 Amazon S3 存储文件类型,包括图像、PDF、视频、文本文件等。

要将所有内容组合在一起,我们将使用 GraphQL API 结合 Amazon S3 作为应用程序后端。GraphQL 模式将保存图像标题、存储在 S3 中的图像键和唯一 ID 的字段。

让我们看看我们将使用的模式:

type Post @model {
  id: ID!
  title: String!
  imageKey: String!
}

创建新帖子时,需要执行两个操作:

  • 图像被赋予唯一的键并存储在 S3 存储桶中。

  • 帖子元数据,包括存储在 GraphQL API 中的图像键。

阅读帖子时,将按以下顺序进行:

  1. 从 GraphQL API 中读取帖子列表的 GraphQL 查询。

  2. 遍历帖子数组,获取每个帖子列表中图像的签名 URL。

  3. 使用签名 URL 渲染帖子作为图像来源。

在本章中,我们将构建一个示例,实现了一个非常常见和有用的模式,用于构建依赖 API 与大对象引用(如存储在 S3 中的图像、视频和文件等)的应用程序。

创建基础项目

要开始,我们将创建一个新的 React 项目,初始化一个 Amplify 应用,并安装依赖项。

首先,我们将创建 React 项目:

~ npx create-react-app photo-app
~ cd photo-app

接下来,我们将安装本地依赖项。此项目将使用 Ant Design 进行样式化 (antd),使用 UUID 包创建唯一标识符 (uuid),以及 AWS Amplify 和 AWS Amplify React 包。

~ npm install antd uuid aws-amplify @aws-amplify/ui-react

接下来,初始化一个新的 Amplify 项目:

~ amplify init

# Follow the steps to give the project a name, environment name, and set the
  default text editor.
# Accept defaults for everything else and choose your AWS Profile.

添加认证

接下来,使用 auth 类别添加认证:

~ amplify add auth

? Do you want to use the default authentication and security configuration?
  Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings? No, I am done.

创建 API

接下来,我们将创建 AppSync GraphQL API:

~ amplify add api

? Please select from one of the below mentioned services: GraphQL
? Provide API name: photoapp
? Choose an authorization type for the API: Amazon Cognito User Pool
? Do you want to configure advanced settings for the API? No
? Do you have an annotated GraphQL schema? N
? Do you want a guided schema creation? Y
? What best describes your project: Single object with fields
? Do you want to edit the schema now? Yes

对于 GraphQL 架构,请使用以下内容:

type Post @model {
  id: ID!
  title: String!
  imageKey: String!
}

最后,我们将使用 storage 类别添加存储:

~ amplify add storage

? Please select from one of the below mentioned services: Content
? Please provide a friendly name for your resource that will be used to label
  this category in the project: photos
? Please provide bucket name: <your_unique_bucket_name>
? Who should have access: Auth users only
? What kind of access do you want for Authenticated users? Choose all
  (create / update, read, & delete)
? Do you want to add a Lambda Trigger for your S3 Bucket? N

现在服务已配置完成,准备部署:

~ amplify push

现在后端已经部署完成,我们可以开始编写客户端代码。

编写客户端代码

首先,打开 src/index.js 并通过在最后一个 import 下添加以下代码来配置 Amplify 应用:

import 'antd/dist/antd.css'
import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)

此应用将有两个视图:一个用于列出帖子,另一个用于创建帖子。接下来,在 src 目录中创建这两个视图的两个新组件:

~ cd src
~ touch Posts.js CreatePost.js
~ cd ..

接下来,打开 src/App.js 并使用以下代码进行更新:

/* src/App.js */
import React, { useState } from 'react';
import { Radio } from 'antd'
import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react'
import Posts from './Posts'
import CreatePost from './CreatePost'

function App() {
  const [viewState, updateViewState] = useState('viewPosts')

  return (
    <div style={container}>
      <h1>Photo App</h1>
      <Radio.Group
        value={viewState}
        onChange={e => updateViewState(e.target.value)}
      >
        <Radio.Button value="viewPosts">View Posts</Radio.Button>
        <Radio.Button value="addPost">Add Post</Radio.Button>
      </Radio.Group>
      {
        viewState === 'viewPosts' ? (
          <Posts />
        ) : (
          <CreatePost updateViewState={updateViewState} />
        )
      }
      <AmplifySignOut />
    </div>
  );
}

const container = { width: 500, margin: '0 auto', padding: 50 }

export default withAuthenticator(App);

此组件导入 PostsCreatePost 组件,并根据 viewState 组件状态渲染其中一个。

要创建 viewState,我们使用了 useState hook。要切换 viewState 的值,我们使用 Ant Design 渲染了一个单选按钮组,以查看帖子(查看帖子)或添加新帖子(添加帖子)。

接下来,打开 src/CreatePost.js 并使用以下代码进行更新:

/* src/CreatePost.js */
import React, { useState } from 'react';
import { Button, Input } from 'antd'
import { v4 as uuid } from 'uuid'
import { createPost } from './graphql/mutations'
import { API, graphqlOperation, Storage } from 'aws-amplify'

const initialFormState = {
  title: '',
  image: {}
}

function CreatePost({ updateViewState }) {
  const [formState, updateFormState] = useState(initialFormState)

  function onChange(key, value) {
    updateFormState({ ...formState, [key]: value })
  }

  function setPhoto(e) {
    if (!e.target.files[0]) return
    const file = e.target.files[0]
    updateFormState({ ...formState, image: file })
  }

  async function savePhoto() {
    const { title, image } = formState
    if (!title || !image.name ) return

    const imageKey =
      uuid() + formState.image.name.replace(/\s/g, '-').toLowerCase()
    await Storage.put(imageKey, formState.image)
    const post = { title, imageKey }
    await API.graphql(graphqlOperation(createPost, { input: post }))
    updateViewState('viewPosts')
  }

  return (
    <div>
      <h2 style={heading}>Add Photo</h2>
      <Input
        onChange={e => onChange('title', e.target.value)}
        style={withMargin}
        placeholder="Title"
      />
      <input
        type='file'
        onChange={setPhoto}
        style={button}
      />
      <Button
       style={button}
       type="primary"
       onClick={savePhoto}
      >
      Save Photo</Button>
    </div>
  );
}

const heading = { margin: '20px 0px' }
const withMargin = { marginTop: 10 }
const button = { marginTop: 10 }

export default CreatePost

关于此组件

在此组件中,我们允许用户上传图像并使用图像和标题创建新帖子:

  1. 此组件持有的状态存储在 formState 对象中,使用 useState hook 创建。此对象包含帖子的 title 和帖子的 image

  2. onChange 在用户输入时更新 formStatetitle

  3. setPhoto 允许用户上传图片并将其存储在 formState 中的 image 字段中。

  4. savePhoto 是我们将图像存储在 S3 并使用 GraphQL mutation 将帖子信息保存到 AppSync 的地方:

    1. 我们首先使用图像的 nameuuid 的组合创建一个名为 imageKey 的变量。

    2. 然后,我们使用 imageKey 将图像存储在 S3 中作为参考。

    3. 图像存储后,我们通过 AppSync 进行 API 调用,使用 GraphQL Mutation 创建一个新的 Post,并传递帖子的 titleimageKey 作为字段。

接下来,打开 src/Posts.js 并使用以下代码进行更新:

/* src/Posts.js */
import React, { useReducer, useEffect } from 'react';
import { listPosts } from './graphql/queries'
import { onCreatePost } from './graphql/subscriptions'
import { API, graphqlOperation, Storage } from 'aws-amplify'

function reducer(state, action) {
  switch(action.type) {
    case 'SET_POSTS':
      return  action.posts
    case 'ADD_POST':
      return [action.post, ...state]
    default:
      return state
  }
}

async function getSignedPosts(posts) {
  const signedPosts = await Promise.all(
    posts.map(async item => {
      const signedUrl = await Storage.get(item.imageKey)
      item.imageUrl = signedUrl
      return item
    })
  )
  return signedPosts
}

function Posts() {
  const [posts, dispatch] = useReducer(reducer, [])

  useEffect(() => {
    fetchPosts()

    const subscription = API.graphql(graphqlOperation(onCreatePost)).subscribe({
      next: async post => {
        const newPost = post.value.data.onCreatePost
        const signedUrl = await Storage.get(newPost.imageKey)
        newPost.imageUrl = signedUrl
        dispatch({ type: 'ADD_POST', post: newPost })
      }
    })
    return () => subscription.unsubscribe()
  }, [])

  async function fetchPosts() {
    const postData = await API.graphql(graphqlOperation(listPosts))
    const { data: { listPosts: { items }}} = postData
    const signedPosts = await getSignedPosts(items)
    dispatch({ type: 'SET_POSTS', posts: signedPosts })
  }

  return (
    <div>
      <h2 style={heading}>Posts</h2>
      {
        posts.map(post => (
          <div key={post.id} style={postContainer}>
            <img style={postImage} src={post.imageUrl} />
            <h3 style={postTitle}>{post.title}</h3>
          </div>
        ))
      }
    </div>
  )
}

const postContainer = {
  padding: '20px 0px 0px',
  borderBottom: '1px solid #ddd'
}
const heading = { margin: '20px 0px' }
const postImage = { width: 400 }
const postTitle = { marginTop: 4 }

export default Posts

useReducer

在此组件中,我们使用 useReducer 钩子来管理应用程序状态。因为我们将使用 GraphQL 订阅来处理实时传入的数据,所以我们这样做。由于 useState 创建闭包,我们必须将组件外部的状态移到一个 reducer 中。

reducer 有两个操作,一个用于添加单个帖子 (ADD_POST),一个用于设置帖子数组 (SET_POSTS)。

关于此组件

此组件中有两件主要事情发生:

useEffect

当组件加载时,此钩子将触发,创建新的 GraphQL 订阅,然后调用我们将在下一步中讨论的 fetchPosts 函数:

  1. 订阅将监听通过 onCreatePost 订阅创建的新帖子。

  2. 当创建新帖子时,next 函数将触发,并且新帖子的数据将通过函数参数 (post) 传入。

  3. 然后,我们使用帖子图像 imageKey 通过 Storage API 获取签名 URL,调用 Storage.get

  4. 获得图像的签名 URL 后,我们将 imageURL 字段添加到帖子中,并分发 ADD_POST 以将新帖子添加到状态中。

fetchPosts

此函数从 API 获取帖子,然后调用 getSignedPosts,将帖子传递给它:

  1. getSignedPosts 函数将映射数组中的所有帖子,获取帖子中图像的签名 URL,并使用签名图像 URL 为帖子分配新的 imageUrl 字段。

  2. 返回已签名的帖子后,将调度 SET_POSTS,使用帖子数组更新状态。

就这样,现在我们应该能够运行应用程序并测试它:

~ npm start

为了测试订阅/实时功能,请尝试打开新窗口并在两个窗口中运行应用程序,在一个窗口中查看帖子,在另一个窗口中创建帖子。

摘要

从本章中记住以下几点:

  • 在处理存储时,不能直接通过其 URL 引用图像;必须使用 Storage.get 调用对其进行签名。

  • 一旦文件返回带有签名 URL,它将默认有效 15 分钟;之后将会过期。可以通过传递 expires 选项来覆盖此行为,设置 URL 的可用性。

  • 在处理图像数组时,可以映射整个数组并使用 Promise.all 获取数组中每个项目的签名 URL。

第十一章:托管:使用 CI 和 CD 将您的应用程序部署到 Amplify Console

现在我们已经看了如何构建我们的应用程序,接下来我们如何使它们在线并展示给世界看呢?在本章中,我们将看一些不同的使用 Amplify 的托管选项,以及如何使用自定义域名部署您的应用程序。

我们将使用的服务是 Amplify Console 托管服务。Amplify Console 是一个完全托管的托管服务,提供了一个简单的工作流程,用于部署静态站点和全栈无服务器应用程序。使用 Amplify Console,您可以使用 CLI、GitHub 仓库或手动方式部署您的代码,该服务将为您构建和部署您的应用程序。

当使用像 React、Vue、Angular 这样的框架,甚至像 Gatsby、Next 或 Nuxt 这样的框架时,通常需要运行一个构建阶段。这个阶段将使用像 webpack 这样的模块捆绑工具,将所有的 JavaScript、CSS 和图像文件创建为可部署的网站构建。

Amplify Console 将允许您配置应用程序的构建设置,以便当您准备部署新版本时,服务将能够获取您的原始文件,然后构建和部署您的应用程序到您的实时域名。

在本章中,我们将学习以下内容:

基于 CLI 的部署

使用我们的本地项目,我们将直接从 CLI 向 Amplify Console 托管部署一个应用程序。

基于 Git 的部署

使用 GitHub 仓库,我们将向 Amplify Console 托管部署一个应用程序,并学习如何在更改合并到主分支时触发新的构建。

访问控制

添加访问控制以通过用户名和密码限制对您分支的访问。

自定义域名

使用您的自定义域名进行部署。

让我们开始吧。

基于 CLI 的部署

在本节中,我们将学习如何直接从 CLI 将项目部署到 Amplify Console 托管。

要开始,请创建一个新的 React 应用程序:

~ npx create-react-app fullstack-app
~ cd full-stack-app
~ npm install aws-amplify @aws-amplify/ui-react

接下来,我们将初始化一个新的 Amplify 项目,并添加一个单一的服务,即身份验证:

~ amplify init
# Follow the steps to give the project a name, environment name, and set the
  default text editor.
# Accept defaults for everything else and choose your AWS Profile.

~ amplify add auth
? Do you want to use the default authentication and security configuration?
  Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings? No, I am done.

当运行init命令时,我们将按照所有先前章节中的相同问题步骤进行操作。

我们被询问我们的源和分发目录是什么,以及构建命令是什么。默认情况下,Amplify CLI 将检测框架,并为您设置这些值,对于像我们的 React 项目这样的流行框架。

如果您使用的是 Amplify CLI 不认可的框架,或者有自定义的构建配置,您可能需要将这些值设置为不同的值。

要添加托管,我们可以使用hosting类别:

~ amplify add hosting

? Select the plugin module to execute: Hosting with Amplify Console
? Choose a type: Manual Deployment

接下来,让我们更新我们的前端代码,添加一个基本的问候语和认证功能。

首先打开src/index.js,并通过在最后一个导入项下添加以下代码配置 Amplify 应用程序:

import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)

然后更新src/App.js,添加以下代码:

import React from 'react'
import logo from './logo.svg'
import './App.css'

import { withAuthenticator } from '@aws-amplify/ui-react'

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <h1>Hello World!</h1>
      </header>
    </div>
  );
}

export default withAuthenticator(App, { includeGreetings: true })

我们的应用现在已准备部署。要同时部署前端和后端,可以运行publish命令。publish命令将前端和后端代码都部署到 Amplify 控制台:

~ amplify publish

现在,我们应该能够在控制台中查看应用程序,包括前端部署和后端服务配置:

~ amplify console

从 Amplify 控制台仪表板,点击刚刚部署的应用程序名称。在这里,您应该能够看到一个切换按钮,可以查看前端(前端环境)以及后端(后端环境)部署,如图 11-1 所示。

Amplify 控制台概述

图 11-1. Amplify 控制台概述

前端环境视图中,您应该能够点击域名以查看 Amplify 控制台托管的实时网站。域名 URL 应该看起来像这样:

https://env_name.deployment_id.amplifyapp.com

在左侧菜单中,有关自定义域名的域管理选项(在“自定义域名”中介绍)、构建事件的电子邮件通知、访问控制(本章我们将讨论的内容)、日志和重定向等选项。

当您更新并需要部署新版本时,可以再次运行publish命令以部署应用程序的更新版本。

基于 Git 的部署

现在让我们看看如何通过存储在 GitHub 仓库中的 Amplify 应用程序启用基于 Git 的部署。虽然从本地项目部署效果很好,但很多时候你会使用 Git 仓库进行个人或团队开发。Amplify 控制台支持 Git-based 托管应用程序,同时内置功能支持合并时自动部署和特性分支部署(将分支部署与每个特性分支链接起来)。

让我们看看如何将我们已经构建的应用程序从 GitHub 仓库部署到 Amplify 控制台。

第一步是删除我们设置的现有 Amplify 后端:

~ amplify delete

然后,创建一个新的 Amplify 应用并添加认证:

~ amplify init
# Follow the steps to give the project a name, environment name, and set the
  default text editor.
# Accept defaults for everything else and choose your AWS Profile.

~ amplify add auth
? Do you want to use the default authentication and security configuration?
  Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings? No, I am done.

现在,使用 Amplify 的push命令部署后端:

~ amplify push

现在我们需要创建一个 GitHub 仓库来存放应用程序。

创建 GitHub 仓库

接下来需要做的是转到 GitHub.com 并创建一个新的仓库。我将创建一个名为my-react-app的新仓库,如图 11-2 所示。

创建 GitHub 仓库

图 11-2. 创建 GitHub 仓库

创建仓库后,您将获得一个类似于图 11-3 所示的仓库 URI:

git@github.com:dabit3/my-react-app.git

GitHub 项目 URI

图 11-3. GitHub 项目 URI

复制此仓库 URI 并返回到命令行。在这里,我们将在本地应用程序中初始化一个新的 GitHub 项目:

~ git init
~ git remote add origin git@github.com:your_github_username/my-react-app.git

然后,将要跟踪的文件添加并将更改推送到我们的仓库:

~ git add .
~ git commit -m 'initial commit'
~ git push origin master

现在应用程序已推送至 GitHub,我们可以连接到 Amplify 控制台托管。为此,让我们通过 CLI 添加它:

~ amplify add hosting

? Select the plugin module to execute: Hosting with Amplify Console
? Choose a type: Continuous deployment (Git-based deployments)
注意

CLI 应该会在您的 Web 浏览器中打开 Amplify Console,使您能够选择 GitHub 作为您的源代码提供者。

  1. 作为您的第一步(在 Amplify Console 中),选择 GitHub 作为源代码提供者,然后单击连接分支。

  2. 接下来,使用 GitHub 登录,然后选择您刚创建的新仓库和主分支。单击下一步。

  3. 在配置构建设置页面中,当要求“选择后端环境”时,请选择您已经创建的环境名称。

  4. 接下来,在配置构建设置页面中,当要求“选择现有服务角色或创建新角色以便 Amplify Console 可以访问您的资源”时,请单击创建新角色以创建新的 IAM 角色:

    1. 单击下一步:权限,下一步:标签,下一步:审查,然后创建角色以创建新的 IAM 角色。

    2. 返回到配置构建设置页面,单击刷新按钮,并从下拉菜单中选择新创建的角色。

  5. 单击下一步。

  6. 在审查页面中,单击保存并部署以部署应用程序。

应用程序现已部署到 Amplify Console,并将开始新的构建。构建完成后,您应该会收到一个实时 URL 来查看您的应用程序。

基于 Git 的 CI/CD

现在应用程序已部署完成,让我们看看如何将 CD 实施到应用程序中。

基于 Git 的 CI/CD 的基本思想是,您可以通过直接推送到 Git 来部署和测试任何分支的构建。一旦合并更改,将启动新的构建并为您提供一个实时 URL 供您尝试。

这样,您可以进行功能/分支部署,如 prod(用于生产)、dev(用于开发)和 feature_name(用于新功能)。以这种方式构建时,您可以在实时环境中测试新的更改,不仅测试前端而且测试后端更改。

让我们尝试启动一个新的构建。为此,请对其中一个本地文件进行更改。更新 src/App.js 的一些文本,然后添加更改并推送到 GitHub:

~ git add .
~ git commit -m 'updates to App.js'
~ git push origin master

现在,当您在 Amplify Console 中打开应用程序时,您应该注意到已为您自动启动了新的构建。

访问控制

接下来,让我们看看如何启用访问控制以保护我们的部署。

使用访问控制,您可以指定访问者必须具有用户名和密码才能查看部署或特定分支部署。如果您正在测试希望对外团队保持不可发现的新私有功能,这将特别有用。

下面是如何启用访问控制的方法:

  1. 在左侧菜单中,单击访问控制。

  2. 接下来,单击管理访问。

  3. 在这里,对于主分支,请将访问设置设置为受限,然后设置用户名和密码。

现在,打开部署的 URL。您会注意到,如果不输入用户名和密码,您将无法查看它。

在访问控制菜单中,您还可以选择按分支设置访问控制。

自定义域名

最后,让我们学习如何为我们的应用程序使用自定义域名。

要启用自定义域名,我们需要做三件事情:

  • 在 Amazon Route53 中添加域名。

  • 在域名提供商的 DNS 设置中设置域名。

  • 配置 Amplify 控制台应用程序以使用在 Route53 中添加的域名。

让我们详细介绍如何做到这一点:

  1. 在 AWS 主仪表板的服务下拉菜单中搜索或点击 Route53。

  2. 点击托管区域。

  3. 点击创建托管区域。

  4. 通过向域名输入字段添加 URL 来设置域名,然后点击创建。

创建托管区域后,您将获得四个名称服务器值。在下一步中您将需要这些值,所以请将它们保持方便。您也可以随时通过访问 Route53 仪表板并点击您想要检索值的域名来返回到它们。名称服务器看起来可能是这样的:

ns-1020.awsdns-63.net
ns-1523.awsdns-62.org
ns-244.awsdns-30.com
ns-1724.awsdns-23.co.uk
  1. 现在,进入您的托管账户(例如 GoDaddy 或 Google Domains),并在您正在使用的域名的 DNS 设置中设置这些自定义名称服务器。

  2. 接下来,在您想要启用自定义域名的应用的 Amplify 控制台中,点击左侧菜单中的域名管理,然后点击添加域名按钮。

  3. 在这里,下拉菜单应该显示您在 Route53 中拥有的域名。选择这个域名,然后点击配置域名。

这将部署应用到您的自定义域名(DNS 传播需要 5 至 30 分钟)。

摘要

本章需要记住的几点内容:

  • Amplify 控制台同时托管后端和前端部署。

  • 将前端部署到 Amplify 控制台有两种主要方法,可以从您的本地项目或从 Git 存储库进行。您还可以手动上传项目或从 Dropbox 托管它们。

  • 一旦您的应用程序被托管,您可以通过在 Amplify 控制台中配置部署来设置诸如密码保护、自定义域名和分支部署等功能。

posted @ 2025-11-24 09:13  绝不原创的飞龙  阅读(9)  评论(0)    收藏  举报