我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!
对于绝对定位,你还在用开发者工具来调试具体的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) };
}
},