Vue.js 系列教程 5:動畫

原文:intro-to-vue-5-animationscss

譯者:nzbinhtml

譯者的話:通過兩週的努力,終於完成了這個系列的翻譯,因爲時間因素及我的水平有限,並無詳細的校對,其中仍然有不少不易理解的地方。我和原做者的初衷同樣,但願你們可以經過這個系列文章有所收穫,至少能夠增長學習的樂趣,我也在學習的路上,所學心得必將與你們共勉。前端

這是 JavaScript 框架 Vue.js 五篇教程的第五部分。在這個系列的最後一部分,咱們將學習動畫(若是你瞭解我,你知道這一章早晚會來)。這個系列教程並非一個完整的用戶手冊,而是經過基礎知識讓你快速瞭解 Vuejs 以及它的用途。vue

系列文章:

  1. 渲染, 指令, 事件
  2. 組件, Props, Slots
  3. Vue-cli
  4. Vuex
  5. 動畫 (你在這!)

背景知識

內置的 <transition><transition-group> 組件同時支持 CSS 和 JS 鉤子。若是你熟悉 React , transition 組件的概念對你並不陌生,由於在生命週期鉤子中,它與 ReactCSSTransitionGroup 相似,但也有顯著的差別 ,這讓書呆子的我很興奮。git

咱們先討論 CSS 過渡,而後再討論 CSS 動畫,以後介紹 JS 動畫鉤子以及動畫的生命週期方法。過渡狀態超出了本文的範圍,但這是可能的。這是我爲此作的一個評價不錯的例子 。只要能獲得充足的休息,我確信會寫那篇文章。github

過渡 vs. 動畫

你可能不明白爲何過渡和動畫在這篇文章中分紅了不一樣的部分,讓我解釋一下,雖然它們很類似,但也有不一樣的地方。過渡就是從一個狀態向另外一個狀態插入值。咱們能夠作不少複雜的事情,可是很簡單。從起始狀態,到結束狀態,再回來。api

動畫有點不一樣,你能夠在一個聲明中設置多個狀態。好比,你能夠在動畫 50% 的位置設置一個關鍵幀,而後在 70% 的位置設置一個徹底不一樣的狀態,等等。你能夠經過設置延遲屬性實現很複雜的運動。動畫也能夠實現過渡的功能,只須要從頭至尾插入狀態,可是過渡沒法像動畫同樣插入多個值。架構

在工具方面,二者都是有用的。過渡如同一把「鋸」而動畫如同「電鋸」。有時你須要明白一件事,購買昂貴的設備多是愚蠢的。對於大型項目,投資「電鋸」更有意義。app

瞭解了這些知識以後,再來討論 Vue!框架

CSS 過渡

假設有一個簡單的模態窗。經過點擊按鈕顯示或隱藏模態窗。根據前面的部分, 咱們能夠這樣作:建立一個按鈕的 Vue 實例,在實例中建立一個子組件,設置數據的狀態,這樣能夠經過切換布爾值並添加事件處理實現子組件的顯示及隱藏。咱們可使用 v-if 或者 v-show 來切換組件可見性。也可使用 slot 放置模態窗的切換按鈕。

<div id="app">
  <h3>Let's trigger this here modal!</h3>
  <button @click="toggleShow">
    <span v-if="isShowing">Hide child</span>
    <span v-else>Show child</span>
  </button>
  <app-child v-if="isShowing" class="modal">
    <button @click="toggleShow">
      Close
    </button>
  </app-child>
</div>

<script type="text/x-template" id="childarea">
  <div>
    <h2>Here I am!</h2>
    <slot></slot>
  </div>
</script>
const Child = {
  template: '#childarea'
};

new Vue({
  el: '#app',
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  },
  components: {
    appChild: Child
  }
});

See the Pen Transition Demo- base without transition by Sarah Drasner (@sdras) on CodePen.

能夠正常工做,可是這樣的模態窗有點不和諧。 😳

咱們已經使用 v-if 實現組件的加載及卸載,所以咱們若是在過渡組件上添加條件,Vue 能夠跟蹤事件變化:

<transition name="fade">
  <app-child v-if="isShowing" class="modal">
    <button @click="toggleShow">
      Close
    </button>
  </app-child>
</transition>

如今,咱們可使用現成的 <transition> 組件。過渡鉤子會添加 v- 前綴,咱們能夠在 CSS 中使用。其中 enter/leave 定義動畫開始第一幀的位置, enter-active/leave-active 定義動畫運行階段—— 你須要把動畫屬性放在這裏, enter-to/leave-to 指定元素在最後一幀上的位置。

