最近準備對團隊裏公共的插件作一些小動效,優化用戶體驗。此次的先從最簡單的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
裏的正確覆蓋了,這樣就能實現動態修改動效時間的需求。