使用 GraphQL 无限滚动

在构建为用户提供一长串信息的应用程序时,例如新闻提要、论坛中的帖子或聊天应用程序中的消息,我们的目标是向用户显示合理数量的信息。

用户开始滚动浏览初始列表后,Web 客户端应立即加载更多内容以继续向用户提供此信息。 这个过程被称为无限滚动。

想象一下,您正在浏览地球上每个人的姓名列表。 尽管该列表并非完全无限,但确实感觉如此。 您的浏览器可能难以处理从 GraphQL 服务器抛出的超过 80 亿个项目的列表。

这就产生了分页的需要。 GraphQL API 中的分页允许客户端或我们的前端从 API 中查询部分项目列表。 在我们的前端,我们现在可以构建无限滚动功能。

本文将讨论 GraphQL 中的分页概念及其工作原理。 我们将深入探讨无限滚动的概念及其应用、优缺点。 我们还将了解如何将 Vue 前端连接到提供数据列表的演示 GraphQL API。 有了它,我们将演示如何在前端实现无限滚动以获取数据。

  • 分页和无限滚动

    • 编号分页

    • 加载更多分页

    • 无限滚动

  • GraphQL 中的分页

    • 基于偏移的分页

    • 基于光标的分页

    • 基于 ID 的分页

  • 无限滚动的工作原理

    • 滚动事件处理程序

    • 交叉口观察者 API

  • 构建我们的应用程序

    • 设置我们的演示 API

    • 创建我们的 Vue 应用程序

    • 构建无限滚动功能

    • 使用滚动事件处理程序进行无限滚动

    • 使用 Intersection Observer API 进行无限滚动

分页和无限滚动

分页是将内容分离或划分为称为页面的离散部分的过程。

虽然我们在本文中试图完成的工作并不涉及我们创建页面来显示内容,但它仍然依赖于将内容分成几部分以便在用户滚动时加载它的概念。

无限滚动是我们今天在应用程序中看到的三种主要分页形式之一 。淘游街机游戏平台App,内置5000多款的街机游戏全免费,自带模拟器运行! 让我们快速浏览一下所有三种常见的分页形式:编号、加载更多和无限滚动。

编号分页

编号分页通常是我们谈论分页时的意思。 在这种形式中,内容被划分并显示在单独的页面上。

这种分页形式的一个很好的例子是谷歌搜索结果。

加载更多分页

加载更多方法是另一种常见的分页形式。 这是“加载更多”按钮位于列表末尾的位置,单击时会加载更多项目。 它也可以以“下一步”按钮的形式出现,加载更多项目但不一定在同一页面上。


超过 20 万开发人员使用 LogRocket 来创造更好的数字体验了解更多 →


加载更多分页方法的一个很好的例子是在博客上。 列表底部通常有一个按钮,单击该按钮可加载更多博客文章。 这种方法非常适合您希望用户在页面末尾看到页脚的网站。

对于未编号的页面,还有另一种形式的加载更多分页方法。 例如,您可以通过这些按钮在旧版 Reddit 中加载更多内容。

无限滚动

无限滚动在用户可以继续滚动一长串项目的提要中尤其常见。 这种方法没有页面的概念,但用户滚动浏览的列表是较短列表部分的组合。 主列表的这些子集在用户滚动时被获取和添加。

无限滚动是新版 Reddit 以及大多数社交媒体平台所使用的。

GraphQL 中的分页

我们已经看到了在应用程序中实现分页的常用方法,现在让我们看看它在 GraphQL 中是如何工作的。 喜马拉雅FM机车版App,开车必备听书神器,有声小说资源丰富种类全!虽然没有一种特定的分页方式,但 GraphQL 官方网站为我们提供了三种主要的分页方式 :

  • 基于偏移的分页

  • 基于光标的分页

  • 基于 ID 的分页

基于偏移的分页

