原文:https://blog.heroku.com
作者:CHRIS CASTLE

微信搜寻【前端全栈开发者】关注这个脱发、摆摊、卖货、继续学习的程序员,第一工夫浏览最新文章,会优先两天发表新文章。关注即可大礼包,准能为你节俭不少钱!

在过来的几年中,GraphQL曾经成为一种十分风行的API标准,该标准专一于使客户端(无论客户端是前端还是第三方)的数据获取更加容易。

在传统的基于REST的API办法中,客户端发出请求,而服务器决定响应:

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中,客户端能够准确地确定其从服务器获取的数据。例如,客户端可能只须要用户名和电子邮件,而不须要任何地址信息:

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?

与任何技术决策一样,理解GraphQL为你的我的项目提供了哪些劣势是很重要的,而不是简略地因为它是一个风行词而抉择它。

思考一个应用API连贯到近程数据库的SaaS应用程序。你想要出现用户的个人资料页面,你可能须要进行一次API GET 调用,以获取无关用户的信息,例如用户名或电子邮件。而后,你可能须要进行另一个API调用以获取无关地址的信息,该信息存储在另一个表中。随着应用程序的倒退,因为其构建形式的起因,你可能须要持续对不同地位进行更多的API调用。尽管每一个API调用都能够异步实现,但你也必须解决它们的响应,无论是谬误、网络超时,甚至暂停页面渲染,直到收到所有数据。如上所述,这些响应的有效载荷可能超过了渲染你以后页面的须要,而且每个API调用都有网络提早,总的提早加起来可能很可观。

应用GraphQL,你无需进行多个API调用(例如 GET /user/:idGET /user/:id/addresses ),而是进行一次API调用并将查问提交到单个端点:

query {  user(id: 1) {    name    email    addresses {    street    city    country    }  }}

而后,GraphQL仅提供一个端点来查问所需的所有域逻辑。如果你的应用程序一直增长,你会发现自己在你的架构中增加了更多的数据存储——PostgreSQL可能是存储用户信息的好中央,而Redis可能是存储其余品种信息的好中央——对GraphQL端点的一次调用将解决所有这些不同的地位,并以他们所申请的数据响应客户端。

如果你不确定应用程序的需要以及未来如何存储数据,则GraphQL在这里也很有用。要批改查问,你只需增加所需字段的名称:

        addresses {      street+     apartmentNumber   # new information      city      country    }

这极大地简化了随着工夫的推移而倒退你的应用程序的过程。

定义一个GraphQL schema

有各种编程语言的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 字段,别离是 nameemail ,它还包含一个称为 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];    },  }}

你会留神到,咱们没有明确定义 UserAddresses 的解析器,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为 1User。实际上,因为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 // 以后通过身份验证的用户    }  }));

Schema最佳实际

GraphQL标准中短少的一个方面是不足对版本控制模式的领导。随着应用程序的成长和变动,它们的API也会随之变动,很可能须要删除或批改GraphQL字段和对象。但这个毛病也是踊跃的:通过认真设计你的GraphQL schema,你能够防止在更容易实现(也更容易毁坏)的REST端点中显著的陷阱,如命名的不统一和凌乱的关系。

此外,你应该尽量将业务逻辑与解析器逻辑离开。你的业务逻辑应该是整个应用程序的繁多事实起源。在解析器中执行验证查看是很有诱惑力的,但随着模式的增长,这将成为一种难以维持的策略。

GraphQL什么时候不适合?

GraphQL不能像REST一样准确地满足HTTP通信的需要。例如,无论查问胜利与否,GraphQL仅指定一个状态码——200 OK。在这个响应中会返回一个非凡的谬误键,供客户端解析和辨认出错的中央,因而,错误处理可能会有些辣手。

同样,GraphQL只是一个标准,它不会主动解决你的应用程序面临的每个问题。性能问题不会隐没,数据库查问不会变得更快,总的来说,你须要从新思考对于你的API的所有:受权、日志、监控、缓存。版本化你的GraphQL API也可能是一个挑战,因为官网标准目前不反对解决中断的变动,这是构建任何软件不可避免的一部分。如果你有趣味摸索GraphQL,你须要投入一些工夫来学习如何将其与你的需要进行最佳整合。

理解更多

社区围绕这个新范例汇集,并为前端和后端工程师提供了很棒的GraphQL资源列表。前端和后端工程师都能够应用。你也能够通过在官网的游乐场上提出实在的申请来查看查问和类型是什么样子的。

咱们还有一个[Code[ish]播客集](https://www.heroku.com/podcas...,专门介绍GraphQL的益处和老本。