在GraphQL中使用游标分页实现分页

分页是数据库和Web开发中广泛使用的技术。在GraphQL中,分页的实现可以有两种方式:基于页码的分页和基于游标的分页。通常情况下,使用基于游标的分页可以提供更佳的性能与用户体验。

基于页码的分页

基于页码的分页,顾名思义是基于页码进行分页操作。例如,在一次查询中,每页显示10个结果,我们可以为该查询定义一个page参数:

------------ ----- -
  ----------- ------ ------ --- -
    --
    ----
  -
-

上面这个查询将返回第$page页的10个用户数据,当page=1时,返回结果['User1', 'User2', ..., 'User10'];当page=2时,返回结果['User11', 'User12', ..., 'User20']

为了实现基于页码的分页,GraphQL服务器必须对结果进行限制,其结果会带有前后页的按钮链接,但是这种方式有几个缺点:

  • 首先,基于页码的分页不适合大型数据集,因为查询开销大。计算出所有可用的页码非常困难,因为它们取决于许多后端因素(例如集合大小和筛选器条件),并且也不适合最终用户的经验。
  • 其次,当集合被修改时,页码会改变。例如,当添加新的记录时,就会在后续页码中看到之前看到的内容,这损害了缓存兼容性和可预测性。

在GraphQL中,我们通常使用基于游标的分页来解决这些问题。

基于游标的分页

游标分页在GraphQL中的使用方式是调整现有查询需要的参数值即可,使用序列化的游标作为参数传递。一个基本的GraphQL请求包含以下参数:first表示每页取n条数据,after表示每页的游标值。例如:

----- -
  ------------ --- ------ ----- -
    ----- -
      ---- -
        --
        ----
      -
      ------
    -
    -------- -
      -----------
      -----------
      -----------
      ---------
    -
  -
-

这个查询将返回我们所有用户的列表,每一页展示10个。查询会在第一页中含有前10个用户数据,当再次请求第二页时,只需要将after参数设为当前页页尾节点的游标值即可。

这种方式有几个好处:

  • 第一,我们将返回所有的数据,用户将无法找到任何数据条目的漏洞。在后台,我们仅返回所请求的10项(或其他数量)数据,比起所有数据,这相对容易得多。
  • 其次,由于客户端和服务器不会混合缓存数据,因此缓存的可预测性更强。 接下来,我们将详细解释如何为GraphQL添加游标分页。

游标形式

通常,游标的形式为字符串或数字类型,它随着列表中的每个元素而变化,并且客户端通过请求下一个页面来现定游标的value。

我们可以使用全局唯一标识符(GUID)作为游标值:

------------ ----- -------

上面的示例代码是一个用于创建游标值的自定义指令,它通过GraphQL schema插入,然后在每个可能用作游标(Strings,Ints等) 中使用该指令。然后我们可以像这样将查询参数传递到schema:

----- -
  ------------ -- ------ --------------------------- -
    ----- -
      ---- -
        --
        ----
      -
      ------
    -
    -------- -
      -----------
      -----------
      -----------
      ---------
    -
  -
-

在上面的查询中,游标值是字符串"MTM6UHJvZmlsZURiOlZhbHVl"。我们使用上一个子集的最后一个元素ID作为游标值的生成方法,因此我们每次请求前一个子集时都可以使用它。

基于游标的分页的实现

我们可以看一下GraphQL的分页规范,理解基于游标的分页的实现。

--------- ---- -
  --- ---
-

---- -------- -
  ------------ --------
  ------------ --------
  ------------ ------
  ---------- ------
-

---- ---- -
  ------- -------
  ----- -----
-

---- ---------- -
  ------ ------
  --------- ---------
  ----------- ---
-

上面的类型定义包括三个类型:NodeEdgeConnectionNode是一个公共接口,它为每个拥有ID的对象定义一个字段。此外,我们定义了一个PageInfo类型,其中包含分页所需数据的有关信息。Edge类型表示Connection中的子集,connection作为对象列表的特定属性返回,并返回查询中使用的游标和查询中最后一次检索到的Node使用的游标。

