GraphQL初体验,Node.js构建GraphQLAPI指南

在过去的几年中,GraphQL[1]已经成为一种非常流行的API规范,该规范专注于使客户端(无论客户端是前端还是第三方)的数据获取更加容易。

创新互联公司是一家网站设计公司,集创意、互联网应用、软件技术为一体的创意网站建设服务商,主营产品:响应式网站开发品牌网站设计成都全网营销推广。我们专注企业品牌在网站中的整体树立,网络互动的体验,以及在手机等移动端的优质呈现。成都网站建设、网站制作、移动互联产品、网络运营、VI设计、云产品.运维为核心业务。为用户提供一站式解决方案,我们深知市场的竞争激烈,认真对待每位客户,为客户提供赏析悦目的作品,网站的价值服务。

目录:

  • 为什么选择GraphQL?
  • 定义一个GraphQL schema
  • 设置解析器
  • 运行服务器
  • 性能考量
    • 缓存
    • 授权
  • Schema最佳实践
  • GraphQL什么时候不合适?
  • 了解更多

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

 
 
 
 
  1. curl https://api.heroku.space/users/1
  2. {
  3.   "id": 1,
  4.   "name": "Luke",
  5.   "email": "luke@heroku.space",
  6.   "addresses": [
  7.     {
  8.     "street": "1234 Rodeo Drive",
  9.     "city": "Los Angeles",
  10.     "country": "USA"
  11.     }
  12.   ]
  13. }

但是,在GraphQL中,客户端可以精确地确定其从服务器获取的数据。例如,客户端可能只需要用户名和电子邮件,而不需要任何地址信息:

 
 
 
 
  1. curl -X POST https://api.heroku.space/graphql -d '
  2. query {
  3.   user(id: 1) {
  4.     name
  5.     email
  6.   }
  7. }
  8. {
  9.   "data":
  10.     {
  11.     "name": "Luke",
  12.     "email": "luke@heroku.space"
  13.     }
  14. }

通过这种新的模式,客户可以通过缩减响应来满足他们的需求,从而向服务器进行更高效的查询。对于单页应用(SPA)或其他前端重度客户端应用,可以通过减少有效载荷大小来加快渲染时间。但是,与任何框架或语言一样,GraphQL也需要权衡取舍。在本文中,我们将探讨使用GraphQL作为API的查询语言的利弊,以及如何开始构建实现。

为什么选择GraphQL?

与任何技术决策一样,了解GraphQL为你的项目提供了哪些优势是很重要的,而不是简单地因为它是一个流行词而选择它。

考虑一个使用API连接到远程数据库的SaaS应用程序。你想要呈现用户的个人资料页面,你可能需要进行一次API GET 调用,以获取有关用户的信息,例如用户名或电子邮件。然后,你可能需要进行另一个API调用以获取有关地址的信息,该信息存储在另一个表中。随着应用程序的发展,由于其构建方式的原因,你可能需要继续对不同位置进行更多的API调用。虽然每一个API调用都可以异步完成,但你也必须处理它们的响应,无论是错误、网络超时,甚至暂停页面渲染,直到收到所有数据。如上所述,这些响应的有效载荷可能超过了渲染你当前页面的需要,而且每个API调用都有网络延迟,总的延迟加起来可能很可观。

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

 
 
 
 
  1. query {
  2.   user(id: 1) {
  3.     name
  4.     email
  5.     addresses {
  6.     street
  7.     city
  8.     country
  9.     }
  10.   }
  11. }

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

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

 
 
 
 
  1. addresses {
  2.       street
  3. +     apartmentNumber   # new information
  4.       city
  5.       country
  6.     }

这极大地简化了随着时间的推移而发展你的应用程序的过程。

定义一个GraphQL schema

有各种编程语言的GraphQL服务器实现,但在你开始之前,你需要识别你的业务域中的对象,就像任何API一样。就像REST API可能会使用JSON模式一样,GraphQL使用SDL或Schema定义语言来定义它的模式,这是一种描述GraphQL API可用的所有对象和字段的幂等方式。SDL条目的一般格式如下:

 
 
 
 
  1. type $OBJECT_TYPE {
  2.   $FIELD_NAME($ARGUMENTS): $FIELD_TYPE
  3. }

