前情提要:
《台北一日遊》是我 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 手刻一個圖片輪播。大家如果有不同的作法,也歡迎一起交流!