การใช้งานการเลื่อนได้ไม่รู้จบนี้ออกแบบมาเพื่อให้มั่นใจว่าจะไม่มีการเปลี่ยนแปลงของเลย์เอาต์ ไม่ว่าเซิร์ฟเวอร์จะใช้เวลานานเท่าใดในการตอบสนองด้วยเนื้อหาใหม่ก็ตาม
ปัญหาหนึ่งที่พบบ่อยที่สุดกับการใช้งานการเลื่อนได้ไม่รู้จบส่วนใหญ่คือ ส่วนท้ายของหน้า (หรือองค์ประกอบ UX ที่คล้ายกัน) จะถูกเลื่อนลงไปด้านล่างทุกครั้งที่มีการเพิ่มรายการใหม่ การใช้งานการเลื่อนได้ไม่รู้จบนี้ จะไม่เกิดขึ้นเลย
แนวทางระดับสูง
เมื่อเป็นไปได้ ระบบจะแทรกรายการใหม่ลงในหน้าก่อนที่ผู้ใช้จะไปถึง เนื่องจากการแทรกนี้จะเกิดขึ้นนอกหน้าจอ (และผู้ใช้จะมองไม่เห็น) จึงไม่มีการเปลี่ยนเลย์เอาต์
ในกรณีที่ไม่สามารถแทรกเนื้อหาใหม่ได้ทันเวลา ระบบจะแสดงปุ่ม "แสดงเพิ่มเติม" แทน อย่างไรก็ตาม ปุ่มจะใช้งานได้เมื่อรายการใหม่พร้อมแสดงเท่านั้น วิธีนี้ช่วยป้องกันไม่ให้ผู้ใช้คลิกปุ่มดังกล่าวเพียงเพราะเห็นว่าไม่มีสิ่งใดเกิดขึ้น ดังนั้นไม่ว่าเซิร์ฟเวอร์จะตอบสนองต่อเนื้อหาใหม่ได้ช้าแค่ไหน (หรือเลื่อนหน้าจอผู้ใช้ก็ตาม) ก็จะไม่มีการเปลี่ยนเลย์เอาต์ที่ไม่คาดคิดเลย
การใช้งาน
Intersection Observer API เป็นวิธีที่มีประสิทธิภาพในการตรวจสอบตำแหน่งและระดับการเข้าถึงขององค์ประกอบหน้าเว็บ การออกแบบนี้ดำเนินการโดยใช้ผู้สังเกตการณ์การทดสอบ 2 รายการแยกกัน ดังนี้
listObserver
สังเกตการณ์ตำแหน่งของ#infinite-scroll-button
ที่อยู่ท้ายรายการการเลื่อนได้ไม่รู้จบ เมื่อปุ่มอยู่ใกล้กับวิวพอร์ต ระบบจะเพิ่มเนื้อหาที่ไม่ได้แทรกลงใน DOMsentinelObserver
จะดูตำแหน่งขององค์ประกอบ#sentinel
เมื่อเซนติเนลปรากฏให้เห็น ระบบจะขอเนื้อหาเพิ่มเติมจากเซิร์ฟเวอร์ การปรับตำแหน่งของเซนเซอร์เป็นวิธีการควบคุมล่วงหน้าว่าควรขอเนื้อหาใหม่จากเซิร์ฟเวอร์มากเพียงใด
นี่ไม่ใช่วิธีเดียวที่จะจัดการกับการเปลี่ยนแปลงของเลย์เอาต์ที่เกิดจากการใช้การเลื่อนได้ไม่รู้จบ วิธีอื่นๆ ในการแก้ไขปัญหานี้ ได้แก่ การเปลี่ยนไปใช้การใส่เลขหน้า การใช้ระบบรายการเสมือน และการปรับเลย์เอาต์หน้าเว็บ
HTML
<div id="infinite-scroll-container">
<div id="sentinel"></div>
<div class="item">A</div>
<div class="item">B</div>
<div class="item">C</div>
<div class="item">D</div>
<div class="item">E</div>
<button id="infinite-scroll-button" disabled>
<span class="disabled-text">Loading more items...</span>
<span class="active-text">Show more</span>
</button>
</div>
CSS
:root {
--active-button-primary: #0080ff;
--active-button-font:#ffffff;
--disabled-button-primary: #f5f5f5;
--disabled-button-secondary: #c4c4c4;
--disabled-button-font: #000000;
}
#infinite-scroll-container {
position: relative;
}
#sentinel {
position: absolute;
bottom: 150vh;
}
#infinite-scroll-button {
cursor: pointer;
border: none;
padding: 1em;
width: 100%;
font-size: 1em;
}
#infinite-scroll-button:enabled {
color: var(--active-button-font);
background-color: var(--active-button-primary)
}
#infinite-scroll-button:disabled {
color: var(--disabled-button-font);
background-color: var(--disabled-button-primary);
cursor: not-allowed;
animation: 3s ease-in-out infinite loadingAnimation;
}
#infinite-scroll-button:enabled .disabled-text {
display: none;
}
#infinite-scroll-button:disabled .active-text {
display: none;
}
@keyframes loadingAnimation {
0% {
background-color: var(--disabled-button-primary);
}
50% {
background-color: var(--disabled-button-secondary);
}
100% {
background-color: var(--disabled-button-primary);
}
}
JS
function infiniteScroll() {
let responseBuffer = [];
let hasMore;
let requestPending = false;
const loadingButtonEl = document.querySelector('#infinite-scroll-button');
const containerEl = document.querySelector('#infinite-scroll-container');
const sentinelEl = document.querySelector("#sentinel");
const insertNewItems = () => {
while (responseBuffer.length > 0) {
const data = responseBuffer.shift();
const el = document.createElement("div");
el.textContent = data;
el.classList.add("item");
el.classList.add("new");
containerEl.insertBefore(el, loadingButtonEl);
console.log(`inserted: ${data}`);
}
sentinelObserver.observe(sentinelEl);
if (hasMore === false) {
loadingButtonEl.style = "display: none";
sentinelObserver.unobserve(sentinelEl);
listObserver.unobserve(loadingButtonEl);
}
loadingButtonEl.disabled = true
}
loadingButtonEl.addEventListener("click", insertNewItems);
const requestHandler = () => {
if (requestPending) return;
console.log("making request");
requestPending = true;
fakeServer.fakeRequest().then((response) => {
console.log("server response", response);
requestPending = false;
responseBuffer = responseBuffer.concat(response.items);
hasMore = response.hasMore;
loadingButtonEl.disabled = false;;
});
}
const sentinelObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
observer.unobserve(sentinelEl);
requestHandler();
}
});
});
const listObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0 && entry.intersectionRatio < 1) {
insertNewItems();
}
});
}, {
rootMargin: "0px 0px 200px 0px"
});
sentinelObserver.observe(sentinelEl);
listObserver.observe(loadingButtonEl);
}
infiniteScroll();