我打算使用官網文檔中的示意圖說明,由於我認爲它把類名描述的直觀清晰:

就我我的而言,我並不常用默認的 v- 前綴。我常常給過渡命名,這樣若是我想應用到另外一個動畫時就不會有衝突。這是不難作到的,正如你所看到的,咱們只是簡單地給過渡組件添加了一個 name 屬性: name="fade"

既然有了鉤子,咱們就能夠利用它們建立過渡:

.fade-enter-active, .fade-leave-active {
  transition: opacity 0.25s ease-out;
}

.fade-enter, .fade-leave-to {
  opacity: 0;
}

.fade-enter-active.fade-leave-active 類將會應用到實際的過渡中。這是普通的 CSS ,你能夠在過渡中使用 cubic-beziers 實現 eases, delays, 或者指定其它屬性。其實,若是把這些類的過渡屬性放到組件的類中做爲默認設置,也一樣有效。這些並不一須要由過渡組件鉤子來定義。它們只是靜靜地等待組件的變化而後將變化添加到過渡中 ( 所以你仍然須要過渡組件以及 .fade-enter ,.fade-leave-to )。我使用 enter-active 和 leave-active 類的緣由是我能夠在其它元素上重用這些過渡屬性,而不須要在每一個實例上應用一樣的 CSS 。

須要注意的另一點:我在每個 active 類上都使用了 ease-out 屬性。這些屬性只適用於透明元素。可是若是你使用了過渡屬性好比 transform ,你可能想把二者分開, 將 ease-out 應用於 enter-active 類而將 ease-in 應用於 enter-leave 類 (或者大體表現相同曲線的 cubic-beziers )。我發現它使動畫看起來更…優雅的(哈哈)。

你也注意到我將 .fade-enter 和 the .fade-to 屬性設置爲 opacity: 0 。這是動畫的初始和結束位置,載入時的初始狀態,卸載時的結束狀態。你可能認爲 .fade-enter-to.fade-leave 應該設置 opacity: 1 。可是沒有必要,由於它是組件的默認狀態,因此這將是多餘的。CSS 過渡和動畫若是沒有設置,老是會使用默認狀態。

See the Pen Transition Demo- without bk classes by Sarah Drasner (@sdras) on CodePen.

運行很好!可是,若是咱們想使背景內容淡出視野,使模態窗居中顯示而背景丟失焦點,會發生什麼呢? 咱們不能使用 <transition> 組件,由於組件是基於被加載或被卸載的部分工做的,而背景只是圍繞在周圍。咱們可使用基於狀態的過渡類,使用類改變 CSS 過渡來變換背景:

<div v-bind:class="[isShowing ? blurClass : '', bkClass]">
    <h3>Let's trigger this here modal!</h3>
    <button @click="toggleShow">
      <span v-if="isShowing">Hide child</span>
      <span v-else>Show child</span>
    </button>
  </div>
.bk {
  transition: all 0.1s ease-out;
}

.blur {
  filter: blur(2px);
  opacity: 0.4;
}
new Vue({
  el: '#app',
  data() {
    return {
      isShowing: false,
      bkClass: 'bk',
      blurClass: 'blur'
    }
  },
  ...
});

See the Pen Transition Demo by Sarah Drasner (@sdras) on CodePen.

CSS 動畫

既然已經瞭解了過渡(transitions)的工做原理,咱們能夠經過這些核心概念建立不錯的 CSS 動畫。咱們仍然使用 <transition> 組件,而且給它命名,這樣就可使用類鉤子(class hooks)了。動畫和過渡的區別並不只僅是設置最終的狀態或者在開始和結束之間插入狀態,咱們將使用 CSS 中的 @keyframes 建立有趣可愛的效果。

在上一部分中,咱們講了能夠給 transition 組件起一個特殊的名字,這樣能夠做爲類鉤子使用。可是在這一部分中,咱們將進一步, 在不一樣的動畫中應用不一樣的類鉤子。你可能還記得全部有趣的動畫都是基於 enter-active 和 leave-active 。咱們能夠給每個類鉤子設置不一樣的屬性,可是咱們能夠進一步給每一個實例一個特殊的類 :

enter-active-class="toasty"
leave-active-class="bounceOut"

這意味着咱們能夠重用這些類,甚至能夠設置 CSS 動畫庫中的類。

好比咱們但願一個小球彈進來再滾出去:

<div id="app">
  <h3>Bounce the Ball!</h3>
  <button @click="toggleShow">
    <span v-if="isShowing">Get it gone!</span>
    <span v-else>Here we go!</span>
  </button>
  <transition
    name="ballmove"
    enter-active-class="bouncein"
    leave-active-class="rollout">
  <div v-if="isShowing">
    <app-child class="child"></app-child>
  </div>
  </transition>
