初次接触 GraphQL 的开发者在解决分页问题时,通常会直接和开发其他 Restful API 一样在 parameters 里增加诸如 pageNum, pageSize 这样的参数实现分页功能。
而实际上,官方 GraphQL 会使用 Connection, Edge 和 Node 的概念来实现分页功能。下面是 github 的 GraphQL API 查询用户的软件仓库的例子:
以 GitHub GraphQL API 为例看分页的实现模式
查询第 1 页
以下脚本查询的是我在 github 上的仓库的第一页的内容。每页包含3个仓库。
query {
viewer {
login
repositories(first:3) {
edges {
cursor
node {
id
name
}
}
pageInfo {
endCursor
startCursor
hasNextPage
hasPreviousPage
}
}
}
}
以下是执行结果:
{
"data": {
"viewer": {
"login": "surfirst",
"repositories": {
"edges": [
{
"cursor": "Y3Vyc29yOnYyOpHOBhQrBg==",
"node": {
"id": "MDEwOlJlcG9zaXRvcnkxMDE5ODUwMzA=",
"name": "graphql_typescript_starter"
}
},
{
"cursor": "Y3Vyc29yOnYyOpHOBkEhwQ==",
"node": {
"id": "MDEwOlJlcG9zaXRvcnkxMDQ5MzE3Nzc=",
"name": "moge_game"
}
},
{
"cursor": "Y3Vyc29yOnYyOpHOEG_wXw==",
"node": {
"id": "MDEwOlJlcG9zaXRvcnkyNzU3NzE0ODc=",
"name": "ladder_ui"
}
},
{
"cursor": "Y3Vyc29yOnYyOpHOEkqDlw==",
"node": {
"id": "MDEwOlJlcG9zaXRvcnkzMDY4NzMyMzk=",
"name": "ros-pipe"
}
}
],
"pageInfo": {
"endCursor": "Y3Vyc29yOnYyOpHOEkqDlw==",
"startCursor": "Y3Vyc29yOnYyOpHOBhQrBg==",
"hasNextPage": true,
"hasPreviousPage": false
}
}
}
}
}
查询第 2 页
以下是查询第 2 页的 GraphQL 脚本。脚本的内容是显示光标 “Y3Vyc29yOnYyOpHOEkqDlw==” 之后的 4 个元素。而光标 “Y3Vyc29yOnYyOpHOEkqDlw==” 就是第 1 页的 endCursor 的位置。
query {
viewer {
login
repositories(first:4, after: "Y3Vyc29yOnYyOpHOEkqDlw==") {
edges {
cursor
node {
id
name
}
}
pageInfo {
endCursor
startCursor
hasNextPage
hasPreviousPage
}
}
}
}
查询结果
{
"data": {
"viewer": {
"login": "surfirst",
"repositories": {
"edges": [
{
"cursor": "Y3Vyc29yOnYyOpHOEnC_Ug==",
"node": {
"id": "MDEwOlJlcG9zaXRvcnkzMDkzNzg4OTg=",
"name": "springboot-lessons"
}
},
{
"cursor": "Y3Vyc29yOnYyOpHOEocLDg==",
"node": {
"id": "MDEwOlJlcG9zaXRvcnkzMTA4NDAwNzg=",
"name": "workshop"
}
},
{
"cursor": "Y3Vyc29yOnYyOpHOF1taJw==",
"node": {
"id": "MDEwOlJlcG9zaXRvcnkzOTE4NjI4MjM=",
"name": "ddd-cotrip"
}
},
{
"cursor": "Y3Vyc29yOnYyOpHOF3rTJQ==",
"node": {
"id": "MDEwOlJlcG9zaXRvcnkzOTM5MjU0MTM=",
"name": "ddd-shipping"
}
}
],
"pageInfo": {
"endCursor": "Y3Vyc29yOnYyOpHOF3rTJQ==",
"startCursor": "Y3Vyc29yOnYyOpHOEnC_Ug==",
"hasNextPage": true,
"hasPreviousPage": true
}
}
}
}
}
查询第 3 页
以下是查询第 3 页的 GraphQL 脚本。脚本的目的是显示光标 “Y3Vyc29yOnYyOpHOF3rTJQ==” 之后的 4 个元素,而光标 “Y3Vyc29yOnYyOpHOF3rTJQ==” 就是第 2 页中的 endCursor.
query {
viewer {
login
repositories(first:4, after: "Y3Vyc29yOnYyOpHOF3rTJQ==") {
edges {
cursor
node {
id
name
}
}
pageInfo {
endCursor
startCursor
hasNextPage
hasPreviousPage
}
}
}
}
执行结果:
{
"data": {
"viewer": {
"login": "surfirst",
"repositories": {
"edges": [
{
"cursor": "Y3Vyc29yOnYyOpHOGL8BQw==",
"node": {
"id": "R_kgDOGL8BQw",
"name": "keycloak-test"
}
},
{
"cursor": "Y3Vyc29yOnYyOpHOHNIyZw==",
"node": {
"id": "R_kgDOHNIyZw",
"name": "prometheus"
}
}
],
"pageInfo": {
"endCursor": "Y3Vyc29yOnYyOpHOHNIyZw==",
"startCursor": "Y3Vyc29yOnYyOpHOGL8BQw==",
"hasNextPage": false,
"hasPreviousPage": true
}
}
}
}
}
GraphQL 的分页是如何实现的
我们可以从 GitHub 的例子里看到,和通常的 RESTful API 不同的是:一个 GraphQL 的分页查询中包含了以下元素
- edges
- cursor
- node
实际上除了以上三个元素以外,GraphQL 还包含了一个隐含元素
- connection
下图展示了,当我们把鼠标悬浮在脚本中的 repositories 单词上时可以看到 repositories 的类型是 RepositoryConnection。

