logo

Extra Block Types (EBT) - New Layout Builder experience❗

Extra Block Types (EBT) - styled, customizable block types: Slideshows, Tabs, Cards, Accordions and many others. Built-in settings for background, DOM Box, javascript plugins. Experience the future of layout building today.

Demo EBT modules Download EBT modules

❗Extra Paragraph Types (EPT) - New Paragraphs experience

Extra Paragraph Types (EPT) - analogical paragraph based set of modules.

Demo EPT modules Download EPT modules

Scroll
04/09/2025, by Ivan

分页可能是一个看似简单却复杂的话题。很容易陷入陷阱而没有遵循最佳实践。本页将帮助你“正确”地实现分页。换句话说,如果你阅读并理解本页的内容,我们认为你的客户端会更加健壮、具有前瞻性,并且让你未来的开发更轻松。

如果你只记住本指南中的一件事,那就是:不要自己构造分页 URL

JSON:API 模块返回的每个分页响应中,已经包含了指向集合下一页的链接,你只需使用该链接即可。

在本文开头,我们将介绍 API 的一些重要特性,以及如何“正确”实现分页。在文末,你会找到一些常见问题与陷阱的解答

如何实现?

JSON:API 模块返回的每个分页响应中都内置了分页链接。下面是一个小示例:

{
  "data": [
    {"type": "sample--type", "id": "abcd-uuid-here"},
    {"type": "sample--type", "id": "efgh-uuid-here"}
  ],
  "links": {
    "self": "<collection_url>?page[offset]=3&page[limit]=3",
    "next": "<collection_url>?page[offset]=6&page[limit]=3",
    "prev": "<collection_url>?page[offset]=0&page[limit]=3"
  }
}

几点需要注意:

  • links 键下有 3 个分页链接:
    • self:当前页面的 URL。
    • next:下一页的 URL。
    • prev:上一页的 URL。
  • page[limit] 设置为 3,但结果中只有 2 个资源(?!)。

分页链接的存在与否非常重要。 需要注意以下几点:

  1. 如果存在 next 链接,说明还有更多页面
  2. 如果 next 链接不存在说明你在最后一页
  3. 如果存在 prev 链接,说明你不在第一页
  4. 如果既没有 next 也没有 prev 链接,说明只有一页

即使页面限制为 3,这里也只有 2 个资源! 这是因为某个实体因安全原因被移除了。我们知道这不是因为资源不足而无法填充响应,因为我们可以看到 next 链接。如果你想了解更多,请阅读下文详细解释

现在我们已经明确了一些重要事实。接下来我们考虑如何构建客户端。下面使用一些伪 JavaScript 来帮助理解。🧐

假设你要展示站点上的最新内容,并且有一些“高级”内容。只有付费订阅用户才能查看高级内容。同时我们决定要展示一个“前 5 条”组件,如果还有更多内容,用户可以点击“下一页”查看接下来的 5 条。

一个天真的实现可能如下:

const baseUrl = 'http://example.com';
const path = '/jsonapi/node/content';
const pager = 'page[limit]=5';
const filter = `filter[field_premium][value]=${user.isSubscriber()}`;

fetch(`${baseUrl}${path}?${pager}&${filter}`)
  .then(resp => {
    return resp.ok ? resp.json() : Promise.reject(resp.statusText);
  })
  .then(document => listComponent.setContent(document.data))
  .catch(console.log);

然而,即使忽略糟糕的错误处理,我们也知道这并不是一个健壮的实现。

正如上文所示,我们无法确保响应中一定会有 5 条内容。如果其中 2 个实体不可访问(例如未发布),那么我们的“前 5 条”组件可能只会显示 3 条!

此外,这里还有一个不必要的过滤条件。服务器本应已经移除用户无权访问的内容。否则,我们的应用可能存在访问绕过的安全漏洞,因为恶意用户可以轻易修改查询来查看“高级”内容。务必确保在服务器端强制执行访问控制;不要依赖查询参数。

下面是改进后的实现:

const listQuota = 5;
const content = [];
const baseUrl = 'http://example.com';
const path = '/jsonapi/node/content';
const pager = `page[limit]=${listQuota}`;

