2️⃣

JavaScript 20 Projects - 2 Infinity Scroll

2023/08/17に公開

前言

這系列是課程 JavaScript Web Projects: 20 Projects to Build Your Portfolio 的筆記,學習利用 Javascript 做出各種互動網站。

以下是這系列的文章:

Project 1 Quote Generator
Project 2 Infinity Scroll

目標

要實作出一個可以無限往下滑的網頁。

下面是這次要實作的畫面。範例的連結

Image.png

建立 html 內容與 CSS 設定

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Infinity Scroll</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <!-- Titile -->
    <h1 class="titleContent">Unsplash API - Infinite Scroll</h1>
    <!-- Loader -->
    <div class="loader" id="loader" hidden>
        <img src="loader.svg" alt="Loading">
    </div>
    <!-- Image Container -->
    <div class="image-container" id="image-container"></div>
    <!-- Script -->
    <script src="script.js"></script>
</body>
</html>

思考 javascript 所需要的動作

  1. 從哪裡取得圖片?
  2. 怎麼展示取得的圖片?
  3. 要如何判定捲動到畫面的底部?
  4. 如何持續載入?

動作解讀

從哪裡取得圖片?

  1. 從 Unsplash 的 Unsplash Developers 申請一組 API key。
  2. 設定一個 empty array 來裝取得的圖片資料。
  3. 設定一個 getPhoto() function 來取得圖片。
const count = 30; //設定載入圖片數量
const apiKey = "YOUR API KEY";
const apiUrl = `https://api.unsplash.com/photos/random?client_id=${apiKey}&count=${count}`;

let photoArray = [];

async function getPhoto() {
  try {
    const response = await fetch(apiUrl);
    photoArray = await response.json();
  } catch (error) {
    console.log("I am suck !");
  }
}

getPhoto();

console.log(photoArray); 我們會得到以下的資訊。綠框的內容會用來設定引入圖片的 attributes。

截圖 2023-08-16 下午1.43.28.png

怎麼展示取得的圖片?

  1. 先設定來操作 HTML 元素的 DOM。
  2. 建立 function displayPhotos() 來展示圖片。
  3. 利用 forEach() 的方式讀取每一張圖片。
  4. 每一批載入的圖片我們都遇一個 <a> tag 來包全部的 <img>。
  5. 並利用 setAttribute 為 <a> 以及 <img> 設定 attributes。
  6. 最後利用 appendChild 來加入 DOM 中。
const imageContainer = document.getElementById('image-container');

function displayPhotos() {
  // Run function for each object in photosArray
  photosArray.forEach((photo) => {
    // Create <a> to link to full photo
    const item = document.createElement("a");
    item.setAttribute('href', photo.links.html);
    item.setAttribute('target', '_blank');
    // Create <img> for photo
    const img = document.createElement("img");
    img.setAttribute('src', photo.urls.regular);
    img.setAttribute('alt', alt_description);
    img.setAttribute('title', alt_description);
    // Put <img> inside <a>, then put both inside imageContainer Element
    item.appendChild(img);
    imageContainer.appendChild(item);
  });
}

並把 displayPhotos() 設置在 getPhoto() 中。

async function getPhoto() {
  try {
    const response = await fetch(apiUrl);
    photoArray = await response.json();
    displayPhotos()
  } catch (error) {
    console.log("I am suck !");
  }
}

優化:

目前在 displayPhotos() 裡面 setAttributes 的動作有點重複,為了簡化所以把這部分的功能再獨立出來做一個function setAttributes(),並把 displayPhotos() 裡的部分做修改。

// Helper Function to Set Attributes on DOM Elements
function setAttributes(element, attributes) {
  for (const key in attributes) {
    element.setAttribute(key, attributes[key]);
  }
}

// Create Elements For Links & Photos, Add to DOM
function displayPhotos() {
  // Run function for each object in photosArray
  photosArray.forEach((photo) => {
    // Create <a> to link to full photo
    const item = document.createElement("a");
    setAttributes(item, {
      href: photo.links.html,
      target: "_blank",
    });
    // Create <img> for photo
    const img = document.createElement("img");
    setAttributes(img, {
      src: photo.urls.regular,
      alt: photo.alt_description,
      title: photo.alt_description,
    });
    // Put <img> inside <a>, then put both inside imageContainer Element
    item.appendChild(img);
    imageContainer.appendChild(item);
  });
}

要如何判定捲動到畫面的底部?

下面是課程提供的說明圖。這邊設定觸發載入圖片的條件是
視窗的高度(window.innerHeight)+滑鼠捲動的高度(window.scrollY)大於圖片們內容的高度(document.body.offsetHeight),而更近一步地要在還沒到底之前就觸發,所以在圖片內容的這一邊再多減1000px。若此條件達成,就載入更多的圖片。

Infinite+Scroll+Functionality.png

from JavaScript Web Projects: 20 Projects to Build Your Portfolio

// Check to see if scrolling near bottom of page, Load More Photos
window.addEventListener("scroll", () => {
  if (
    window.innerHeight + window.scrollY >= document.body.offsetHeight - 1000
  ) {
    getPhotos();
  }
});

如何持續載入?

目前的狀況就是捲動觸發的載入判定只會發生一次,還需要設計一個條件是判定他什麼時候可以再次載入?

