《台北一日遊》前端技術文 #1 向左按向右按之圖片輪播


Posted by yunhu412 on 2025-05-16

前情提要:

《台北一日遊》是我 2024 年在 WeHelp Bootcamp 花了快兩個月獨立完成的專案,是一個能讓使用者「探索雙北捷運沿線特色景點」與「預約付費導覽服務」的全端旅遊電商網站。

前端 UI 依照 WeHelp Bootcamp 提供的 Figma 設計稿進行開發,後端依照 WeHelp Bootcamp 提供的景點資料和 RESTful API 文件進行開發。

Bootcamp 會在每週一指派當週的專案進度,而完成進度需要用到的實作技術和知識、開發過程遇到的難題,主要都靠我自己找線上資源自學和突破。

前端我採用 HTML、SCSS 和 JavaScript 來開發,全部的 UI 都是手刻的,更多專案說明和技術 Demo 歡迎參考我的 GitHub


《台北一日遊》中,使用者在首頁點擊自己感興趣的景點後,會跳轉到該景點的介紹頁面(以下稱為「景點頁」)。

景點頁上有景點名稱、介紹、交通資訊等資料,也有讓使用者訂購付費行程的表單等互動區塊,當然還有我們今天的主角:圖片輪播。

圖片輪播(Image Carousel)存在著多種樣貌,但一切都離不開它的本質:它是一個可以輪流播放圖片的相簿。

根據我的觀察,目前常見的網站圖片輪播主要分成以下三種類型:
1. 自動輪播型:固定在 n 秒後自動播放下一張圖片,播到最後一張又重返第一張,常見於電商網站的 Banner 或商品圖。
2. 手動切換型:當使用者手動點擊圖片上的向左/向右按鈕(正式中文名稱:輪播控制項),將會觸發往前/往後一張圖片的行為,例如本文分享的《台北一日遊》圖片輪播。
3. 自動輪播 + 手動切換型:顧名思義,就是上面兩種類型的綜合版。

除了上面提到的三種類型,如果你看到圖片輪播上有一排小圓點(正式中文名稱:輪播指示器),可以留意一下,看看它是否擁有能夠觸發圖片輪播效果的「隱藏版小功能」。

小圓點的功用是顯示整個輪播的圖片總數,以及目前的圖片位在整個輪播中的順序,辨識的方法,就是找出顏色和「眾點」不同的那個小圓點(「萬綠叢中一點紅」的概念),以及該小圓點位在整排小圓點的順序。

而這邊提到的「隱藏版小功能」就是:點擊第 n 個小圓點,就可以直接跳到輪播的第 n 張圖!不過,不見得每個輪播都有這樣的設計,下次你可以留意看看!

根據我上面定義的三種輪播類型和隱藏小功能,其實可以歸納出圖片輪播基本的組成有:
1. 一張以上的圖片/照片(必要),對應的就是最基礎的圖片輪播類型:「自動輪播型」
2. 輪播控制項(非必要),可以讓使用者決定要返回前一張或者前往下一張圖片(就輪播的編排順序),對應的就是「手動切換型」
3. 輪播指示器(非必要),可以顯示圖片總數與目前圖片位在輪播的順序

為大家介紹完圖片輪播的組成和類型,接下來我們就要進入本文的正題──示範《台北一日遊》中的「手動切換型」的圖片輪播要怎麼實作。

1. 用 HTML 撰寫圖片輪播的結構

HTML 可以說是網頁的骨架,告訴瀏覽器的渲染引擎網頁具備了哪些元素。

以《台北一日遊》的圖片輪播來說,必備的 HTML 元素有:圖片輪播主容器、景點圖片、小圓點區,還有向左切換圖片的 icon 和向右切換圖片的 icon。

其中,景點圖片的 src 屬性和小圓點的數量會根據不同景點而有所差異,所以我們留到 JavaScript 的部分再來處理。

        <!--圖片輪播的主要容器,包裹了景點圖片、小圓點,和可以向左/右切換的 icon--> 
        <div id="attraction__top__pics">

          <!--顯示當前景點圖片-->
          <img id="attraction__top__pic" src="" alt="景點照片" /> 

          <!--顯示小圓點區,用來包裹之後由 JavaScript 動態生成的小圓點-->
          <div id="attraction__top__pic__dots"></div>

          <!--向左切換圖片的 icon-->
          <img
            id="attraction__top__pics__left-btn"
            src="/static/styles/images/attraction/left-btn.png"
            alt="往左滑"
          />   

          <!--向右切換圖片的 icon-->        
          <img
            id="attraction__top__pics__right-btn"
            src="/static/styles/images/attraction/right-btn.png"
            alt="往右滑" 
          />
        </div>