让我们以前面的例子为基础,定义一下user和address的条目是什么样子的。

 
 
 
 
  1. type User {
  2.   name:     String
  3.   email:    String
  4.   addresses:   [Address]
  5. }
  6. type Address {
  7.   street:   String
  8.   city:     String
  9.   country:  String
  10. }

user 定义了两个 String 字段,分别是 name 和 email ,它还包括一个称为 addresses 的字段,它是 Addresses 对象的数组。Addresses 还定义了它自己的几个字段。(顺便说一下,GraphQL模式不仅有对象,字段和标量类型,还有更多,你也可以合并接口,联合和参数,以构建更复杂的模型,但本文中不会介绍。)

我们还需要定义一个类型,这是我们GraphQL API的入口点。你还记得,前面我们说过,GraphQL查询是这样的:

 
 
 
 
  1. query {
  2.   user(id: 1) {
  3.     name
  4.     email
  5.   }
  6. }

该 query 字段属于一种特殊的保留类型,称为 Query ,这指定了获取对象的主要入口点。(还有用于修改对象的 Mutation 类型。)在这里,我们定义了一个 user 字段,该字段返回一个 User 对象,因此我们的架构也需要定义此字段:

 
 
 
 
  1. type Query {
  2.   user(id: Int!): User
  3. }
  4. type User { ... }
  5. type Address { ... }

字段中的参数是逗号分隔的列表,格式为 $NAME: $TYPE。! 是GraphQL表示该参数是必需的方式,省略表示它是可选的。

根据你选择的语言,将此模式合并到服务器中的过程会有所不同,但通常,将信息用作字符串就足够了。Node.js有 graphql[2] 包来准备GraphQL模式,但我们将使用 graphql-tools[3] 包来代替,因为它提供了一些更多的好处。让我们导入该软件包并阅读我们的类型定义,以为将来的开发做准备:

 
 
 
 
  1. const fs = require('fs')
  2. const { makeExecutableSchema } = require("graphql-tools");
  3. let typeDefs = fs.readFileSync("schema.graphql", {
  4.   encoding: "utf8",
  5.   flag: "r",
  6. });

设置解析器

schema设置了构建查询的方式,但建立schema来定义数据模型只是GraphQL规范的一部分。另一部分涉及实际获取数据,这是通过使用解析器完成的,解析器是一个返回字段基础值的函数。

让我们看一下如何在Node.js中实现解析器。我们的目的是围绕着解析器如何与模式一起操作来巩固概念,所以我们不会围绕着如何设置数据存储来做太详细的介绍。在“现实世界”中,我们可能会使用诸如knex[4]之类的东西建立数据库连接。现在,让我们设置一些虚拟数据:

 
 
 
 
  1. const users = {
  2.   1: {
  3.     name: "Luke",
  4.     email: "luke@heroku.space",
  5.     addresses: [
  6.     {
  7.       street: "1234 Rodeo Drive",
  8.       city: "Los Angeles",
  9.       country: "USA",
  10.     },
  11.     ],
  12.   },
  13.   2: {
  14.     name: "Jane",
  15.     email: "jane@heroku.space",
  16.     addresses: [
  17.     {
  18.       street: "1234 Lincoln Place",
  19.       city: "Brooklyn",
  20.       country: "USA",
  21.     },
  22.     ],
  23.   },
  24. };

Node.js中的GraphQL解析器相当于一个Object,key是要检索的字段名,value是返回数据的函数。让我们从初始 user 按id查找的一个简单示例开始:

 
 
 
 
  1. const resolvers = {
  2.   Query: {
  3.     user: function (parent, { id }) {
  4.       // 用户查找逻辑
  5.     },
  6.   },
  7. }