Connection包含一组Edge对象以及一个关于这个集合的所有信息的PageInfo对象,其中包括该集合的总数以及可以在该集合中的键之间进行导航的光标。将Edge和PageInfo字面意义地串联到Connection类型中,并在返回的结果中使用connection属性即可实现链接类型,见下面的示例代码:

---- ----- -
  - -------------
  ------------ ---- ------ -------- ---------------
-

---- -------------- -
  --------- ---------
  ------ -----------
  ----------- ---
-

---- -------- -
  ----- -----
  ------- -------
-

---- ---- -
  --- ---
  ----- -------
  -------- -------
-

在我们定义的users查询中,我们会查询到一些用户,然后作为Connection对象返回。UserConnection对象有total_count属性,firstlast参数用于决定分页和游标。我们将使用users分配给UserConnectionedges来返回UserEdge对象的数组。而且,我们通过利用用户的唯一id,将节点的游标值与其唯一id绑定在一起,每次查询只需重复最后一个游标,即可定位到接下来的结果集。

客户端再fetch数据

在我们定义基于游标的分页之后,我们的客户端可以使用以下伪代码来获取真正的数据集,我们已假设在服务器上检索到第一子集并返回结果对象:

--- ------ - ----- --------------
  ------ ------------
  ---------- -
    ------ --
  -
--

-- -------- --- ---- ----
--- ------- - ----- --------------
  ------ ------------
  ---------- -
    ------ ---
    ------ ---------------------------------
  -
--

在上面的代码中,我们通过应用游标after来Fecth下一子集。它依赖于它在前一组中定位到最后一个元素的脚步result.data.users.edges[9].cursor。它定位到这一点只是因为我们传递了first=10请求,导致我们每个集包含了十个元素,而且也是因为我们在返回数据的时候传回了game edges节点的数组。

提交cursor到后台

客户端通常会在查询之间保留上一个关键游标,并根据需要将其提交到后端,以避免重复fetch以及浪费时间和资源。为此,我们可以将游标缓存于本地状态中,然后在用户尝试加载下一页时将其提交。

--- ----------- - ----
--- ------ - ------------------ - ---------

----- ------------- -
  ----- --- - ----- --------------
    ------ ------------
    ---------- -
      ------ ---
      ------ ------
    -
  --

  ----- -
    ------ ---------
    --------- - ------------ -------------- -
  - - --------------

  ----------- - --------------

  ------------------- -- --
    ------ ----------------------------
  ---

  ------ - ------------------------ - ---------
-

在上面的代码中,我们尝试不断地向后加载页面,直到所有数据都被加载。首先,我们定义了一个hasNextPage变量并将其设置为“true”,表示我们还有页面可以加载。然后我们将当前页的“endCursor”存储为一个变量cursor,以便我们可以将其提交到后端以拾取下一页。接下来,我们使用graphql客户端执行查询,并提供先前游标为'after: cursor'。由于我们定义了first变量为10,因此它只会返回下一页的前10个数据。每个查询都会返回一个PageInfo对象,其中包含hasNextPage值。只要我们的查询返回hasNextPage=true,我们就会持续地使用cursor和新edges向我们目前缓存的数据中添加新数据,直到我们返回的hasNextPage=false

另外,我们可以引入一个UI组件,例如Load More按钮,通过与游标相结合来加载数据。

游标的类型

基本上,游标可以是任何数据类型,它随列表中的每个元素而异并连续增长。我们可以使用不同的数据类型作为游标value,例如:

  • 日期或时间戳
  • 整数或双精度浮点数
  • 全球唯一标识符(GUID)
  • etc.

首要的是保证游标值的唯一性,并且它们的排序逻辑正确。一般情况下,使用时间戳类型的游标值较为流行,但使用字符串或共享游标的作用确实将它们广泛应用于各种类型。

结论

在使用基于游标的分页时,我们能够提供更好的性能和用户体验。当我们需要一个完整的,大型集合数据时,游标分页是正确的选择。而且,通过增加我们的GraphQL schema,我们可以轻松地添加基于游标的分页,指导我们在客户端中向后提取更多数据。

来源:JavaScript中文网 ,转载请注明来源 本文地址:https://www.javascriptcn.com/post/66fb5d8144713626015be300