2. 用 SCSS 設置圖片輪播的樣式

由於 CSS 的部分較為繁瑣,所以我會直接把各個屬性的功用寫在註解中。因為希望能讓版面整潔一些,所以前面有寫過的 CSS 屬性,我後面就不會再寫了喔,還請見諒!

   // 設定整體圖片輪播的樣式
    #attraction__top__pics {
      width: 50%; // 設寬度為父元素的一半
      padding: 0 1.3%; 
      /* 設上下內距 0、左右內距為父元素的 1.3%(後來我多用 rem 當 padding 
      單位,這邊保持當初的寫法) */
      height: 400px; // 設高度為 400px
      overflow: hidden;  // 若元素內容溢出,將其隱藏
      position: relative; 
      // 將 position 設為相對定位,作為下方三個直接子元素的絕對定位依據 

    // 設定景點圖片樣式
      #attraction__top__pic {
        width: 100%;
        height: 400px;
        position: relative;
        object-fit: cover; 
        /* 讓圖片在保持寬高比的情況下填滿元素框,
        若寬高比與元素框不符則剪裁超出部分 */
      }

    // 設定小圓點容器的樣式
      #attraction__top__pic__dots {
        width: 100%;
        height: auto;
        text-align: center; // 讓小圓點們保持置中
        position: absolute;
        /* 將 position 設為絕對定位,
        定位依據是父元素 #attraction__top__pics */
        bottom: 3%; // 以定位點為基準,上移 3% 的距離

        // 設定小圓點的樣式
        .dot {
          display: inline-block; 
          /* 這樣設置可讓小圓點像行內元素一樣同行排列,
          也能像區塊元素一樣設定寬高 */
          width: 12px;
          height: 12px;
          border-radius: 50%; 
          // 將邊框半徑設為 50%,就會獲得一個圓
          background-color: rgba(232, 232, 232, 1); 
          margin: 0 3px; 
          /* 設上下邊距 0、左右邊距為3px,製造小圓點間的留白
          (後來我多用 rem 當 padding 單位,這邊保持當初的寫法)*/
          opacity: 0.7; 
          /* 將小圓點的透明度設為 0.7
          (透明度的範圍為 0.0 到 1.0,數值越小越透明) */
        }

        // 設定標記當頁輪播的小圓點樣式
        .dot--active {
          background-color: black; // 設定小圓色的顏色
        }
      }

    // 統一設定向左和向右切換圖片 icon 的樣式
      #attraction__top__pics__left-btn,
      #attraction__top__pics__right-btn {
        width: 100%;
        max-width: 30px; // 設寬度最大為 30px
        position: absolute; 
        top: 50%;
        opacity: 0.5;
        cursor: pointer; // 設游標圖示為 pointer

        &:hover {
          opacity: 1; 
          /* 游標懸停時,讓切換圖片 icon 的透明度變成 1
          (原本為0.5),提高 icon 能見度 */
        }
      }

    // 設定向左切換圖片 icon 的單獨樣式
      #attraction__top__pics__left-btn {
        left: 3%; // 以定位點為基準,右移 3% 的距離
      }

     // 設定向右切換圖片 icon 的單獨樣式
      #attraction__top__pics__right-btn {
        right: 3%; // 以定位點為基準,左移 3% 的距離
      }
    }

3. 用 JavaScript 實現圖片輪播的動態內容與互動邏輯

前面提到,景點圖片和小圓點的數量會依照不同景點而有所差異,所以需要交給 JavaScript 來處理。

其實不只是景點圖片和小圓點數量,更完整地說,因為每個景點都有各自的名稱、分類、最近的捷運站、介紹、地址、交通資訊等等,所以無法事先將這些資訊寫死在 HTML 上,而是必須等到頁面載入後,透過 JavaScript 和後端 API 互動來獲取對應的資料,並處理這些得到的資料,才能渲染到畫面中。

而上面提到的這些動作,我把它們統一封裝進 renderAttractionPage 這個主函式,並在景點頁初步載入後(DOMContentLoaded)執行它。

document.addEventListener("DOMContentLoaded", () => {
  renderAttractionPage();
});

為了加快使用者看到頁面主要內容的體感速度,我選擇在 DOMContentLoaded 事件觸發後,立即執行 renderAttractionPage 函式。

你也許會好奇:為什麼我會選擇監聽 DOMContentLoaded 事件,而不是更常見的 window.onload 事件呢?

原因是,景點頁上絕大部分的資料都要透過與後端 API 互動來取得,如果選擇監聽 window.onload 事件,就必須等到整個頁面的靜態資源(包含HTML、JavaScript、CSS 、圖片等)都載入完畢,才能執行回乎函式。如此一來,便會延遲頁面初始化的時間。