这个解析器需要两个参数:一个代表父的对象(在最初的根查询中,这个对象通常是未使用的),一个包含传递给你的字段的参数的JSON对象。并非每个字段都具有参数,但是在这种情况下,我们将拥有参数,因为我们需要通过用户ID来检索其用户。该函数的其余部分很简单:

 
 
 
 
  1. const resolvers = {
  2.   Query: {
  3.     user: function (_, { id }) {
  4.       return users[id];
  5.     },
  6.   }
  7. }

你会注意到,我们没有明确定义 User 或 Addresses 的解析器,graphql-tools 包足够智能,可以自动为我们映射这些。如果我们选择的话,我们可以覆盖这些,但是现在我们已经定义了我们的类型定义和解析器,我们可以建立我们完整的模式:

 
 
 
 
  1. const schema = makeExecutableSchema({ typeDefs, resolvers });

运行服务器

最后,让我们来运行这个demo吧!因为我们使用的是Express,所以我们可以使用 express-graphql[5] 包来暴露我们的模式作为端点。该程序包需要两个参数:schema和根value,它有一个可选参数 graphiql,我们将稍后讨论。

使用GraphQL中间件在你喜欢的端口上设置Express服务器,如下所示:

 
 
 
 
  1. const express = require("express");
  2. const express_graphql = require("express-graphql");
  3. const app = express();
  4. app.use(
  5.   "/graphql",
  6.   express_graphql({
  7.     schema: schema,
  8.     graphiql: true,
  9.   })
  10. );
  11. app.listen(5000, () => console.log("Express is now live at localhost:5000"));

将浏览器导航到 http://localhost:5000/graphql,你应该会看到一种IDE界面。在左侧窗格中,你可以输入所需的任何有效GraphQL查询,而在右侧你将获得结果。

这就是 graphiql: true 所提供的:一种方便的方式来测试你的查询,你可能不想在生产环境中公开它,但是它使测试变得容易得多。

尝试输入上面展示的查询:

 
 
 
 
  1. query {
  2.   user(id: 1) {
  3.     name
  4.     email
  5.   }
  6. }

要探索GraphQL的类型化功能,请尝试为ID参数传递一个字符串而不是一个整数。

 
 
 
 
  1. # 这不起作用
  2. query {
  3.   user(id: "1") {
  4.     name
  5.     email
  6.   }
  7. }

你甚至可以尝试请求不存在的字段:

 
 
 
 
  1. # 这不起作用
  2. query {
  3.   user(id: 1) {
  4.     name
  5.     zodiac
  6.   }
  7. }

只需用schema表达几行清晰的代码,就可以在客户机和服务器之间建立强类型的契约。这样可以防止你的服务接收虚假数据,并向请求者清楚地表明错误。

性能考量

尽管GraphQL为你解决了很多问题,但它并不能解决构建API的所有固有问题。特别是缓存和授权这两个方面,只是需要一些预案来防止性能问题。GraphQL规范并没有为实现这两种方法提供任何指导,这意味着构建它们的责任落在了你身上。

缓存

基于REST的API在缓存时不需要过度关注,因为它们可以构建在web的其他部分使用的现有HTTP头策略之上。GraphQL不具有这些缓存机制,这会对重复请求造成不必要的处理负担。考虑以下两个查询:

 
 
 
 
  1. query {
  2.   user(id: 1) {
  3.     name
  4.   }
  5. }
  6. query {
  7.   user(id: 1) {
  8.     email
  9.   }
  10. }

在没有某种缓存的情况下,只是为了检索两个不同的列,会导致两个数据库查询来获取ID为 1 的 User。实际上,由于GraphQL还允许使用别名,因此以下查询有效,并且还执行两次查找:

 
 
 
 
  1. query {
  2.   one: user(id: 1) {
  3.     name
  4.   }
  5.   two: user(id: 2) {
  6.     name
  7.   }
  8. }

第二个示例暴露了如何批处理查询的问题。为了快速高效,我们希望GraphQL以尽可能少的往返次数访问相同的数据库行。