const getAndSetContent = (link) => {
  fetch(link)
  .then(resp => {
    return resp.ok ? resp.json() : Promise.reject(resp.statusText);
  })
  .then(document => {
    content.push(...document.data);
    listContent.setContent(content.slice(0, listQuota));

    const hasNextPage = document.links.hasOwnProperty("next");
    if (content.length <= listQuota && hasNextPage) {
      getAndSetContent(document.links.next);
    }

    if (content.length > listQuota || hasNextPage) {
      const nextPageLink = hasNextPage
        ? document.links.next
        : null;
      listComponent.showNextPageLink(nextPageLink);
    }
  })
  .catch(console.log);
}

getAndSetContent(`${baseUrl}${path}?${pager}`)

首先,你会注意到 filter 被移除了。这是因为我们假设访问检查在服务器端完成,而不是依赖过滤器。这是唯一安全的解决方案。我们可以作为性能优化再加回来,但通常没必要。

其次,由于服务器会移除用户无权访问的资源,我们确实需要检查响应中实际包含多少资源。

在“天真”的实现中,我们假设每个响应一定有 5 个项目。而在改进后的示例中,我们设置了一个“配额”为 5 个资源。发出请求后,我们检查是否达到了配额,以及服务器是否还有更多页面(通过 next 链接判断)。

如果未达到配额不是最后一页,我们就使用从文档中提取的 next 链接发起另一个请求。重要的是,我们没有手动构造下一页的 URL。无需重复造轮子,JSON:API 服务器已经为我们生成好了!

另一个有趣的点是,由于 fetch 是异步的,我们可以在第一个请求完成后立即将内容添加到组件中,而无需等待所有请求完成。当第二个请求返回时,我们再更新组件以包含新结果。

最后,我们确保虚拟的 listComponent 知道是否需要显示“下一页”链接。只有在已有额外内容服务器还有更多页面时才显示。

第一种情况是:如果第一次请求只有 4 项,第二次请求有 5 项,但没有next 链接。此时总共有 9 项,但 listComponent 只显示前 5 项。所以我们仍然希望组件显示“下一页”链接,但实际上不会再发出请求。这时我们将 nextPageLink 设置为 null

第二种情况是:如果确实有 next 链接,我们将它传递给组件,组件可以在用户点击“下一页”时发起请求。如果用户不点击,就不会发送请求。

最后几段说明了一个非常重要的概念:HTML 中的“下一页”链接不需要与 API 的分页完全对应!事实上,如果两者严格对应,这反而可能是错误的做法。

常见问题

… 为什么不能设置大于 50 的页面限制?

首先,请阅读上面的示例。理解 JSON:API 必须对响应中的每个实体执行单独的访问检查。其次,理解 JSON:API 模块的目标是“零配置”。你不需要安装、修改或配置任何东西就能使用该模块。

原因是为了保护你的应用免受 DDoS 攻击。如果恶意客户端设置页面限制为 200,000 个资源,JSON:API 模块将需要为每个实体执行访问检查。这会迅速导致内存溢出错误和响应缓慢。服务器必须设置上限。限制 50 是一个相对合理的选择。

请理解,这个决定经过了许多讨论,需要在支持成本、合理默认值和前端性能之间做出权衡。虽然 JSON:API 模块维护者知道这对某些用例可能不理想,但他们有信心如果客户端遵循本文档中的建议,这对你几乎没有影响 :)

如果你仍然希望更高的限制,可以使用 JSON:API Page Limit 模块

… 为什么响应中资源数量与期望不符?

JSON:API 模块允许你指定页面限制,但这经常被误解为保证响应中一定会包含相应数量的资源。例如,你可能知道有足够的资源来“填满”响应,但结果却少于预期。

原因与上文类似。JSON:API 仅运行数据库查询以返回 page[limit] 参数指定数量的项目。这只是一个最大值。如果查询结果中的某些资源用户无权访问,这些资源会被移除。因此,响应中的资源数量可能少于预期。

这种情况在请求可能包含未发布实体(如节点)时很常见,而这些实体并未通过 filter 参数提前过滤。

 

文章来源:Drupal 文档