(By: Kath & kimmy)
最近在做的一個幾月vue的移動端小demo,其中有一塊是實現(xiàn)各個頁面的統(tǒng)一換膚功能的。想著寫一篇文章,來寫一寫實現(xiàn)過程中遇到的一些問題。
項目在線demo
項目github地址
demo里有這么一個較隱蔽的修改頭像操作

正常的上傳頭像都帶選取裁切功能,這里先實現(xiàn)一張完整圖片的縮放和居中顯示,下個迭代開發(fā)再加入自定義選取框吧,
主要介紹的是下面兩點實現(xiàn)
1. 用transition 實現(xiàn)無縫過渡
2. 用directive (vue 指令)實現(xiàn)圖片的按寬高比縮放和居中顯示
一 用transition 實現(xiàn)無縫過渡
Kath 說為什么我做起來好像很好看樣子,果然年輕人都是喜歡特效的。
transition 在項目里面一般多少有人用到,主要用于實現(xiàn)一些動態(tài)交互效果,它的出現(xiàn)解決了部分vue 在動畫方面的薄弱——我們?nèi)耘f可以通過數(shù)據(jù)驅(qū)動的形式,用v-show 和 v-if 去控制我們想要的效果,避免過多的dom操作。
我用transition實現(xiàn)的是一個入場離場的效果,home頁和修改頭像的info頁其實是兩個不同頁面,通過路由跳轉(zhuǎn),為了制造無縫的效果,我在兩個頁面都保留了頭像圖片這一個相同元素,制造了兩個頁面相關(guān)的假象,
所以實際的實現(xiàn)其實是

- 點擊home 頁頭像, 路由跳轉(zhuǎn)到info頁, 觸發(fā)info頁入場transition, 使圖片從起始位置,即home 頁頭像所在位置,過渡到當前頁面的實際位置。 觸發(fā)info頁入場的操作,通過定義一個appear Boolean變量控制,用于v-show。而文字上升的效果,同樣是在進場時候觸發(fā)transition, 而進場動畫的交互效果, 參考了ant design的設計風格,看了人家那些列表元素進場效果是怎樣的···
<!-- 頭像區(qū)域 -->
<transition name="slide">
<div class="head-field" v-show="appear">
<span class="head-field-pic">
<span class="img-hover" @click.stop="uploadHeadImg">

</span>
</span>
</div>
</transition>
···
data () {
return {
appear: false // 控制進場
}
},
mounted () {
this.$nextTick(() => {
this.appear = true
})
},
···
<style lang="scss" rel="stylesheet/scss">
.slide-enter-active,
.slide-leave-active {
transform: translateY(0);
transition: transform 1s;
}
.slide-enter,
.slide-leave-to/* .fade-leave-active in below version 2.1.8 */
{
transform: translateY(-50px);
}
</style>
關(guān)于文字效果的實現(xiàn),這里又可以普及小scss的小眾用法,我的實現(xiàn)看起來是這樣的
<div class="info-field">
<transition name="slide-1">
<p v-show="appear">K.K</p>
</transition>
<transition name="slide-2">
<p v-show="appear">wanna to be a Brilliant gentle</p>
</transition>
<transition name="slide-3">
<p v-show="appear">And a pretty girl</p>
</transition>
</div>
···
<style lang="scss" rel="stylesheet/scss">
@for $i from 1 to 4 {
.slide-#{$i}-enter-active {
transform: translateY(0);
opacity: 1;
transition: transform 1s, opacity 1s;
transition-delay: ($i - 1s) / 5;
}
.slide-#{$i}-leave-active {
transform: translateY(0);
opacity: 1;
transition: transform .5s, opacity .5s;
}
.slide-#{$i}-enter,
.slide-#{$i}-leave-to {
opacity: 0;
transform: translateY(50px);
}
}
</style>
太多個transition以及還沒循環(huán)的頁面模板還要優(yōu)化,這個還在考慮一個好的實現(xiàn),想說的是transition-delay: ($i - 1s) / 5; 這句看起來就很優(yōu)雅有沒有, 主要功能是給他們進場時候打了個時間差,通過變量加上一些修正就可以制造契合優(yōu)雅的數(shù)列,在css里面寫表達式還是有種成就感的···
2. 離場
離場的效果和入場如出一轍,樣式交互以及在上面定義好了,主要我們要考慮的是轉(zhuǎn)場需要一點時間去完成這系列出場動畫,(否則下一個進來的頁面就會立刻出現(xiàn),動畫會中止或覆蓋)
beforeRouteLeave (to, from, next) {
this.appear = false
setTimeout(() => {
next()
}, 800)
},
二 用directive 指令實現(xiàn)圖片縮放居中顯示
和jquery有很多插件一樣,vue 也有很多逐漸完善的插件, 而directive 可以說是vue插件開發(fā)里面的很重要的一個部分。
和我們寫組件不一樣,我們的組件大多針對一個功能或或一個業(yè)務塊,實現(xiàn)完整的功能。然后插件我理解為比較嵌入式的,針對多是全局的通用的,輔助性質(zhì)功能。比如在一張圖片綁定一個v-preview 指令,實現(xiàn)圖片預覽, 在一個div綁定指令,實現(xiàn)popover功能等。
觀察element.ui 源碼發(fā)現(xiàn)也有很多值得借鑒的東西,比如我在項目的指令里面加了clickoutside的功能,在對應的元素綁定 v-myclickoutside, 用戶在點擊除該元素外的頁面其他地方都會觸發(fā)綁定事件。常用的場景就是我們自己寫下拉框,彈出框時候,點擊頁面外部會自動收起下拉框,(換做以前我們得監(jiān)聽body點擊事件,可能還要解綁,一個元素寫一次綁定那種),具體實現(xiàn)可以參照項目代碼

