使用 HTMX 构建无限滚动
随着用户滚动加载内容可以消除分页点击,创造更流畅的浏览体验。但传统的无限滚动实现需要管理交叉观察器(intersection observers)、跟踪状态,并编写大量 JavaScript 代码。HTMX 提供了一种更简单的方法:使用 HTML 属性实现服务器驱动的无限滚动。
本指南涵盖了标准的 HTMX 无限滚动模式,解释何时使用 revealed 与 intersect,并展示如何构建一个在没有 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 时,加载器通常位于项目容器外部,并在加载最后一页后被替换或删除。
Discover how at OpenReplay.com.
使用标准分页的渐进增强
使用 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 以包含过滤参数。服务器处理过滤逻辑并返回适当的第一页。每个后续加载器都包含相同的过滤参数,以保持跨页面的一致性。
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. Check our GitHub repo and join the thousands of developers in our community.