iT邦幫忙

2021 iThome 鐵人賽

DAY 16
0
Modern Web

[ 重構倒數30天,你的網站不Vue白不Vue ] 系列 第 16

[重構倒數第15天] - Vue3處理動態效果(一)

前言

該系列是為了讓看過Vue官方文件或學過Vue但是卻不知道怎麼下手去重構現在有的網站而去規畫的系列文章,在這邊整理了許多我自己使用Vue重構很多網站的經驗分享給讀者們。

我們在開發網站的時候很常會處理 UI 的動態效果,以前大家會很常使用 JQuery 的 animate 的函式去做動態效果,但是 JQuery 的 animate 效能不是這麼好,再加上它只能使用在DOM上面,多少覺得有些不方便,所以今天要來介紹四種我自己在Vue上面開發網頁動態上面很常使用的方式。

第一種:CSS3 製作動態

在製作網頁的時候有許多小地方的動態其實可以不用透過JS的方式來處理,透過 CSS3 的 transition 還有 @keyframes 可以很輕易地達成,也好去把我們動畫的邏輯還有程式的業務邏輯給分開,我們來看一下這兩個的範例。

1. CSS3 的 transition + class Toggle

Vue Mike

codepen 範例 https://codepen.io/MikeCheng1208/pen/NWgRGpp

<script>
import { ref } from 'vue'
export default {
  setup(){
    const isOpen = ref(false);
    
    const handleMenuOpen = (bool) => {
      isOpen.value = bool
    }
    
    return {
      isOpen,
      handleMenuOpen
    }
  }
};
</script>

<template>
	<!-- 開啟選單的按鈕 -->
    <a class="menuBtn" @click="handleMenuOpen(true)">
      <i class="fas fa-bars fa-3x"></i>
    </a>

    <div class="content"></div>

	<!-- 選單 -->
    <div :class="['menu', {open: isOpen}]">
        
	    <!-- 關閉選單的按鈕 -->
        <a class="closeBtn" @click="handleMenuOpen(false)">
          <i class="fas fa-times fa-3x"></i>
        </a>
        
        <ul class="nav">
            <li><a>abous</a></li>
            <li><a>content</a></li>
            <li><a>user</a></li>
            <li><a>address</a></li>
        </ul>
    </div>

</template>

<style lang="scss">
#app{
    // 以上省略...
    
    .menu{
        position: fixed;
        top: 0;
        right: -350px;
        width: 350px;
        height: 100%;
        z-index: 20;
        background-color: #fff;
		
        // 對 right 屬性添加 transition 動畫
        transition: right 0.3s;
        &.open{
          right: 0px;
        }
        
    	// 以下省略...
    }
}
</style>

我們在這邊使用了 css3 的 transition 屬性,因為我們的選單是用定位的方式去擺放位置,所以針對 right這個屬性去處理,只要 right 一變動 transition 就會自動把改變的 value 中間給做上補間動畫。

關於 transition 的細節我們可以參考 MDN https://developer.mozilla.org/zh-TW/docs/Web/CSS/transition

不過要特別注意一件事情,很多人會為了貪圖方便,所以這樣寫

.menu{
    // 對全部的屬性添加 transition 動畫
    transition: all 0.3s;
    &.open{
        right: 0px;
    }
}

all的方式可以對 .menu全部的屬性都給予動畫,但是這樣不是很好,因為我們並非全部都要使用 transition,為了可以一眼就看出哪個屬性被賦予的動畫,也可以避免無謂的效能浪費,建議還是乖乖寫上單一屬性就好,如果要寫多個屬性的話可以這樣,用 , 的方式串起來。

transition: right 0.3s, background 0.3s, color 0.3s;

2. CSS3 的 @keyframes + class Toggle

Vue3 mike

codepen 範例 https://codepen.io/MikeCheng1208/pen/QWgKjVa

<script>
import { ref } from 'vue'
export default {
  setup(){
    const isError = ref(false);
    
    const handleAlert = (bool) => {
      isError.value = bool;
    }
    
    return {
      isError,
      handleAlert
    }
  }
};
</script>

<template>
    <div class="content">
      
      <button @click="handleAlert(true)">開啟Alert</button>
      
      <div :class="['alert', {open: isError}]">
          <i class="fas fa-times fa-6x" @click="handleAlert(false)"></i>
          <h1>發生未知的錯誤</h1>
      </div>
    </div>
</template>

