(By: Kath & kimmy)
最近在做的一个几月vue的移动端小demo,其中有一块是实现各个页面的统一换肤功能的。想着写一篇文章,来写一写实现过程中遇到的一些问题。
项目在线demo
项目github地址
demo里有这么一个较隐蔽的修改头像操作
正常的上传头像都带选取裁切功能,这里先实现一张完整图片的缩放和居中显示,下个迭代开发再加入自定义选取框吧,
主要介绍的是下面两点实现
1. 用transition 实现无缝过渡
2. 用directive (vue 指令)实现图片的按宽高比缩放和居中显示
一 用transition 实现无缝过渡
Kath 说为什么我做起来好像很好看样子,果然年轻人都是喜欢特效的。
transition 在项目里面一般多少有人用到,主要用于实现一些动态交互效果,它的出现解决了部分vue 在动画方面的薄弱——我们仍旧可以通过数据驱动的形式,用v-show 和 v-if 去控制我们想要的效果,避免过多的dom操作。
我用transition实现的是一个入场离场的效果,home页和修改头像的info页其实是两个不同页面,通过路由跳转,为了制造无缝的效果,我在两个页面都保留了头像图片这一个相同元素,制造了两个页面相关的假象,
所以实际的实现其实是
- 点击home 页头像, 路由跳转到info页, 触发info页入场transition, 使图片从起始位置,即home 页头像所在位置,过渡到当前页面的实际位置。 触发info页入场的操作,通过定义一个appear Boolean变量控制,用于v-show。而文字上升的效果,同样是在进场时候触发transition, 而进场动画的交互效果, 参考了ant design的设计风格,看了人家那些列表元素进场效果是怎样的···
<!-- 头像区域 -->
<transition name="slide">
<div class="head-field" v-show="appear">
<span class="head-field-pic">
<span class="img-hover" @click.stop="uploadHeadImg">
![](userinfo.headUrl)
</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>
关于文字效果的实现,这里又可以普及小scss的小众用法,我的实现看起来是这样的
<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以及还没循环的页面模板还要优化,这个还在考虑一个好的实现,想说的是transition-delay: ($i - 1s) / 5; 这句看起来就很优雅有没有, 主要功能是给他们进场时候打了个时间差,通过变量加上一些修正就可以制造契合优雅的数列,在css里面写表达式还是有种成就感的···
2. 离场
离场的效果和入场如出一辙,样式交互以及在上面定义好了,主要我们要考虑的是转场需要一点时间去完成这系列出场动画,(否则下一个进来的页面就会立刻出现,动画会中止或覆盖)
beforeRouteLeave (to, from, next) {
this.appear = false
setTimeout(() => {
next()
}, 800)
},
二 用directive 指令实现图片缩放居中显示
和jquery有很多插件一样,vue 也有很多逐渐完善的插件, 而directive 可以说是vue插件开发里面的很重要的一个部分。
和我们写组件不一样,我们的组件大多针对一个功能或或一个业务块,实现完整的功能。然后插件我理解为比较嵌入式的,针对多是全局的通用的,辅助性质功能。比如在一张图片绑定一个v-preview 指令,实现图片预览, 在一个div绑定指令,实现popover功能等。
观察element.ui 源码发现也有很多值得借鉴的东西,比如我在项目的指令里面加了clickoutside的功能,在对应的元素绑定 v-myclickoutside, 用户在点击除该元素外的页面其他地方都会触发绑定事件。常用的场景就是我们自己写下拉框,弹出框时候,点击页面外部会自动收起下拉框,(换做以前我们得监听body点击事件,可能还要解绑,一个元素写一次绑定那种),具体实现可以参照项目代码
这里我说下对图片绑定v-autofix, 实现图片自动压缩居中显示的功能, 来简述指令插件的开发过程
/**
// 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
首先第一步,关于vue directive, 我们可以用directive这么注册一个指令, 参照vue directive
// 注册一个全局自定义指令 v-focus
Vue.directive('focus', {
// 当绑定元素插入到 DOM 中。
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
其中, 我们可以绑定的钩子函数有几个,他们的参数都为 el,binding,vnode,oldVnode等,先看官方描述,再看我的理解
bind:指令第一次绑定到元素时调用,可以定义一个在绑定时执行一次的初始化动作,和inserted区别是,这个过程发生在这个节点生成,但还没有插入dom时候,所以你会发现,你企图在这个钩子里面获取到el.parentNode时候是失败的。
inserted:被绑定元素插入父节点时调用,如果说我们希望我们的动作只执行一次,但又需要和其他节点关联(如获取父元素宽高,修改他们属性值等),那么我们就应该在inserted执行我们的操作。
update:任何节点变化,属性值变化等都会执行该钩子,所以可以作为一个监听事件,而且他有其他钩子不具备的oldValue等参数值,方便我们判断是否该变化需要执行我们的操作。
unbind:只调用一次,指令与元素解绑时调用。
2. 了解我们的需求
我们需要的是这么个东西,在图片上绑定一个v-autofix指令,当这张图片src变化后(我们获取到上传的图片后,修改图片src), 能自动根据获取的图片的宽高,根据他们比例去压缩成我们div的大小,
所以我们可以确定我们要触发的时机,一个是页面加载时候,一个是src变化时候,所以我们可以确定用
bind/inserted 以及 update作为钩子函数
- 理解各参数意义,实现逻辑
bind/inserted 以及 update函数都提供了我们 el(绑定元素), binding对象等值,我们思考我们获取图片宽高的方法,实际上是等待image加载完毕,获取img 宽高的过程,因此,我们可以通过以下实现,获取元素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的偏移长宽差一半来实现居中效果
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 的操作,还有更完善的实现,就需要一起探讨学习啦
最后一提,修改头像的功能到这里差不多就没什么好讲了,只要做好显示,剩下的工作,我只是把更新的图片转成base64存在localstorage里而已,多多指教。