跳至内容

分页

通常,您将在应用程序中有一些视图,您需要在其中显示一个包含太多数据而无法一次全部获取或显示的列表。分页是解决此问题的最常见方法,Apollo Client 具有内置功能,使分页变得非常容易。

基本上有两种方法可以获取分页数据:编号页面和游标。还有两种方法可以显示分页数据:离散页面和无限滚动。有关差异以及何时使用一种而不是另一种的更深入的解释,我们建议您阅读 Apollo 博客文章:了解分页

在本文中,我们将介绍使用 Apollo 实现这两种方法的技术细节。

使用 fetchMore

在 Apollo 中,执行分页的最简单方法是使用名为 fetchMore 的函数,该函数由 useQuery 组合函数返回。这基本上允许您执行新的 GraphQL 查询并将结果合并到原始结果中。

js
const { fetchMore } = useQuery(...)

您可以指定要用于新查询的查询和变量,以及如何在客户端上将新查询结果与现有数据合并。您如何确切地做到这一点将决定您正在实现哪种分页。

基于偏移量

基于偏移量的分页(也称为编号页面)是一种非常常见的模式,在许多网站上都可以找到,因为它通常是最容易在后端实现的。例如,在 SQL 中,编号页面可以通过使用 OFFSET 和 LIMIT 轻松生成。

让我们以这个示例查询为例,它加载一个可能包含无限数量帖子的提要

vue
<script>
import { useQuery } from '@vue/apollo-composable'
import gql from 'graphql-tag'

const FEED_QUERY = gql`
  query getFeed ($type: FeedType!, $offset: Int, $limit: Int) {
    currentUser {
      login
    }
    feed (type: $type, offset: $offset, limit: $limit) {
      id
      # ...
    }
  }
`

export default {
  props: ['type'],

  setup (props) {
    const { result } = useQuery(FEED_QUERY, () => ({
      type: props.type,
    }))

    return {
      result,
    }
  },
}
</script>

我们可以使用 useQuery 返回的 fetchMore 函数来加载提要中的更多帖子

js
export default {
  props: ['type'],

  setup (props) {
    const { result, fetchMore } = useQuery(FEED_QUERY, () => ({
      type: props.type,
      offset: 0,
      limit: 10,
    }))

    function loadMore () {
      fetchMore({
        variables: {
          offset: result.feed.length,
        },
      })
    }

    return {
      result,
      loadMore,
    }
  },
}

默认情况下,fetchMore 将使用原始 query,因此我们只需传入新的变量。

一旦从服务器返回了新数据,updateQuery 选项将用于将其与现有数据合并,这将导致您的 UI 组件重新渲染,并显示一个扩展的列表

js
export default {
  props: ['type'],

  setup (props) {
    const { result, fetchMore } = useQuery(FEED_QUERY, () => ({
      type: props.type,
      offset: 0,
      limit: 10,
    }))

    function loadMore () {
      fetchMore({
        variables: {
          offset: result.feed.length,
        },
        updateQuery: (previousResult, { fetchMoreResult }) => {
          // No new feed posts
          if (!fetchMoreResult) return previousResult

          // Concat previous feed with new feed posts
          return {
            ...previousResult,
            feed: [
              ...previousResult.feed,
              ...fetchMoreResult.feed,
            ],
          }
        },
      })
    }

    return {
      result,
      loadMore,
    }
  },
}

以上方法非常适合 limit/offset 分页。使用编号页面或偏移量进行分页的一个缺点是,当同时在列表中插入或删除项目时,可能会跳过或返回两次项目。这可以通过基于游标的分页来避免。

请注意,为了使 UI 组件在调用 fetchMore 后接收更新的 loading ref,您必须在 useQuery 选项中将 notifyOnNetworkStatusChange 设置为 true

基于游标

在基于游标的分页中,使用“游标”来跟踪应从数据集中获取下一个项目的哪个位置。有时游标可以非常简单,只需引用最后一个获取对象的 ID,但在某些情况下(例如根据某些条件排序的列表),游标需要除了最后一个获取对象的 ID 之外还编码排序条件。

在客户端上实现基于游标的分页与基于偏移量的分页并没有太大区别,但我们不是使用绝对偏移量,而是保留对最后一个获取对象的引用以及有关所用排序顺序的信息。

在下面的示例中,我们使用 fetchMore 查询来连续加载新帖子,这些帖子将被预先添加到列表中。用于 fetchMore 查询的游标在初始服务器响应中提供,并在每次获取更多数据时更新。