这种方法通常被认为是 GraphQL 中最简单的分页形式。 对于基于偏移量的分页,查询通常包含两个参数: first(或者 limit) 和 offset(或者 skip)。 这 first参数定义列表返回的项目数和 offset参数定义了我们应该在列表中跳过多少。

使用这种分页设置,您的查询可能如下所示:

query {
  people(first: 3, offset: 0) {
    name
  }
}

此查询将从 0 开始,即第一个人,从列表中获取三个人。 我们最终得到了名单上的前三个人。

您也可以决定不从列表中的第一个人开始。 也许您希望第二页从列表中的第四人开始再显示另外三个人。 不是问题! 您的查询参数只需稍作更改:

query {
  people(first: 3, offset: 3) {
    name
  }
}

结果现在将偏移三个项目,并从第四个项目而不是第一个项目开始。

我们也可以自由更改 first和 offset值来满足我们的需要。 以下查询从列表中获取四个项目,其中 offset的 1:

query {
  people(first: 4, offset: 1) {
    name
  }
}

这意味着列表将包含从第二个开始的四个项目。


来自 LogRocket 的更多精彩文章:

  • 不要错过 The Replay 来自 LogRocket 的精选时事通讯

  • 了解 LogRocket 的 Galileo 如何消除噪音以主动解决应用程序中的问题

  • 使用 React 的 useEffect 优化应用程序的性能

  • 之间切换 在多个 Node 版本

  • 了解如何 使用 AnimXYZ 为您的 React 应用程序制作动画

  • 探索 Tauri ,一个用于构建二进制文件的新框架

  • 比较 NestJS 与 Express.js


这就是基于偏移的分页的基础。 尽管它很简单,但它也有其缺点——即它容易重复或省略数据。 此问题主要发生在分页期间将新项目添加到列表中时。

添加新项目时,数组中项目的位置可能会发生变化。电视电台直播App,涵盖了国内各地区直播频道,高清视频画质无限制播放! 而且因为 offset依赖于项目位置,如果一个项目被添加到列表的顶部,前一页上的最后一个列表项目可能会成为下一页上的第一个项目,因为它的位置现在较低。

基于光标的分页

基于光标的分页是 GraphQL 中最广泛接受的分页标准。 它对分页时列表中发生的更改具有弹性,因为它不依赖于项目的位置,而是依赖于 cursor.

基于光标的分页可以通过多种方式实现。 最常见且被广泛接受的一种遵循 Relay GraphQL 连接规范 。

为基于光标的分页实现 Relay 连接规范可能很复杂,但为我们提供了更多的灵活性和信息。

该规范为我们提供了一些可用于分页的参数。 他们包括 first和 after(或者 afterCursor) 用于前向分页和 last和 before用于向后分页。 我们还可以访问多个字段。

让我们评估这个查询:

query ($first: Int, $after: String) {
  allPeople(first: $first, after: $after){
    pageInfo {
      hasNextPage
      endCursor
    }
    edges {
      cursor
      node {
        id
        name
      }
    }
  }
}

你会注意到参数 first和 after对于这些领域,我们有:

  • pageInfo

    : 包含页面上的信息,包括:

    • hasNextPage:当前页面(或部分、子集)之后是否还有其他项目

    • endCursor: 当前页面最后一个列表项的光标

  • edges

    : 包含列表中每个项目的以下数据:

    • cursor:通常是 的不透明值 可以从项目数据中生成

    • node:实际项目数据

现在,用以下变量回顾上面的查询:

{
  "first": 2,
  "after": "YXJyYXljb25uZWN0aW9uOjI="
}

这是我们得到的回应:

{
  "allPeople": {
    "pageInfo": {
      "hasNextPage": true,
      "endCursor": "YXJyYXljb25uZWN0aW9uOjQ="
    },
    "edges": [
      {
        "cursor": "YXJyYXljb25uZWN0aW9uOjM=",
        "node": {
          "id": "cGVvcGxlOjQ=",
          "name": "Darth Vader"
        }
      },
      {
        "cursor": "YXJyYXljb25uZWN0aW9uOjQ=",
        "node": {
          "id": "cGVvcGxlOjU=",
          "name": "Leia Organa"
        }
      }
    ]
  }
}