這里我說下對圖片綁定v-autofix, 實現(xiàn)圖片自動壓縮居中顯示的功能, 來簡述指令插件的開發(fā)過程
/**
// v-autofix指令
export default {
install (Vue) {
let handleImg = (el, binding, vnode) => {
if (!el || !el.parentNode) {
return
}
// console.log('carry', el, binding, el.parentNode)
let img = new Image()
let boxWidth = el.parentNode.offsetWidth
img.onload = () => {
// 以長度小的邊為基準, 按比例縮放,然后偏移最長邊和當前邊框長度差的一半
if (img.width < img.height) {
el.style.height = Math.floor(img.height / img.width * boxWidth) + 'px'
el.style.width = boxWidth + 'px'
el.style.marginTop = -(el.offsetHeight - boxWidth) / 2 + 'px'
} else {
el.style.width = Math.floor(img.width / img.height * boxWidth) + 'px'
el.style.height = boxWidth + 'px'
el.style.marginLeft = -(el.offsetWidth - boxWidth) / 2 + 'px'
}
}
img.src = el.src
}
Vue.directive('autofix', {
inserted (el, binding, vnode) {
handleImg(el, binding, vnode)
},
update (el, binding, vnode) {
handleImg(el, binding, vnode)
},
unbind (el) {
}
})
}
}
1. directive
首先第一步,關(guān)于vue directive, 我們可以用directive這么注冊一個指令, 參照vue directive
// 注冊一個全局自定義指令 v-focus
Vue.directive('focus', {
// 當綁定元素插入到 DOM 中。
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
其中, 我們可以綁定的鉤子函數(shù)有幾個,他們的參數(shù)都為 el,binding,vnode,oldVnode等,先看官方描述,再看我的理解
bind:指令第一次綁定到元素時調(diào)用,可以定義一個在綁定時執(zhí)行一次的初始化動作,和inserted區(qū)別是,這個過程發(fā)生在這個節(jié)點生成,但還沒有插入dom時候,所以你會發(fā)現(xiàn),你企圖在這個鉤子里面獲取到el.parentNode時候是失敗的。
inserted:被綁定元素插入父節(jié)點時調(diào)用,如果說我們希望我們的動作只執(zhí)行一次,但又需要和其他節(jié)點關(guān)聯(lián)(如獲取父元素寬高,修改他們屬性值等),那么我們就應該在inserted執(zhí)行我們的操作。
update:任何節(jié)點變化,屬性值變化等都會執(zhí)行該鉤子,所以可以作為一個監(jiān)聽事件,而且他有其他鉤子不具備的oldValue等參數(shù)值,方便我們判斷是否該變化需要執(zhí)行我們的操作。
unbind:只調(diào)用一次,指令與元素解綁時調(diào)用。
2. 了解我們的需求
我們需要的是這么個東西,在圖片上綁定一個v-autofix指令,當這張圖片src變化后(我們獲取到上傳的圖片后,修改圖片src), 能自動根據(jù)獲取的圖片的寬高,根據(jù)他們比例去壓縮成我們div的大小,


所以我們可以確定我們要觸發(fā)的時機,一個是頁面加載時候,一個是src變化時候,所以我們可以確定用
bind/inserted 以及 update作為鉤子函數(shù)
- 理解各參數(shù)意義,實現(xiàn)邏輯
bind/inserted 以及 update函數(shù)都提供了我們 el(綁定元素), binding對象等值,我們思考我們獲取圖片寬高的方法,實際上是等待image加載完畢,獲取img 寬高的過程,因此,我們可以通過以下實現(xiàn),獲取元素src,
通過new image加載圖片,獲取對應寬高
let img = new Image()
img.onload = () => {
// get img.width
// get img.height
}
img.src = el.src
緊接著,我們可以計算長寬比,以最小的寬或高為準縮放圖片
let img = new Image()
img.onload = () => {
// 以長度小的邊為基準, 按比例縮放,然后偏移最長邊和當前邊框長度差的一半
if (img.width < img.height) {
el.style.height = Math.floor(img.height / img.width * boxWidth) + 'px'
el.style.width = boxWidth + 'px'
} else {
el.style.width = Math.floor(img.width / img.height * boxWidth) + 'px'
el.style.height = boxWidth + 'px'
}
}
img.src = el.src
最后一步居中顯示,這里我通過在圖片上層定義父元素,通過img的偏移長寬差一半來實現(xiàn)居中效果
let handleImg = (el, binding, vnode) => {
if (!el || !el.parentNode) {
return
}
// console.log('carry', el, binding, el.parentNode)
let img = new Image()
let boxWidth = el.parentNode.offsetWidth
img.onload = () => {
// 以長度小的邊為基準, 按比例縮放,然后偏移最長邊和當前邊框長度差的一半
if (img.width < img.height) {
el.style.height = Math.floor(img.height / img.width * boxWidth) + 'px'
el.style.width = boxWidth + 'px'
el.style.marginTop = -(el.offsetHeight - boxWidth) / 2 + 'px'
} else {
el.style.width = Math.floor(img.width / img.height * boxWidth) + 'px'
el.style.height = boxWidth + 'px'
el.style.marginLeft = -(el.offsetWidth - boxWidth) / 2 + 'px'
}
}
img.src = el.src
}
值得注意的是,這里我需要獲取父元素寬度,所以前面說的,在bind過程獲取不到父元素,只能用inserted啦
這是個相對簡單的指令應用,目前也只用了el 的操作,還有更完善的實現(xiàn),就需要一起探討學習啦
最后一提,修改頭像的功能到這里差不多就沒什么好講了,只要做好顯示,剩下的工作,我只是把更新的圖片轉(zhuǎn)成base64存在localstorage里而已,多多指教。