
介绍网络接口的TGRS栈
TGRS是TypeScript、GraphQL、React和serverless的缩写。在过去的几年里,我们使用这个技术栈成功地构建了一些企业级的单页应用程序(SPA),因为它们可以很好地互补。在这篇文章中,我将谈谈我们选择这个堆栈中的技术的动机是什么,我们如何使用每种技术来获得最大的收益,哪些技术是必须的,哪些可以用其他东西代替。
动机
TGRS堆栈的出现是因为我们在现有的SPA工作中看到了三个问题:
第一个问题是失去控制的JavaScript。根据我们的经验,一个大型的JavaScript代码库有点像一个很小的孩子。对它的父母来说,它是一个美丽的东西,具有完美的意义,并且表现得像一个天使。对其他人来说,它是非常不可预测的,不可能理解,而且一般来说味道很难闻。更糟糕的是,随着JavaScript项目的发展,情况会恶化而不是改善(与小孩子相反)。
第二个问题是失控的数据加载代码。SPA的目标是为用户提供一个良好的、统一的体验,同时在幕后处理后端REST服务的杂乱集合,每一个都有自己的怪癖和具体的做事方式。然而,直接从浏览器联系所有这些REST服务并不是一个好主意。互联网不是一个特别快或可靠的网络。高延迟意味着你应该避免一个用户的互动需要在网上进行多次网络调用的情况。低带宽意味着在启动时应向浏览器发送尽可能少的代码,并且在随后的每个请求中应发送尽可能少的数据。
我们看到的SPA的最后一个问题是基础设施的过度使用。很多时候,当我们问当地的基础设施专家如何最好地托管一个适度的单页网页应用程序时,所提出的解决方案涉及到像在Docker容器中运行的Nginx,在ECS中运行,由ELB支撑。这对于提供一组静态资产来说似乎有些过分。对于前端开发者来说,最好的基础设施是尽可能少的基础设施。此外,我们宁愿自己拥有它,而不是等待几周的时间让别人为我们设置所有的基础设施,然后在有问题的时候不得不联系基础设施专家。
堆栈概述
TGRS堆栈的目的是解决我们在SPA中遇到的一些问题。TypeScript有助于保持JavaScript代码库的可管理性,因为它在增长。GraphQL为你提供了一个强类型的协议,用于在客户端和单个服务器之间进行通信。React为构建用户界面提供了一套出色的基元。最后,无服务器技术使你可以很容易地建立起提供SPA所需的最低限度的基础设施。
在我们继续之前,让我们明确一下,当我们说 "无服务器 "时,我们指的是无服务器技术的 一般概念,而不是无服务器框架。另外,在本篇文章的其余部分,我们在描述TGRS堆栈的无服务器组件时将使用AWS服务。这是因为AWS是我们通常用来构建这些Web应用的云平台。不过,如果你使用的是其他平台,你可以用适当的替代品来代替。
下图总结了TGRS栈的各个组成部分是如何结合在一起的。
你可以看到,首先,用户从AWS CloudFront分布中获取一个React应用程序(我们将其称为 "客户端"),并在浏览器中运行它。当客户端需要获取或接收数据时,它使用GraphQL与服务器通信。GraphQL服务器在AWS Lambda函数内运行,由一个AWS API网关端点前置。该服务器充当了一组上游服务的聚合层。这些上游服务通常有REST接口,尽管它们也可以是联合的GraphQL服务器。
这种架构有三个关键属性。首先,用户界面被认为是由客户端和服务器组成的。其次,它到处使用TypeScript。最后,无服务器技术被用于托管客户端和服务器。现在我们将深入了解堆栈的每个部分,看看它们是如何结合在一起的。
TypeScript
TypeScript是TGRS架构的核心。这是因为它可以帮助JavaScript代码库在变大后保持可管理性。回到我之前关于小孩子的比喻,TypeScript就像一个保姆,在你的孩子长大后给他们的生活带来一些纪律和秩序。
也就是说,为了最大限度地利用TypeScript,在使用它时启用几个关键标志是很重要的。
[noImplicitAny](https://www.typescriptlang.org/tsconfig/#noImplicitAny)和 [strictNullChecks](https://www.typescriptlang.org/tsconfig/#strictNullChecks):
noImplicitAny 迫使开发者在他们想绕过类型系统的时候,明确说明,而不是所有东西都默认为未类型化。我可以想象你不使用它的唯一情况是,你正在将一个现有的JavaScript项目移植到TypeScript。否则,如果你不使用noImplicitAny ,使用TypeScript就没有什么意义了。
strictNullChecks 迫使开发者在编译时明确处理 null或undefined 值的可能性,而不是等到运行时。这对你的代码有多大的积极影响,怎么说都不过分。整个类别的缺陷都会消失。也就是说,在项目开始时就开启这个功能是非常重要的,因为以后再加装这个功能会很困难。
虽然这些标志可能看起来只是一些小的命令行选项,但它们引导开发者编写的代码从根本上比没有这些标志的代码更可靠。起初,这可能会让不习惯的开发者感到沮丧,因为这需要他们提前考虑他们正在做什么。然而,这种投资很快就会得到回报,因为运行时的缺陷更少,而且其他开发人员更容易推理出这些代码。
GraphQL
GraphQL使客户能够很容易地从单一的服务器上有效和灵活地获得他们需要的数据。它还为将尽可能多的逻辑从用户界面的客户端推到服务器端提供了一个很好的基础,这种模式也被称为后端换前端(BFF)。
虽然用REST实现BFF是可能的,但像Swagger和OpenAPI这样的正式接口定义必须单独安装,而且只有当你使用Java这样的语言时才真正实用。相比之下,GraphQL服务器不可能没有GraphQL模式。要了解更多,请查看我的文章GraphQL。超越技术的思考。
TGRS架构建议你编写自己的GraphQL服务器,而不是使用AppSync等托管服务。这是因为托管服务倾向于让实现驱动模式,而不是反过来。根据我们用GraphQL构建企业应用程序的经验,实现对客户最有用的模式不可避免地需要在服务器中编写自定义解析器。但以AppSync为例,你必须使用Velocity模板编写自定义解析器,这很难开发、调试和测试。自然,人们希望避免困难的事情,所以他们避免需要自定义解析器的模式设计。最终的结果是一个次优的GraphQL模式,例如,它要求客户端对服务器进行多次调用以响应单一的用户交互。这就违背了使用GraphQL的初衷。
好消息是,使用像Apollo Server这样的框架在TypeScript中编写自己的GraphQL服务器很容易。这也使得同一个开发者可以很容易地在客户端和服务器端开发新的功能,这是使用BFF模式时的一个重要做法。
Apollo Server配备了用于在不同环境下运行服务器的垫片。这意味着你可以同时在AWS Lambda函数和Express服务器中运行,前者适合于生产发布,后者更适合于日常开发和测试。关于这样做的更多信息,请参阅构建一个可移植的Apollo服务器。
GraphQL的类型化性质也与TypeScript相吻合。代码生成器如
Apollo CLI和GraphQL代码生成器可以消费GraphQL模式,并为GraphQL客户端和服务器自动生成TypeScript类型。这有助于避免许多运行时的缺陷,这些缺陷会因客户端和服务器之间无意的不匹配而产生。
尽管如此,值得注意的是,围绕GraphQL的所有新概念和技术意味着新人需要时间来学习。因此,虽然我们强烈建议在TGRS堆栈中使用GraphQL,但如果你需要快速启动和运行一些东西,如果你暂时直接从客户端到上游的REST服务,这并不是一个破绽。然而,一旦你的应用程序开始增长和发展,我们建议引入GraphQL作为你的线上协议。
React(带钩子)
TGRS推荐React,因为它的组件模型简单而强大。它还具有功能性编程思维,专注于最大限度地减少状态和副作用,这是用户界面中出现错误的两个最大原因。此外,由于React和GraphQL都来自于Facebook,这两种技术有着共同的理念,往往能很好地合作。最后,虽然React的TypeScript类型并不完美,但对于大多数使用情况来说,它们已经足够好了。
React钩子意味着管理状态和效果的代码是非常简洁和可组合的。在绝大多数情况下,钩子是比使用基于类的React组件更简单的选择。因此,我们强烈建议你使用它们。我们的TGRS项目包含数百个基于钩子的功能组件,但只有一到两个基于类的组件(通常是错误边界)。
还值得注意的是,如果你使用钩子来管理UI状态,使用像Apollo Client这样的GraphQL客户端库来查询和改变服务器端的状态,那么对Redux这样的状态管理框架的需求就会下降。Redux存储通常有两个部分。entities ui ,它包含UI状态,其结构大致反映了你的组件层次结构。Apollo Client有自己的规范化缓存,有效地消除了对Redux存储的entities 部分的需要。React的useState 钩子使得在组件层次结构本身而不是在一个单独的结构中存储UI状态变得微不足道,消除了对Redux存储中的ui 部分的需求。将该状态存储在组件层次结构中的好处是,可以避免Redux存储中陈旧的UI状态引起的内存泄漏。
虽然React是TGRS的首选网络框架,但它的无主见性也意味着在项目的早期,你必须对如何使用它做出重要而明智的决定。TGRS对如何使用React进行状态管理有明确的意见,但你仍然要做出决定,例如,你如何构建你的项目文件,你使用什么构建工具,你使用哪个路由器,以及你如何处理错误。此外,随着你的团队的成长,你将不得不执行这些标准。因此,如果你选择使用Angular、Vue.js或Ember.js这样的替代品,如果它更适合你的团队,我们也不会强烈反对。
无服务器
TGRS建议你使用无服务器云技术来托管你的客户端和服务器。这是因为无服务器技术对于非专业人士来说,设置起来相对简单,有按使用付费的定价模式,并且需要最少的持续维护。也就是说,如果你的工作环境禁止或限制无服务器技术,那么如果你不得不使用更传统的基础设施,也不是什么问题(虽然工作量更大)。
AWS CloudFront
我们建议使用AWS CloudFront这样的CDN来分发你的客户端。通常这个建议会得到这样的回应。"但我们不需要CDN的性能!"。然而,性能不是这里的重点。用CDN来分发一个单页网络应用的静态资产,通常比设置和管理你自己的网络服务器、容器和负载平衡器要容易和便宜。
唯一需要注意的是安全问题。因为你的发布将在公共网络上,它将有一个可以被世界上任何人访问的名字。这意味着,如果你只想让私人网络上的用户能够访问该应用程序,那么你就必须通过AWS WAF的IP白名单来限制访问。同样地,如果你只想让特定的用户能够使用该应用程序,你就必须向客户端引入某种认证和授权机制。
AWS Lambda (w/ API Gateway)
我们在AWS Lambda函数内托管我们的GraphQL服务器取得了巨大的成功,绕过了对应用服务器、容器、负载平衡器及其所有相关配置的需求。此外,由于GraphQL服务器只需要暴露一个HTTP端点,AWS API网关的配置非常简单。
如上所述,Apollo服务器可以在lambda函数中轻松运行。它在lambda函数中唯一不能做的事情是支持GraphQL订阅。这是因为订阅在本质上是持久的,而lambdas是瞬时的。如果你发现你真的需要订阅,你将不得不把它嵌入到一个更持久的服务器中,比如Express。然而,到目前为止,我们发现使用针对lambda函数的轮询就足以满足我们近乎实时的数据同步要求。
请注意,为了被通过AWS CloudFront分发的客户端访问,你的lambda函数也必须通过一个公共名称来访问。这意味着,就像你的CloudFront分布一样,如果你想限制只有私人网络上的人可以访问,你就必须通过IP白名单来实现。同样,如果你想限制某些用户的访问,你必须在每个请求中从客户端向服务器发送访问令牌,并在服务器处理该请求之前对其进行验证。
最后,值得注意的是,虽然lambdas的运行成本非常低,但它们最适合于只承受中低负荷的应用程序。这使得它们成为内部企业应用的理想选择,因为这些应用的并发用户数量通常相对较少。如果你正在建立一个面向消费者的应用程序,并可能有大量的并发用户,你应该留出额外的时间来测试你的lambda函数将满足你的性能要求,而不会变得过于昂贵。如果它确实变得过于昂贵,你可能需要引入缓存基础设施--例如,CDN。另外,如果lambda的扩展速度不够快(不管费用如何),你应该考虑像提供并发性这样的功能。
示例项目
为了演示TGRS堆栈中的各种技术是如何结合在一起的,我们创建了一个示例项目。除了Apollo客户端和Apollo服务器之外,它还使用了。
- 创建React App来打包客户端并在本地运行
- AWS SAM CLI来模拟在本地运行lambda函数中的GraphQL服务器
- Jest和Puppeteer来做一个简单的基于浏览器的集成测试
- Apollo Server Express,在开发和测试期间在本地运行GraphQL服务器
- Yarn 1工作空间,用于管理客户端、服务器和集成测试的代码,以及它们之间共享的任何东西。
- Apollo CLI和GraphQL代码生成器,为客户端和服务器生成TypeScript类型。
请注意,这些额外的技术选择不被认为是TGRS架构的强制性部分。然而,我们已经在生产的TGRS项目中成功使用了它们。
总结
我们发现,在构建单页网络应用时,TypeScript、GraphQL、React和无服务器技术的结合在代码质量和日常灵活性之间提供了一个很好的平衡。一旦基础建立起来,就很容易增加新的功能,同时仍然保持代码库的可管理性。此外,对于只接受中低负荷的应用(企业应用通常如此),在无服务器基础设施上运行所有内容可以保持很低的成本。简而言之,我们认为这些技术将使你在现在和未来的应用发展中都能获得丰厚的回报。