</div>

對於反彈動畫,若是使用 CSS 的話,咱們須要設置大量關鍵幀(而使用 JS 只須要一行代碼),咱們會使用 SASS mixin 保持樣式的簡練 (無需重複設置)。爲了讓小球組件從屏幕外開始,咱們設置了一個 .ballmove-enter 的類:

@mixin ballb($yaxis: 0) {
  transform: translate3d(0, $yaxis, 0);
}

@keyframes bouncein { 
  1% { @include ballb(-400px); }
  20%, 40%, 60%, 80%, 95%, 99%, 100% { @include ballb() }
  30% { @include ballb(-80px); }
  50% { @include ballb(-40px); }
  70% { @include ballb(-30px); }
  90% { @include ballb(-15px); }
  97% { @include ballb(-10px); }
}

.bouncein { 
  animation: bouncein 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}

.ballmove-enter {
  @include ballb(-400px);
}

對於小球滾出動畫,咱們須要建立兩個不一樣的動畫。這是由於 transform 會應用於整個子組件,那樣的話整個組件都會旋轉。因此咱們使用 translation 將組件移出屏幕, 經過 rotation 給小球添加旋轉:

@keyframes rollout { 
  0% { transform: translate3d(0, 300px, 0); }
  100% { transform: translate3d(1000px, 300px, 0); }
}

@keyframes ballroll {
  0% { transform: rotate(0); }
  100% { transform: rotate(1000deg); }
}

.rollout { 
  width: 60px;
  height: 60px;
  animation: rollout 2s cubic-bezier(0.55, 0.085, 0.68, 0.53) both; 
  div {
    animation: ballroll 2s cubic-bezier(0.55, 0.085, 0.68, 0.53) both; 
  }
}

See the Pen Ball Bouncing using Vue transition and CSS Animation by Sarah Drasner (@sdras) on CodePen.

過渡模式

你是否還記得我說過 Vue 在過渡中提供了好用的功能讓我這個書呆子很高興?這是我很是喜歡的一點。若是一個組件過渡離開的時候,你給另外一個組件添加過渡,你將在一個奇怪的時刻看到兩個組件同時存在,而後又迅速回到原位(這是 Vue 文檔中的例子):

Vue 提供了過渡模式,這樣當一個組件過渡出去的時候,另外一個過渡進來的組件並不會有奇怪的位置的閃動或阻塞。其緣由就是經過有序的過渡而不是同時發生。有兩種模式可供選擇:

In-out: 新元素先進行過渡,完成以後當前元素過渡離開。

Out-in: 當前元素先進行過渡,完成以後新元素過渡進入。

看看下面的例子。你能夠觀察過渡組件的- out-in 模式,只有當一張圖片翻過去以後,組件纔會出現:

<transition name="flip" mode="out-in">
  <slot v-if="!isShowing"></slot>
  <img v-else src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/cartoonvideo14.jpeg" />
</transition>

See the Pen Vue in-out modes by Sarah Drasner (@sdras) on CodePen.

若是咱們去掉這種過渡模式,你會看到一部分翻轉的時候會擋住另外一部分,並且動畫有些不協調,這並非咱們想要的效果:

See the Pen Vue in-out modes - no modes contrast by Sarah Drasner (@sdras) on CodePen.

JS 動畫

有不少適合咱們動畫的易於使用的 JS 鉤子。全部的鉤子都會傳入 el 參數 ( element 的縮寫) ,除了動畫鉤子(enter 和 leave),還會傳入 done 做爲參數,正如你所猜的,它的做用就是告知 Vue 動畫結束。你會注意到咱們給 CSS 綁定了 false 值,這是爲了讓組件知道咱們將使用 JavaScript 而不是 CSS 。

<transition 
  @before-enter="beforeEnter"
  @enter="enter"
  @after-enter="afterEnter"
  @enter-cancelled="enterCancelled"

  @before-Leave="beforeLeave"
  @leave="leave"
  @after-leave="afterLeave"
  @leave-cancelled="leaveCancelled"
  :css="false">
 
 </transition>

從最基本的層面看,這是開始動畫和結束動畫所須要的,包括相關的方法:

<transition 
  @enter="enterEl"
  @leave="leaveEl"
  :css="false">
 <!-- put element here-->
 </transition>
methods: {
   enterEl(el, done) {
     //entrance animation
     done();
  },
  leaveEl(el, done) {
    //exit animation
    done();
  },
}

