iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
JavaScript

解決學了JavaScript不知道能做什麼:用DOM操作打造實用網頁效果 系列 第 11 篇

【第十一課】動態生成:無窮滾動與卡片動態渲染(createElement 與觸底偵測)

  • 分享至 

  • xImage
  •  

前言

在滑 FB、IG 或各大新聞網站時,頁面永遠沒有盡頭——當你快滑到底部時,新內容就自動補充進來。這種「無窮滾動(Infinite Scroll)」能大幅提升留存率與瀏覽體驗。今天我們就來學習如何結合 DOM 動態渲染與滾動距離算式,實作這個經典功能。

核心觀念與語法拆解

  1. 資料驅動 DOM 渲染:將 API 傳回的資料陣列(Array),透過 forEach 結合 document.createElement() 動態產出對應的卡片 HTML 結構並插入頁面。
  2. 觸底公式與 Loading 防重複鎖:當 window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 100 時觸發新資料載入;並設定 isLoading 布林變數防止連續滾動造成的重複請求與渲染衝突。

觀念與函式深度解析

1. 資料驅動畫面 (Data-Driven UI) 與 createElement 動態渲染

  • 為什麼選用這個觀念?

    在實際開發中,網頁的資料通常是從伺服器端傳回來的 JSON 陣列。我們不該在 HTML 寫死靜態標籤,而是透過 JavaScript 讀取資料陣列後,動態將資料「轉化」為 DOM 節點並掛載(appendChild)到頁面上。

  • 這個機制在幹嘛?

    將資料與畫面解耦:資料(Data)負責記錄內容,DOM 操作負責把資料呈現出來。

  • 有沒有其他替代方案?

    • 方案 A:innerHTML 樣板字串(Template Literals)
      • 簡介:用整串 HTML 字串反引號 <div class="card">${title}</div> 拼接後丟給 innerHTML。
      • 比較:樣板字串寫起來很快,但在需要對產生的卡片單獨綁定點擊事件(如「按讚」、「收藏」按鈕)時,使用 createElement 建立節點後綁定事件會更安全且不會干擾已有節點。

2. 觸底偵測與防止重複載入 (Loading Lock)

  • 為什麼需要這個公式與狀態鎖?

    觸底算式為:視窗高度 (innerHeight) + 滾動距離 (scrollY) >= 網頁總高度 (scrollHeight) - 預留值。

    當使用者滑動到接近底部時,scroll 事件會以非常高的頻率觸發。如果沒有使用「載入中標記 (Loading Lock)」,系統會瞬間重複觸發數十次載入請求,造成畫面塞爆或效能卡頓。

  • 這個機制在幹嘛?

    設定一個布林變數 isLoading = true,當正在載入新資料時拒絕再次觸發,直到資料渲染完成才解鎖(isLoading = false)。

完整實作程式碼

<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8">
  <title>第十一課 - 卡片動態渲染與無窮滾動</title>
  <style>
    body { font-family: sans-serif; padding: 20px; background-color: #f0f2f5; }
    .container { max-width: 600px; margin: 0 auto; }
    
    /* 卡片容器網格 */
    .card-list { display: flex; flex-direction: column; gap: 15px; }

    /* 單張卡片樣式 */
    .card {
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.05);
      transition: transform 0.2s ease;
    }
    .card:hover { transform: translateY(-3px); }
    .card h4 { margin: 0 0 10px 0; color: #333; }
    .card p { margin: 0; color: #666; font-size: 14px; }

    /* 載入中提示文字 */
    .loading-text {
      text-align: center;
      padding: 20px;
      color: #888;
      font-weight: bold;
      display: none; /* 預設隱藏 */
    }
    .loading-text.show { display: block; }
  </style>
</head>
<body>

  <div class="container">
    <h2>動態探索卡片牆</h2>
    <div id="cardList" class="card-list"></div>
    <div id="loading" class="loading-text">⏳ 正在載入更多卡片...</div>
  </div>

  <script>
    // ----------------------------------------------------
    // JavaScript 邏輯 (資料驅動與無窮滾動)
    // ----------------------------------------------------

    const cardList = document.getElementById("cardList");
    const loading = document.getElementById("loading");

    // 全域狀態:記錄目前載入到第幾批資料,以及是否正在載入中 (防重複觸發)
    let pageCount = 1;
    let isLoading = false;

    // 模擬後端 API 產生資料的函式
    function fetchMockData(page) {
      const items = [];
      for (let i = 1; i <= 5; i++) {
        const id = (page - 1) * 5 + i;
        items.push({
          title: `卡片標題 #${id}`,
          content: `這是第 ${page} 頁產生的動態卡片內容,編號為 ${id}。DOM 動態生成的元件能彈性展示多變的資料格式!`
        });
      }
      return items;
    }

    // 核心渲染函式:將資料陣列轉化為 DOM 節點並貼上頁面
    function renderCards(data) {
      data.forEach(function(item) {
        // 1. 建立外層卡片 div
        const card = document.createElement("div");
        card.className = "card";

        // 2. 建立標題 h4
        const title = document.createElement("h4");
        title.innerText = item.title;

        // 3. 建立內文 p
        const content = document.createElement("p");
        content.innerText = item.content;

        // 4. 組合 HTML 結構
        card.appendChild(title);
        card.appendChild(content);

        // 5. 掛載到畫面的卡片清單中
        cardList.appendChild(card);
      });
    }

    // 模擬非同步載入新資料的流程
    function loadMoreCards() {
      if (isLoading) return; // 如果正在載入中,立刻擋掉,不重複執行
      
      isLoading = true; // 上鎖
      loading.classList.add("show"); // 顯示「載入中」提示

      // 模擬網路延遲 0.8 秒後載入資料
      setTimeout(function() {
        const newData = fetchMockData(pageCount);
        renderCards(newData);
        
        pageCount++; // 頁數加 1
        isLoading = false; // 解鎖
        loading.classList.remove("show"); // 隱藏「載入中」提示
      }, 800);
    }

    // 第一步:初始化頁面,預設先載入第一批卡片
    loadMoreCards();

    // 第二步:監聽視窗滾動事件,實作觸底自動載入
    window.addEventListener("scroll", function() {
      // 算出目前視窗底部距離整份文件底部的剩餘像素數
      const scrollPosition = window.innerHeight + window.scrollY;
      const threshold = document.documentElement.scrollHeight - 100; // 距離底部 100px 時觸發

      // 條件成立且非載入狀態時,自動載入下一頁
      if (scrollPosition >= threshold) {
        loadMoreCards();
      }
    });
  </script>
</body>
</html>

總結與實用情境

今天我們打通了「資料陣列 → DOM 卡片渲染」與「視窗距離算式 → 動態觸發載入」的完整流程。這是前端開發中蠻關鍵的思維轉換,代表我們大致已經具備處理動態資料串接與流暢 UX 控制的能力。

下集預告

網站中經常需要在同一個區塊切換顯示不同頁籤內容(例如:商品詳情 / 規格說明 / 顧客評價),這要怎麼做得整齊呢?

【第十二課】頁籤切換:Tab 頁籤內容切換器(Data Attribute 自訂屬性與狀態控制),我們將學習如何運用 HTML5 的 data-* 屬性輕鬆打造多頁籤組件。


上一篇
【第十課】彈窗效果:輕量化 Modal 燈箱效果(遮罩層控制與阻擋滾動)
下一篇
【第十二課】頁籤切換:Tab 頁籤內容切換器(Data Attribute 自訂屬性與狀態控制)
系列文
解決學了JavaScript不知道能做什麼:用DOM操作打造實用網頁效果 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言