发布于 2026-01-05 0 阅读
0

使用 React 和 GraphQL 创建无限加载列表

使用 React 和 GraphQL 创建无限加载列表

照片由 Lysander Yuen 拍摄,来自 Unsplash


这周我需要在为现有客户开发的 React 应用中实现一个无限加载列表来显示用户。我之前也做过类似的功能,但从未使用过 GraphQL 作为服务器端。是时候学习如何实现GraphQL 规范中描述的分页机制了。

无限加载列表非常实用,它可以将大量数据拆分成小块,并在用户向下滚动列表时逐步加载。dev.to 的首页就是一个很好的例子。它避免了页面完全加载,也为移动应用提供了更好的用户体验。

无限加载人员列表
我们将创建一个无限加载的人员列表。当用户向下滚动列表时,可以看到数据正在加载。

本文涵盖的内容

本教程将介绍如何使用Apollo Server在 GraphQL 端实现分页。我们不会介绍服务器的配置,但您可以在我之前写的文章中找到如何使用 Koa 进行配置的方法。

其次,我们将使用最近添加到Apollo Client中的hooks在 React 端消费分页查询。

最后,我们将使用Brian Vaughn的react-window库来实现无限加载列表

分页

我们想要显示一个人员列表。我们将使用一个简单的数据模型,每个人有一个姓名firstname和一个电子邮件地址lastname。这足以说明我们正在实现的功能。

type Person {
    id: ID,
    lastname: String,
    firstname: String,
}
Enter fullscreen mode Exit fullscreen mode

简单的部分已经完成。现在轮到难点了:分页。分页有很多种。你可能知道基于页面的分页,它使用页面索引和元素偏移量。但这种分页模型在常见场景中存在一些局限性。你可以阅读Caleb Meredith的这篇文章了解更多相关信息。

我们将采用基于游标的分页方式。

其原理在于,游标是列表中每个元素的唯一标识符。在我们的例子中,它将是人的 ID(但也可以是任何其他内容)。

我认为举例子是最好的理解方式。让我们来执行一系列查询,加载前两组人员数据。

persons(first: 10)
Enter fullscreen mode Exit fullscreen mode

它将返回前十个人。每个人都有一个游标。我们将使用最后获取到的那个人的游标再次查询 GraphQL 服务器,以获取新的一批人员信息。

persons(first: 10, cursor: "ZmY3OTI0YWMtYTY0Ny00NTIyLWE2ZjEtNzJmMTNhN2E3NjAx")
Enter fullscreen mode Exit fullscreen mode

它会返回我们获取到的最后一个人之后的十个人。

在 GraphQL 中,分页查询返回一个connection

type Query {
    persons(first: Int!, cursor: ID): PersonConnection
}
Enter fullscreen mode Exit fullscreen mode

Aconnection允许您提供比查询元素更多的信息,例如元素总数或与当前页面相关的信息。

type PersonConnection {
    edges: [PersonEdge]
    pageInfo: PageInfo
}

type PageInfo {
    endCursor: ID!,
    hasNextPage: Boolean!,
}
Enter fullscreen mode Exit fullscreen mode

元素集合存储在一个edges属性中。该edge属性由我们之前讨论过的游标和一个node包含列表中元素所有相关信息的集合组成。由于游标并非Person直接位于数据中,因此我们可以自由地更改服务器实现而不会影响数据模型。此外,它还允许我们增强边缘节点携带的信息,例如添加一个relations属性来列出与该元素有联系的人员。

type PersonEdge {
    cursor: ID!,
    node: Person!,
}
Enter fullscreen mode Exit fullscreen mode

现在是时候实现查询的解析器了persons。我们使用 faker 生成随机数据,并提供一个种子,以确保每次请求中人员信息不会改变。

const range = (size, callback) => {
  return Array.from({length: size}, callback);
};

