基于 Stimulus 实现滚动加载组件
滚动加载是一种常见的分页交互方式,经常用在 Timeline 之类希望体验平滑又不需要跳跃浏览的场景。
本文介绍一种基于 Stimulus 实现滚动加载的方法。
前置知识:Intersection Observer API
首先考虑一个问题,当用户浏览到列表底部的时候,用什么方法触发内容加载。
以前的方法是监听浏览器窗口的滚动事件,在滚动位移量接近窗口底部的时候触发加载动作。
现代的浏览器提供了一个专门 API 实现这类需求,这就是 Intersection Observer API。这个 API 用于检测两个元素是否相交,在元素有相交部分时,执行回调。
创建 observer 的代码如下:
let options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
其中参数的含义为:
-
root
:参照的可视区域,默认为浏览器视窗。 -
rootMargin
:可视区域的边距,target
进入边距范围内就认为相交。 -
threshold
:回调触发的阈值,可以是 0...1 之间的值,也可以是数组。例如 0.5 就target
在root
内有 50% 相交时触发。
启动 observer 的代码如下:
let target = document.querySelector('#listItem');
observer.observe(target);
开始监视后,target
在进入 root
范围内达到相应阈值时就会启动 callback
。注意,无论 threshold
设置什么值,都会在 observe
开始时触发一次回调,这时相交比例为 0。
在 callback
中,可以获得监视对象的具体属性,实现业务逻辑:
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
更多关于 Intersection Observer API 的内容可以查看 https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API 。
在了解这个 API 之后,可以设计滚动加载的逻辑如下:
- 在列表底部放一个元素用作
target
。 - 找到列表的可滚动父级元素作为
root
(默认是浏览器窗口)。 - 用 Intersection Observer API 监视
target
和root
的相交状态,当target
进入root
的rootMargin
范围内时,触发加载事件。
结构和样式
首先假设要处理的列表结构如下:
<div id="list" class="list">
<div class="list__item">List Item 1</div>
<div class="list__item">List Item 2</div>
<div class="list__item">List Item 3</div>
<a class="list__next-link" href="#">Loading...</a>
</div>
添加样式:
.list__item {
padding: 16px;
border: 1px solid #ccc;
list-style: none;
background: white;
}
.list__next-link {
display: block;
padding: 16px;
text-align: center;
color: #999;
text-decoration: none;
}
目前看起来是这个样子:
现在我们能直接看到下一页的链接,下面要实现当这个链接出现在视图里时,自动加载链接的内容,并且拼接到列表尾部。
用 Stimulus 绑定行为
给列表加上 Stimulus 的标记:
<div id="list" class="list" data-controller="pagination">
<!-- 省略 -->
<a class="list__next-link" href="#" data-pagination-target="nextPageLink">Loading...</a>
</div>
创建 controller
:
// pagination_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "name", "output" ]
}
当组件加载时,监视 nextPageLink
目标:
connect() {
this.observeNextPageLink()
}
observeNextPageLink() {
// nextPageLinkTarget 存在时才监控
if (this.hasNextPageLinkTarget) {
let observer = new IntersectionObserver((entries, observer) => {
// observer 开始监视时就会触发 callback,所以要检查监视对象是否已经出现在目标区域。
if (entries[0].isIntersecting) {
// 关闭监视
observer.disconnect()
// 加载下一页
this.loadNextPage()
}
}, {
// 监视参数
root: null,
rootMargin: '40px',
})
// 开始监视
observer.observe(this.nextPageLinkTarget)
}
}
接下来实现 loadNextPage
。具体怎么加载下一页内容需要和后端配合,这里有很多方法,例如后端返回 json 数据,前端渲染;后端返回 HTML 片段,前端拼接;使用 Turbo Stream 等等。
这里使用的方法是,复用后端已有的分页输出,前端获取下一页内容后,提取其中 ID 相同的部分,将它拼接到当前内容的尾部。这样后端逻辑不需要为这个前端组件添加新的接口,只要确保各个分页的内容有相同的 ID 就行。
具体实现为:
loadNextPage() {
// 获取 nextPageLink 的链接内容
fetch(this.nextPageLinkTarget.href)
.then((response) => { return response.text()})
.then((html) => {
let parser = new DOMParser()
let doc = parser.parseFromString(html, 'text/html')
let content = doc.getElementById(this.element.id)
// 将新内容替换到 nextPageLink 的位置
this.nextPageLinkTarget.outerHTML = content.innerHTML
// 旧的 nextPageLinkTarget 已经移除,监控下一个 link。
this.observeNextPageLink()
})
}
以上就是滚动加载的实现。示例的完整代码可以看下方链接:
https://codepen.io/chloerei/pen/OJggLrz
参考资料
- Intersection Observer API https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API