這部分需要再思考的幾個點

  1. 捲動的觸發可能需要跟一個開關條件綁在一起,當捲動條件達成且開關條件也成立時,才能載入圖片。
  2. 如何知道什麼時候載入會完成來把開關關掉?

解決方法

  1. 捲動的觸發可能需要跟一個開關條件綁在一起,當捲動條件達成且開關條件也成立時,才能載入圖片。
    • 這邊設立一個變數 ready,初始值為 false,且跟捲動觸發條件做 and 運算,當條件達成時才設為 true,才會載入圖片。
let ready = false;

// Check to see if scrolling near bottom of page, Load More Photos
window.addEventListener("scroll", () => {
  if (
    window.innerHeight + window.scrollY >= document.body.offsetHeight - 1000 &&
    ready
  ) {
    ready = false;
    getPhotos();
  }
});
  1. 如何知道什麼時候載入會完成來把開關關掉?

就是當每次30張圖片載完之後。

  • 利用onload Event來觸發一個 imageLoaded() function 來計算載入了多少張圖片。
    • 設定變數 imagesLoaded 以及 totalImages 來計算數量。
    • img.addEventListener("load", imageLoaded); 當有載入就觸發imageLoaded()
  • imageLoaded() 中設定 imagesLoaded++; 計算數量。
    • 當載入圖片到達預定數量時,把 ready 設為 true 並把 loader 關掉。
const loader = document.getElementById("loader");

let ready = false;
let imagesLoaded = 0;
let totalImages = 0;
let photosArray = [];

// Check if all images were loaded
function imageLoaded() {
  imagesLoaded++;
  if (imagesLoaded === totalImages) {
    ready = true;
    loader.hidden = true;
  }
}

// Create Elements For Links & Photos, Add to DOM
function displayPhotos() {
  imagesLoaded = 0;
  totalImages = photosArray.length;
  // Run function for each object in photosArray
  photosArray.forEach((photo) => {
    // Create <a> to link to full photo
    const item = document.createElement("a");
    setAttributes(item, {
      href: photo.links.html,
      target: "_blank",
    });
    // Create <img> for photo
    const img = document.createElement("img");
    setAttributes(img, {
      src: photo.urls.regular,
      alt: photo.alt_description,
      title: photo.alt_description,
    });
    // Event Listener, check when each is finished loading
    img.addEventListener("load", imageLoaded);
    // Put <img> inside <a>, then put both inside imageContainer Element
    item.appendChild(img);
    imageContainer.appendChild(item);
  });
}

最後的Javascript程式碼

const imageContainer = document.getElementById("image-container");
const loader = document.getElementById("loader");

let ready = false;
let imagesLoaded = 0;
let totalImages = 0;
let photosArray = [];

// Unsplash API
const count = 30;
const apiKey = "YOUR API KEY";
const apiUrl = `https://api.unsplash.com/photos/random?client_id=${apiKey}&count=${count}`;

// Check if all images were loaded
function imageLoaded() {
  imagesLoaded++;
  if (imagesLoaded === totalImages) {
    ready = true;
    loader.hidden = true;
  }
}

// Helper Function to Set Attributes on DOM Elements
function setAttributes(element, attributes) {
  for (const key in attributes) {
    element.setAttribute(key, attributes[key]);
  }
}

// Create Elements For Links & Photos, Add to DOM
function displayPhotos() {
  imagesLoaded = 0;
  totalImages = photosArray.length;
  // Run function for each object in photosArray
  photosArray.forEach((photo) => {
    // Create <a> to link to full photo
    const item = document.createElement("a");
    setAttributes(item, {
      href: photo.links.html,
      target: "_blank",
    });
    // Create <img> for photo
    const img = document.createElement("img");
    setAttributes(img, {
      src: photo.urls.regular,
      alt: photo.alt_description,
      title: photo.alt_description,
    });
    // Event Listener, check when each is finished loading
    img.addEventListener("load", imageLoaded);
    // Put <img> inside <a>, then put both inside imageContainer Element
    item.appendChild(img);
    imageContainer.appendChild(item);
  });
}

// Get photos from Unsplash API
async function getPhotos() {
  try {
    const response = await fetch(apiUrl);
    photosArray = await response.json();
    // console.log(photosArray);
    displayPhotos();
  } catch (error) {
    // Catch Error Here
  }
}

// Check to see if scrolling near bottom of page, Load More Photos
window.addEventListener("scroll", () => {
  if (
    window.innerHeight + window.scrollY >= document.body.offsetHeight - 1000 &&
    ready
  ) {
    ready = false;
    getPhotos();
  }
});

// On Load
getPhotos();

Code Review

可以利用 Chrome 的 Developer Tool 來觀察,當我們每次打開頁面或是重新整理時,載入的時間都會很久,一開始就會讓體驗變差。

Image.png

為了解決最初載入時時間過久的問題,可以直接去調低最初載入的圖片數量,等需要再載入圖片時,再把需求調回原本的30張。

// Unsplash API
let count = 5;
const apiKey = "YOUR API KEY";
const apiUrl = `https://api.unsplash.com/photos/random?client_id=${apiKey}&count=${count}`;

// Check if all images were loaded
function imageLoaded() {
  imagesLoaded++;
  if (imagesLoaded === totalImages) {
    ready = true;
    loader.hidden = true;
    count = 30;
  }
}

Discussion