Vue.js 系列教程 5:动画

原文:intro-to-vue-5-animations 译者:nzbin 译者的话:经过两周的努力,终于完成了这个系列的翻译,由于时间因素及个人水平有限,并没有详细的校对,其中仍然有很多不易理解的地方。我和原作者的初衷一样,希望大家能够通过这个系列文章有所收获,至少可以增加学习的乐趣,我也在学习的路上,所学心得必将与大家共勉。

这是 JavaScript 框架 Vue.js 五篇教程的第五部分。在这个系列的最后一部分,我们将学习动画(如果你了解我,你知道这一章迟早会来)。这个系列教程并不是一个完整的用户手册,而是通过基础知识让你快速了解 Vuejs 以及它的用途。

背景知识

内置的 <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.ioUdemy 上面的课程。

感谢 Robin Rendle、Chris Coyier、Blake Newman 及 Evan You 对本系列各部分的校对。我希望这个系列可以解释为什么我对 Vue 如此兴奋,并且帮助你入门以及尝试新鲜东西。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏一“技”之长

iOS动画开发之一——UIViewAnimation动画的使用

      一款APP的成功与否,除了完善的功能外,用户体验也占有极大的比重,动画的合理运用,可以很好的增强用户体验。iOS开发中,常用的动画处理有UIView...

12830
来自专栏前端说吧

css笔记 - transition学习笔记(二)

transition过渡 :用于当元素 从一种样式变换为另一种样式 时为元素添加效果。

9630
来自专栏Windows Community

UWP 手绘视频创作工具技术分享系列 - SVG 的解析和绘制

本篇作为技术分享系列的第一篇,详细讲一下 SVG 的解析和绘制,这部分功能的研究和最终实现由团队的 @黄超超 同学负责,感谢提供技术文档和支持。  首先我们来看...

41790
来自专栏数据小魔方

条件格式单元格图表

今天跟大家分享条件格式单元格图表! ▼ 这类图表比较特殊,不是通过excel的内置图标库制作,而是通过excel的条件格式工具制作的存放在单元格中的图表。这种图...

43980
来自专栏一“技”之长

AppleWatch开发入门九——Watch帧动画的实现

        动画一直是iOS系统的一大亮点,CoreAnimation和粒子效果的支持,开发者可以很容易的做出效果炫酷的动画特效。在watchOS中,由于性...

9920
来自专栏守候书阁

编写自己的代码库(css3常用动画的实现)

在月初的时候,发了CSS3热身实战--过渡与动画(实现炫酷下拉,手风琴,无缝滚动)。js的代码库也发过两次,两篇文章。之前也写了css3的热身实战,既然热身完了...

21120
来自专栏非著名程序员

目标:双向拖动的自定义View

国际惯例先预览后实现 ? 我们要实现的就是一个段位样式的拖动条,用来做筛选条件用的, 细心的朋友可能会发现微信设置里面有个一个通用字体的设置, 拖动然后改变字...

24960
来自专栏进步博客

深入理解视觉格式化模型( VISUAL FORMATTING MODEL)

“理论不懂就实践,实践不会就学理论”,非常赞同bluedavy的这句话。实践过程中经常会遇到某个属性的使用,浏览器渲染效果与预期效果不符,虽然通过死记硬背能避免...

14030
来自专栏coding for love

CSS进阶09-定位体系差异分析

(注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!) (注2:更多内容请查看我的目录。)

10630
来自专栏服务端技术杂谈

iOS 开发从 UIView 动画说起

毋庸置疑的:在iOS开发中,制作动画效果是最让开发者享受的环节之一。一个设计严谨、精细的动画效果能给用户耳目一新的效果,吸引他们的眼光 —— 这对于app而言是...

41770

扫码关注云+社区

领取腾讯云代金券