相對地,如果選擇監聽 DOMContentLoaded 事件,就可以在 HTML 解析完畢、DOM Tree 建置完成後(此時可以安全地選取與操作 DOM 元素),立即開始與後端 API 互動。這樣一來,就能僅早取得和處理景點資料,更快渲染出景點頁的主要內容,來創造更好的使用者體驗。

async function renderAttractionPage() {
  const attractionId = getAttractionIdFromURL();
  const data = await fetchAttractionData(attractionId);
  renderAttraction(data);
}

renderAttractionPage 主函式的功用是讓景點頁初始化,它調用了三個子函式,以下是它們的名稱和作用:

1. getAttractionIdFromURL從當前景點頁 URL 獲取景點 ID

function getAttractionIdFromURL() {
  const pathName = window.location.pathname;
  const pathParts = pathName.split("/");
  const attractionId = pathParts[pathParts.length - 1];
  return attractionId;
}

2. fetchAttractionData(attractionId)傳入取得的景點 ID 作為參數,向後端發送非同步的 GET 請求,以獲得該景點的專屬資料

async function fetchAttractionData(attractionId) {
  const url = `/api/attraction/${attractionId}`;

  try {
    const response = await fetch(url);
    const responseJSON = await response.json();
    const data = responseJSON.data;
    return data;
  } catch (err) {
    console.error("獲取景點數據失敗:", err);
  }
}

3. renderAttraction(data)處理從後端獲得的專屬景點資料,並指定資料要在頁面何處渲染

let imgs;

function renderAttraction(data) {
  const name = data.name;
  const h2Name = document.querySelector("#attraction__top__info__name");
  h2Name.textContent = name;

   // 因為本文主角是圖片輪播,這邊省略其他景點資料

  imgs = data.images[0].split(",");
  renderImgsAndDots(); // 顯示第一張圖片和第一個點點
}

到目前為止,我們已經從後端 API 獲取和處理了對應的資料,並指定要在何處渲染。接下來,我們要來處理景點圖片和小圓點的渲染邏輯,還有向左/右切換圖片 icon 的互動邏輯。

景點圖片和小圓點的渲染邏輯

前面提到,景點頁的主函式 renderAttractionPage 調用了三個子函式,其中的 renderAttraction(data) 子函式又調用了 renderImgsAndDots 這個函式,而它的功用正是渲染景點圖片和小圓點。

// 先選取 DOM 中的小圓點容器元素,方便後續操作
const dots = document.querySelector("#attraction__top__pic__dots");

// 先選取 DOM 中的景點圖片元素,方便後續操作
const imgPic = document.querySelector("#attraction__top__pic");

// 宣告並賦值變數 i,用於控制輪播的圖片索引值
let i = 0;

function renderImgsAndDots() {
  renderImgs();
  renderDots();
}

負責渲染圖片和小圓點的 renderImgsAndDots 還包括了下面兩個子函式:
1. renderImgs功用是渲染景點圖片。

function renderImgs() {
  imgPic.src = imgs[i];
}

而決定要渲染的第幾張圖的關鍵,在於我們稍早宣告和賦值的變數 i 。它將作為索引值,代入陣列變數 imgs,並將對應到的圖片網址賦值給變數 imgPic 的 src 屬性。

換句話說,就是從 imgs 這個陣列中,取出第 i 個元素(圖片網址),再將取得的網址指定給 DOM 中負責顯示景點圖片的 img 標籤 的 src 屬性。如此一來,使用者便能透過操作 icon,確實切換到對應的圖片。

另外,由於變數 i 的預設值是 0,對應到的是陣列的第一個位置,所以使用者造訪景點頁時,看到的輪播圖片都會是第一張。

2. renderDots功用是渲染小圓點。

function renderDots() {
  dots.innerHTML = "";
  for (let j = 0; j < imgs.length; j++) {
    const spanDot = document.createElement("span");
    spanDot.classList.add("dot");
    dots.appendChild(spanDot);

    if (j === i) {
      const dot = document.querySelector(`.dot:nth-child(${i + 1})`);
      // 同const dot = dots.children[j];
      dot.classList.add("dot--active");
    }
  }
}

首先,我們需要將上一張圖的小圓點清空,因為我們要重新渲染當頁的小圓點。接下來,我們用一個迴圈來生成全部的小圓點,小圓點的數量要和輪播圖片的總數相同。而生成小圓點的同時,也需要指定它的 HTML 標籤種類和 CSS 類名,以及它會被放在哪個特定的 HTML 元素中作為子元素。

