Vue自定义指令—通过拖拽画一只可爱的小熊猫

239 阅读5分钟

我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!

对于绝对定位,你还在用开发者工具来调试具体的left和right值吗?不,只需实现一个自定义指令,就能做到元素随鼠标动,想放哪里就放哪里。回想起我先前画的一个小青蛙,眼泪汪汪,当时可太痛苦了,尤其刚开始调好了的,后面可能有改动还得重新调。下面来看看我的思路吧。

Vue2自定义指令

需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。下面内容介绍来自vue2官方文档。

自定义指令的方法:

  • 全局注册:
Vue.directive('focus', {
    // 当被绑定的元素插入到 DOM 中时……
    inserted: function (el) {
        // 聚焦元素
        el.focus()
    }
})
  • 局部注册:
directives: {
    focus: {
        // 指令的定义
        inserted: function (el) {
            el.focus()
         }
    }
}
  • 在元素中使用:
    <input v-focus>

钩子函数

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。有点类似于React中的shouldComponentUpdate生命周期函数,用于性能优化。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用,一般用于移除一些事件。

钩子函数参数

指令钩子函数会被传入以下参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM。

  • binding:一个对象,包含以下 property:

    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
  • vnode:Vue 编译生成的虚拟节点。

  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

案例:

<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>

Vue.directive('demo', {
    bind: function (el, binding, vnode) {
        var s = JSON.stringify
        el.innerHTML =
        'name: ' + s(binding.name) + '<br>' +
        'value: ' + s(binding.value) + '<br>' +
        'expression: ' + s(binding.expression) + '<br>' +
        'argument: ' + s(binding.arg) + '<br>' +
        'modifiers: ' + s(binding.modifiers) + '<br>' +
        
    }
})
new Vue({
    el: '#hook-arguments-example',
    data: {
        message: {a:12,b:25}
    }
})
// 运行结果为
name: demo // 指令名称
value:{a:12,b:25} // 所绑定的值
expression:message // 就是把绑定的表达式当成一个字符串,保存起来,并不进行翻译
argument:foo // 类似于绑定值的属性名称,类比v-bind:foo
modifiers:{a:true,b:true} // 类比v-model.trim,使用它可以对元素显示的值格式化

项目的实现思路

首先我们要自定义一个为绑定元素注册mouseDown,mouseUp,mouseMove事件的自定义指令,在mouseMove事件中,不断的更改元素的left和top值,才能使元素可以拖拽并改变位置。实现代码如下:

directives: {
    drag: {
      inserted: (el, binding) => {
        const target = el;

        const { event = undefined, key = undefined } = binding.value;

        el.onmousedown = (e) => {
          const disX = e.pageX - target.offsetLeft;
          const disY = e.pageY - target.offsetTop;
          document.onmousemove = (de) => {
            target.style.left = de.pageX - disX + 'px';
            target.style.top = de.pageY - disY + 'px';
          };
          document.onmouseup = () => {
            event(target.style.left, target.style.top, key);
            document.onmousemove = document.onmouseup = null;
          };
        };
      }
    }
  }

然后,我们来思考应该怎么写好小熊猫的html结构,最后我决定把小熊猫数据化,做成数据可配置的形式,并且数据还需要保存元素的类名和数据信息。数据结构如下:

<template>
  <div> 
    <div class="panadas" style="position: relative">
      <div
        v-for="(item, key) in panada"
        :key="key"
        :class="[key, item.class]"
        :style="{ left: item.left, top: item.top, position: 'absolute' }"
        v-drag="{ event: changePanadaPosition, key }"
      ></div>     
    </div>
  </div>
</template>
data() {
    return {
      panada: {
        'left-ear': {
          left: '82px',
          top: '87px',
          class: 'ear'
        },
        'right-ear': {
          left: '155px',
          top: '87px',
          class: 'ear'
        },
        'inner-left-ear': {
          left: '90px',
          top: '94px',
          class: 'inner-ear'
        },
        'inner-right-ear': {
          left: '160px',
          top: '94px',
          class: 'inner-ear'
        },
        head: {
          left: '88px',
          right: 0,
          top: '87px'
        },
        'left-eye': {
          left: '108px',
          top: '107px',
          class: 'eye'
        },
        'right-eye': {
          left: '141px',
          top: '107px',
          class: 'eye'
        },
        'left-blusher': {
          left: '151px',
          top: '127px',
          class: 'blusher'
        },
        'right-blusher': {
          left: '101px',
          top: '125px',
          class: 'blusher'
        },
        'left-hand': {
          left: '81px',
          top: '140px',
          class: 'hand'
        },
        'right-hand': {
          left: '162px',
          top: '138px',
          class: 'hand'
        },
        nose: {
          left: '127px',
          top: '120px'
        },
        desk: {
          left: '60px',
          top: '139px'
        },
        'nose-center': {
          left: '133px',
          top: '123px'
        },
        'nose-right': {
          left: '132px',
          top: '125px'
        },
        'nose-left': {
          left: '126px',
          right: 0,
          top: '125px'
        }
      }
    };
  },

注意:这些数据的left和right信息我最开始给的初始值为0,等到小熊猫完成后,我再将数据信息复制进来。现在我的<template>里面的元素就精简多了,后续我们只需要关注css样式即可。

然后还涉及到一个问题,当鼠标移动的时候,我们需要不断的更新panada属性里面的left和top信息,但是在自定义指令里面,所有的钩子函数内部的this为undefiend,我们无法直接操作panada数据进行修改,我们需要定义一个方法来更新我们的数据,而且考虑到鼠标一直在移动,我们没有必要更新如此频繁,只需在mouseMoveUp处把元素的最终位置交给panada即可。代码如下

<div v-drag="{ event: changePanadaPosition,key }"></div>
 directives: {
    drag: {
      inserted: (el, binding) => {
        const target = el;

        const { event = undefined ,key=undefined} = binding.value;

        el.onmousedown = (e) => {
        /**
        省略部分代码
        */
 
          document.onmouseup = () => {
            event(target.style.left, target.style.top, key);
            document.onmousemove = document.onmouseup = null;
          };
        };
      }
    }
  },
changePanadaPosition(left, top, key) {
  this.panada[key] = { ...this.panada[key], left, top };
},

最后到目前我们已经能够保存位置信息了,但是每次浏览器一刷新数据就没有了,所有我们要借用localStorage来存储一下我们的拖动位置信息。在mouted生命周期函数中给panada初始化。

mounted() {
    console.log(JSON.parse(localStorage.getItem('panadas')));
    let localData = localStorage.getItem('panadas');
    if (localData) {
      this.panada = { ...this.panada, ...JSON.parse(localData) };
    }
 },