譯者:nzbin
譯者的話:經過兩周的努力,終於完成了這個系列的翻譯,由於時間因素及個人水平有限,並沒有詳細的校對,其中仍然有很多不易理解的地方。我和原作者的初衷一樣,希望大家能夠通過這個系列文章有所收獲,至少可以增加學習的樂趣,我也在學習的路上,所學心得必將與大家共勉。
這是 JavaScript 框架 Vue.js 五篇教程的第五部分。在這個系列的最后一部分,我們將學習動畫(如果你了解我,你知道這一章遲早會來)。這個系列教程並不是一個完整的用戶手冊,而是通過基礎知識讓你快速了解 Vuejs 以及它的用途。

系列文章:
- 渲染, 指令, 事件
- 組件, Props, Slots
- Vue-cli
- Vuex
- 動畫 (你在這!)
背景知識
內置的 <transition> 和 <transition-group> 組件同時支持 CSS 和 JS 鈎子。如果你熟悉 React , transition 組件的概念對你並不陌生,因為在生命周期鈎子中,它與 ReactCSSTransitionGroup 類似,但也有顯著的差異 ,這讓書呆子的我很興奮。
我們先討論 CSS 過渡,然后再討論 CSS 動畫,之后介紹 JS 動畫鈎子以及動畫的生命周期方法。過渡狀態超出了本文的范圍,但這是可能的。這是我為此做的一個評價不錯的例子 。只要能得到充足的休息,我確信會寫那篇文章。
過渡 vs. 動畫
你可能不明白為什么過渡和動畫在這篇文章中分成了不同的部分,讓我解釋一下,雖然它們很相似,但也有不同的地方。過渡就是從一個狀態向另一個狀態插入值。我們可以做很多復雜的事情,但是很簡單。從起始狀態,到結束狀態,再回來。
動畫有點不同,你可以在一個聲明中設置多個狀態。比如,你可以在動畫 50% 的位置設置一個關鍵幀,然后在 70% 的位置設置一個完全不同的狀態,等等。你可以通過設置延遲屬性實現很復雜的運動。動畫也可以實現過渡的功能,只需要從頭到尾插入狀態,但是過渡無法像動畫一樣插入多個值。
在工具方面,兩者都是有用的。過渡如同一把“鋸”而動畫如同“電鋸”。有時你需要明白一件事,購買昂貴的設備可能是愚蠢的。對於大型項目,投資“電鋸”更有意義。
了解了這些知識之后,再來討論 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.io 和 Udemy 上面的課程。
感謝 Robin Rendle、Chris Coyier、Blake Newman 及 Evan You 對本系列各部分的校對。我希望這個系列可以解釋為什么我對 Vue 如此興奮,並且幫助你入門以及嘗試新鮮東西。
