原文: https://blog.heroku.com
做者:CHRIS CASTLE微信搜索【前端全栈开发者】关注这个脱发、摆摊、卖货、持续学习的程序员,第一时间阅读最新文章,会优先两天发表新文章。关注便可大礼包,准能为你节省很多钱!javascript
在过去的几年中,GraphQL已经成为一种很是流行的API规范,该规范专一于使客户端(不管客户端是前端仍是第三方)的数据获取更加容易。前端
在传统的基于REST的API方法中,客户端发出请求,而服务器决定响应:java
curl https://api.heroku.space/users/1 { "id": 1, "name": "Luke", "email": "luke@heroku.space", "addresses": [ { "street": "1234 Rodeo Drive", "city": "Los Angeles", "country": "USA" } ] }
可是,在GraphQL中,客户端能够精确地肯定其从服务器获取的数据。例如,客户端可能只须要用户名和电子邮件,而不须要任何地址信息:git
curl -X POST https://api.heroku.space/graphql -d ' query { user(id: 1) { name email } } { "data": { "name": "Luke", "email": "luke@heroku.space" } }
经过这种新的模式,客户能够经过缩减响应来知足他们的需求,从而向服务器进行更高效的查询。对于单页应用(SPA)或其余前端重度客户端应用,能够经过减小有效载荷大小来加快渲染时间。可是,与任何框架或语言同样,GraphQL也须要权衡取舍。在本文中,咱们将探讨使用GraphQL做为API的查询语言的利弊,以及如何开始构建实现。程序员
与任何技术决策同样,了解GraphQL为你的项目提供了哪些优点是很重要的,而不是简单地由于它是一个流行词而选择它。github
考虑一个使用API链接到远程数据库的SaaS应用程序。你想要呈现用户的我的资料页面,你可能须要进行一次API GET
调用,以获取有关用户的信息,例如用户名或电子邮件。而后,你可能须要进行另外一个API调用以获取有关地址的信息,该信息存储在另外一个表中。随着应用程序的发展,因为其构建方式的缘由,你可能须要继续对不一样位置进行更多的API调用。虽然每个API调用均可以异步完成,但你也必须处理它们的响应,不管是错误、网络超时,甚至暂停页面渲染,直到收到全部数据。如上所述,这些响应的有效载荷可能超过了渲染你当前页面的须要,并且每一个API调用都有网络延迟,总的延迟加起来可能很可观。web
使用GraphQL,你无需进行多个API调用(例如 GET /user/:id
和 GET /user/:id/addresses
),而是进行一次API调用并将查询提交到单个端点:typescript
query { user(id: 1) { name email addresses { street city country } } }
而后,GraphQL仅提供一个端点来查询所需的全部域逻辑。若是你的应用程序不断增加,你会发现本身在你的架构中添加了更多的数据存储——PostgreSQL多是存储用户信息的好地方,而Redis多是存储其余种类信息的好地方——对GraphQL端点的一次调用将解决全部这些不一样的位置,并以他们所请求的数据响应客户端。数据库
若是你不肯定应用程序的需求以及未来如何存储数据,则GraphQL在这里也颇有用。要修改查询,你只需添加所需字段的名称:express
addresses { street + apartmentNumber # new information city country }
这极大地简化了随着时间的推移而发展你的应用程序的过程。
有各类编程语言的GraphQL服务器实现,但在你开始以前,你须要识别你的业务域中的对象,就像任何API同样。就像REST API可能会使用JSON模式同样,GraphQL使用SDL或Schema定义语言来定义它的模式,这是一种描述GraphQL API可用的全部对象和字段的幂等方式。SDL条目的通常格式以下:
type $OBJECT_TYPE { $FIELD_NAME($ARGUMENTS): $FIELD_TYPE }
让咱们之前面的例子为基础,定义一下user和address的条目是什么样子的。
type User { name: String email: String addresses: [Address] } type Address { street: String city: String country: String }
user
定义了两个 String
字段,分别是 name
和 email
,它还包括一个称为 addresses
的字段,它是 Addresses
对象的数组。 Addresses
还定义了它本身的几个字段。 (顺便说一下,GraphQL模式不只有对象,字段和标量类型,还有更多,你也能够合并接口,联合和参数,以构建更复杂的模型,但本文中不会介绍。)
咱们还须要定义一个类型,这是咱们GraphQL API的入口点。你还记得,前面咱们说过,GraphQL查询是这样的:
query { user(id: 1) { name email } }
该 query
字段属于一种特殊的保留类型,称为 Query
,这指定了获取对象的主要入口点。(还有用于修改对象的 Mutation
类型。)在这里,咱们定义了一个 user
字段,该字段返回一个 User
对象,所以咱们的架构也须要定义此字段:
type Query { user(id: Int!): User } type User { ... } type Address { ... }
字段中的参数是逗号分隔的列表,格式为 $NAME: $TYPE
。!
是GraphQL表示该参数是必需的方式,省略表示它是可选的。
根据你选择的语言,将此模式合并到服务器中的过程会有所不一样,但一般,将信息用做字符串就足够了。Node.js有 graphql 包来准备GraphQL模式,但咱们将使用 graphql-tools 包来代替,由于它提供了一些更多的好处。让咱们导入该软件包并阅读咱们的类型定义,觉得未来的开发作准备:
const fs = require('fs') const { makeExecutableSchema } = require("graphql-tools"); let typeDefs = fs.readFileSync("schema.graphql", { encoding: "utf8", flag: "r", });
schema设置了构建查询的方式,但创建schema来定义数据模型只是GraphQL规范的一部分。另外一部分涉及实际获取数据,这是经过使用解析器完成的,解析器是一个返回字段基础值的函数。
让咱们看一下如何在Node.js中实现解析器。咱们的目的是围绕着解析器如何与模式一块儿操做来巩固概念,因此咱们不会围绕着如何设置数据存储来作太详细的介绍。在“现实世界”中,咱们可能会使用诸如knex之类的东西创建数据库链接。如今,让咱们设置一些虚拟数据:
const users = { 1: { name: "Luke", email: "luke@heroku.space", addresses: [ { street: "1234 Rodeo Drive", city: "Los Angeles", country: "USA", }, ], }, 2: { name: "Jane", email: "jane@heroku.space", addresses: [ { street: "1234 Lincoln Place", city: "Brooklyn", country: "USA", }, ], }, };
Node.js中的GraphQL解析器至关于一个Object,key是要检索的字段名,value是返回数据的函数。让咱们从初始 user
按id查找的一个简单示例开始:
const resolvers = { Query: { user: function (parent, { id }) { // 用户查找逻辑 }, }, }
这个解析器须要两个参数:一个表明父的对象(在最初的根查询中,这个对象一般是未使用的),一个包含传递给你的字段的参数的JSON对象。并不是每一个字段都具备参数,可是在这种状况下,咱们将拥有参数,由于咱们须要经过用户ID来检索其用户。该函数的其他部分很简单:
const resolvers = { Query: { user: function (_, { id }) { return users[id]; }, } }
你会注意到,咱们没有明肯定义 User
或 Addresses
的解析器,graphql-tools
包足够智能,能够自动为咱们映射这些。若是咱们选择的话,咱们能够覆盖这些,可是如今咱们已经定义了咱们的类型定义和解析器,咱们能够创建咱们完整的模式:
const schema = makeExecutableSchema({ typeDefs, resolvers });
最后,让咱们来运行这个demo吧!由于咱们使用的是Express,因此咱们可使用 express-graphql 包来暴露咱们的模式做为端点。该程序包须要两个参数:schema和根value,它有一个可选参数 graphiql
,咱们将稍后讨论。
使用GraphQL中间件在你喜欢的端口上设置Express服务器,以下所示:
const express = require("express"); const express_graphql = require("express-graphql"); const app = express(); app.use( "/graphql", express_graphql({ schema: schema, graphiql: true, }) ); app.listen(5000, () => console.log("Express is now live at localhost:5000"));
将浏览器导航到 http://localhost:5000/graphql,你应该会看到一种IDE界面。在左侧窗格中,你能够输入所需的任何有效GraphQL查询,而在右侧你将得到结果。
这就是 graphiql: true
所提供的:一种方便的方式来测试你的查询,你可能不想在生产环境中公开它,可是它使测试变得容易得多。
尝试输入上面展现的查询:
query { user(id: 1) { name email } }
要探索GraphQL的类型化功能,请尝试为ID参数传递一个字符串而不是一个整数。
# 这不起做用 query { user(id: "1") { name email } }
你甚至能够尝试请求不存在的字段:
# 这不起做用 query { user(id: 1) { name zodiac } }
只需用schema表达几行清晰的代码,就能够在客户机和服务器之间创建强类型的契约。这样能够防止你的服务接收虚假数据,并向请求者清楚地代表错误。
尽管GraphQL为你解决了不少问题,但它并不能解决构建API的全部固有问题。特别是缓存和受权这两个方面,只是须要一些预案来防止性能问题。GraphQL规范并无为实现这两种方法提供任何指导,这意味着构建它们的责任落在了你身上。
基于REST的API在缓存时不须要过分关注,由于它们能够构建在web的其余部分使用的现有HTTP头策略之上。GraphQL不具备这些缓存机制,这会对重复请求形成没必要要的处理负担。考虑如下两个查询:
query { user(id: 1) { name } } query { user(id: 1) { email } }
在没有某种缓存的状况下,只是为了检索两个不一样的列,会致使两个数据库查询来获取ID为 1
的 User
。实际上,因为GraphQL还容许使用别名,所以如下查询有效,而且还执行两次查找:
query { one: user(id: 1) { name } two: user(id: 2) { name } }
第二个示例暴露了如何批处理查询的问题。为了快速高效,咱们但愿GraphQL以尽量少的往返次数访问相同的数据库行。
dataloader程序包旨在解决这两个问题。给定一个ID数组,咱们将一次性从数据库中获取全部这些ID;一样,后续对同一ID的调用也将从缓存中获取该项目。要使用 dataloader
来构建这个,咱们须要两样东西。首先,咱们须要一个函数来加载全部请求的对象。在咱们的示例中,看起来像这样:
const DataLoader = require('dataloader'); const batchGetUserById = async (ids) => { // 在现实生活中,这将是数据库调用 return ids.map(id => users[id]); }; // userLoader如今是咱们的“批量加载功能” const userLoader = new DataLoader(batchGetUserById);
这样能够解决批处理的问题。要加载数据并使用缓存,咱们将使用对 load
方法的调用来替换以前的数据查找,并传入咱们的用户ID:
const resolvers = { Query: { user: function (_, { id }) { return userLoader.load(id); }, }, }
对于GraphQL来讲,受权是一个彻底不一样的问题。简而言之,它是识别给定用户是否有权查看某些数据的过程。咱们能够想象一下这样的场景:通过认证的用户能够执行查询来获取本身的地址信息,但应该没法获取其余用户的地址。
为了解决这个问题,咱们须要修改解析器函数。 除了字段的参数外,解析器还能够访问它的父节点,以及传入的特殊上下文值,这些值能够提供有关当前已认证用户的信息。由于咱们知道地址是一个敏感字段,因此咱们须要修改咱们的代码,使对用户的调用不仅是返回一个地址列表,而是实际调用一些业务逻辑来验证请求:
const getAddresses = function(currUser, user) { if (currUser.id == user.id) { return user.addresses } return []; } const resolvers = { Query: { user: function (_, { id }) { return users[id]; }, }, User: { addresses: function (parentObj, {}, context) { return getAddresses(context.currUser, parentObj); }, }, };
一样,咱们不须要为每一个 User
字段显式定义一个解析程序,只需定义一个咱们要修改的解析程序便可。
默认状况下,express-graphql
会将当前的HTTP请求做为上下文的值来传递,但在设置服务器时能够更改:
app.use( "/graphql", express_graphql({ schema: schema, graphiql: true, context: { currUser: user // 当前通过身份验证的用户 } }) );
GraphQL规范中缺乏的一个方面是缺少对版本控制模式的指导。随着应用程序的成长和变化,它们的API也会随之变化,极可能须要删除或修改GraphQL字段和对象。但这个缺点也是积极的:经过仔细设计你的GraphQL schema,你能够避免在更容易实现(也更容易破坏)的REST端点中明显的陷阱,如命名的不一致和混乱的关系。
此外,你应该尽可能将业务逻辑与解析器逻辑分开。你的业务逻辑应该是整个应用程序的单一事实来源。在解析器中执行验证检查是颇有诱惑力的,但随着模式的增加,这将成为一种难以维持的策略。
GraphQL不能像REST同样精确地知足HTTP通讯的需求。例如,不管查询成功与否,GraphQL仅指定一个状态码——200 OK
。在这个响应中会返回一个特殊的错误键,供客户端解析和识别出错的地方,所以,错误处理可能会有些棘手。
一样,GraphQL只是一个规范,它不会自动解决你的应用程序面临的每一个问题。性能问题不会消失,数据库查询不会变得更快,总的来讲,你须要从新思考关于你的API的一切:受权、日志、监控、缓存。版本化你的GraphQL API也多是一个挑战,由于官方规范目前不支持处理中断的变化,这是构建任何软件不可避免的一部分。若是你有兴趣探索GraphQL,你须要投入一些时间来学习如何将其与你的需求进行最佳整合。
社区围绕这个新范例汇集,并为前端和后端工程师提供了很棒的GraphQL资源列表。前端和后端工程师均可以使用。你也能够经过在官方的游乐场上提出真实的请求来查看查询和类型是什么样子的。
咱们还有一个[Code[ish]播客集](https://www.heroku.com/podcas...,专门介绍GraphQL的好处和成本。