(By: Kath & kimmy)
最近在做的一個幾月vue的移動端小demo,其中有一塊是實(shí)現(xiàn)各個頁面的統(tǒng)一換膚功能的。想著寫一篇文章,來寫一寫實(shí)現(xiàn)過程中遇到的一些問題。
項(xiàng)目在線demo
項(xiàng)目在線演示demo(切換到移動端調(diào)試模式哦)
項(xiàng)目github地址
demo里有這么一個較隱蔽的修改頭像操作
正常的上傳頭像都帶選取裁切功能,這里先實(shí)現(xiàn)一張完整圖片的縮放和居中顯示,下個迭代開發(fā)再加入自定義選取框吧,
主要介紹的是下面兩點(diǎn)實(shí)現(xiàn)
1. 用transition 實(shí)現(xiàn)無縫過渡
2. 用directive (vue 指令)實(shí)現(xiàn)圖片的按寬高比縮放和居中顯示
一 用transition 實(shí)現(xiàn)無縫過渡
Kath 說為什么我做起來好像很好看樣子,果然年輕人都是喜歡特效的。
transition 在項(xiàng)目里面一般多少有人用到,主要用于實(shí)現(xiàn)一些動態(tài)交互效果,它的出現(xiàn)解決了部分vue 在動畫方面的薄弱——我們?nèi)耘f可以通過數(shù)據(jù)驅(qū)動的形式,用v-show 和 v-if 去控制我們想要的效果,避免過多的dom操作。
我用transition實(shí)現(xiàn)的是一個入場離場的效果,home頁和修改頭像的info頁其實(shí)是兩個不同頁面,通過路由跳轉(zhuǎn),為了制造無縫的效果,我在兩個頁面都保留了頭像圖片這一個相同元素,制造了兩個頁面相關(guān)的假象,
所以實(shí)際的實(shí)現(xiàn)其實(shí)是
- 點(diǎn)擊home 頁頭像, 路由跳轉(zhuǎn)到info頁, 觸發(fā)info頁入場transition, 使圖片從起始位置,即home 頁頭像所在位置,過渡到當(dāng)前頁面的實(shí)際位置。 觸發(fā)info頁入場的操作,通過定義一個appear Boolean變量控制,用于v-show。而文字上升的效果,同樣是在進(jìn)場時(shí)候觸發(fā)transition, 而進(jìn)場動畫的交互效果, 參考了ant design的設(shè)計(jì)風(fēng)格,看了人家那些列表元素進(jìn)場效果是怎樣的···
<!-- 頭像區(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 // 控制進(jìn)場
}
},
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)于文字效果的實(shí)現(xiàn),這里又可以普及小scss的小眾用法,我的實(shí)現(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)化,這個還在考慮一個好的實(shí)現(xiàn),想說的是transition-delay: ($i - 1s) / 5; 這句看起來就很優(yōu)雅有沒有, 主要功能是給他們進(jìn)場時(shí)候打了個時(shí)間差,通過變量加上一些修正就可以制造契合優(yōu)雅的數(shù)列,在css里面寫表達(dá)式還是有種成就感的···
2. 離場
離場的效果和入場如出一轍,樣式交互以及在上面定義好了,主要我們要考慮的是轉(zhuǎn)場需要一點(diǎn)時(shí)間去完成這系列出場動畫,(否則下一個進(jìn)來的頁面就會立刻出現(xiàn),動畫會中止或覆蓋)
beforeRouteLeave (to, from, next) {
this.appear = false
setTimeout(() => {
next()
}, 800)
},
二 用directive 指令實(shí)現(xiàn)圖片縮放居中顯示
和jquery有很多插件一樣,vue 也有很多逐漸完善的插件, 而directive 可以說是vue插件開發(fā)里面的很重要的一個部分。
和我們寫組件不一樣,我們的組件大多針對一個功能或或一個業(yè)務(wù)塊,實(shí)現(xiàn)完整的功能。然后插件我理解為比較嵌入式的,針對多是全局的通用的,輔助性質(zhì)功能。比如在一張圖片綁定一個v-preview 指令,實(shí)現(xiàn)圖片預(yù)覽, 在一個div綁定指令,實(shí)現(xiàn)popover功能等。
觀察element.ui 源碼發(fā)現(xiàn)也有很多值得借鑒的東西,比如我在項(xiàng)目的指令里面加了clickoutside的功能,在對應(yīng)的元素綁定 v-myclickoutside, 用戶在點(diǎn)擊除該元素外的頁面其他地方都會觸發(fā)綁定事件。常用的場景就是我們自己寫下拉框,彈出框時(shí)候,點(diǎn)擊頁面外部會自動收起下拉框,(換做以前我們得監(jiān)聽body點(diǎn)擊事件,可能還要解綁,一個元素寫一次綁定那種),具體實(shí)現(xiàn)可以參照項(xiàng)目代碼
這里我說下對圖片綁定v-autofix, 實(shí)現(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 = () => {
// 以長度小的邊為基準(zhǔn), 按比例縮放,然后偏移最長邊和當(dāng)前邊框長度差的一半
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', {
// 當(dāng)綁定元素插入到 DOM 中。
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
其中, 我們可以綁定的鉤子函數(shù)有幾個,他們的參數(shù)都為 el,binding,vnode,oldVnode等,先看官方描述,再看我的理解
bind:指令第一次綁定到元素時(shí)調(diào)用,可以定義一個在綁定時(shí)執(zhí)行一次的初始化動作,和inserted區(qū)別是,這個過程發(fā)生在這個節(jié)點(diǎn)生成,但還沒有插入dom時(shí)候,所以你會發(fā)現(xiàn),你企圖在這個鉤子里面獲取到el.parentNode時(shí)候是失敗的。
inserted:被綁定元素插入父節(jié)點(diǎn)時(shí)調(diào)用,如果說我們希望我們的動作只執(zhí)行一次,但又需要和其他節(jié)點(diǎn)關(guān)聯(lián)(如獲取父元素寬高,修改他們屬性值等),那么我們就應(yīng)該在inserted執(zhí)行我們的操作。
update:任何節(jié)點(diǎn)變化,屬性值變化等都會執(zhí)行該鉤子,所以可以作為一個監(jiān)聽事件,而且他有其他鉤子不具備的oldValue等參數(shù)值,方便我們判斷是否該變化需要執(zhí)行我們的操作。
unbind:只調(diào)用一次,指令與元素解綁時(shí)調(diào)用。
2. 了解我們的需求
我們需要的是這么個東西,在圖片上綁定一個v-autofix指令,當(dāng)這張圖片src變化后(我們獲取到上傳的圖片后,修改圖片src), 能自動根據(jù)獲取的圖片的寬高,根據(jù)他們比例去壓縮成我們div的大小,
所以我們可以確定我們要觸發(fā)的時(shí)機(jī),一個是頁面加載時(shí)候,一個是src變化時(shí)候,所以我們可以確定用
bind/inserted 以及 update作為鉤子函數(shù)
- 理解各參數(shù)意義,實(shí)現(xiàn)邏輯
bind/inserted 以及 update函數(shù)都提供了我們 el(綁定元素), binding對象等值,我們思考我們獲取圖片寬高的方法,實(shí)際上是等待image加載完畢,獲取img 寬高的過程,因此,我們可以通過以下實(shí)現(xiàn),獲取元素src,
通過new image加載圖片,獲取對應(yīng)寬高
let img = new Image()
img.onload = () => {
// get img.width
// get img.height
}
img.src = el.src
緊接著,我們可以計(jì)算長寬比,以最小的寬或高為準(zhǔn)縮放圖片
let img = new Image()
img.onload = () => {
// 以長度小的邊為基準(zhǔn), 按比例縮放,然后偏移最長邊和當(dāng)前邊框長度差的一半
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的偏移長寬差一半來實(shí)現(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 = () => {
// 以長度小的邊為基準(zhǔn), 按比例縮放,然后偏移最長邊和當(dāng)前邊框長度差的一半
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啦
這是個相對簡單的指令應(yīng)用,目前也只用了el 的操作,還有更完善的實(shí)現(xiàn),就需要一起探討學(xué)習(xí)啦
最后一提,修改頭像的功能到這里差不多就沒什么好講了,只要做好顯示,剩下的工作,我只是把更新的圖片轉(zhuǎn)成base64存在localstorage里而已,多多指教。