const resolvers = {
  Query: {
    persons: (query, {cursor, first}) => {
      faker.seed(123);
      const persons = range(200, () => ({
        id: random.uuid(),
        firstname: name.firstName(),
        lastname: name.lastName(),
      }));

      const cursorIndex = !cursor
        ? 0
        : persons.findIndex(person => person.id === cursor) + 1;
      const sliceOfPersons = persons.slice(cursorIndex, cursorIndex + first);

      return {
        edges: sliceOfPersons.map(person => ({
          cursor: person.id,
          node: {...person},
        })),
        pageInfo: {
          endCursor: sliceOfPersons[sliceOfPersons.length - 1].id,
          hasNextPage: cursorIndex + first < persons.length,
        },
      };
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

如果未提供游标参数,则first在数组开头创建一个包含指定数量元素的切片,切片数量由该参数决定。如果cursor提供了游标参数,则找到该人员在数组中的索引,并在该索引处创建一个切片。

我们不忘提供与当前页面相关的信息,方法是将切片中最后一个人的endCursor姓名设置为该属性的值。此外,我们还会添加一个属性,告知用户是否可以查询更多人员信息。indexhastNextPage

服务器端部分已完成。您可以使用 GraphQL Playground 测试查询。Apollo Server 实现默认在您的 GraphQL 端点上提供此功能。http://localhost:4000/graphl在浏览器中打开以下 URL 并输入此查询(URL 取决于您在服务器中配置的端点):

{
  persons(first: 10) {
    edges {
      node {
        lastname
        firstname
      }
    }
    pageInfo {
      endCursor
      hasNextPage
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

我们应该会在右侧面板看到人员列表。

使用 Apollo Client 中的钩子来执行查询

我将其用于create-react-appReact 应用程序。我将使用以下文件夹结构:

.
├── package.json
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── InfiniteList.css
    ├── InfiniteList.hooks.js
    ├── InfiniteList.js
    ├── index.css
    └── index.js
Enter fullscreen mode Exit fullscreen mode

首先,我们需要安装 Apollo Client 依赖项。

yarn add apollo-boost @apollo/react-hooks graphql
Enter fullscreen mode Exit fullscreen mode

在文件中,App.js我们使用实例化一个 Apollo 客户端apollo-boost,并将其传递给Providerfrom @apollo/react-hooks

import React from 'react';
import ApolloClient from 'apollo-boost';
import {ApolloProvider} from '@apollo/react-hooks';

import InfiniteList from './InfiniteList';

import './App.css';

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
});

function App() {
  return (
    <ApolloProvider client={client}>
      <div className="App">
        <InfiniteList />
      </div>
    </ApolloProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

我喜欢将数据获取逻辑与渲染逻辑分开。我们将在文件中创建一个[自定义钩子] InfiniteList.hooks.js

我们在 GraphQL 查询中创建了一个常量。

import {gql} from 'apollo-boost';

const GET_PERSONS = gql`
  query getPersons($cursor: ID) {
    persons(first: 20, cursor: $cursor) {
      edges {
        node {
          lastname
          firstname
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

我们定义了一个名为 `finally` 的自定义钩子usePersons,它会返回实现无限加载列表所需的所有变量。要调用我们的查询,我们使用 ` useQueryfinally` 中的钩子@apollo/react-hooks。它接受查询和选项作为参数。我们将选项设置notifyOnNetworkStatusChange为 `true`,这样loading每次调用查询时,变量都会更新。

import {useQuery} from '@apollo/react-hooks';

// ...

function usePersons() {
  const {data, loading, fetchMore} = useQuery(GET_PERSONS, {
    notifyOnNetworkStatusChange: true,
  });

  if (loading && !data.persons) return {loading, persons: []};

  const loadMore = () => {
    return fetchMore({
      query: GET_PERSONS,
      notifyOnNetworkStatusChange: true,
      variables: {
        cursor: data.persons.pageInfo.endCursor,
      },
      updateQuery: (previousResult, {fetchMoreResult}) => {
        const newEdges = fetchMoreResult.persons.edges;
        const pageInfo = fetchMoreResult.persons.pageInfo;

        return newEdges.length
          ? {
              persons: {
                __typename: previousResult.persons.__typename,
                edges: [...previousResult.persons.edges, ...newEdges],
                pageInfo,
              },
            }
          : previousResult;
      },
    });
  };

  return {
    persons: data.persons.edges.map(({node}) => node),
    hasNextPage: data.persons.pageInfo.hasNextPage,
    loading,
    loadMore,
  };
}

export default usePersons;
Enter fullscreen mode Exit fullscreen mode

useQuery返回一个fetchMore函数,我们可以在用户每次向下滚动列表时使用它。我们准备一个loadMore函数,该函数会使用fetchMore之前解释过的最后一个已获取人员的光标来调用它。该updateQuery选项允许我们描述如何处理新获取的人员数据块。我们将新获取的边与之前的边合并。

实现无限加载列表

我们将使用react-window这个组件来实现显示无限加载列表的功能。在实现之前,我们需要先安装依赖项。

yarn add react-window react-window-infinite-loader react-virtualized-auto-sizer
Enter fullscreen mode Exit fullscreen mode

有必要对这些依赖关系做一些解释。react-window该函数旨在高效地显示大型列表。它仅为可见元素创建组件并重用节点。

react-window-infinite-loader是一个 HOC,它会在用户向下滚动列表时即时加载元素,并且react-virtualized-auto-sizer是一个小小的附加组件,可以帮助您显示列表,使其适应父容器中的可用空间。

这些工具都是布莱恩·沃恩设计的,它们彼此之间能够完美配合。

首先,我们调用自定义钩子来获取第一批人员数据。

import React from 'react';

import usePersons from './InfiniteList.hooks';

import './InfiniteList.css';

function InfiniteList() {
  const {persons, loading, loadMore, hasNextPage} = usePersons();
}

export default InfiniteList;
Enter fullscreen mode Exit fullscreen mode

现在我们添加AutoSizer组件以获取一个width属性,height该属性表示组件容器中的可用空间。

import AutoSizer from 'react-virtualized-auto-sizer';

// ...

return (
  <div className="InfiniteList-list">
    <AutoSizer>{({height, width}) => <div />}</AutoSizer>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

我们添加了一个InfiniteLoader需要三个属性的组件:

  • isItemLoaded判断行是否已加载
  • itemCount这是列表中将显示的元素总数。在我们的例子中,由于我们可能不知道这个数字(想想推特信息流),所以我们会使用一个小技巧。如果还有下一页需要加载,我们就将总数加 1。
  • loadMoreItems是一个用于获取新一批人员数据的函数。

它还接受一个函数作为参数children。传递的对象中有两个变量,可以InfiniteLoader用来控制List下一步要添加的组件。

const personsCount = hasNextPage ? persons.length + 1 : persons.length;
const loadMorePersons = loading ? () => {} : loadMore;
const isPersonLoaded = index => !hasNextPage || index < persons.length;

// ...

return (
  <div className="InfiniteList-list">
    <AutoSizer>
      {({height, width}) => (
        <InfiniteLoader
          isItemLoaded={isPersonLoaded}
          itemCount={personsCount}
          loadMoreItems={loadMorePersons}>
          {({onItemsRendered, ref}) => <div />}
        </InfiniteLoader>
      )}
    </AutoSizer>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

List最后,我们添加了显示实际人员列表的组件。

要使其正常工作,需要一个 ` heightand`width属性。我们应该传递组件提供的值AutoSizer。`or` 属性还需要一个 `row` 值itemSize。我们还需要将变量InfiniteLoader作为 props 传递。

List组件也接受children一个函数作为参数。它会返回index当前人员的姓名和一个style属性。您必须将该属性传递style给行的父元素,以便列表正确显示。

如果尚未加载该人物,我们会显示占位符“正在加载……”。

// ...

return (
  <div className="InfiniteList-list">
    <AutoSizer>
      {({height, width}) => (
        <InfiniteLoader
          isItemLoaded={isPersonLoaded}
          itemCount={personsCount}
          loadMoreItems={loadMorePersons}>
          {({onItemsRendered, ref}) => (
            <List
              height={height}
              itemCount={personsCount}
              itemSize={40}
              onItemsRendered={onItemsRendered}
              ref={ref}
              width={width}>
              {({index, style}) => {
                let content;
                if (!isPersonLoaded(index)) {
                  content = 'Loading...';
                } else {
                  const {firstname, lastname} = persons[index];
                  content = `${firstname} ${lastname}`;
                }

                return (
                  <div className="InfiniteList-item" style={style}>
                    {content}
                  </div>
                );
              }}
            </List>
          )}
        </InfiniteLoader>
      )}
    </AutoSizer>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

一切就绪🙌!

我创建了一个存储库,其中包含了本文中提到的所有资源。

您可以通过在仓库根目录下执行以下命令来运行它:

yarn && yarn start
Enter fullscreen mode Exit fullscreen mode

非常感谢您的反馈🙏 如果您有任何问题,请在推特上联系我@YvonnickFrin

编辑:我为 React 实现部分创建了一个名为 `React.Abstract` 的抽象层。您可以在这篇文章react-simple-infinite-loading中了解更多关于该项目及其工作原理的信息

文章来源:https://dev.to/yvonnickfrin/create-an-infinite-loading-list-with-react-and-graphql-19hh