vue移动助手实践(二)——用vue指令实现插件并完成一个修改头像功能

(By: Kath & kimmy)

最近在做的一个几月vue的移动端小demo,其中有一块是实现各个页面的统一换肤功能的。想着写一篇文章,来写一写实现过程中遇到的一些问题。

项目在线demo

项目在线演示demo(切换到移动端调试模式哦)

项目github地址

项目github地址

demo里有这么一个较隐蔽的修改头像操作

修改头像

正常的上传头像都带选取裁切功能,这里先实现一张完整图片的缩放和居中显示,下个迭代开发再加入自定义选取框吧,
主要介绍的是下面两点实现

1. 用transition 实现无缝过渡

2. 用directive (vue 指令)实现图片的按宽高比缩放和居中显示

一 用transition 实现无缝过渡

Kath 说为什么我做起来好像很好看样子,果然年轻人都是喜欢特效的。
transition 在项目里面一般多少有人用到,主要用于实现一些动态交互效果,它的出现解决了部分vue 在动画方面的薄弱——我们仍旧可以通过数据驱动的形式,用v-show 和 v-if 去控制我们想要的效果,避免过多的dom操作。
我用transition实现的是一个入场离场的效果,home页和修改头像的info页其实是两个不同页面,通过路由跳转,为了制造无缝的效果,我在两个页面都保留了头像图片这一个相同元素,制造了两个页面相关的假象,
所以实际的实现其实是


别人的出现效果
  1. 点击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作为钩子函数

  1. 理解各参数意义,实现逻辑
    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里而已,多多指教。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容