作者:CHRISCASTLE原文:https://blog.heroku.com译者:Duneb过去几年,GraphQL已经成为非常流行的API规范,它专注于让客户端(无论客户端是前端还是第三方)更容易获取数据。在传统的基于REST的API方法中,客户端发出请求,服务器决定响应:curlhttps://api.heroku.space/users/1{"id":1,"name":"Luke","email":"luke@heroku.space","addresses":[{"street":"1234RodeoDrive","city":"LosAngeles","country":"USA"}]}但是,在在GraphQL中,客户端可以准确地确定它从服务器获取了哪些数据。例如,客户端可能只需要用户名和电子邮件,而不需要任何地址信息:curl-XPOSThttps://api.heroku.space/graphql-d'query{user(id:1){nameemail}}{"data":{"name":"Luke","email":"luke@heroku.space"}}有了这个新模式,客户端可以通过缩小响应来满足他们的需求,从而对服务器进行更新高效查询.对于单页应用程序(SPA)或其他前端重客户端应用程序,您可以通过减少负载大小来加快渲染时间。然而,与任何框架或语言一样,GraphQL也有取舍。在本文中,我们将探讨使用GraphQL作为API查询语言的优缺点,以及如何开始构建实现。为什么选择GraphQL?与任何技术决策一样,重要的是要了解GraphQL为您的项目提供哪些优势,而不是仅仅因为它是一个流行词而选择它。考虑一个使用API连接到远程数据库的SaaS应用程序。如果要呈现用户的个人资料页面,您可能需要进行APIGET调用以获取有关用户的信息,例如用户名或电子邮件。然后您可能需要进行另一个API调用以获取有关存储在另一个表中的地址的信息。随着应用程序的增长,由于其构建方式,您可能需要不断对不同位置进行更多API调用。虽然每个API调用都可以异步完成,但您还必须处理它们的响应,无论是错误、网络超时,还是暂停页面呈现,直到收到所有数据。如上所述,这些响应的负载可能超过呈现当前页面所需的负载,并且每个API调用都有网络延迟,总延迟加起来可能会很大。使用GraphQL,您无需进行多次API调用(例如GET/user/:id和GET/user/:id/addresses),而是进行一次API调用并将查询提交到单个端点:query{user(id:1){nameemailaddresses{streetcitycountry}}}GraphQL然后只提供一个端点来查询所需的所有域逻辑。如果您的应用程序不断增长,您可能会发现自己向架构中添加了更多数据存储-PostgreSQL可能是存储用户信息的好地方,而Redis可能是存储其他类型信息的好地方-对GraphQL的一次调用端点将解析所有这些不同的位置,并用他们请求的数据响应客户端。如果您不确定应用程序的需求以及将来如何存储数据,GraphQL在这里也很有用。要修改查询,您只需添加必填字段的名称:addresses{street+apartmentNumber#newinformationcitycountry}这大大简化了应用程序随时间增长的过程。定义GraphQL模式有各种编程语言的GraphQL服务器实现,但在开始之前,您需要识别业务领域中的对象,就像任何API一样。就像RESTAPI可能使用JSON模式一样,GraphQL使用SDL或模式定义语言定义其模式,这是一种描述GraphQLAPI可用的所有对象和字段的幂等方式。SDL条目的一般格式如下:type$OBJECT_TYPE{$FIELD_NAME($ARGUMENTS):$FIELD_TYPE}让我们在前面的示例的基础上定义用户和地址的条目。typeUser{name:Stringemail:Stringaddresses:[Address]}typeAddress{street:Stringcity:Stringcountry:String}user定义了两个String字段,name和email,它还包含一个addresses字段,是Addresses对象的数组。地址还定义了它自己的几个字段。(顺便说一下,GraphQL模式不仅有对象、字段和标量类型,您还可以合并接口、联合和参数来构建更复杂的模型,但这不在本文中介绍。)我们还需要定义一种类型,它是我们GraphQLAPI的入口点。回想一下,我们之前说过GraphQL查询看起来像这样:query{user(id:1){nameemail}}查询字段是一种特殊的保留类型,称为Query,它指定了主要入口点。(还有一个用于修改对象的Mutation类型。)这里我们定义了一个返回User对象的user字段,所以我们的schema也需要定义这个:typeQuery{user(id:Int!):User中的参数}typeUser{...}typeAddress{...}字段是一个以逗号分隔的列表,格式为$NAME:$TYPE。这!是GraphQL表示参数是必需的方式,省略它意味着它是可选的。根据您选择的语言,将此架构合并到服务器的过程会有所不同,但通常,将信息用作字符串就足够了。Node.js有graphql包来准备GraphQL模式,但我们将使用graphql-tools包,因为它提供了更多好处。让我们导入那个包并阅读我们的typedef为将来的开发做准备:{编码:“utf8”,标志:“r”,});设置解析器架构会设置查询的构建方式,但是构建架构来定义数据模型只是GraphQL规范的一部分。另一部分涉及实际获取数据,这是通过使用解析器完成的,解析器是一个返回字段基础值的函数。让我们来看看如何在Node.js中实现解析器。我们的目标是巩固有关解析器如何使用模式进行操作的概念,因此我们不会详细介绍如何设置数据存储。在“现实世界”中,我们可能会使用knex之类的东西来建立数据库连接。现在,让我们设置一些虚拟数据:constusers={1:{name:"Luke",email:"luke@heroku.space",addresses:[{street:"1234RodeoDrive",city:"LosAngeles",country:"USA",},],},2:{name:"Jane",email:"jane@heroku.space",addresses:[{street:"1234LincolnPlace",城市:"Brooklyn",国家:"美国",},],},};Node.js中的GraphQL解析器相当于一个Object,key是要检索的字段名,value是返回数据的函数。让我们从一个通过id进行初始用户查找的简单示例开始:表示父级(在原始根查询中,此对象通常未使用),一个包含传递给您的字段的参数的JSON对象。并非每个字段都有参数,但在本例中我们会有,因为我们需要通过用户ID检索其用户。函数的其余部分很简单:constresolvers={Query:{user:function(_,{id}){returnusers[id];},}}你会注意到我们没有明确定义User或Addresses实现者的解析,graphql-tools包足够智能,可以自动为我们映射这些。如果我们愿意,我们可以覆盖它们,但现在我们已经定义了我们的类型定义和解析器,我们可以构建我们的完整模式:constschema=makeExecutableSchema({typeDefs,resolvers});运行服务器最后,让我们来运行这个演示吧!由于我们使用的是Express,因此我们可以使用express-graphql包将我们的模式公开为端点。该包有两个参数:架构和根值,它有一个可选参数graphiql,我们将在稍后讨论。使用GraphQL中间件在您的首选端口上设置Express服务器,如下所示:constexpress=require("express");constexpress_graphql=require("express-graphql");constapp=express();app.use("/graphql",express_graphql({schema:schema,graphiql:true,}));app.listen(5000,()=>console.log("Express现在在localhost:5000"));将浏览器导航到http://localhost:5000/graphql,你应该会看到一种IDE界面。在左侧窗格中,您可以输入所需的任何有效GraphQL查询,而在右侧您将获得结果。这就是graphiql:true提供的:一种测试查询的便捷方式,您可能不想在生产中公开它,但它使测试变得容易得多。尝试输入上面显示的查询:query{user(id:1){nameemail}}要探索GraphQL的类型化功能,请尝试为ID参数传递一个字符串而不是整数。#Thiswon'tworkquery{user(id:"1"){nameemail}}你甚至可以尝试请求不存在的字段:#Thiswon'tworkquery{user(id:1){namezodiac}}只需用模式表达几行清晰的代码即可在客户端和服务器之间建立强类型契约。这可以防止您的服务接收虚假数据,并向请求者清楚地指示错误。性能注意事项虽然GraphQL为您解决了很多问题,但它并没有解决构建API时固有的所有问题。特别是缓存和授权,只需要一些计划来防止性能问题。GraphQL规范没有为实现这两种方法中的任何一种提供任何指导,这意味着构建它们的责任落在您身上。缓存基于REST的API在缓存时无需过分关注,因为它们可以构建在网络其他部分使用的现有HTTP标头策略之上。GraphQL没有这些缓存机制,这对重复请求造成了不必要的处理负担。考虑以下两个查询:query{user(id:1){name}}query{user(id:1){email}}没有某种缓存,只是为了检索两个不同的列,将导致两个数据库查询到获取ID为1的用户。实际上,由于GraphQL也允许使用别名,因此以下查询有效并执行两次查找:query{one:user(id:1){name}two:user(id:2){name}}second该示例暴露了如何批量查询的问题。为了快速高效,我们希望GraphQL以尽可能少的往返次数访问同一数据库行。dataloader包旨在解决这两个问题。给定一个ID数组,我们将一次性从数据库中获取所有这些ID;同样,对同一ID的后续调用将从缓存中获取项目。要使用数据加载器构建它,我们需要两件事。首先,我们需要一个函数来加载所有请求的对象。在我们的示例中,它看起来像这样:constDataLoader=require('dataloader');constbatchGetUserById=async(ids)=>{//在现实生活中这将是数据库调用returnids.map(id=>users[id]);};//userLoader现在是我们的“批量加载函数”constuserLoader=newDataLoader(batchGetUserById);这样可以解决批处理的问题。要加载数据并使用缓存,我们将调用load方法替换之前的数据查找,并传入我们的用户ID:constresolvers={Query:{user:function(_,{id}){returnuserLoader.load(id);},},}授权授权是GraphQL的一个完全不同的问题。简而言之,它是识别给定用户是否有权查看某些数据的过程。我们可以想象这样一种场景,经过身份验证的用户可以执行查询来获取自己的地址信息,但不应该获取其他用户的地址。为了解决这个问题,我们需要修改解析器函数。除了字段的参数之外,解析器还可以访问其父节点,以及传入的特殊上下文值,这些值提供有关当前经过身份验证的用户的信息。因为我们知道地址是一个敏感字段,所以我们需要修改我们的代码,以便对用户的调用不只是返回地址列表,而是实际调用一些业务逻辑来验证请求:constgetAddresses=function(currUser,user){if(currUser.id==user.id){returnuser.addresses}return[];}constresolvers={查询:{user:function(_,{id}){returnusers[id];},},用户:{地址:函数(parentObj,{},context){returngetAddresses(context.currUser,parentObj);},},};同样,我们不需要为每个User字段显式定义解析器,只需定义一个我们要修改的解析器即可。默认情况下,express-graphql会将当前HTTP请求作为上下文值传递,但这可以在设置服务器时更改:app.use("/graphql",express_graphql({schema:schema,graphiql:true,context:{currUser:user//currentauthenticateduser}}));Schema最佳实践GraphQL规范中缺少的一个方面是缺乏关于版本控制模式的指导。随着应用程序的增长和变化,它们的API也会发生变化,可能需要删除或修改GraphQL字段和对象。但这个缺点也是一个积极的方面:通过仔细设计您的GraphQL模式,您可以避免在更容易实现(也更容易破坏)的REST端点中出现明显的缺陷,例如不一致的命名和混乱的关系。此外,您应该尝试将业务逻辑与解析器逻辑分开。您的业??务逻辑应该是整个应用程序的唯一真实来源。在解析器中执行验证检查很诱人,但随着模式的增长,这将成为一种站不住脚的策略。GraphQL什么时候不适合?GraphQL无法像REST那样精确地满足HTTP通信的需求。例如,GraphQL仅指定一个状态代码——200OK——无论查询是否成功。在此响应中,返回一个特殊的错误键供客户端解析和识别出了什么问题,因此错误处理可能有点棘手。同样,GraphQL只是一个规范,它不会自动解决您的应用程序面临的所有问题。性能问题不会消失,数据库查询不会变得更快,而且通常,您需要重新考虑有关API的一切:授权、日志记录、监控、缓存。对您的GraphQLAPI进行版本控制也可能是一个挑战,因为官方规范目前不支持处理重大变更,而这是构建任何软件都不可避免的一部分。如果您对探索GraphQL感兴趣,则需要投入一些时间来学习如何最好地将它与您的需求相结合。了解更多社区聚集在这个新范式周围,为前端和后端工程师提供了大量GraphQL资源。前端和后端工程师都可以使用它。您还可以通过在官方playground上发出真实请求来查看查询和类型。我们还有一个[Code[ish]播客合集](https://www.heroku.com/podcas...,专门介绍GraphQL的收益和成本。微信搜索[前端全栈开发者]来支付关注本次掉发,摆摊、卖货、持续学习的程序员们,第一时间阅读最新文章,这两天会优先发布新文章,关注的话,可以获得一个大礼包,绝对让你省钱!