dataloader[6]程序包旨在解决这两个问题。给定一个ID数组,我们将一次性从数据库中获取所有这些ID;同样,后续对同一ID的调用也将从缓存中获取该项目。要使用 dataloader 来构建这个,我们需要两样东西。首先,我们需要一个函数来加载所有请求的对象。在我们的示例中,看起来像这样:

 
 
 
 
  1. const DataLoader = require('dataloader');
  2. const batchGetUserById = async (ids) => {
  3.    // 在现实生活中,这将是数据库调用
  4.   return ids.map(id => users[id]);
  5. };
  6. // userLoader现在是我们的“批量加载功能”
  7. const userLoader = new DataLoader(batchGetUserById);

这样可以解决批处理的问题。要加载数据并使用缓存,我们将使用对 load 方法的调用来替换之前的数据查找,并传入我们的用户ID:

 
 
 
 
  1. const resolvers = {
  2.   Query: {
  3.     user: function (_, { id }) {
  4.       return userLoader.load(id);
  5.     },
  6.   },
  7. }

授权

对于GraphQL来说,授权是一个完全不同的问题。简而言之,它是识别给定用户是否有权查看某些数据的过程。我们可以想象一下这样的场景:经过认证的用户可以执行查询来获取自己的地址信息,但应该无法获取其他用户的地址。

为了解决这个问题,我们需要修改解析器函数。除了字段的参数外,解析器还可以访问它的父节点,以及传入的特殊上下文值,这些值可以提供有关当前已认证用户的信息。因为我们知道地址是一个敏感字段,所以我们需要修改我们的代码,使对用户的调用不只是返回一个地址列表,而是实际调用一些业务逻辑来验证请求:

 
 
 
 
  1. const getAddresses = function(currUser, user) {
  2.   if (currUser.id == user.id) {
  3.     return user.addresses
  4.   }
  5.   return [];
  6. }
  7. const resolvers = {
  8.   Query: {
  9.     user: function (_, { id }) {
  10.       return users[id];
  11.     },
  12.   },
  13.   User: {
  14.     addresses: function (parentObj, {}, context) {
  15.       return getAddresses(context.currUser, parentObj);
  16.     },
  17.   },
  18. };

同样,我们不需要为每个 User 字段显式定义一个解析程序,只需定义一个我们要修改的解析程序即可。

默认情况下,express-graphql 会将当前的HTTP请求作为上下文的值来传递,但在设置服务器时可以更改:

 
 
 
 
  1. app.use(
  2.   "/graphql",
  3.   express_graphql({
  4.     schema: schema,
  5.     graphiql: true,
  6.     context: {
  7.       currUser: user // 当前经过身份验证的用户
  8.     }
  9.   })
  10. );

Schema最佳实践

GraphQL规范中缺少的一个方面是缺乏对版本控制模式的指导。随着应用程序的成长和变化,它们的API也会随之变化,很可能需要删除或修改GraphQL字段和对象。但这个缺点也是积极的:通过仔细设计你的GraphQL schema,你可以避免在更容易实现(也更容易破坏)的REST端点中明显的陷阱,如命名的不一致和混乱的关系。

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

GraphQL什么时候不合适?

GraphQL不能像REST一样精确地满足HTTP通信的需求。例如,无论查询成功与否,GraphQL仅指定一个状态码——200 OK。在这个响应中会返回一个特殊的错误键,供客户端解析和识别出错的地方,因此,错误处理可能会有些棘手。

同样,GraphQL只是一个规范,它不会自动解决你的应用程序面临的每个问题。性能问题不会消失,数据库查询不会变得更快,总的来说,你需要重新思考关于你的API的一切:授权、日志、监控、缓存。版本化你的GraphQL API也可能是一个挑战,因为官方规范目前不支持处理中断的变化,这是构建任何软件不可避免的一部分。如果你有兴趣探索GraphQL,你需要投入一些时间来学习如何将其与你的需求进行最佳整合。

本文转载自微信公众号「前端全栈开发者」,可以通过以下二维码关注。转载本文请联系前端全栈开发者公众号。

当前题目:GraphQL初体验,Node.js构建GraphQLAPI指南
浏览路径:http://www.gawzjz.com/qtweb2/news24/13074.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联