Vue虽然是个JS框架,但也提供了对CSS的一定控制能力,主要体现在进入和离开时的过渡,也就是在插入、更新或从 DOM 中移除项时,这包括以下工具:
Vue 自带一个叫做 transition
的封装组件,用于在下列情形中,给任何元素和组件添加进入/离开过渡:
v-if
)v-show
)下面是一个典型的例子:
<div id="demo"> <button @click="show = !show"> Toggle </button> <transition name="fade"> <p v-if="show">hello</p> </transition> </div>
使用transition将p标签包裹起来,相当于做了CSS封装。
const Demo = { data() { return { show: true } } } Vue.createApp(Demo).mount('#demo')
剩下的主要工作就是制定CSS效果,也考验各位的美术能力和想象力。
.fade-enter-active, .fade-leave-active { transition: opacity 0.5s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; }
当包含在 transition
组件中的DOM元素发生插入和删除动作时,Vue 将会做以下处理:
nextTick
概念不同)在进入/离开的过渡中,会有 6 个 class 被逐一切换。
v-enter-from
:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。v-enter-active
:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。v-enter-to
:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from
被移除),在过渡/动画完成之后移除。v-leave-from
:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。v-leave-active
:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。v-leave-to
:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from
被删除),在过渡/动画完成之后移除。注意这些类名开头的v-,它不是v-bind中的v-,而是一个代指,要被替换的。
对于这些在过渡中切换的类名来说,如果你使用一个没有命名的 <transition>
,则 v-
是这些class名的默认前缀。如果你使用了 <transition name="my-transition">
,那么 v-enter-from
会替换为 my-transition-enter-from
。
v-enter-active
和 v-leave-active
可以控制进入/离开过渡的不同的缓和曲线。
下面是另外一个 CSS 过渡的例子。
<div id="demo"> <button @click="show = !show"> Toggle render </button> <transition name="slide-fade"> <p v-if="show">hello</p> </transition> </div>
const Demo = { data() { return { show: true } } } Vue.createApp(Demo).mount('#demo')
/* 可以设置不同的进入和离开动画 */ /* 设置持续时间和动画函数 */ .slide-fade-enter-active { transition: all 0.3s ease-out; } .slide-fade-leave-active { transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1); } .slide-fade-enter-from, .slide-fade-leave-to { transform: translateX(20px); opacity: 0; }
CSS 动画用法同 CSS 过渡,区别是在动画中 v-enter-from
类名在节点插入 DOM 后不会立即删除,而是在 animationend
事件触发时删除。
下面是一个例子,为了简洁起见,省略了带前缀的 CSS 规则:
<div id="demo"> <button @click="show = !show">Toggle show</button> <transition name="bounce"> <p v-if="show"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis enim libero, at lacinia diam fermentum id. Pellentesque habitant morbi tristique senectus et netus. </p> </transition> </div>
const Demo = { data() { return { show: true } } } Vue.createApp(Demo).mount('#demo')
.bounce-enter-active { animation: bounce-in 0.5s; } .bounce-leave-active { animation: bounce-in 0.5s reverse; } @keyframes bounce-in { 0% { transform: scale(0); } 50% { transform: scale(1.25); } 100% { transform: scale(1); } }
Vue原生的六个CSS类名可能不符合你的项目规范,你也许想自己指定它们。
我们可以通过以下属性在HTML中自定义过渡类名:
enter-from-class
enter-active-class
enter-to-class
leave-from-class
leave-active-class
leave-to-class
他们的优先级高于普通的类名,这对于 Vue 的过渡系统和其他第三方 CSS 动画库,如 Animate.css. 结合使用十分有用。
示例:
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.0/animate.min.css" rel="stylesheet" type="text/css" /> <div id="demo"> <button @click="show = !show"> Toggle render </button> <transition name="custom-classes-transition" enter-active-class="animate__animated animate__tada" leave-active-class="animate__animated animate__bounceOutRight" > <p v-if="show">hello</p> </transition> </div>
const Demo = { data() { return { show: true } } } Vue.createApp(Demo).mount('#demo')
Vue 会自动计算过渡效果的完成时机。
默认情况下,Vue 会等待其在过渡效果的根元素的第一个 transitionend
或 animationend
事件。
你也可以自己指定过渡的持续时间,只需要在 <transition>
组件上增加一个 duration
属性,并提供过渡持续时间 (以毫秒计):
<transition :duration="1000">...</transition>
可以单独指定进入和移出的持续时间:
<transition :duration="{ enter: 500, leave: 800 }">...</transition>
Vue为过渡提供了一系列的钩子,可以在HTML标签中按需声明它们:
<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>
// ... methods: { // -------- // ENTERING // -------- beforeEnter(el) { // ... }, // 当与 CSS 结合使用时 // 回调函数 done 是可选的 enter(el, done) { // ... done() }, afterEnter(el) { // ... }, enterCancelled(el) { // ... }, // -------- // 离开时 // -------- beforeLeave(el) { // ... }, // 当与 CSS 结合使用时 // 回调函数 done 是可选的 leave(el, done) { // ... done() }, afterLeave(el) { // ... }, // leaveCancelled 只用于 v-show 中 leaveCancelled(el) { // ... } }
这些钩子函数可以结合 CSS transitions/animations
使用,也可以单独使用。
当只用 JavaScript 过渡的时候,在 enter
和 leave
钩中必须使用 done
进行回调。否则,它们将被同步调用,过渡会立即完成。添加 :css="false"
,也会让 Vue 会跳过 CSS 的检测,除了性能略高之外,这可以避免过渡过程中 CSS 规则的影响。
现在让我们来看一个例子。下面是一个使用 GreenSock 的 JavaScript 过渡:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js"></script> <div id="demo"> <button @click="show = !show"> Toggle </button> <transition @before-enter="beforeEnter" @enter="enter" @leave="leave" :css="false" > <p v-if="show"> Demo </p> </transition> </div>
const Demo = { data() { return { show: false } }, methods: { beforeEnter(el) { gsap.set(el, { scaleX: 0.8, scaleY: 1.2 }) }, enter(el, done) { gsap.to(el, { duration: 1, scaleX: 1.5, scaleY: 0.7, opacity: 1, x: 150, ease: 'elastic.inOut(2.5, 1)', onComplete: done }) }, leave(el, done) { gsap.to(el, { duration: 0.7, scaleX: 1, scaleY: 1, x: 300, ease: 'elastic.inOut(2.5, 1)' }) gsap.to(el, { duration: 0.2, delay: 0.5, opacity: 0, onComplete: done }) } } } Vue.createApp(Demo).mount('#demo')
可以通过 appear
属性设置节点在初始渲染的过渡
<transition appear> <!-- ... --> </transition>
最常见的多元素的过渡是一个列表和描述这个列表为空消息的元素,如下的table和p标签:
<transition> <table v-if="items.length > 0"> <!-- ... --> </table> <p v-else>Sorry, no items found.</p> </transition>
实际上,通过使用 v-if
/v-else-if
/v-else
或将单个元素绑定到一个动态属性,可以在任意数量的元素之间进行过渡。例如:
<transition> <button v-if="docState === 'saved'" key="saved"> Edit </button> <button v-else-if="docState === 'edited'" key="edited"> Save </button> <button v-else-if="docState === 'editing'" key="editing"> Cancel </button> </transition>
也可以写为:
<transition> <button :key="docState"> {{ buttonMessage }} </button> </transition>
// ... computed: { buttonMessage() { switch (this.docState) { case 'saved': return 'Edit' case 'edited': return 'Save' case 'editing': return 'Cancel' } } }
注意要为元素提供key属性,防止Vue重用它。
前面看起来一切都不错,但这里还有一个问题,看下面的例子:
<div id="demo"> <transition name="no-mode-fade"> <button v-if="noActivated" key="on" @click="noActivated = false"> on </button> <button v-else key="off" @click="noActivated = true"> off </button> </transition> </div>
body { margin: 30px; } #demo { position: relative; } button { position: absolute; } .no-mode-fade-enter-active, .no-mode-fade-leave-active { transition: opacity .5s } .no-mode-fade-enter-from, .no-mode-fade-leave-to { opacity: 0 } button { background: #05ae7f; border-radius: 4px; display: inline-block; border: none; padding: 0.5rem 0.75rem; text-decoration: none; color: #ffffff; font-family: sans-serif; font-size: 1rem; cursor: pointer; text-align: center; -webkit-appearance: none; -moz-appearance: none; }
const Demo = { data() { return { on: false } } } Vue.createApp(Demo).mount('#demo')
试着点击on/off按钮。在“on”按钮和“off”按钮的过渡中,两个按钮都被重绘了,一个离开过渡的时候另一个开始进入过渡。这是 <transition>
的默认行为 —— 进入和离开同时发生。
上面例子中的过渡效果不算好,总感觉差那么点意思,为此 Vue 提供了过渡的模式:
in-out
: 新元素先进行过渡,完成之后当前元素过渡离开。out-in
: 当前元素先进行过渡,完成之后新元素过渡进入。现在让我们用 out-in
更新 on/off 按钮的转换:
<transition name="fade" mode="out-in"> <!-- ... the buttons ... --> </transition>
通过指定mode属性,我们获得了效果更好的过渡,而不必添加任何特殊 style。
我们可以用它来协调更具表现力的动作,例如下面这个折叠卡片的例子:
<div id="app"> <h3>点击翻转图片</h3> <main> <app-child> <img src='https://images.unsplash.com/photo-1520182205149-1e5e4e7329b4?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='image of a woman on a train'> </app-child> <app-child> <img src='https://images.unsplash.com/photo-1501421018470-faf26f6b1bef?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='drawing of a woman sharing soda with a zombie'> </app-child> </main> </div>
body { font-family: "Bitter", serif; background: #333; color: white; } #app { text-align: center; margin: 60px; margin: 0 auto; display: table; } button { font-family: "Bitter"; background: #c62735; color: white; border: 0; padding: 5px 15px; margin: 0 10px; border-radius: 4px; outline: 0; cursor: pointer; } .img-contain { width: 250px; height: 160px; overflow: hidden; transform-origin: 50% 50%; } img { width: 100%; transform-origin: 50% 50%; cursor: pointer; transform: scaleY(1) translateZ(0); margin: 5px; } main { display: flex; flex-wrap: wrap; justify-content: center; } .img-contain:hover .overlay { opacity: 1; background: hsla(50, 0%, 0%, 0.6); transition: 0.3s all ease-out; } .img-contain .overlay { position: absolute; z-index: 1000; display: block; width: 245px; height: 155px; margin: 5px; opacity: 0; overflow: hidden; transition: 0.3s all ease-in; } .overlay-text { margin-top: 40px; } h4 { margin: 0 0 15px; } .flip-enter-active { transition: all 0.2s cubic-bezier(0.55, 0.085, 0.68, 0.53); //ease-in-quad transform-origin: 50% 50%; } .flip-leave-active { transform-origin: 50% 50%; transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94); //ease-out-quad } .flip-enter-from, .flip-leave-to { transform-origin: 50% 50%; transform: scaleY(0) translateZ(0); opacity: 0; }
const app = Vue.createApp({}); app.component("app-child", { template: `<div class="img-contain"> <div class="overlay"> <p class="overlay-text">不喜欢这张?</p> <button @click="toggleShow">替换!</button> </div> <transition name="flip" mode="out-in"> <div v-if="!isShowing"> <slot></slot> </div> <img v-else src='https://images.unsplash.com/flagged/photo-1563248101-a975e9a18cc6?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt=''> </transition> </div>`, data() { return { isShowing: false } }, methods: { toggleShow() { this.isShowing = !this.isShowing; } } }); app.mount("#app");
这个例子实际上是两个图片元素在彼此切换,但是由于开始状态和结束状态的比例是相同的:水平为0,它看起来就像一个连续运动。这种效果对于真实的人机交互非常有用。
组件之间的过渡更简单 —— 我们甚至不需要 key
属性。相反,我们包裹了一个动态组件 :
<div id="demo"> <input v-model="view" type="radio" value="v-a" id="a"><label for="a">A</label> <input v-model="view" type="radio" value="v-b" id="b"><label for="b">B</label> <transition name="component-fade" mode="out-in"> <component :is="view"></component> </transition> </div>
const Demo = { data() { return { view: 'v-a' } }, components: { 'v-a': { template: '<div>Component A</div>' }, 'v-b': { template: '<div>Component B</div>' } } } Vue.createApp(Demo).mount('#demo')
.component-fade-enter-active, .component-fade-leave-active { transition: opacity 0.3s ease; } .component-fade-enter-from, .component-fade-leave-to { opacity: 0; }