在下面是例子中,我在鉤子中接入了一個 GreenSock timeline:

new Vue({
  el: '#app',
  data() {
    return {
      message: 'This is a good place to type things.',
      load: false
    }
  },
  methods: {
    beforeEnter(el) {
      TweenMax.set(el, {
        transformPerspective: 600,
        perspective: 300,
        transformStyle: "preserve-3d",
        autoAlpha: 1
      });
    },
    enter(el, done) {
      ...
      
      tl.add("drop");
      for (var i = 0; i < wordCount; i++) {
        tl.from(split.words[i], 1.5, {
          z: Math.floor(Math.random() * (1 + 150 - -150) + -150),
          ease: Bounce.easeOut
        }, "drop+=0." + (i/ 0.5));
       ...
      
    }
  }
});

See the Pen Vue Book Content Typer by Sarah Drasner (@sdras) on CodePen.

在上面的動畫中注意兩個有趣的事情,我向 Timeline 實例中傳遞 {onComplete:done} 做爲參數,而且我使用 beforeEnter 鉤子來放置 TweenMax.set 代碼,這容許我在動畫開始前對單詞設置任意屬性,這種狀況相似 transform-style: preserve-3d

很重要的一點是,你也能夠直接在 CSS 中爲動畫設置你想要的默認狀態。有人問我如何決定是在 CSS 中仍是在 TweenMax.set 中設置屬性。根據經驗來講,我一般把我須要的一些動畫的特殊屬性設置在 TweenMax.set 中。這樣,若是動畫中的某些東西發生變化而我須要更新的話,它已經在個人工做流程中。

動畫中的生命週期鉤子

一切都很好,可是若是動畫很複雜,須要操做大量 DOM 元素會怎樣?如今就是使用生命週期方法的最佳時機。在下面的例子中,咱們使用了 <transition> 組件以及 mounted() 方法來建立動畫。

See the Pen Vue Weather Notifier by Sarah Drasner (@sdras) on CodePen.

若是咱們給一個單獨的元素添加過渡,咱們將使用 transition 組件,好比,當電話按鈕周圍的線條顯示的時候:

<transition 
  @before-enter="beforeEnterStroke"
  @enter="enterStroke"
  :css="false"
  appear>
  <path class="main-button" d="M413,272.2c5.1,1.4,7.2,4.7,4.7,7.4s-8.7,3.8-13.8,2.5-7.2-4.7-4.7-7.4S407.9,270.9,413,272.2Z" transform="translate(0 58)" fill="none"/>
</transition>
beforeEnterStroke(el) {
  el.style.strokeWidth = 0;
  el.style.stroke = 'orange';
},
enterStroke(el, done) {
  const tl = new TimelineMax({
    onComplete: done
  });

  tl.to(el, 0.75, {
    strokeWidth: 1,
    ease: Circ.easeOut
  }, 1);

  tl.to(el, 4, {
    strokeWidth: 0,
    opacity: 0,
    ease: Sine.easeOut
  });
},

可是當一個組件首次顯示的時候,會有 30 個元素以及更多的動畫,把每個都放進 transition 組件中效率較低。因此,咱們將使用第三部分提到的生命週期鉤子綁定和 transition 鉤子使用的相同事件: mounted()

const Tornadoarea = {
  template: '#tornadoarea',
  mounted () {
    let audio = new Audio('https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/tornado.mp3'),
        tl = new TimelineMax();

    audio.play();
    tl.add("tornado");

    //tornado timeline begins
    tl.staggerFromTo(".tornado-group ellipse", 1, {
      opacity: 0
    }, {
      opacity: 1,
      ease: Sine.easeOut
    }, 0.15, "tornado");
    ...
    }
};

咱們可使用更有效率的方法以及建立複雜的效果。Vue 提供了直觀靈活的 API ,不僅是建立組件化的前端架構,還有流暢的運動和視圖間的無縫銜接。

總結

這個系列的文章並不打算成爲文檔。雖然咱們已經講了不少,但仍然還有不少沒有涉及的內容:路由、mixins、服務端渲染等等。有如此多的使人稱奇的東西可使用。深刻研究的話能夠看 詳細的官方文檔 ,這個倉庫中有很全的 例子和資源 。 有一本名爲 The Majesty of Vue.js 的書,還有 Egghead.ioUdemy 上面的課程。

感謝 Robin Rendle、Chris Coyier、Blake Newman 及 Evan You 對本系列各部分的校對。我但願這個系列能夠解釋爲何我對 Vue 如此興奮,而且幫助你入門以及嘗試新鮮東西。

相關文章
相關標籤/搜索