您可以 [在 SWAPI GraphiQL 操场 ](SWAPI GraphQL API query (%24first%3A Int%2C %24after%3A String) { allPeople(first%3A %24first%2C after%3A %24after)上亲自试用此查询。

基于 ID 的分页

虽然基于 ID 的分页不像其他两种方法那样常用,但它仍然值得讨论。

这种方法与基于偏移的分页非常相似。 不同的是,而不是使用 offset为了确定返回的列表应该从哪里开始,它使用 afterID(或者干脆 after) 接受 id列表中的一项。

看看这个查询:

query {
  people(first: 3, after: "C") {
    name
  }
}

这将从列表中的前三个项目中获取 id的 C.

这有助于解决基于偏移的分页问题,因为返回的项目不再依赖于项目的位置。 现在,他们依靠自己使用的物品 id作为唯一标识符。

好的,既然我们已经熟悉了 GraphQL 中的分页是如何工作的,那么让我们深入了解无限滚动!

无限滚动的工作原理

我想假设我们之前都至少使用过一次社交媒体或新闻源应用程序。 因此,我们应该熟悉无限滚动的全部内容。 新内容会在您到达页面底部之前或同时添加到提要中。

For JavaScript applications, we can implement infinite scrolling in two main ways: either with scroll event handlers or the Intersection Observer API.

Scroll event handlers

For scroll event handlers, we run a function that will fetch the following page on two conditions:

  • The scroll position is at the bottom of the page

  • There is a next page to fetch

A generic JavaScript code for this method would look something like this:

 window.addEventListener('scroll', () => {
    let {
        scrollTop,
        scrollHeight,
        clientHeight
    } = document.documentElement;
​
    if (scrollTop + clientHeight >= scrollHeight && hasNextPage) {
      fetchMore()
    }
});

在这里,我们正在听一个 scroll中的事件 window. 在回调函数中,我们得到 scrollTop, scrollHeight, 和 clientHeight的文件。

然后,我们有一个 if检查金额是否滚动的语句( scrollTop) 添加到视口的高度 ( clientHeight) 大于或等于页面高度 ( scrollHeight),以及如果 hasNextPage是真的。

如果语句为真,则运行函数 fetchMore()获取更多项目并将它们添加到列表中。

交叉口观察者 API

与滚动事件处理方法不同,此方法不依赖滚动事件。 相反,它会监视元素何时在视口上可见并触发事件。

这是一个基本示例:

const options = {
  root: document.querySelector("#list"),
  threshold: 0.1,
};

let observer = new IntersectionObserver((entries) => {
  const entry = entries[0];
  if (entry.isIntersecting) {
    fetchMore()
  }
}, options);

observer.observe(document.querySelector("#target"));

我们使用观察者的设置来定义选项。 有了它,我们正在观察 target根中的元素。 这 root这是一个元素 id的 list. 我们还有一个 threshold这决定了有多少 target元素与根元素相交。

我们分配 IntersectionObserver作用于 observer.value. 然后我们传递一个回调函数以及定义的 options.

回调接受参数 entries, 回调收到的条目列表,带有 entry对于报告其交集状态发生变化的每个目标。 每个条目都包含几个属性,例如 isIntersecting,它告诉我们目标元素现在是否与根相交,并且在大多数情况下是可见的。

一次 entry.isIntersecting是真的, fetchMore()函数被触发并将更多项目添加到列表中。

构建我们的应用程序

我们将使用 Apollo Client 构建一个简单的 Vue 应用程序,以与演示 GraphQL API 交互。 您可以 在 Netlify 上找到我们将要构建的最终项目 。

要开始,您需要:

  • 文本编辑器——例如 VSCode

  • 基础知识 Vue

  • 基础知识 GraphQL

  • 安装了最新的 Node.js 版本

设置我们的演示 API

在本教程中,我们将使用 SWAPI GraphQL Wrapper,它是使用 GraphQL 构建的围绕 SWAPI 的包装器。

首先, 获取存储库: 从 GitHub

git clone https://github.com/graphql/swapi-graphql.git

然后使用以下内容安装依赖项:

npm install

使用以下命令启动服务器:

这将在随机 localhost 端口启动 GraphQL API 服务器。

如果您在 Windows 上并在安装时遇到与 此问题中提到的 问题类似的任何问题,您可以按照说明进行解决。 在 package.json, 你也可以编辑第 40 行 ( build:lambda)添加 SET前 NODE_ENV – SET NODE_ENV. 然后运行 npm install再次。

Alternatively, you can simply use this deployed version for your queries.

Creating our Vue app

To create a new app, navigate to the directory of your choice and run:

npm init vue@latest

Now, go through the prompts to configure your installation:

√ Project name: ... vue-infinite-scroll
√ Add TypeScript? ... No / Yes
√ Add JSX Support? ... No / Yes
√ Add Vue Router for Single Page Application development? ... No / Yes
√ Add Pinia for state management? ... No / Yes
√ Add Vitest for Unit Testing? ... No / Yes
√ Add Cypress for both Unit and End-to-End testing? ... No / Yes
√ Add ESLint for code quality? ... No / Yes
​
Scaffolding project in C:\Users\user\Documents\otherprojs\writing\logrocket\vue-infinite-scroll...
​
Done. Now run:
  cd vue-infinite-scroll
  npm install
  npm run dev

导航到您新创建的 vue-infinite-scroll目录,安装上面输出中列出的包并启动应用程序:

cd vue-infinite-scroll
npm install
npm run dev

接下来,我们将安装以下软件包:

npm install --save graphql graphql-tag @apollo/client @vue/apollo-composable

我们正在安装额外的 @vue/apollo-composable使用 Vue Composition API 支持 Apollo 的包。

接下来,让我们进行一些配置。

在里面 ./src/main.js文件,添加以下内容以创建一个 ApolloClient实例:

// ./src/main.js
​
import { createApp, provide, h } from 'vue'
import { createPinia } from 'pinia'
​
import { ApolloClient, InMemoryCache } from '@apollo/client/core'
import { DefaultApolloClient } from '@vue/apollo-composable'
​
import App from './App.vue'
import './assets/base.css'
​
// Cache implementation
const cache = new InMemoryCache()
​
// Create the apollo client
const apolloClient = new ApolloClient({
  cache,
  uri: 'http://localhost:64432'
  // or
  // uri: 'https://swapi-gql.netlify.app/.netlify/functions/index`
})
​
const app = createApp({
  setup() {
    provide(DefaultApolloClient, apolloClient)
  },
  render: () => h(App)
})
​
app.use(createPinia())
app.mount('#app')

在这里,我们创建了一个 apolloClient实例与 InMemoryCache和 uri作为我们之前设置的 SWAPI GraphQL 服务器。

现在,我们将从 GraphQL 获取数据。 在里面 ./src/App.vue文件,让我们设置我们的列表:




我们首先导入 gql来自 graphql-tag包装和 useQuery从 @vue/apollo-composable. useQuery允许我们进行 GraphQL 查询。

接下来,我们设置查询 ALLSTARSHIPS_QUERY与 first和 after我们将在进行查询时定义的变量。

为了进行查询,我们使用 useQuery(). useQuery()提供了几个属性,如 result, loading, error, fetchMore, 和 variables.

打电话时 useQuery(),我们传入实际的查询 ALLSTARSHIPS_QUERY和一个包含我们变量的对象, { first: 5 }, 以获取前五个项目。

另外,我们有一个