# 🏅 Day 22 - Swiper 輪播套件運用
## 今日學習目標
- 學習在 Nuxt3 使用 Swiper.js 輪播套件
- 學習將 Swiper 封裝成元件,並透過 props 傳遞輪播功能的參數至元件中
## 安裝 Swiper
開啟終端機,輸入以下指令安裝 Swiper v11.1.14 :
```bash
npm install swiper@11.1.14
```
## 匯入 Swiper 的功能
安裝完成後,進入元件中匯入 Swiper 的核心 JS 、CSS 與模組 。Swiper 核心不包含模組功能,因此像是 Autoplay(自動播放)、Pagination(頁碼)、Navigation(導航)等功能需要額外匯入。以下示範於 `/pages/index.vue` 進行匯入 :
```html
<!-- /pages/index.vue -->
<script setup>
// Swiper JS 核心功能
import Swiper from "swiper";
// Swiper 模組
import { Navigation, Pagination, Autoplay } from "swiper/modules";
// Swiper 核心與模組的樣式
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
</script>
<template>
<!-- Swiper 的模板結構 -->
</template>
```
## 基礎使用方式
第一種方法是在元件中直接使用 Swiper 的 HTML 結構,接下來將基於上方的匯入設定在 `/pages/index.vue` 進行輪播設定。
### 步驟一. 撰寫 HTML 結構
在最外層用一個 `div.swiper` 作為整個 Swiper 的容器,並將所有輪播內容(即 `div.swiper-slide`)放入 `div.swiper-wrapper`。除此之外,在匯入 Navigation 和 Pagination 模組後,還需額外加入觸發這些功能的 HTML 元素。
例如下方範例 :
```html
<!-- /pages/index.vue -->
<template>
<!-- Swiper 的容器 -->
<div class="swiper">
<!-- 放置所有的輪播內容 -->
<div class="swiper-wrapper">
<!-- 每個 swiper-slide 都是一個輪播內容 -->
<div class="swiper-slide">
<img
src="https://images.unsplash.com/photo-1636909798741-42d59f0265b0?q=80&w=900&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="圖片一"
/>
</div>
<div class="swiper-slide">
<img
src="https://images.unsplash.com/photo-1516007146048-bfcffccbc95c?q=80&w=900&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="圖片二"
/>
</div>
<div class="swiper-slide">
<img
src="https://images.unsplash.com/photo-1516007112993-555f24caca6d?q=80&w=900&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="圖片三"
/>
</div>
</div>
<!-- 有使用 Pagination 模組必需要加入 -->
<div class="swiper-pagination"></div>
<!-- 有使用 Navigation 模組必需要加入 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
</template>
```
### 步驟二. 選擇性調整 Swiper 樣式
如果需要調整 Swiper 的樣式,可以在 `<style>` 區塊內覆蓋預設樣式,例如修改 Swiper 容器的寬度:
```jsx
// /pages/index.vue
<style scoped>
.swiper {
max-width: 900px;
}
</style>
```
### 步驟三. 初始化 Swiper JS
首先,定義 `swiperRef` 變數,並在 `<div class="swiper">` 加入 `ref="swiperRef"`,取得該元素的 DOM 節點。
```jsx
// /pages/index.vue
<script setup>
const swiperRef = ref(null);
</script>
<template>
<div class="swiper" ref="swiperRef" >
<!-- 輪播內容 -->
</div>
</template>
```
接著在 `onMounted()` 生命週期透過 `new Swiper()` 來初始化 Swiper ,需傳入兩個參數 :
- 第一個參數 : 傳入要套用輪播效果的容器,可使用 CSS 選擇器或是 HTML 元素的節點。這邊使用 `swiperRef.value` 取得的 HTML 元素。
```jsx
// /pages/index.vue
<script setup>
import Swiper from "swiper";
import { Navigation, Pagination, Autoplay } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
const swiperRef = ref(null);
onMounted(() => {
new Swiper(swiperRef.value, { /*...*/ })
});
</script>
<template>
<div class="swiper" ref="swiperRef" >
<!-- 輪播內容 -->
</div>
</template>
```
- 第二個參數 : 是一個物件,用來定義 Swiper 的行為和外觀。詳細設定與調整方法可以閱讀 [官方文件](https://swiperjs.com/swiper-api#parameters) ,以下是在範例中使用的屬性設定 :
- `loop` : 循環模式,設定成 `true` 允許播放至最後一個輪播時可以取得第一個輪播繼續播放。
- `slidesPerView` : 每次顯示的輪播 ( swiper-slide ) 數量。
- `breakpoints` : 設定不同斷點輪播顯示的數量以及輪播之間的距離。
- `pagination` : 設定頁碼的點擊操作的元素 ( `el` 屬性 ) 以及頁碼的外觀 ( `type` 屬性 )。
- `navigation` : 設定導航按鈕,例如切換上一個 ( `nextEl`屬性 ) 與下一個 ( `prevEl` 屬性 ) 點擊操作的元素。
- `autoplay`: 自動輪播,在此設定 1000 毫秒作為 swiper-slide 轉換的延遲 ( delay 屬性 ) 。
- `modules` : 帶入輪播需要用到的模組。
```html
<script setup>
import Swiper from "swiper";
import { Navigation, Pagination, Autoplay } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
const swiperRef = ref(null);
onMounted(() => {
new Swiper(swiperRef.value, {
loop: true, // 開啟循環模式
slidesPerView: 1, // 預設每次顯示 1 個輪播
breakpoints: {
// 992px 以上套用響應式斷點設定
992: {
slidesPerView: 2, // 在 992px 以上顯示 2 個輪播
spaceBetween: 20, // 輪播之間的間距為 20px
},
},
// 設定頁碼模組的功能 ( 需要搭配 Pagination 模組使用 )
pagination: {
el: ".swiper-pagination", // 頁碼操作的元素
type: "bullets", // 頁碼的外觀
},
// 設定導航模組的功能 ( 需要搭配 Navigation 模組使用 )
navigation: {
nextEl: ".swiper-button-next", // 下一個按鈕操作的元素
prevEl: ".swiper-button-prev", // 上一個按鈕操作的元素
},
autoplay: { delay: 1000 }, // 自動輪播,延遲 1000 毫秒
modules: [Navigation, Pagination, Autoplay], // 載入輪播需要用到的模組
});
});
</script>
<template>
<div class="swiper" ref="swiperRef" >
<!-- 輪播內容 -->
</div>
</template>
```
#### 注意 :
`new Swiper()` 必需在 `onMounted()` 生命週期定義。因為 `swiperRef` 變數在伺服器端無法取得 DOM 節點,會造成 Swiper 無法正確初始化,所以需要移動到客戶端執行 `onMounted()` 生命週期。
```html
<!-- 錯誤示範 : -->
<script setup>
import Swiper from "swiper";
import { Navigation, Pagination, Autoplay } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
const swiperRef = ref(null);
new Swiper(swiperRef.value, {
loop: true,
slidesPerView: 1,
breakpoints: {
992: {
slidesPerView: 2,
spaceBetween: 20,
},
},
pagination: {
el: ".swiper-pagination",
type: "bullets",
},
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
autoplay: { delay: 1000 },
modules: [Navigation, Pagination, Autoplay],
});
</script>
```
## 封裝成元件進行複用
第二種方法是將 Swiper 封裝成元件,透過 `props` 將輪播的參數傳遞至元件內。這樣的做法有助於元件複用,減少重複程式碼,並提升輪播功能的可維護性與可擴充性。未來如需要調整輪播效果,只需修改元件本身,不必在多個地方進行修改。接下來,我們將根據前面的基礎用法,將 Swiper 進行元件化封裝。
### 步驟一. 定義元件檔案並規劃用途
首先,建立 `components/swiper` 資料夾,並在該資料夾下新增 `Swiper.vue` 和 `SwiperSlide.vue`。`Swiper.vue` 是放置輪播結構的主元件,用於初始化並接收外部傳入的 `props`;而 `SwiperSlide.vue` 則是用來放置每個輪播項目(即 `.swiper-slide` 的內容)。
在 `Swiper.vue` 將會透過 [插槽 Slots](https://cn.vuejs.org/guide/components/slots) 來渲染傳入的 `SwiperSlide` 的內容。最終結構如下:
```html
<!-- /components/swiper/Swiper.vue -->
<Swiper>
<!-- 透過插槽傳遞 SwiperSlide 元件的內容 -->
<SwiperSlide>
<!-- 輪播內容 -->
</SwiperSlide>
</Swiper>
```
### 步驟二. 設定元件的模板結構
在 `Swiper.vue` 中定義輪播的模板結構。於模板最外層加入 `div.swiper` 作為 Swiper 的容器,並包含頁碼和導航按鈕。`div.swiper-wrapper` 中的輪播內容將使用 `<slot />` 插槽來渲染傳入的內容。將來 Swiper 元件嵌套的內容會在 `<slot />` 元件的位置被渲染。
```html
<!-- /components/swiper/Swiper.vue -->
<template>
<div class="swiper">
<div class="swiper-wrapper">
<slot />
</div>
<div class="swiper-pagination"></div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
</template>
```
接下來是定義 `SwiperSlide.vue` 的模板。該元件的用途是顯示每一個輪播項目的內容,模板中僅需加入 `div.swiper-slide`,並使用 `<slot />` 來渲染嵌套在 `SwiperSlide` 標籤內的內容:
```html
<!-- /components/swiper/SwiperSlide.vue -->
<template>
<div class="swiper-slide">
<slot />
</div>
</template>
```
```html
<!-- 預計會這樣使用 : -->
<SwiperSlide>
輪播的內容 <!-- 此處內容會被傳遞至 SwiperSlide.vue 的 <slot /> 渲染 -->
</SwiperSlide>
```
### 步驟三. 定義 Swiper.vue 的 props
在 `Swiper.vue` 中使用 defineProps() 設定以下 `props` 接受來自父元件的參數 :
```jsx
// /components/swiper/Swiper.vue
<script setup>
const props = defineProps({
autoplay: Object,
pagination: Object,
navigation: Object,
loop: Boolean,
slidesPerView: Number,
modules: Array,
breakpoints: Object,
});
</script>
```
### 步驟四. 初始化 Swiper
在 `Swiper.vue` 中設定 `swiperRef` 變數,並使用 `ref="swiperRef"` 取得輪播容器的 DOM 節點。在 `onMounted()` 生命週期把 DOM 節點與元件的 props 傳入 `new Swiper()`。
```html
<script setup>
// Swiper JS 核心功能
import Swiper from "swiper";
// Swiper 核心與模組的樣式
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
const props = defineProps({
autoplay: Object,
pagination: Object,
navigation: Object,
loop: Boolean,
slidesPerView: Number,
modules: Array,
breakpoints: Object,
});
const swiperRef = ref(null);
onMounted(() => {
new Swiper(swiperRef.value, props);
});
</script>
<template>
<div class="swiper" ref="swiperRef">
<div class="swiper-wrapper">
<slot />
</div>
<div class="swiper-pagination"></div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
</template>
```
### 步驟五. 使用封裝好的 Swiper 元件
最後,在頁面元件中使用 `<Swiper>` 元件,將所需的輪播參數透過 `props` 傳入元件。
同時在內層使用 `SwiperSlide` 元件來呈現每個輪播項目。
```html
<script setup>
// Swiper 模組
// 因為不一定每個 Swiper 使用的模組都一樣,所以改成用 props 傳入特定的模組
import { Navigation, Pagination, Autoplay } from "swiper/modules";
</script>
<template>
<Swiper
:loop="true"
:slides-per-view="1"
:breakpoints="{
992: {
slidesPerView: 2,
spaceBetween: 20,
},
}"
:pagination="{ el: '.swiper-pagination', type: 'bullets' }"
:navigation="{
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
}"
:autoplay="{ delay: 1000 }"
:modules="[Navigation, Pagination, Autoplay]"
>
<!-- 內層所有的內容將會渲染至 div.swiper-wrapper內部的 <slot /> -->
<SwiperSlide>
<img
src="https://images.unsplash.com/photo-1636909798741-42d59f0265b0?q=80&w=900&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="圖片一"
/>
</SwiperSlide>
<SwiperSlide>
<!-- SwiperSlide 內的 img 標籤將會渲染至 div.swiper-slide 內部的 <slot /> -->
<img
src="https://images.unsplash.com/photo-1516007146048-bfcffccbc95c?q=80&w=900&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="圖片二"
/>
</SwiperSlide>
<SwiperSlide>
<img
src="https://images.unsplash.com/photo-1516007112993-555f24caca6d?q=80&w=900&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="圖片三"
/>
</SwiperSlide>
</Swiper>
</template>
```
<br>
> 今日學習的[範例 Code - 資料夾: day22-swiper-example](https://github.com/hexschool/nuxt-daily-tasks-2024)
## 題目
請 fork 這一份 [模板](https://github.com/jasonlu0525/nuxt3-live-question/tree/day22-swiper) ,完成以下條件 :
- 在 `/pages/index.vue` 中,已使用 `useFetch()` 取得房型列表並透過 `v-for` 渲染。然而在模板第 17 ~ 19 行的 `imageUrlList` 缺少了呈現房型多圖的輪播功能,請使用 Swiper.js 來完成圖片輪播功能 ( Swiper.js 已包含在模板內,npm install 安裝後即可使用 ) 。
- 輪播需要包含以下功能 :
- 需要使用頁碼 ( pagination ) 、導航 ( navigation ) 與自動撥放功能。
- 頁碼 : 使用 bullets 的外觀,並能透過點擊頁碼切換輪播項目。
- 導航: 可以通過前後按鈕導航輪播項目。
- 自動撥放功能的延遲時間為 5 秒。
- 允許輪播項目循環播放。
- 每次顯示 1 個輪播項目。
- 補充,模板有預先在 `plugins/iconify.js` 載入 [iconify/vue](https://iconify.design/) 套件,透過全域元件的方式呈現房型資訊區塊的 icon。 `/pages/index.vue` 可以使用 `<Icon icon=""/>` 元件,在 icon 屬性填入 icon 的名稱,例如 [material-symbols:king-bed](https://icon-sets.iconify.design/material-symbols/king-bed/) :
```jsx
// 安裝
// 官方文件 : https://iconify.design/docs/icon-components/vue/#installation
npm install --save-dev @iconify/vue
// plugins/iconify.js
// 將 Icon 的功能加入到 Nuxt 插件註冊全域元件
import { Icon } from '@iconify/vue';
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component('Icon', Icon);
});
// pages/index.vue
// 使用 icon
<Icon class="mb-2" icon="material-symbols:king-bed" />
```
## 回報流程
將答案上傳至 GitHub 並複製 GitHub repo 連結貼至底下回報就算完成了喔 !
解答位置請參考下圖(需打開程式碼的部分觀看)

<!--
解答 : https://github.com/jasonlu0525/nuxt3-live-answer/tree/day22-swiper
-->
回報區
---
| # | Discord | Github / 答案 |
| --- | ----- | ----- |
|1|眼睛|[Github](https://github.com/Thrizzacode/nuxt3-live-question/tree/day22-swiper)|
| 2 | Steven |[Github](https://github.com/y7516552/nuxt3-live-question/tree/day22)|
| 3 | dragon |[Github](https://github.com/peterlife0617/2024-nuxt-training-homework01/tree/feature/day22)|
|4 | Lilith C | [Github](https://github.com/lilithchen/nuxt3-day22-swiper/tree/master)
|5 | Johnson | [Github](https://github.com/tttom3669/2024_hex_nuxt_daily/tree/day22-swiper)|
| 6 | Rocky |[Github](https://github.com/WuRocky/Nuxt-Day22-Swiper.git)|
| 7 | LinaChen |[Github](https://github.com/Lina-SHU/nuxt3-live-question)|
|8|hsin yu|[Github](https://github.com/dogwantfly/nuxt3-daily-task-live-question/tree/day22-swiper)|
<!--
|---|---|[Github]()|
-->