<style lang="scss">
@keyframes showAnim {
  from {
    bottom: -90%;
  }
  70% {
    bottom: 70%;
  }
  to {
    bottom: 50%;
  }
}
#app{
    // 以下省略 ...
    .content{
      // 以下省略 ...
      .alert{
          position: absolute;
          bottom: -50%;
          left: 50%;
          transform: translateX(-50%) translateY(50%);
          width: 500px;
          height: 400px;
          border-radius: 16px;
          background-color: #fff;
          display: flex;
          justify-content: center;
          align-items: center;
          flex-direction: column;
          box-shadow: 0 0 30px rgba(#000, 0.5);
          &.open {
              animation-name: showAnim;
              animation-duration: 0.45s;
              animation-iteration-count: 1;
              animation-fill-mode: forwards;
              animation-timing-function: ease-in-out;
          }
      }
    }
}
</style>

關於 @keyframes 的細節我們可以參考 MDN https://developer.mozilla.org/zh-CN/docs/Web/CSS/@keyframes

這邊我們一樣是用切換 class 的方式來控制,只是這次的動畫需要更加的處理細節,你會看到我的 demo 有一個彈性的效果,像這樣的細節就可以使用 @keyframes 的方式來處理,再搭配 animation 屬性來處理,就可以很輕易的處理大部分網頁上的動態。

我們現在已經學會了使用 CSS3 來取代以前我們使用 JQuery 的 animate 的動態效果,透過 CSS3 的動畫,我們可以獲得更順暢的體驗,更好的管理動畫的邏輯。

第二種:Vue 的 transition 動畫

除了透過切換 class 加入動畫以外,Vue 提供了一個 transition 的 component,讓我們可以用簡單的方式可以處理 component 之間的過渡動畫,我們只需要把你要執行動畫的 component 給包起來,透過 v-if 或是 v-show 就可以使用了。

  <transition>
      <div v-if="isShow"></div>
  </transition>

以下就是我們用 transition 完成之後的樣子

Vue3_transition mike

codepen 範例 https://codepen.io/MikeCheng1208/pen/mdwrVRM

<script>
import { ref } from 'vue'
export default {
  setup(){
    const isShow = ref(false);
    
    const handleAlert = (bool) => {
      isShow.value = bool;
    }
    
    return {
      isShow,
      handleAlert
    }
  }
};
</script>

<template>
    <div class="content">
      <button @click="handleAlert(true)">開啟Alert</button>
      
      <transition name="fade">
          <div class="alert" v-if="isShow">
              <i class="fas fa-times fa-6x" @click="handleAlert(false)"></i>
              <h1>發生未知的錯誤</h1>
          </div>
      </transition>
        
    </div>
</template>

<style lang="scss">
    
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from, .fade-leave-to {
  opacity: 0;
}
    
// 以下省略...
</style>

你會看到我在 transition 身上給了一個 name 叫做 fade,這個時候我就可以定義我的 transition 動畫在執行的時候它的 class 的定義,透過 v-if 或是 v-show ,來達到過渡的動態效果。

<transition name="mike"></transition>
// 這些 class 就是看你的 transition 的 name 叫什麼,前面的名稱就是你的 name
.mike-enter-active, .mike-leave-active {}
.mike-enter-from, .mike-leave-to {}

我們來仔細探討 transition 在執行的的 6 個狀態,首先來看一下官網的這張圖。

vue3 transitions mike

進場離開 的過程中,會有 6 個 class 的狀態 ( 詳情請看官方文件 ),Vue2 跟 Vue3 的狀態名稱有稍微不一樣,所以看文件不要看錯。

進場

  1. v-enter-from:開始的動畫開始前。
  2. v-enter-active:動畫開始的執行過程。
  3. v-enter-to:開始的動畫結束時。

離開

  1. v-leave-from:離開的動畫開始前。
  2. v-leave-active:離開動畫的執行過程。
  3. v-leave-to:離開動畫的結束時。

既然我們已經知道了 transition 的 6 個狀態,那接下來我們來看一下這個範例。

vue3 mike

這是一個用 transition做出來的輪播效果,我們來看一下怎麼做

<script>
import { ref } from 'vue'
export default {
  setup(){
    const imgIdx = ref(0);
    const slidList = ref([
        { id: '1', src: "https://source.unsplash.com/600x400?1" },
        { id: '2', src: "https://source.unsplash.com/600x400?2" },
        { id: '3', src: "https://source.unsplash.com/600x400?3" },
        { id: '4', src: "https://source.unsplash.com/600x400?4" },
        { id: '5', src: "https://source.unsplash.com/600x400?5" },
        { id: '6', src: "https://source.unsplash.com/600x400?6" },
        { id: '7', src: "https://source.unsplash.com/600x400?7" },
        { id: '8', src: "https://source.unsplash.com/600x400?8" },
    ]);
    
    const handleMenuActive = idx => {
      imgIdx.value = idx;
    };
    
    return {
      imgIdx,
      slidList,
      handleMenuActive,
    }
  }
};
</script>

