学习教程
分页

分页

¥Pagination

不同的分页模型支持不同的客户端功能

¥Different pagination models enable different client capabilities

GraphQL 中的一个常见用例是遍历对象集之间的关系。在 GraphQL 中可以通过多种不同的方式公开这些关系,从而为客户端开发者提供一组不同的功能。

¥A common use case in GraphQL is traversing the relationship between sets of objects. There are a number of different ways that these relationships can be exposed in GraphQL, giving a varying set of capabilities to the client developer.

复数

¥Plurals

公开对象之间连接的最简单方法是使用返回复数类型的字段。例如,如果我们想获取 R2-D2 的朋友列表,我们可以只要求他们全部:

¥The simplest way to expose a connection between objects is with a field that returns a plural type. For example, if we wanted to get a list of R2-D2’s friends, we could just ask for all of them:

切片

¥Slicing

不过,我们很快意识到客户可能还需要其他行为。客户可能希望能够指定他们想要获取多少个朋友;也许他们只想要前两个。所以我们想要公开类似的内容:

¥Quickly, though, we realize that there are additional behaviors a client might want. A client might want to be able to specify how many friends they want to fetch; maybe they only want the first two. So we’d want to expose something like:

{
  hero {
    name
    friends(first: 2) {
      name
    }
  }
}

但如果我们只获取前两个,我们可能还想对列表进行分页;一旦客户端获取了前两个朋友,他们可能想要发送第二个请求来询问接下来的两个朋友。我们怎样才能实现这种行为呢?

¥But if we just fetched the first two, we might want to paginate through the list as well; once the client fetches the first two friends, they might want to send a second request to ask for the next two friends. How can we enable that behavior?

分页和边缘

¥Pagination and Edges

我们可以通过多种方式进行分页:

¥There are a number of ways we could do pagination:

  • 我们可以执行类似 friends(first:2 offset:2) 的操作来请求列表中的下两个。

    ¥We could do something like friends(first:2 offset:2) to ask for the next two in the list.

  • 我们可以做类似 friends(first:2 after:$friendId) 的事情,在我们获取最后一个朋友之后询问接下来的两个。

    ¥We could do something like friends(first:2 after:$friendId), to ask for the next two after the last friend we fetched.

  • 我们可以做类似 friends(first:2 after:$friendCursor) 的事情,我们从最后一项获取光标并使用它来分页。

    ¥We could do something like friends(first:2 after:$friendCursor), where we get a cursor from the last item and use that to paginate.

一般来说,我们发现基于光标的分页是设计中最强大的。特别是如果游标是不透明的,则可以使用基于游标的分页(通过使游标成为偏移量或 ID)来实现基于偏移量或基于 ID 的分页,并且如果将来分页模型发生变化,使用游标可以提供额外的灵活性。提醒你,光标是不透明的,并且不应依赖其格式,我们建议对它们进行 Base64 编码。

¥In general, we’ve found that cursor-based pagination is the most powerful of those designed. Especially if the cursors are opaque, either offset or ID-based pagination can be implemented using cursor-based pagination (by making the cursor the offset or the ID), and using cursors gives additional flexibility if the pagination model changes in the future. As a reminder that the cursors are opaque and that their format should not be relied upon, we suggest base64 encoding them.

这给我们带来了一个问题;尽管;我们如何从对象中获取光标?我们不希望光标停留在 User 类型上;它是连接的属性,而不是对象的属性。所以我们可能想引入一个新的间接层;我们的 friends 字段应该为我们提供一个边列表,并且一条边同时具有光标和底层节点:

¥That leads us to a problem; though; how do we get the cursor from the object? We wouldn’t want cursor to live on the User type; it’s a property of the connection, not of the object. So we might want to introduce a new layer of indirection; our friends field should give us a list of edges, and an edge has both a cursor and the underlying node:

{
  hero {
    name
    friends(first: 2) {
      edges {
        node {
          name
        }
        cursor
      }
    }
  }
}

如果存在特定于边缘而不是特定于对象之一的信息,则边缘的概念也被证明是有用的。例如,如果我们想在 API 中公开 “友谊时间”,那么将其放在边缘是一个自然的放置位置。

