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 如此兴奋,而且帮助你入门以及尝试新鲜东西。

相关文章
相关标签/搜索