我們會先把圖片的資料 slidList 給定義出來,然後還有目前點到的按鈕索引 imgIdx給定義出來。

<template>
    <div class="content">
        <div class="mid">
            <transition name="slids">
                <img 
                    v-for="(item, idx) in slidList" 
                    v-show="imgIdx === idx" 
                    :key="item.id" 
                    :src="item.src"
                />
            </transition>
        </div>
        <nav class="nav_menu">
            <a
                v-for="(item, idx) in slidList"
                :key="item.id"
                :class="{active: imgIdx === idx}"
                @click="handleMenuActive(idx)"
            >
            {{idx + 1}}
            </a>
        </nav>
    </div>
</template>

<style lang="scss">
    
    .slids-enter-active, .slids-leave-active{
        transition: transform .3s ease;
    }
    .slids-enter-from{
        transform: translateX(-550px);
    }
    .slids-leave-to{
        transform: translateX(550px)
    }
    
    .content {
        width: 600px;
        height: 400px;
        .mid{
            position: relative;
            width: 100%;
            height: 100%;
            overflow: hidden; 
            margin-bottom: 20px;
            img{
                position: absolute;
                top: 0;
                right: 0;
            }
        }
    }
    
    // 其他省略...
</style>

然後對裡面的圖片去用 v-for 搭配 v-show 來處理切換的部分,在這邊要注意一下,圖片的部分是透過 position: absolute;來定位,壓在同一個地方,接下來仔細的來看一下 css 的部分

<style lang="scss">
    .slids-enter-active, .slids-leave-active{
        transition: transform .3s ease;
    }
    .slids-enter-from{
        transform: translateX(-550px);
    }
    .slids-leave-to{
        transform: translateX(550px)
    }
</style>

動畫開始的執行過程( slids-enter-active )離開動畫的執行過程 (slids-leave-active) 都給它使用 transition 來處理過場的部分,然後每個圖片進來以前 slids-enter-from ,會被 transform 移到 -550px 這個位置 transform: translateX(-550px);,自然進場的時候就會滑動到原本的地方,再來圖片離開的時候slids-leave-to,就會被 transform 移到 550px 跑出去外面,所以只要能掌握這些狀態,你就用 transition 自由的控制你的組件要怎麼動。

但是現在會發生一個問題,就是使用 <transition></transition> 這個動畫組件,裡面的元件只能有一個,所以透過 v-for 迴圈產生出來的 <img/>會無法執行,所以除了transition 這個動畫組件以外,還有另外一個叫做 transition-group 的組件,就是專門處理 v-for 迴圈產生出來的 DOM 去跑動態。

關於 transition-group 的官方文件 : https://v3.vuejs.org/api/built-in-components.html#transition-group

transition-grouptransition 的 API 一模一樣,所以不用擔心要改其他東西。

<transition-group name="slids">
    <img 
         v-for="(item, idx) in slidList" 
         v-show="imgIdx === idx" 
         :key="item.id" 
         :src="item.src"
    />
</transition-group>

這樣一來就可以正常的執行了。

codepen 範例 : https://codepen.io/MikeCheng1208/pen/oNwzxBq

你以為結束了? 還沒!!!

因為我們現在只能朝一個方向滑動,當我今天希望我點擊的時候可以左右滑動,像是下面範例這樣。

2

所以我必須要去判斷說,我最新點擊的按鈕跟上一個按鈕比,是在左邊還是右邊,然後我要去切換 transition-groupname ,來達到動畫切換左右的部分。

首先我先定義一下左右滑的 css

<style lang="scss">
  .left-enter-active,
  .left-leave-active,
  .right-enter-active,
  .right-leave-active {
      transition: transform .3s ease;
  }
  
  .left-enter-from{
      transform: translateX(-550px);
  }
  .left-leave-to{
      transform: translateX(550px)
  }
  .right-enter-from{
      transform: translateX(550px)
  }
  .right-leave-to{
      transform: translateX(-550px);
  }
</style>

你會看到我的 class 開頭被改成了 leftright,所以可想而知,我們的 transition-groupname 要切換 leftright

所以在這邊我新定義了兩個東西

const prevIdx = ref(0);           // 紀錄上一個點擊的按鈕索引
const transType = ref("right");   // 當前的 transition-group 的 name

然後在我們點擊按鈕的 function 加入一下判斷

const handleMenuActive = idx => {
    imgIdx.value = idx;
    transType.value = idx > prevIdx.value ? "left" : "right";
}

這邊我會判斷當我點擊的索引比上一個索引大的時候,那就是往左滑,不然就右滑,但問題來了,我要什麼時候去紀錄上一個索引的值呢? 不能在 click 的時候,不然會同步的去寫入,所以有人會說,那等它慢一點在寫入,所以寫一個 setTimeout 就好了,拜託不要XDDDDD

