# 2-5 <transition> 漸變與動畫

Vue.js 雖然也可以處理動畫等需求,但與過去大家所熟知的 jQuery.animate() 直接指定 CSS 樣式的操作方式完全不同, 而是當元件或 DOM 節點在新增、移除或更新的時候,進行 CSS 過場漸變的效果與動畫。

# <transition> 漸變

Vue.js 將過場漸變的效果包裝成一個獨立的 <transition> 元件, 再由開發者自行定義元素的進場 (Enter) 、退場 (Leave) 以及動畫過程漸變 (Transitions) 的樣式。

transition 示意 - enter

transition 示意 - leave

如圖所示,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-fromv-leave-from 這兩個 hook 名稱在 Vue.js 2.x 版本時應為 v-enterv-leave,這裡提醒各位讀者小心版本差異。

使用的方式很簡單,我們只需要將要執行過場動畫的元素/元件,透過 <transition> 包覆起來即可:

<transition>
  <!-- 透過 v-show 來控制顯示或隱藏 -->
  <div class="block" v-show="isShow">HELLO VUE</div>
</transition>

<button @click="isShow =! isShow">Toggle</button>
1
2
3
4
5
6
const app = Vue.createApp({
  data() {
    return {
      isShow: true
    }
  }
}).mount('#app');
1
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
試一試

像這樣,我們就完成了一個最基本的 <transition> 漸變動畫了。

當然,除了 v-ifv-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>
1
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;
}
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-ifv-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>
1
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>
1
2
3
4
5

如果是元件之間的切換則不需要 key 屬性,可以直接使用 <component> 搭配前面介紹過的 :is 屬性來做動態切換。 到了 Vue.js 3.x 就可不需另外再加上 key 屬性了,詳細原因可參閱本書【1-6 條件判斷與列表渲染】一節。

另外,需要注意的是,若我們要在 <transition> 進行多個 DOM 元素 (或元件) 切換時,必須寫成 v-ifv-else-ifv-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>
1
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>
1
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>
1
2
3
4
5

<transition-group><transition> 的用法大致相同,有幾個不一樣的地方,如果需要增加包覆的元素,則需要在 <transition-group> 加上 tag 屬性 (也可以不加) 來指定:

transition-group示意

另一個需要特別注意的是,<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>
1
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>
1
2
3
.list-leave-active {
  position: absolute;
}

.list-enter-from {
  transform: translateY(-20px);
}

.list-leave-to {
  transform: translateY(20px);
}
1
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>
1
2
3
/* name="list" */
.list-move {
  transition: all 0.8s ease;
}
1
2
3
4
methods: {
  shuffle() {
    this.list.sort(() => Math.random() - 0.5);
  }
}
1
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 產生器所提供的隨機資料作為範例。

{JSON} Placeholder

當使用者按下取得 [取得隨機 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>
1
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;
    })
  },
}
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
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>
1
2
3
4
5
const app = Vue.createApp({
  data() {
    return {
      noActivated: false
    }
  }
}).mount('#app');
1
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);
  }
}
1
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 事件來處理:

methods: {
  reactivated () {
    this.noActivated = false;
  }
},
mounted() {
  // 在 mounted 階段加入 animationend 事件
  this.$refs.block.addEventListener("animationend", this.reactivated);
},
beforeUnmount () {
  // 記得解除事件
  this.$refs.block.removeEventListener("animationend", this.reactivated);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

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
1
2
3

或者直接透過 CDN 引入至網頁:

<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" rel="stylesheet" />
1

就可以直接使用了。

執行動畫時只需要添加指定的 class 名稱,如 animate__animated animate__bounce

<h1 class="animate__animated animate__bounce">An animated element</h1>
1

指定的元素就會依照 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> 
1
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');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
試一試

像這樣,我們就可以使用 Vue.js 與其他 CSS 工具庫合作,輕鬆地完成動畫效果囉!

Last Updated: 12/28/2020, 7:34:16 PM