概括地讲,GraphQL 使用 Connection 和 Edges 实现了基于光标的分页。Connection 就是要被分页展示的内容,Edges 就是每页,Cursor 只是分页的位置。要显示某页先指定光标的位置,然后指定显示每页多少个元素。
Connection
在 GraphQL 中要被分页显示的内容被叫做 Connection,比如本文例子中的 GitHub Viewer 拥有的代码仓库 repositories,或者 facebook 中一个用户的所有朋友。

GraphQL 来自于 facebook,而在 facebook 中用户和用户之间的连接 (connection) 可能就是朋友 (friends) 的关系。
Connection 是一种 GraphQL 接口。它定义了以下类型:
interface Connection {
edges: [Edge]
pageInfo: PageInfo!
totalCount: int!
}
PageInfo 的定义如下:
interface PageInfo {
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
}
Cursor 光标
GraphQL 推荐基于光标的分页。这是因为数据可能会随时被增删,使用光标可以得到相对位置,避免让用户产生迷惑。下面的例子里有 5 个位置,
- 位置1
- 位置2
- 位置3
- 位置4
- 位置5
如果每页显示3个元素用户期待的第2页结果是
- 位置4
- 位置5
如果我们在位置 1 前增加“位置6”和“位置7”,现在整体内容变为:
- 位置6
- 位置7
- 位置1
- 位置2
- 位置3
- 位置4
- 位置5
在没有光标的情况下,每页3个元素,查看第 2 页,我们会得到如下结果:
- 位置2
- 位置3
如果我们设置最后一个光标的位置为“位置3”,再取第2页的结果就还是,符合用户的预期。
- 位置4
- 位置5
Edges
Edges 可以理解为每一个分页上的内容。我们可以把它定义为
interface Edge {
cursor: String!
node: Node
}
Node
node 类似于面向对象中的 object 或者数据库中的 entity。它表示的是任何带有 ID 的类型。Node 被定义为:
inteface Node {
id: ID!
}
上面例子中 GitHub 的 repository 类型就实现了 Node 接口。
总结
GraphQL 推荐使用 Connection, Edge 实现基于光标 (Cursor) 的分页模式。Connection 就是需要被分页的内容,Edge 就是每一页的内容,而 Node 是一个类似于 Object 的父类接口。使用这种分页模式可以减少 GraphQL 使用者的学习时间,因为它是 GraphQL 的默认分页方式。

340

被折叠的 条评论
为什么被折叠?