transitiontransition-group 有提供動畫的 Lifecycle Hooks,所以這次我們要使用 after-leave這個 hooks,after-leave 是當你動畫執行結束之後會觸發,所以我們要在 after-leave 的時候去寫入我們的 prevIdx.value

const afterLeave = () => {
    prevIdx.value = imgIdx.value;
}

return {
    afterLeave,
}

定義好了 prevIdx.value的 callback 的 function 後,直接 on 這個 hooks

<transition-group 
    :name="transType"
    @after-leave="afterLeave"
>
    <img 
         v-for="(item, idx) in slidList" 
         v-show="imgIdx === idx" 
         :key="item.id" 
         :src="item.src"
    />
</transition-group>

這樣一來就可以讓你在 click 的時候,去切換 transition-group 的 name,以達到左右換圖的效果。

<script>
import { ref } from 'vue'
export default {
  setup(){
    const imgIdx = ref(0);
    const prevIdx = ref(0);
    const transType = ref("right");

    const slidList = ref([
        { id: '1', src: "https://source.unsplash.com/600x400?1" },
        { id: '2', src: "https://source.unsplash.com/600x400?2" },
        { id: '3', src: "https://source.unsplash.com/600x400?3" },
        { id: '4', src: "https://source.unsplash.com/600x400?4" },
        { id: '5', src: "https://source.unsplash.com/600x400?5" },
        { id: '6', src: "https://source.unsplash.com/600x400?6" },
        { id: '7', src: "https://source.unsplash.com/600x400?7" },
        { id: '8', src: "https://source.unsplash.com/600x400?8" },
    ]);
    
    const handleMenuActive = idx => {
      imgIdx.value = idx;
      transType.value = idx > prevIdx.value ? "left" : "right";
    }
    
    const afterLeave = () => {
      prevIdx.value = imgIdx.value;
    }
    
    return {
      imgIdx,
      slidList,
      handleMenuActive,
      afterLeave,
      transType
    }
  }
};
</script>

<template>
    <div class="content">
        <div class="mid">
            <transition-group 
              :name="transType"
              @after-leave="afterLeave"
            >
                <img 
                    v-for="(item, idx) in slidList" 
                    v-show="imgIdx === idx" 
                    :key="item.id" 
                    :src="item.src"
                />
            </transition-group>
        </div>
        <nav class="nav_menu">
            <a
                v-for="(item, idx) in slidList"
                :key="item.id"
                :class="{active: imgIdx === idx}"
                @click="handleMenuActive(idx)"
            >
            {{idx + 1}}
            </a>
        </nav>
    </div>
</template>

<style lang="scss">
  .left-enter-active,
  .left-leave-active,
  .right-enter-active,
  .right-leave-active {
      transition: transform .3s ease;
  }
  
  .left-enter-from{
      transform: translateX(-550px);
  }
  .left-leave-to{
      transform: translateX(550px)
  }
  .right-enter-from{
      transform: translateX(550px)
  }
  .right-leave-to{
      transform: translateX(-550px);
  }
  // 其他省略...
</style>

codepen 範例 : https://codepen.io/MikeCheng1208/pen/eYRdzVL


所以我們在處理動畫的時候不要再依賴 JQuery 的 animate 了,在使用 Vue 上面有更多好用的方式可以處理動態效果。

好啦 ! 今天篇幅也夠長了,先介紹兩個做法,明天我們在來討論另外兩個動畫的處理方式。

Mike Vue

那如果對於Vue3不夠熟的話呢?

Ps. 購買的時候請登入或註冊該平台的會員,然後再使用下面連結進入網站點擊「立即購課」,這樣才可以讓我獲得更多的課程分潤,還可以幫助我完成更多豐富的內容給各位。

我有開設了一堂專門針對Vue3從零開始教學的課程,如果你覺得不錯的話,可以購買我課程來學習
https://hiskio.com/packages/AYR5m7VR3

那如果對於JS基礎不熟的朋友,我也有開設JS的入門課程,可以參考這個課程
https://hiskio.com/packages/Q9R4OYoyD

訂閱Mike的頻道享受精彩的教學與分享

Mike 的 Youtube 頻道
Mike的medium
MIke 的官方 line 帳號,好友搜尋 @mike_cheng


上一篇
[重構倒數第16天] - 選擇套件給我好好選啊!
下一篇
[重構倒數第14天] - Vue3處理動態效果(二)
系列文
[ 重構倒數30天,你的網站不Vue白不Vue ] 31

尚未有邦友留言

立即登入留言