再來,我們需要在迴圈中加入一個判斷式,目的是為當頁的小圓點加上特殊的 CSS 類名。當迴圈中定義的變數 j 與迴圈外宣告的變數 i 的值相同時,我們就使用偽類選擇器 :nth-child() 選取第 i + 1 個小圓點,為它加上能渲染出特殊色的 CSS 類名。

至於為什麼是選取第 i + 1 個小圓點,而不是第 i 個小圓點呢?那是因為,變數 i 和變數 j 的初始值都是 0 ,而 :nth-child() 的起始值是 1(因為在這種情況下,我們要選的至少是第 1 個小圓點,而不是第 0 個小圓點),所以要幫 :nth-child() 內的參數值再 + 1,這樣才能選到我們真正想選的那個小圓點。

向左/右切換圖片 icon 的互動邏輯

首先,我們需要為這兩個 icon 掛上各自的事件監聽器。如此一來,當使用者點擊其中一個 icon ,就能觸發寫在回呼函式(callback)內的行為。

以使用者的角度來看,向左切換圖片的 icon (通常是用向左箭頭呈現,故以下稱為「左箭頭」)代表著:點擊後,能夠返回前一張圖片(就輪播編排的圖片順序)。 

而我希望,點擊左箭頭不僅可以返回前一張圖,當使用者在第一張圖時點擊此 icon 時,還能直接返回到最後一張圖。(有些輪播在首圖時,無法對左箭頭進行點擊,或是點了沒反應。)

所以,在左箭頭的事件監聽器的回呼函式中,我定義的邏輯是:當該 icon 被點擊後,先判斷當前圖片位在輪播中的順序(以下稱「索引值」(index))是否大於 0

// 選取 DOM 中的左箭頭元素
const prevBtn = document.querySelector("#attraction__top__pics__left-btn");

// 為左箭頭掛上事件監聽器,與定義回呼函式的邏輯
prevBtn.addEventListener("click", () => {
  if (i > 0) {
    i--;
    renderImgsAndDots();
  } else {
    i = imgs.length - 1;
    renderImgsAndDots();
  }
});

若大於 0 ,就將圖片索引值 -1 (因為要切回上一張圖,上一張圖的索引值會比當前圖片的索引值少 1),並調用 renderImgsAndDots 來重新渲染圖片和點點;若非大於 0(當前圖片為首圖),就將圖片索引值設為圖片總數(末圖的索引值為圖片總數),再調用 renderImgsAndDots 來重新渲染圖片和點點,如此一來,使用者就能直接看到末圖和對應的點點。

同樣地,以使用者的角度來看,向右切換圖片的 icon(通常是用向右箭頭來呈現,故以下稱為「右箭頭」)代表著:點擊後,能夠前往下一張圖片(就輪播編排的圖片順序)。 

而我希望,這個右箭頭不僅可以前往下一張圖片,當使用者在最後一張圖時點擊右箭頭,還能直接前往第一張圖。(有些輪播在末圖時,無法對右箭頭進行點擊,或是點了沒反應。)

所以在右箭頭的事件監聽器的回呼函式中,我定義的邏輯是:當該 icon 被點擊後,先判斷目前圖片索引值是否小於輪播的圖片總數?

// 選取 DOM 中的右箭頭元素
const nextBtn = document.querySelector("#attraction__top__pics__right-btn");

// 為右箭頭掛上事件監聽器,與定義回呼函式的邏輯
nextBtn.addEventListener("click", () => {
  if (i < imgs.length - 1) {
    i++;
    renderImgsAndDots();
  } else {
    i = 0;
    renderImgsAndDots();
  }
});

如果當前圖片的索引值小於圖片總數,就將索引值 + 1(因為要切換到下一張圖,下一張圖的索引值會比當前圖片的索引值多 1),並以這新的索引調用 renderImgsAndDots 來重新渲染圖片和點點,接下來使用者就會看到下一張圖和對應的小圓點;如果是沒有小於圖片總數(當前圖片為末圖),那就將圖片索引值歸零(首圖的索引值為 0),再調用 renderImgsAndDots,這樣使用者就能直接看到首圖和對應的點點。

以上,就是整個《台北一日遊》圖片輪播的製作邏輯,為大家呈現如何用 HTML 、 SCSS 和 JavaScript 手刻一個圖片輪播。大家如果有不同的作法,也歡迎一起交流!


#圖片輪播 #Image Carousel #API串接 #DOM操作 #事件監聽 #html #SCSS #javascript







Related Posts

number 類型的內建函式

number 類型的內建函式

Integration Test on DB-Related Code with Docker Compose

Integration Test on DB-Related Code with Docker Compose

Day04 git 知己知彼

Day04 git 知己知彼


Comments