¥The concept of an edge also proves useful if there is information that is specific to the edge, rather than to one of the objects. For example, if we wanted to expose “friendship time” in the API, having it live on the edge is a natural place to put it.

列表末尾、计数和连接

¥End-of-list, counts, and Connections

现在我们可以使用游标对连接进行分页,但是我们如何知道何时到达连接的末尾呢?我们必须继续查询,直到返回一个空列表,但我们真的希望连接能够告诉我们何时到达末尾,这样我们就不需要额外的请求。同样,如果我们想了解有关连接本身的附加信息怎么办?例如,R2-D2 总共有多少个朋友?

¥Now we have the ability to paginate through the connection using cursors, but how do we know when we reach the end of the connection? We have to keep querying until we get an empty list back, but we’d really like for the connection to tell us when we’ve reached the end so we don’t need that additional request. Similarly, what if we want to know additional information about the connection itself; for example, how many total friends does R2-D2 have?

为了解决这两个问题,我们的 friends 字段可以返回一个连接对象。然后,连接对象将具有一个用于边缘的字段以及其他信息(例如总数和有关下一页是否存在的信息)。所以我们的最终查询可能看起来更像:

¥To solve both of these problems, our friends field can return a connection object. The connection object will then have a field for the edges, as well as other information (like total count and information about whether a next page exists). So our final query might look more like:

{
  hero {
    name
    friends(first: 2) {
      totalCount
      edges {
        node {
          name
        }
        cursor
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}

请注意,我们还可能在此 PageInfo 对象中包含 endCursorstartCursor。这样,如果我们不需要边缘包含的任何附加信息,则根本不需要查询边缘,因为我们从 pageInfo 获得了分页所需的游标。这可能会提高连接的可用性;我们不仅可以公开 edges 列表,还可以公开仅包含节点的专用列表,以避免间接层。

¥Note that we also might include endCursor and startCursor in this PageInfo object. This way, if we don’t need any of the additional information that the edge contains, we don’t need to query for the edges at all, since we got the cursors needed for pagination from pageInfo. This leads to a potential usability improvement for connections; instead of just exposing the edges list, we could also expose a dedicated list of just the nodes, to avoid a layer of indirection.

完整的连接模型

¥Complete Connection Model

显然,这比我们最初设计的只有复数形式更复杂!但通过采用这种设计,我们为客户解锁了许多功能:

¥Clearly, this is more complex than our original design of just having a plural! But by adopting this design, we’ve unlocked a number of capabilities for the client:

  • 对列表进行分页的能力。

    ¥The ability to paginate through the list.

  • 能够询问有关连接本身的信息,例如 totalCountpageInfo

    ¥The ability to ask for information about the connection itself, like totalCount or pageInfo.

  • 能够询问有关边缘本身的信息,例如 cursorfriendshipTime

    ¥The ability to ask for information about the edge itself, like cursor or friendshipTime.

  • 由于用户只使用不透明的光标,因此能够更改后端的分页方式。

    ¥The ability to change how our backend does pagination, since the user just uses opaque cursors.

为了看到这一点的实际效果,示例结构中有一个名为 friendsConnection 的附加字段,它公开了所有这些概念。你可以在示例查询中查看它。尝试将 after 参数删除到 friendsConnection 以查看分页将如何受到影响。另外,尝试将连接上的 edges 字段替换为辅助程序 friends 字段,这样你就可以直接访问朋友列表,而无需额外的间接边缘层(如果适合客户端)。

¥To see this in action, there’s an additional field in the example schema, called friendsConnection, that exposes all of these concepts. You can check it out in the example query. Try removing the after parameter to friendsConnection to see how the pagination will be affected. Also, try replacing the edges field with the helper friends field on the connection, which lets you get directly to the list of friends without the additional edge layer of indirection, when that’s appropriate for clients.

连接规范

¥Connection Specification

为了确保此结构的一致实现,Relay 项目有一个正式的 specification,你可以遵循它来构建使用基于游标的连接结构的 GraphQL API。

¥To ensure a consistent implementation of this pattern, the Relay project has a formal specification you can follow for building GraphQL APIs which use a cursor based connection pattern.