js
const FEED_QUERY = gql`
  query getFeed ($type: FeedType!) {
    currentUser {
      login
    }
    feed (type: $type) {
      cursor
      posts {
        id
        # ...
      }
    }
  }
`

export default {
  props: ['type'],

  setup (props) {
    const { result, fetchMore } = useQuery(FEED_QUERY, () => ({
      type: props.type,
      offset: 0,
      limit: 10,
    }))

    function loadMore () {
      fetchMore({
        // note this is a different query than the one used in `useQuery`
        query: gql`
          query getMoreFeed ($cursor) {
            moreFeed (type: $type, cursor: $cursor) {
              cursor
              posts {
                id
                # ...
              }
            }
          }
        `,
        variables: {
          cursor: result.feed.cursor,
        },
        updateQuery: (previousResult, { fetchMoreResult }) => {
          return {
            ...previousResult,
            feed: {
              ...previousResult.feed,
              // Update cursor
              cursor: fetchMoreResult.moreFeed.cursor,
              // Concat previous feed with new feed posts
              posts: [
                ...previousResult.feed.posts,
                ...fetchMoreResult.moreFeed.posts,
              ],
            }
          }
        },
      })
    }

    return {
      result,
      loadMore,
    }
  },
}

Relay 风格的游标分页

Relay 是另一个流行的 GraphQL 客户端,它对分页查询的输入和输出有自己的看法,因此人们有时会围绕 Relay 的需求构建服务器的分页模型。如果您有一个设计为与 Relay 游标连接 规范一起工作的服务器,那么您也可以毫无问题地从 Apollo Client 调用该服务器。

使用 Relay 风格的游标与基本的基于游标的分页非常相似。主要区别在于查询响应的格式,这会影响您获取游标的位置。

Relay 在返回的游标连接上提供了一个 pageInfo 对象,其中包含作为属性 startCursorendCursor 分别返回的第一个和最后一个项目的游标。此对象还包含一个布尔属性 hasNextPage,可用于确定是否还有更多结果可用。

以下示例指定一次请求 10 个项目,并且结果应在提供的 cursor 之后开始。如果为游标传递 null,relay 将忽略它并提供从数据集开头开始的结果,这允许对初始请求和后续请求使用相同的查询。

js
const FEED_QUERY = gql`
  query getFeed ($type: FeedType!, $cursor: String) {
    currentUser {
      login
    }
    feed (type: $type, first: 10, after: $cursor) {
      edges {
        node {
          id
          # ...
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
`

export default {
  props: ['type'],

  setup (props) {
    const { result, fetchMore } = useQuery(FEED_QUERY, () => ({
      type: props.type,
    }))

    function loadMore () {
      fetchMore({
        variables: {
          cursor: result.feed.pageInfo.endCursor,
        },
        updateQuery: (previousResult, { fetchMoreResult }) => {
          const newEdges = fetchMoreResult.feed.edges
          const pageInfo = fetchMoreResult.feed.pageInfo

          return newEdges.length ? {
            ...previousResult,
            feed: {
              ...previousResult.feed,
              // Concat edges
              edges: [
                ...previousResult.feed.edges,
                ...newEdges,
              ],
              // Override with new pageInfo
              pageInfo,
            }
          } : previousResult
        },
      })
    }

    return {
      result,
      loadMore,
    }
  },
}

@connection 指令

当使用分页查询时,累积查询的结果可能难以在存储中找到,因为传递给查询的参数用于确定默认存储键,但通常在执行查询的代码段之外是未知的。这对命令式存储更新来说是有问题的,因为没有稳定的存储键供更新目标使用。为了指示 Apollo Client 对分页查询使用稳定的存储键,您可以使用可选的 @connection 指令为查询的某些部分指定存储键。例如,如果我们想为之前的提要查询有一个稳定的存储键,我们可以调整我们的查询以使用 @connection 指令

graphql
query Feed($type: FeedType!, $offset: Int, $limit: Int) {
  currentUser {
    login
  }
  feed(type: $type, offset: $offset, limit: $limit) @connection(key: "feed", filter: ["type"]) {
    id
    # ...
  }
}

这将导致每次查询或 fetchMore 中累积的提要被放置在 feed 键下的存储中,我们稍后可以将其用于命令式存储更新。在此示例中,我们还使用 @connection 指令的可选 filter 参数,该参数允许我们将查询的某些参数包含在存储键中。在这种情况下,我们希望将 type 查询参数包含在存储键中,这会导致多个存储值,这些值会累积来自每种类型的提要的页面。