12k
All articles

使用 HTMX 构建无限滚动

基于 HTMX 实现无限滚动,涵盖 intersect 与 revealed 触发器、服务端驱动加载器,以及无需 JavaScript 即可运行的分页降级方案。

OpenReplay Team
OpenReplay Team
使用 HTMX 构建无限滚动

随着用户滚动加载内容可以消除分页点击,创造更流畅的浏览体验。但传统的无限滚动实现需要管理交叉观察器(intersection observers)、跟踪状态,并编写大量 JavaScript 代码。HTMX 提供了一种更简单的方法:使用 HTML 属性实现服务器驱动的无限滚动。

本指南涵盖了标准的 HTMX 无限滚动模式,解释何时使用 revealedintersect,并展示如何构建一个在没有 JavaScript 的情况下也能工作的渐进增强实现。

核心要点

  • HTMX 无限滚动使用自我延续的加载器元素模式,每个服务器响应都包含下一个加载器
  • 对于全页滚动使用 revealed,对于带有 overflow 的可滚动容器使用 intersect once
  • 基于标准分页链接构建渐进增强,确保在没有 JavaScript 的情况下也能工作
  • 服务器通过每个响应中的加载器元素控制所有分页状态

服务器驱动的无限滚动工作原理

核心模式是在”加载器”元素上放置 HTMX 属性——通常是列表中的最后一项。当这个元素变为可见时,HTMX 向服务器请求下一页。服务器返回包含新项目和指向后续页面的新加载器元素的 HTML。

以下是基本结构:

<div id="items">
  <div class="item">First item</div>
  <div class="item">Second item</div>

  <!-- Last rendered item doubles as the loader -->
  <div class="item"
       hx-get="/items?page=2"
       hx-trigger="revealed"
       hx-swap="afterend">
    Last item of page 1
  </div>
</div>

第 2 页的服务器响应包含项目和新的加载器:

<div class="item">First item of page 2</div>
<div class="item">Second item of page 2</div>

<div class="item"
     hx-get="/items?page=3"
     hx-trigger="revealed"
     hx-swap="afterend">
  Last item of page 2
</div>

这种自我延续的模式会持续进行,直到服务器返回不带加载器的内容,表示可用数据已结束。

HTMX Intersect 与 Revealed:选择正确的触发器

HTMX 提供两种基于可见性的触发器,选择错误会导致常见的 bug。

使用 revealed 用于文档本身滚动的全页滚动。当元素进入浏览器视口时,此触发器会触发。

使用 intersect once 当内容位于带有 CSS 属性如 overflow-y: scroll 的可滚动容器内时。revealed 触发器监视文档视口,而不是单个可滚动元素,因此在 overflow 容器中无法正确触发。intersect 触发器依赖于现代 Intersection Observer API,所有主流浏览器都支持该 API。

<!-- Full-page scroll: use revealed -->
<div hx-trigger="revealed" hx-get="/more">...</div>

<!-- Scrollable container: use intersect once -->
<div class="scroll-container" style="overflow-y: scroll; height: 400px;">
  <div hx-trigger="intersect once" hx-get="/more">...</div>
</div>

once 修饰符可防止元素反复进入和退出交叉阈值时产生重复请求。

交换策略:afterend 与 beforeend

交换策略决定了新内容出现的位置以及如何防止重复加载。

afterend 在加载器元素之后立即插入内容。当加载器是最后渲染的项目时使用此策略——响应会出现在它下方。

beforeend 将内容追加到目标容器内部。将其与 hx-target 结合使用以指定项目的位置:

<div hx-get="/items?page=2"
     hx-trigger="revealed"
     hx-target="#items"
     hx-swap="beforeend">
  Loading indicator...
</div>

使用 beforeend 时,加载器通常位于项目容器外部,并在加载最后一页后被替换或删除。

使用标准分页的渐进增强

使用 HTMX 的无限滚动应该建立在现有分页的基础上,而不是完全替代它。从可用的分页链接开始:

<div id="items">
  {% for item in items %}
    <div class="item">{{ item.name }}</div>
  {% endfor %}
</div>

<a href="/items?page={{ next_page }}">Load more</a>

然后使用 HTMX 属性升级分页链接:

<a href="/items?page={{ next_page }}"
   hx-get="/items?page={{ next_page }}"
   hx-trigger="revealed"
   hx-target="#items"
   hx-swap="beforeend"
   hx-select="#items > *">
  Load more
</a>

没有 JavaScript 的用户看到标准分页。使用 HTMX 的用户获得无限滚动。两者的服务器端点保持相同。

使用带外交换更新 UI 元素

有时您需要更新主内容区域之外的元素——例如项目计数器或加载状态。HTMX 带外交换(out-of-band swaps) 可以处理这个问题:

<!-- In server response -->
<div class="item">New item</div>
<span id="item-count" hx-swap-oob="outerHTML">Showing 20 of 100</span>

hx-swap-oob="outerHTML" 属性告诉 HTMX 在页面上的任何位置查找并替换具有匹配 ID 的元素,而不管主交换目标是什么。

服务器端分页策略

服务器完全控制分页状态。两种常见方法:

偏移分页(Offset pagination) 使用页码或偏移值:/items?page=3/items?offset=20。实现简单,但如果在浏览期间添加项目,可能会显示重复内容。

游标分页(Cursor pagination) 使用指向最后看到的项目的指针:/items?after=item_xyz。对于动态内容更可靠,但需要稳定的标识符。

两种方法都适用于 HTMX——客户端只需传递服务器在下一个加载器元素中提供的任何参数。

结论

HTMX 无限滚动将复杂性转移到已经存在分页逻辑的服务器端。对于文档滚动选择 revealed,对于 overflow 容器选择 intersect once。基于标准分页构建,使功能能够优雅降级。让服务器通过每个响应的加载器元素驱动状态,而不是在客户端跟踪页面。

常见问题

为什么我的无限滚动会一次触发多个请求?

这通常发生在使用 intersect 而不带 once 修饰符时。随着内容加载和布局变化,元素可能会反复跨越交叉阈值。在触发器中添加 once,如 hx-trigger intersect once,以确保每个加载器元素只触发一次请求。

如何在获取下一页时显示加载指示器?

添加指向加载元素的 hx-indicator 属性。例如,在加载器元素上使用值为 hash loading 的 hx-indicator,以及一个包含加载动画的 id 为 loading 的单独 span 元素。HTMX 会在请求期间自动显示此元素,并在完成时隐藏它。

当用户滚动速度快于内容加载速度时会发生什么?

HTMX 有助于防止重叠请求,并且每个加载器元素通常配置为只触发一次。服务器响应决定下一页。如果需要更严格的协调,可以使用 hx-sync 来控制请求行为。

我可以实现带有过滤或搜索的无限滚动吗?

可以。当过滤器更改时,重置内容容器并更新加载器 URL 以包含过滤参数。服务器处理过滤逻辑并返回适当的第一页。每个后续加载器都包含相同的过滤参数,以保持跨页面的一致性。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.