乐趣区

关于graphql:GraphQL初体验Nodejs构建GraphQL-API指南

原文: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 的益处和老本。

退出移动版