# 2-5 <transition>
漸變與動畫
Vue.js 雖然也可以處理動畫等需求,但與過去大家所熟知的 jQuery.animate()
直接指定 CSS 樣式的操作方式完全不同,
而是當元件或 DOM 節點在新增、移除或更新的時候,進行 CSS 過場漸變的效果與動畫。
# <transition>
漸變
Vue.js 將過場漸變的效果包裝成一個獨立的 <transition>
元件,
再由開發者自行定義元素的進場 (Enter) 、退場 (Leave) 以及動畫過程漸變 (Transitions) 的樣式。
如圖所示,Vue.js 為 <transition>
預先定義了幾種場景,分別是元素進場 (顯示) :
v-enter-from
: 定義元素在進場「之前」的樣式。v-enter-active
: 定義元素在進場「過程」的樣式。v-enter-to
: 定義元素在進場「結束時」的樣式。
以及元素退場 (消失) :
v-leave-from
: 定義元素在退場「之前」的樣式。v-leave-active
: 定義元素在退場「過程」的樣式。v-leave-to
: 定義元素在退場「結束時」的樣式。
各三組 CSS 的 class。
小提醒
v-enter-from
與 v-leave-from
這兩個 hook 名稱在 Vue.js 2.x 版本時應為
v-enter
與 v-leave
,這裡提醒各位讀者小心版本差異。
使用的方式很簡單,我們只需要將要執行過場動畫的元素/元件,透過 <transition>
包覆起來即可:
<transition>
<!-- 透過 v-show 來控制顯示或隱藏 -->
<div class="block" v-show="isShow">HELLO VUE</div>
</transition>
<button @click="isShow =! isShow">Toggle</button>
2
3
4
5
6
const app = Vue.createApp({
data() {
return {
isShow: true
}
}
}).mount('#app');
2
3
4
5
6
7
接著,我們需要在 CSS 定義它的過場漸變的樣式,這裡以 opacity
為例:
.v-enter-active,
.v-leave-active {
transition: opacity 1s;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.v-enter-to,
.v-leave-from {
opacity: 1;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
像這樣,我們就完成了一個最基本的 <transition>
漸變動畫了。
當然,除了 v-if
、 v-show
之外,像是前面曾介紹過的 <component :is>
以及元件的根節點(Component Root Nodes)也都可以套用 <transition>
來處理漸變動畫。
不過要記得,相對應的 CSS 的樣式還是得由開發者自行撰寫。
# 替 <transition>
取名稱
前面的範例是 Vue.js 針對 <transition>
的預設樣式設定,所以統一以 v-
作為 CSS Class 的前綴開頭。
如果我們想要多定義幾組漸變的樣式,可以怎麼做呢?
還好 Vue.js 提供了開發者為 <transition>
自行定義漸變樣式的名稱,開發者只需要在 <transition>
加上 name
屬性,如:
<!-- slide -->
<transition name="slide">
<div class="block" v-show="isShow">HELLO VUE</div>
</transition>
<!-- fade -->
<transition name="fade">
<div class="block" v-show="isShow">HELLO VUE</div>
</transition>
<button @click="isShow =! isShow">Toggle</button>
2
3
4
5
6
7
8
9
10
11
對應的 CSS 就將 v-
前綴改寫成我們自己定義的名稱:
/* <transition name="slide"> */
.slide-leave-active,
.slide-enter-active {
transition: all .9s ease;
}
.slide-enter-from {
transform: translateX(-100%);
}
.slide-leave-to {
transform: translateX(100%);
}
/* <transition name="fade"> */
.fade-enter-active,
.fade-leave-active {
transition: opacity 1s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
像這樣,我們就可以根據不同的 name
屬性來指定對應不同的進/退場的動畫漸變處理了。
# 條件與動態切換
前面提到的都是單一節點的漸變處理,現在加點變化,搭配大家都很熟悉的 v-if
與 v-else
的條件判斷來示範切換顯示元素。
<button @click="isShow =! isShow">Toggle</button>
<transition name="fade">
<div class="block" v-if="isShow">Block 1</div>
<div class="block" v-else>Block 2</div>
</transition>
2
3
4
5
6
小提醒
過去在 Vue.js 2.x 使用 <transition>
與 v-if
的條件渲染時,若是同個標籤名稱,必須加上唯一識別的 key
屬性來告訴 Vue.js 這個元素需要重新渲染:
<!-- for Vue.js 2.x -->
<transition name="fade">
<div class="block" v-if="isShow" key="block-1">Block 1</div>
<div class="block" v-else key="block-2">Block 2</div>
</transition>
2
3
4
5
如果是元件之間的切換則不需要 key
屬性,可以直接使用 <component>
搭配前面介紹過的 :is
屬性來做動態切換。 到了 Vue.js 3.x 就可不需另外再加上 key
屬性了,詳細原因可參閱本書【1-6 條件判斷與列表渲染】一節。
另外,需要注意的是,若我們要在 <transition>
進行多個 DOM 元素 (或元件) 切換時,必須寫成 v-if
、 v-else-if
與 v-else
的形式,或採用 :is
的方式進行切換才能正常運作。
<!-- 錯誤 -->
<transition name="fade">
<div v-if="mode === 'mode-a'" class="block">Block A</div>
<div v-if="mode === 'mode-b'" class="block">Block B</div>
<div v-if="mode === 'mode-c'" class="block">Block C</div>
</transition>
<!-- 正確 -->
<transition name="fade">
<div v-if="mode === 'mode-a'" class="block">Block A</div>
<div v-else-if="mode === 'mode-b'" class="block">Block B</div>
<div v-else-if="mode === 'mode-c'" class="block">Block C</div>
</transition>
2
3
4
5
6
7
8
9
10
11
12
13
這是因為 <transition>
內同時只能有一個原生 DOM 元素 (或元件) 存在,如果使用多組 v-if
或者 v-show
就會破壞這個規則。 若想使用多可元素進行漸變,可參考後面會提到的 <transition-group>
。
# 漸變效果的順序: transition mode
Vue.js 針對漸變效果的切換,除了預設的 新元素進場的動畫先執行,再移除現有的元素 (in-out) 以外,同時也提供了先移除現有的元素,再執行新元素進場的動畫 (out-in) 方式:
<transition name="fade" mode="out-in">
<div class="block" v-if="isShow">Block 1</div>
<div class="block" v-else>Block 2</div>
</transition>
2
3
4
只需要在 <transition>
加入 mode
屬性與指定的順序即可,實際轉場效果可參考下面範例:
小提醒
在大多數情況下, out-in
會是比較好的選擇。
# 複數元素/元件的漸變渲染 <transition-group>
前面有提到,由於在 <transition>
內部只能有一個原生 DOM 元素或者元件,
所以如果遇到同時有多組 v-if
的時候,就必須改用 <transition-group>
來處理:
<transition-group name="fade">
<div v-if="demo === 'A'" key="block-a" class="block">A Block</div>
<div v-if="demo === 'B'" key="block-b" class="block">B Block</div>
<div v-if="demo === 'C'" key="block-c" class="block">C Block</div>
</transition-group>
2
3
4
5
<transition-group>
與 <transition>
的用法大致相同,有幾個不一樣的地方,如果需要增加包覆的元素,則需要在 <transition-group>
加上 tag
屬性 (也可以不加) 來指定:
另一個需要特別注意的是,<transition-group>
的內層元素都需要加上 key
的唯一屬性來確保動畫正常運作,而且 <transition-group>
不支援 mode
。
<!-- 在外層加一層 <section> 來包覆-->
<transition-group name="fade" tag="section">
<!-- 子元素需加入唯一的 key 屬性作為識別 -->
<div v-if="demo === 'A'" key="block-a" class="block">A Block</div>
<div v-if="demo === 'B'" key="block-b" class="block">B Block</div>
<div v-if="demo === 'C'" key="block-c" class="block">C Block</div>
</transition-group>
2
3
4
5
6
7
小提醒
在 Vue.js 2.x 的版本, <transition-group>
會渲染真實的元素,而且預設會在最外層用 <span>
元素來包覆。
如果不希望外層包上 <span>
,則可透過 tag
屬性修改標籤名稱。
Vue.js 3.x 則是預設不會包覆任何標籤。
另外, <transition-group>
在實務上最常被拿來與 v-for
來做搭配,尤其是列表的顯示。
這裏我們用一個 v-for
搭配隨機數字陣列來示範:
<transition-group tag="ul" class="number-list" name="list">
<li v-for="(item, index) in list" :key="index" class="item">{{ item }}</li>
</transition-group>
2
3
.list-leave-active {
position: absolute;
}
.list-enter-from {
transform: translateY(-20px);
}
.list-leave-to {
transform: translateY(20px);
}
2
3
4
5
6
7
8
9
10
11
除了列表元素的進/退場漸變之外, Vue.js 也新增了 v-move
這個 CSS class,用來處理當元素改變定位時進行的動畫。
使用方式與前面的 v-enter-
、 v-leave-
等 class 一樣。
<transition-group tag="ul" class="number-list" name="list">
<li v-for="(item, index) in list" :key="item" class="item">{{ item }}</li>
</transition-group>
2
3
/* name="list" */
.list-move {
transition: all 0.8s ease;
}
2
3
4
methods: {
shuffle() {
this.list.sort(() => Math.random() - 0.5);
}
}
2
3
4
5
# 結合漸變動畫的 Hooks 函式處理事件
與元件的生命週期同樣概念,在執行漸變動畫的時候,我們可以搭配 Hooks 函式來處理執行動畫之前、過程中,以及結束後要做的事。
Hooks 名稱 | 說明 |
---|---|
before-enter | (進場) 漸變動畫開始前 |
enter | (進場) 漸變動畫執行時 |
after-enter | (進場) 漸變動畫執行完畢 |
enter-cancelled | (進場) 漸變動畫執行時取消 |
before-leave | (退場) 漸變動畫開始前 |
leave | (退場) 漸變動畫執行時 |
after-leave | (退場) 漸變動畫執行完畢 |
leave-cancelled | (退場) 漸變動畫執行時取消 (只有 v-show 有效) |
appear | (初始渲染) 初始渲染的漸變動畫 |
after-appear | (初始渲染) 初始渲染的漸變動畫執行完畢 |
appear-cancelled | (初始渲染) 初始渲染的漸變動畫執行時取消 |
這裏我們使用 {JSON} Placeholder (https://jsonplaceholder.typicode.com/ (opens new window)) 這個線上的假資料 API 產生器所提供的隨機資料作為範例。
當使用者按下取得 [取得隨機 User 資訊] 按鈕時,畫面先會顯示 loading 的圖片,並從 {JSON} Placeholder 所提供的 API 取得隨機文字,而這個文字區塊會隨著內容而動態變化。
<!-- 由 :style 控制容器高度 -->
<div class="flexbox-wrapper" :style="{ height: height + 'px' }">
<div class="flexbox-body" ref="content">
<div class="user-block" v-if="userInfo.name">
<!-- 內容略 -->
</div>
</div>
<!-- hooks event 需搭配 v-on 使用 -->
<transition
@before-enter="beforeEnter"
@before-leave="beforeLeave">
<!-- loading 圖 -->
<div v-if="isLoading" class="loading">
<img src="./loading.gif" alt="loading">
</div>
</transition>
</div>
<button @click="getUserInfo">取得隨機 User 資訊</button>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
data() {
return {
height: 0,
userInfo: {},
isLoading: false
}
},
methods: {
getRandomUserId() {
// 隨機從 1 ~ 10 取出一數字作為 user id
return Math.floor(Math.random() * 10) + 1;
},
async getUserInfo() {
// 從 jsonplaceholder 取得隨機假資料
this.isLoading = true;
this.userInfo = {};
const userId = this.getRandomUserId();
const res = await fetch('https://jsonplaceholder.typicode.com/users/' + userId)
.then((response) => response.json())
.then((json) => json);
// 加上 setTimeout 模擬延遲,避免回傳太快看不到 loading
window.setTimeout(() => {
this.isLoading = false;
this.userInfo = res;
}, 3000);
},
beforeEnter() {
// 開始執行動畫前,將文字區塊的高度歸零
this.height = 0;
},
beforeLeave () {
// 取得隨機文字後,為確保資料與畫面同步,加上 $nextTick
// 並透過 getBoundingClientRect() 來取得 DOM 實際高度
this.$nextTick(() => {
// $refs 取得實際 DOM
this.height = this.$refs.content.getBoundingClientRect().height;
})
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# Vue.js 與 CSS keyframes
影格動畫
雖然 Vue.js 提供的 <transition>
很方便,但我們還是可以直接使用 CSS 的 keyframes
來進行網頁的動畫處理,而無需添加 <transition>
元件。
<div id="app">
<button @click="noActivated = true">Shack It!</button>
<div ref="block" class="block" :class="{ shake: noActivated }">Block</div>
</div>
2
3
4
5
const app = Vue.createApp({
data() {
return {
noActivated: false
}
}
}).mount('#app');
2
3
4
5
6
7
.shake {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@keyframes shake {
10%, 90% {
transform: translate3d(-1px, 0, 0);
}
20%, 80% {
transform: translate3d(2px, 0, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 0, 0);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
按一下就失效了怎麼辦? 這時候就可以利用 DOM 原生的 animationend
事件來處理:
<div class="block"
:class="{ shake: noActivated }"
@animationend="reactivated">Block</div>
2
3
animationend
事件會在指定的 DOM 節點 CSS 動畫結束後觸發,所以我們可以在元件的 mounted
階段加入 animationend
事件,
這樣就可以在 shack
的動畫結束後恢復 noActivated
的狀態了。
# 與其他 CSS 工具庫搭配 - Animate.css
當然,除了自己寫 keyframes
之外,我們也可以與其他 CSS 工具褲搭配,如 Animate.css。
Animate.css (https://animate.style/ (opens new window)) 內建了數十種 CSS 的動畫特效,方便開發者直接套用對應的 class
名稱。
安裝時只需要透過 npm 或 yarn:
$ npm install animate.css --save
# 或 yarn add animate.css
2
3
或者直接透過 CDN 引入至網頁:
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" rel="stylesheet" />
就可以直接使用了。
執行動畫時只需要添加指定的 class
名稱,如 animate__animated animate__bounce
:
<h1 class="animate__animated animate__bounce">An animated element</h1>
指定的元素就會依照 class
的不同而有不同的效果。
關於 Animate.css 的介紹就到此,讀者若有興趣可以直接到 Animate.css 的官網查閱更多用法。
那麼, Vue.js 要如何與 Animate.css 做搭配呢? 很簡單,只要透過前面介紹過的指令: v-bind:class
:
<div class="wrap">
<button
v-for="c in animateClasses"
:key="c"
@click="activedAnimate(c)">{{ c }}</button>
</div>
<!-- 透過 v-bind:class 來控制 class 屬性 -->
<div ref="block" class="block" :class="animatedClassName">Block</div>
2
3
4
5
6
7
8
9
const app = Vue.createApp({
data() {
return {
// 簡單列出幾個作為示範用
animateClasses: [
'bounce',
'rubberBand',
'tada',
'shakeY',
'shakeX',
],
animatedClassName: ''
}
},
methods: {
activedAnimate(className){
this.animatedClassName = `animate__animated animate__${className}`;
}
}
}).mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
像這樣,我們就可以使用 Vue.js 與其他 CSS 工具庫合作,輕鬆地完成動畫效果囉!