该篇为翻译文章:原文为:Intro to Vue.js: Animations

该系列文章目录:
Vue.js #1-渲染、指令、事件(原文)

Vue.js #2-组件、属性、Slots(原文)

Vue.js #3-Vue-cli及生命周期(原文)
Vue.js #4-Vuex (原文)

Vue.js #5-动画 (当前)

一些背景

在Vue当中,有像<transition></transition><transition -group>这样的内置组件供我们使用。如果你以前用过React的话,可能用过它里面的ReactCSSTransitionGroup组件,其实它们是很相似的,但仍有一些让人眼前一亮的不同点。

我们先从CSS的Transitions动画开始说起,然后就是CSS的Animations动画,再然后就是JS控制动画及通过生命周期的勾子函数还控制动画。这篇文章中将不会社稷到「过渡动画」,但这也比较容易。这里有我写的一个关于「过渡动画」的例子,我可能将会针对它单独写一篇文章。

译者:原文这中间介绍了CSS3的Transition和Animation的区别,这里我没有翻译,如果有读者对此不是很了解,可以去查阅相关资料。

CSS Transitions

假如现在有一个非常简单的弹窗。我们通过点击一个按钮让它显示或隐藏。通过之前的文章我们可以写出大概的结构:新建一个Vue实例,里面有一个按钮,然后在里面新建一个组件,通过设置数据状态来控制组件的显示与隐藏,当然,需要使用到v-ifv-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
  }
});

结果:http://codepen.io/sdras/pen/df955fc6a1f482dcf104f2648746f8b3

这样,弹窗是可以工作的,但是没动画,太呆板了~
我们已经通过使用v-if来安装和卸载组件了,现在,我们只需要添加transition组件就可以实现动画:

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

我们只是在组件外面加了一个<transition>。然后,我们可以通过我们在CSS当中写的一些以v-开头的动画样式进行动画。enter/leave用来设置动画的起点状态,enter-active/leave-active里面用来设置一些动画属性(例如动画时间,延时,属性等等),enter-to/leave-to则是设置动画的终点状态。

通过一张图来理解比较好:

transition

就我个人而言,我不喜欢使用默认的以v-来写样式。我会给transition组件取个动画名字:name="fade"

那么样式可以这样来写:

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

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

注意,我给动画的两个状态都加了ease-out的动画方式。这种动画方式针对于我当前的这个opacity进行动画是效果比较好的。但是当你对一些transform属性进行动画的时候,可能就需要对enter-active和enter-leave进行单独的设置,例如给enter-active添加ease-out,给enter-leave添加ease-in。总之效果需根据实际情况来进行调整。

我们把.fade-enter.fade-to都设置了opacity:0,这个是动画的起点状态和终点状态。你可能觉得需要加一个opacity:1.fade-enter-to.fade-leave,但这是没有必要的,因为组件的默认状态就是这样的。

结果:http://codepen.io/sdras/pen/6ef951b970faf929d8580199fe8ea6ba

动画可以正常工作了!但是,如果我们现在想在弹窗显示的时候把背景进行淡化该怎么办呢?我们不能针对背景元素进行使用</transition><transition>组件,因为它只会在组件被安装和卸载的时候才会起作用,而我们的背景是一直待在那里的。我们可以来改变背景元素的class来应用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'
    }
  },
  ...
});

结果:http://codepen.io/sdras/pen/4daa105fc18da0e223b6be9a200b349d

CSS Animation

现在,我们了解了transition是怎么工作的了,现在我们使用animation动画的话,还是使用</transition><transition>组件,仍然可以给它取一个名字用来和CSS进行关联。唯一不同的是,我们只需要设置结束的状态并且它中间的动画方式即可,那么中间的动画形式则使用@keyframes来设置。

之前使用transition动画时,我们知道了通过给</transition><transition>设置一个名字来应用对应的CSS。而现在,我们直接指定不同的class来应用不同的动画,比如:
enter-active-class="toasty"
leave-active-class="bounceOut"

这就意味着,我们可以使用一些现成的CSS动画库(因为可以直接指定class名)。

现在,我们让一个球「跳」进来,并且「滚」出去:

<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:


@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);
}

「滚」出去CSS:

@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; 
  }
}

结果:http://codepen.io/sdras/pen/pRWxGg

Transition Modes-动画模式

如果你在一个组件进行消失的时候,让另一个组件进行动画载入,你可能会发现这样一个奇怪的现象(这个例子来自官方文档):
pre-transition-mode

对此,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>

结果:http://codepen.io/sdras/pen/mRpoOG
如果我们把out-in模式去掉,结果则不是我们想要的。

JS动画

</transition><transition>为我们提供了一些很好用的JS钩子函数。所有的钩子函数都接收一个el参数(即element-元素的简称),enter和leave还会传入另外一个done参数,看名称就应该能猜出来,它是动画结束时的回调。注意,我们需要把css绑定一个false来关闭CSS动画,从而使用JS动画来取而代之:

<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动画来完成JS动画:

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));
       ...

    }
  }
});

结果:http://codepen.io/sdras/pen/MJedjd

上面的动画中有两个比较有趣的点是需要注意的。第一,我在GreenSock的Timeline实例中设置了{onComplete:done},即在动画结束时执行回调函数。第二,在beforeEnter中加入了TweenMax.set的代码来在动画开始之前设置文字的一些初始属性,这里设置的是transform-style: preserve-3d

注意,我们当然也可以直接用CSS来设置初始化属性状态。一个经常被问到的问题就是,我们到底应该是把初始化属性写在CSS里还是写在TweenMax.set(JS)里呢?一般说来,我会把它都写在TweenMax.set当中,因为这样只需要改一个地方,比较容易维护。

生命周期里的动画

之前说的一些动画应该比较好懂。可是,如果你需要一些比较复杂的动画,比如需要操作大量的DOM元素,该怎么办呢?这个时候就需要一些生命周期钩子函数介入了。下面的例子当中,我们同时使用了</transition><transition>组件和mounted()方法来创建动画。

结果:http://codepen.io/sdras/pen/YNpaoJ

当我们在进行单个元素的动画的时候,我们会用到</transition><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");
    ...
    }
};

结语

虽然这个系列文章已经社稷了很多东西,但它终归不会像官方文档那样很详细,仍然有很多东西是我没的提及到的,例如:routing,mixins,服务端渲染等等。下面,列出一些学习资源给大家: