所属分类:web前端开发
GraphQL 是一个令人兴奋的全新 API,用于即席查询和操作。它非常灵活并提供许多好处。它特别适合公开以图形和树形式组织的数据。 Facebook 于 2012 年开发了 GraphQL,并于 2015 年开源。
它迅速发展并成为最热门的技术之一。许多创新公司在生产中采用并使用了 GraphQL。在本教程中,您将学习:
当您的数据按层次结构或图形组织并且前端希望访问该层次结构或图形的不同子集时,GraphQL 处于最佳状态。考虑一个公开 NBA 的应用程序。你有球队、球员、教练、冠军,以及每一项的大量信息。以下是一些示例查询:
我可以提出数百个这样的查询。想象一下,您必须设计一个 API 将所有这些查询公开给前端,并且当您的用户或产品经理提出新的令人兴奋的查询内容时,能够使用新的查询类型轻松扩展 API。
这并不是一件小事。 GraphQL 旨在解决这个确切的问题,并且通过单个 API 端点,它提供了巨大的功能,您很快就会看到。
在深入了解 GraphQL 的具体细节之前,我们先将其与 REST 进行比较,后者是目前最流行的 Web API 类型。
REST 遵循面向资源的模型。如果我们的资源是球员、教练和球队,那么可能会有这样的端点:
通常,没有 id 的端点仅返回 id 列表,而具有 id 的端点则返回一个资源的完整信息。当然,您可以以其他方式设计 API(例如 /players 端点可能还返回每个玩家的姓名或有关每个玩家的所有信息)。
在动态环境中,这种方法的问题在于,您要么获取不足(例如,您只获取 id 并且需要更多信息),要么过度获取(例如,在你只是对名字感兴趣)。
这些都是难题。获取不足时,如果您获取 100 个 id,则需要执行 100 个单独的 API 调用才能获取每个玩家的信息。当过度获取时,您会浪费大量后端时间和网络带宽来准备和传输大量不需要的数据。
有多种方法可以通过 REST 来解决这个问题。您可以设计许多定制端点,每个端点都准确返回您需要的数据。该解决方案不可扩展。保持 API 的一致性是很困难的。很难进化它。很难记录和使用它。当这些定制端点之间存在大量重叠时,很难维护它。
考虑这些额外的端点:
另一种方法是保留少量通用端点,但提供大量查询参数。该解决方案避免了多端点问题,但它违背了 REST 模型的原则,而且难以一致地发展和维护。
你可以说 GraphQL 已经将这种方法发挥到了极限。它并不考虑明确定义的资源,而是考虑整个域的子图。
GraphQL 使用由类型和属性组成的类型系统对域进行建模。每个属性都有一个类型。属性类型可以是 GraphQL 提供的基本类型之一,例如 ID、String 和 Boolean,也可以是用户定义的类型。图的节点是用户定义的类型,边是具有用户定义类型的属性。
例如,如果“玩家”类型具有“团队”类型的“团队”属性,则意味着每个玩家节点到团队节点之间存在一条边。所有类型都在描述 GraphQL 域对象模型的架构中定义。
这是 NBA 域的一个非常简化的架构。球员有一个名字,与他最相关的球队(是的,我知道球员有时会从一支球队转到另一支球队),以及球员赢得的冠军数量。
球队有名字、球员阵容以及球队赢得的冠军数量。
type Player { id: ID name: String! team: Team! championshipCount: Integer! } type Team { id: ID name: String! players: [Player!]! championshipCount: Integer! }
还有预定义的入口点。它们是查询、变更和订阅。前端通过入口点与后端进行通信,并根据需要进行自定义。
这是一个简单返回所有玩家的查询:
type Query { allPlayers: [Player!]! }
感叹号表示该值不能为空。对于 allPlayers
查询,它可以返回空列表,但不能返回 null。另外,这意味着列表中不能有空玩家(因为它包含 Player!)。
这是一个基于 Node-Express 的成熟 GraphQL 服务器。它有一个内存中的硬编码数据存储。通常,数据将位于数据库中或从其他服务获取。数据定义如下(如果您最喜欢的球队或球员未能入选,请提前致歉):
let data = { "allPlayers": { "1": { "id": "1", "name": "Stephen Curry", "championshipCount": 2, "teamId": "3" }, "2": { "id": "2", "name": "Michael Jordan", "championshipCount": 6, "teamId": "1" }, "3": { "id": "3", "name": "Scottie Pippen", "championshipCount": 6, "teamId": "1" }, "4": { "id": "4", "name": "Magic Johnson", "championshipCount": 5, "teamId": "2" }, "5": { "id": "5", "name": "Kobe Bryant", "championshipCount": 5, "teamId": "2" }, "6": { "id": "6", "name": "Kevin Durant", "championshipCount": 1, "teamId": "3" } }, "allTeams": { "1": { "id": "1", "name": "Chicago Bulls", "championshipCount": 6, "players": [] }, "2": { "id": "2", "name": "Los Angeles Lakers", "championshipCount": 16, "players": [] }, "3": { "id": "3", "name": "Golden State Warriors", "championshipCount": 5, "players": [] } } }
我使用的库是:
const express = require('express'); const graphqlHTTP = require('express-graphql'); const app = express(); const { buildSchema } = require('graphql'); const _ = require('lodash/core');
这是构建架构的代码。请注意,我向 allPlayers
根查询添加了几个变量。
schema = buildSchema(` type Player { id: ID name: String! championshipCount: Int! team: Team! } type Team { id: ID name: String! championshipCount: Int! players: [Player!]! } type Query { allPlayers(offset: Int = 0, limit: Int = -1): [Player!]! }`
关键部分来了:连接查询并实际提供数据。 rootValue
对象可能包含多个根。
这里,只有 allPlayers
。它从参数中提取偏移量和限制,对所有玩家数据进行切片,然后根据团队 ID 设置每个玩家的团队。这使得每个玩家都是一个嵌套对象。
rootValue = { allPlayers: (args) => { offset = args['offset'] limit = args['limit'] r = _.values(data["allPlayers"]).slice(offset) if (limit > -1) { r = r.slice(0, Math.min(limit, r.length)) } _.forEach(r, (x) => { data.allPlayers[x.id].team = data.allTeams[x.teamId] }) return r }, }
最后,这是 graphql
端点,传递架构和根值对象:
app.use('/graphql', graphqlHTTP({ schema: schema, rootValue: rootValue, graphiql: true })); app.listen(3000); module.exports = app;
将 graphiql
设置为 true
使我们能够使用出色的浏览器内 GraphQL IDE 来测试服务器。我强烈推荐它来尝试不同的查询。
万事俱备。让我们导航到 http://localhost:3000/graphql 并享受一些乐趣。
我们可以从简单的开始,只包含玩家姓名列表:
query justNames { allPlayers { name } } Output: { "data": { "allPlayers": [ { "name": "Stephen Curry" }, { "name": "Michael Jordan" }, { "name": "Scottie Pippen" }, { "name": "Magic Johnson" }, { "name": "Kobe Bryant" }, { "name": "Kevin Durant" } ] } }
好吧。我们这里有一些超级巨星。毫无疑问。让我们尝试一些更奇特的东西:从偏移量 4 开始,有 2 名玩家。对于每个球员,返回他们的名字和他们赢得的冠军数量以及他们的球队名称和球队赢得的冠军数量。
query twoPlayers { allPlayers(offset: 4, limit: 2) { name championshipCount team { name championshipCount } } } Output: { "data": { "allPlayers": [ { "name": "Kobe Bryant", "championshipCount": 5, "team": { "name": "Los Angeles Lakers", "championshipCount": 16 } }, { "name": "Kevin Durant", "championshipCount": 1, "team": { "name": "Golden State Warriors", "championshipCount": 5 } } ] } }
所以科比·布莱恩特随湖人队赢得了 5 个总冠军,湖人队总共获得了 16 个总冠军。凯文·杜兰特在勇士队只赢得了一次总冠军,而勇士队总共赢得了五次总冠军。
魔术师约翰逊无疑是场上的魔术师。但如果没有他的朋友卡里姆·阿卜杜勒·贾巴尔,他不可能做到这一点。让我们将 Kareem 添加到我们的数据库中。我们可以定义 GraphQL 突变来执行从图表中添加、更新和删除数据等操作。
首先,让我们向架构添加突变类型。它看起来有点像函数签名:
type Mutation { createPlayer(name: String, championshipCount: Int, teamId: String): Player }
然后,我们需要实现它并将其添加到根值中。该实现只是获取查询提供的参数并将一个新对象添加到 data['allPlayers']
。它还确保正确设置团队。最后,它返回新的玩家。
createPlayer: (args) => { id = (_.values(data['allPlayers']).length + 1).toString() args['id'] = id args['team'] = data['allTeams'][args['teamId']] data['allPlayers'][id] = args return data['allPlayers'][id] },
要实际添加 Kareem,我们可以调用突变并查询返回的玩家:
mutation addKareem { createPlayer(name: "Kareem Abdul-Jabbar", championshipCount: 6, teamId: "2") { name championshipCount team { name } } } Output: { "data": { "createPlayer": { "name": "Kareem Abdul-Jabbar", "championshipCount": 6, "team": { "name": "Los Angeles Lakers" } } } }
这是一个关于突变的黑暗小秘密......它们实际上与查询完全相同。您可以在查询中修改数据,并且可能只从突变中返回数据。 GraphQL 不会查看您的代码。查询和突变都可以接受参数并返回数据。它更像是语法糖,可以使您的架构更具人类可读性。
订阅是 GraphQL 的另一个杀手级功能。通过订阅,客户端可以订阅每当服务器状态发生变化时就会触发的事件。订阅是后来引入的,不同的框架以不同的方式实现。
GraphQL 将验证针对架构的每个查询或突变。当输入数据具有复杂形状时,这是一个巨大的胜利。您不必编写烦人且脆弱的验证代码。 GraphQL 将为您处理好它。
您可以检查和查询当前架构本身。这为您提供了动态发现架构的元能力。下面是一个返回所有类型名称及其描述的查询:
query q { __schema { types { name description } }
GraphQL 是一项令人兴奋的新 API 技术,与 REST API 相比,它具有许多优势。它背后有一个充满活力的社区,更不用说Facebook了。我预测它将很快成为前端的主要内容。试一试。你会喜欢的。