最近准备对团队里公共的插件作一些小动效,优化用户体验。此次的先从最简单的toast
插件入手。
主要的文件有以下两个:
index.jscss
import Toast from './Toast.vue'; const _TOAST = { show: false, component: null }; export default { install(Vue) { // 添加实例方法 Vue.prototype.$toast = (text, options = {duration: 2000}) => { if (_TOAST.show) { return; } if (!_TOAST.component) { let ToastComponent = Vue.extend(Toast); _TOAST.component = new ToastComponent(); let element = _TOAST.component.$mount().$el; document.body.appendChild(element); } _TOAST.component.duration = options.duration || 2000; _TOAST.component.whiteSpace = options.whiteSpace || 'inherit'; _TOAST.component.position = options.position || 'center'; _TOAST.component.text = text; _TOAST.component.show = _TOAST.show = true; setTimeout(() => { _TOAST.component.show = _TOAST.show = false; }, options.duration); }; Vue.prototype.$killToast = () => { if (_TOAST.component) { _TOAST.component.show = _TOAST.show = false; } }; } };
Toast.vuehtml
<template> <div v-show="show" class="toast" :style="styleObject"> {{text}} </div> </template> <script> export default { name: 'Toast', data() { return { show: false, text: 'toast', // 默认显示2s duration: 2000, // 默认换行 whiteSpace: 'inherit', // 显示的位置 position: 'center' }; }, computed: { styleObject() { return { webkitAnimation: 'show-toast ' + this.duration / 1000 + 's linear forwards', animation: 'show-toast ' + this.duration / 1000 + 's linear forwards', whiteSpace: this.whiteSpace, // toast的位置 top: this.position === 'up' ? '15%' : this.position === 'bottom' ? '85%' : '50%' }; } } }; </script> <style scoped> @keyframes show-toast { 0% {opacity: 0;} 25% {opacity: 1; z-index: 9999} 50% {opacity: 1; z-index: 9999} 75% {opacity: 1; z-index: 9999} 100% {opacity: 0; z-index: 0} } .toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 999; background-color: #000; opacity: .7; color: #fff; box-sizing: border-box; min-height: 80px; padding: 20px 30px; line-height: 50px; min-width: 364px; max-width: 80%; border-radius: 15px; font-size: 28px; text-align: center; word-wrap: break-word; } </style>
这都是最普通的插件写法,使用的时候,improt toast form XXX
引入index.js
,而且Vue.use
一下,就能直接在组件中用this.$toast
使用。vue
再来讲一下动效的问题。上面的Toast.vu
e代码中,在styleObject
里默认写了一个动效show-toast
,而且根据duration
计算他的动效时长。
上面的代码逻辑上没有毛病,可是在实际运行时,看不出动效的效果。
难道是动效时长太快了?我用Chrome的Performance工具录制了整个toast
出现时每一帧的渲染状况:
能够看到,toast
是直接出现的,并无一个咱们想要的过渡动效。web
那么,问题出在哪里了呢?浏览器
由于v-show
的本质是display
,参考周俊鹏大神的《解决transition动画与display冲突的几种方法》,会不会是由于,浏览器的UI线程在处理UI操做时,将多个css
属性的set
操做加入在同一个tick
中处理,因此就形成了这样一种状况:
咱们在display=block
的同时加入了一个animation
属性,这两个操做被同时执行,因此获得了一个瞬间显示出来的效果。app
要验证这样的猜测其实很简单,只须要把v-show
改为v-if
:dom
<div v-if="show" class="toast" :style="styleObject"> {{text}} </div>
惹不起咱们曲线救国总行吧,让视图重绘,从注释直接渲染成一个dom
,绕过display
的问题,这样问题是否是就解决了呢?函数
too young too simple。
动画仍是依旧没有出现。工具
咱们经过打断点的方式,一步一步看插件渲染流程。咱们发现插件的render
函数是这样实现的:优化
在class中的样式,好比宽高等都能正常渲染,可是style
中的动效就是不行,那么会不会是由于render
的时候,一个是staicClass
,一个是绑定的_vm.styleObject
,一个是静态,一个是动态。难道是由于静态的才能生效?
为了验证猜测,咱们就直接暴力的把style
改为静态的
<div v-if="show" class="toast" style="animation: show-toast 10s linear forwards"> {{text}} </div>
这时候插件渲染流程就变成了这样:
而dom上也渲染出了style
里的animation
属性。这样问题是否是就解决了呢?
sometimes naive。
动画仍是依旧没有出现。
折腾了半天,连个动画都没有搞出来,连个正常的对照都没有。因此咱们用最原始最暴力的方法,直接在class
里面加上这个show-toas
t动画,而后去掉styleObject
,看看他能不能正常渲染:
.toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 999; background-color: #000; opacity: .7; color: #fff; box-sizing: border-box; min-height: 80px; padding: 20px 30px; line-height: 50px; min-width: 364px; max-width: 80%; border-radius: 15px; font-size: 28px; text-align: center; word-wrap: break-word; animation: show-toast 2s linear forwards; }
此次动画终于出现了!这时候咱们在看看toast
出现时每一帧的渲染状况:
能够明显看出,有一个透明度的渐变效果。
那么为何猜测2里暴力style
不生效,这里的暴力class
就行呢?
咱们来比较一下渲染后的样式:
暴力style:
暴力class:
仔细对比二者,终于发现了问题的症结:show-toast
show-toast-data-v-19ed0bfa
这两个动画的名称为何不同呢?那是由于scoped
的缘由。
在vue
文件中的style
标签上,有一个特殊的属性:scoped
。当一个style
标签拥有scoped
属性时,它的CSS
样式就只能做用于当前的组件,也就是说,该样式只能适用于当前组件元素。经过该属性,可使得组件之间的样式不互相污染。
vue
中的scoped
属性的效果主要经过PostCSS
转译实现,在加上scoped
后,咱们的dom
在编译前是这样
<template> <div v-show="show" class="toast"> {{text}} </div> </template> <style scoped> .toast { position: fixed; } </style>
编译后是这样
<template> <div data-v-19ed0bfa class="toast" style="display: none;"> 请勾选受权信息 </div> </template> <style> .toast[data-v-19ed0bfa] { position: fixed; } </style>
PostCSS
给一个组件中的全部dom
添加了一个独一无二的动态属性,而后,给CSS
选择器额外添加一个对应的属性选择器来选择该组件中dom
,这种作法使得样式只做用于含有该属性的dom
——组件内部dom
。
因此问题的症结就在于,经过scoped
的做用,咱们写在<style>
里的动效名show-toast
被编译成了show-toast-data-v-19ed0bfa
真正致使动效不生效的缘由,是由于咱们在styleObject里写的动效名是show-toast
,而不是编译后的show-toast-data-v-19ed0bfa
,动效名对不上,因此并无执行里面的动画。
缘由是找到了,可是问题尚未解决。
若是咱们直接暴力的在class
里写动效,就像猜测3里作的那样,动效是能实现,可是咱们怎么去动态更改动效时长呢?毕竟这个toast
的插件是能够经过设置duration
来改变他的展现时长的。
首先,须要明确的是,scoped
做用的是class
中的名称,而不是属性,
咱们把最开始的那个带有动态styleObject
的dom
生成的样式拿出来看看:
其实出问题的只是animation-name
这一个属性,其余的属性由于style
的优先级要高于class
,因此都能正确覆盖.toast
里的属性。例如这里的top
。
其次,仔细分析一下styleObject
里的东西,其实只有同样是动态的,那就是动效时长这个属性,因而咱们能够绕开animation-name
,直接去修改animation-duration
computed: { styleObject() { return { animationDuration: this.duration / 1000 + 's', whiteSpace: this.whiteSpace, // toast的位置 top: this.position === 'up' ? '15%' : this.position === 'bottom' ? '85%' : '50%' }; } }
而后再像上面同样在class里写animation-name
,这样作后,样式就变成了:
这样,咱们再css里写的animation-duration
就被styleObject
里的正确覆盖了,这样就能实现动态修改动效时间的需求。