גלילה מתמשכת

ההטמעה של גלילה מתמשכת נועדה להבטיח שאף פעם לא יהיו שינויי פריסה – לא משנה כמה זמן לוקח לשרת להגיב עם תוכן חדש.

אחת מהבעיות הנפוצות ביותר בהטמעות של גלילה מתמשכת היא שהכותרת התחתונה של הדף (או רכיב דומה של חוויית המשתמש) נדחפת למטה בכל פעם שמוסיפים פריטים חדשים. בזכות ההטמעה של גלילה מתמשכת, זה אף פעם לא קורה.

גישה ברמה גבוהה

כשאפשר, פריטים חדשים נוספים לדף לפני שהמשתמש מגיע אליהם. מכיוון שהוספה זו מתרחשת מחוץ למסך (ולא גלויה למשתמש), המשתמשים לא יכולים לראות שינויים בפריסה.

במקרה שלא ניתן להוסיף תוכן חדש בזמן, מוצג במקומו לחצן "Show More". עם זאת, הלחצן מופעל רק כשפריטים חדשים מוכנים להצגה, כדי להבטיח שהמשתמש לא ילחץ על הלחצן רק כדי לגלות שלא קורה דבר. לכן, לא משנה באיזו מהירות השרת מגיב לתוכן חדש (או באיזו מהירות המשתמש גולל), לעולם לא יהיו תזוזות בלתי צפויות.

הטמעה

ה-API של Intersection Visiter הוא דרך יעילה לעקוב אחרי המיקום והחשיפה של רכיבי דפים. התכנון הזה מיושם באמצעות שני צופים נפרדים של רצף מודעות:

  • listObserver מזהה את המיקום של #infinite-scroll-button שנמצא בסוף רשימת הגלילה האינסופית. כשהלחצן מתקרב לאזור התצוגה, תוכן שלא הוכנס מתווסף ל-DOM.
  • sentinelObserver מבחין במיקום של הרכיב #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();