抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

Mr.wang

Time flies and people come and go

好用的 vue 自定义指令,封装对于 Dom 操作的指令(相当于 v-model 语法糖)

vue 自定义指令

自定义指令的钩子函数

(1)bind:指令第一次绑定要元素时调用,这个自定义指令只能用一次(日抛型)

(2)unbind:指令与元素解绑时,只使用一次

(3)inserted:被绑定的元素,插入到父节点的dom上的时候调用

(4)update:组件更新时调用

(5)componentUpdated:组件和子组件更新时调用。 注意点:

(1)el指的是绑定这个指令的DOM元素,el的参数是js对象

(2)除了el,其他的都是可读的

(3)除了update和componentUpdated之外,其他的钩子函数都有el,binding,vnode这三个参数

(4)binding里有很多属性:name,value,oldValue,expression、arg、modifiers

(5)oldVnode只在update和componentUpdated这两个钩子函数中生效(Vnode是虚拟节点的意思)

如何使用?

全局注册

1
2
3
4
5
6
7
8
// 注册一个全局自定义指令 `v-focus`
Vue.directive("focus", {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus();
},
});

局部组件注册

1
2
3
4
5
6
7
8
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}

在标签中使用

1
<input v-focus>

vue-cli 批量注册全局指令

新建directives/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const modulesFiles = require.context("./modules", true, /\.js$/); //匹配此文件夹内的js文件
const directives = modulesFiles.keys().reduce((modules, modulePath) => {
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, "$1");
const value = modulesFiles(modulePath);
modules[moduleName] = value.default;
return modules;
}, {});

export default {
install(Vue) {
Object.keys(directives).forEach((key) => {
Vue.directive(key, directives[key]);
});
},
};

main.js 引入并调用

1
2
3
import Vue from "vue";
import Directives from "@/directives";
Vue.use(Directives);

limit 限制数字和浮点数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const trigger = (el, type) => {
const e = document.createEvent("HTMLEvents");
e.initEvent(type, true, true);
el.dispatchEvent(e);
};
export default {
bind: function (el, binding, vNode) {
let {
arg = "",
value: decimal = "2",
modifiers: { int, float, minus },
} = binding;
const handler = (e) => {
let result = el.value;
let symbol = "";
if (minus && result.charAt(0) == "-") {
result = el.value.slice(1);
symbol = "-";
} else {
symbol = "";
}
if (float) {
result = result.match(
new RegExp("^\\d*(\\.?\\d{0," + decimal + "})", "g")
)[0];
if (
(result.length > 1 &&
result.charAt(0) == 0 &&
result.charAt(1) != ".") ||
result.charAt(0) == "."
) {
result = result.slice(1);
}
trigger(el, "input");
} else if (int | true) {
result = result.replace(/[^\d]/g, "");
result = result != "0" ? result.replace(/^0*/g, "") : 0;
trigger(el, "input");
}
el.value = symbol + result;
};
el.addEventListener("keyup", handler);
el.addEventListener("blur", handler);
},
unbind(el) {
el.removeEventListener("keyup", el.handler);
el.removeEventListener("blur", el.handler);
},
};

使用 v-limit 后可带多个 modifiers

1
2
3
4
5
6
7
8
9
<input type="text" v-limit />//默认大于0的正整数
<input type="text" v-limit.int />//默认大于0的正整数
<input type="text" v-limit.minus />//整数(带负号-)
<input type="text" v-limit.minus.int />//整数(带负号-)

<input type="text" v-limit.float />//0以上浮点数,默认小数点为2位
<input type="text" v-limit.float="3" />//0以上浮点数,小数点为3位
<input type="text" v-limit.minus.float />//浮点数(可带负号-),默认小数点为2位
<input type="text" v-limit.minus.float="4" />//浮点数(可带负号-),小数点为4位

color

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
export default {
inserted: function (el, binding) {
const interval = binding.arg ? binding.arg : 1000;
if (Array.isArray(binding.value)) {
let i = 0;
el.style.color = binding.value[i];
el.dataset.time = setInterval(() => {
if (i > binding.value.length - 1) {
i = 0;
}
el.style.color = binding.value[i];
i++;
}, interval);
} else {
el.style.color = binding.value;
}
},
componentUpdated: function (el, binding) {
clearInterval(el.dataset.time);
const interval = binding.arg ? binding.arg : 1000;
if (Array.isArray(binding.value)) {
let i = 0;
el.style.color = binding.value[i];
el.dataset.time = setInterval(() => {
if (i > binding.value.length - 1) {
i = 0;
}
el.style.color = binding.value[i];
i++;
}, interval);
} else {
el.style.color = binding.value;
}
},
unbind: function (el, binding) {
clearInterval(el.dataset.time);
},
};

Sticky 吸顶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
const vueSticky = {};
let listenAction;
export default {
inserted(el, binding) {
const params = binding.value || {};
const stickyTop = params.stickyTop || 0;
const zIndex = params.zIndex || 1000;
const elStyle = el.style;

elStyle.position = "-webkit-sticky";
elStyle.position = "sticky";
// if the browser support css sticky(Currently Safari, Firefox and Chrome Canary)
// if (~elStyle.position.indexOf('sticky')) {
// elStyle.top = `${stickyTop}px`;
// elStyle.zIndex = zIndex;
// return
// }
const elHeight = el.getBoundingClientRect().height;
const elWidth = el.getBoundingClientRect().width;
elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}`;

const parentElm = el.parentNode || document.documentElement;
const placeholder = document.createElement("div");
placeholder.style.display = "none";
placeholder.style.width = `${elWidth}px`;
placeholder.style.height = `${elHeight}px`;
parentElm.insertBefore(placeholder, el);

let active = false;

const getScroll = (target, top) => {
const prop = top ? "pageYOffset" : "pageXOffset";
const method = top ? "scrollTop" : "scrollLeft";
let ret = target[prop];
if (typeof ret !== "number") {
ret = window.document.documentElement[method];
}
return ret;
};

const sticky = () => {
if (active) {
return;
}
if (!elStyle.height) {
elStyle.height = `${el.offsetHeight}px`;
}

elStyle.position = "fixed";
elStyle.width = `${elWidth}px`;
placeholder.style.display = "inline-block";
active = true;
};

const reset = () => {
if (!active) {
return;
}

elStyle.position = "";
placeholder.style.display = "none";
active = false;
};

const check = () => {
const scrollTop = getScroll(window, true);
const offsetTop = el.getBoundingClientRect().top;
if (offsetTop < stickyTop) {
sticky();
} else {
if (scrollTop < elHeight + stickyTop) {
reset();
}
}
};
listenAction = () => {
check();
};

window.addEventListener("scroll", listenAction);
},

unbind() {
window.removeEventListener("scroll", listenAction);
},
};

使用

1
<div v-Sticky>我在顶部</div>

copy 复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
export default {
bind(el, { value }) {
el.$value = value;
el.handler = () => {
if (!el.$value) {
// 值为空的时候,给出提示。可根据项目UI仔细设计
console.log("无复制内容");
return;
}
// 动态创建 textarea 标签
const textarea = document.createElement("textarea");
// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
textarea.readOnly = "readonly";
textarea.style.position = "absolute";
textarea.style.left = "-9999px";
// 将要 copy 的值赋给 textarea 标签的 value 属性
textarea.value = el.$value;
// 将 textarea 插入到 body 中
document.body.appendChild(textarea);
// 选中值并复制
textarea.select();
const result = document.execCommand("Copy");
if (result) {
console.log("复制成功"); // 可根据项目UI仔细设计
}
document.body.removeChild(textarea);
};
// 绑定点击事件,就是所谓的一键 copy 啦
el.addEventListener("click", el.handler);
},
// 当传进来的值更新的时候触发
componentUpdated(el, { value }) {
el.$value = value;
},
// 指令与元素解绑的时候,移除事件绑定
unbind(el) {
el.removeEventListener("click", el.handler);
},
};

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<button v-copy="copyText">复制</button>
</template>

<script>
export default {
data() {
return {
copyText: "a copy directives",
};
},
};
</script>

longpress 长按事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
export default {
bind: function (el, binding, vNode) {
let { arg: delay = 2000 } = binding;
if (typeof binding.value !== "function") {
throw "callback must be a function";
}
// 定义变量
let pressTimer = null;
// 创建计时器( 2秒后执行函数 )
let start = (e) => {
if (e.type === "click" && e.button !== 0) {
return;
}
if (pressTimer === null) {
pressTimer = setTimeout(() => {
handler();
}, delay);
}
};
// 取消计时器
let cancel = (e) => {
if (pressTimer !== null) {
clearTimeout(pressTimer);
pressTimer = null;
}
};
// 运行函数
const handler = (e) => {
binding.value(e);
};
// 添加事件监听器
el.addEventListener("mousedown", start);
el.addEventListener("touchstart", start);
// 取消计时器
el.addEventListener("click", cancel);
el.addEventListener("mouseout", cancel);
el.addEventListener("touchend", cancel);
el.addEventListener("touchcancel", cancel);
},
// 当传进来的值更新的时候触发
componentUpdated(el, { value }) {
el.$value = value;
},
// 指令与元素解绑的时候,移除事件绑定
unbind(el) {
el.removeEventListener("click", el.handler);
},
};

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<button v-longpress="longpress">长按</button>//默认两秒
<button v-longpress:4000="longpress">长按</button>//可设置4000毫秒
</template>

<script>
export default {
methods: {
longpress() {
alert("长按指令生效");
},
},
};
</script>

debounce 按钮防抖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default {
bind: function (el, binding) {
let { arg: delay = 1000 } = binding;
let timer;
const handler = () => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
binding.value();
}, delay);
};
el.addEventListener("click", handler);
},
unbind(el) {
el.removeEventListener("click", el.handler);
},
};

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<button v-debounce="debounceClick">防抖</button>//默认1000毫秒内只执行一次
<button v-debounce:400="debounce">防抖</button>//400毫秒内只执行一次
</template>

<script>
export default {
methods: {
debounceClick() {
console.log("只触发一次");
},
},
};
</script>

LazyLoad 图片懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//图片默认src
let defaultSrc = "";
// 加载真实图片
const load = (el) => {
let windowHeight = document.documentElement.clientHeight;
let elTop = el.getBoundingClientRect().top;
let elBtm = el.getBoundingClientRect().bottom;
let realSrc = el.dataset.src;
if (elTop - windowHeight < 0 && elBtm > 0) {
if (realSrc) {
el.src = realSrc;
el.removeAttribute("data-src");
}
}
};
// 节流
const throttle = (fn, delay) => {
let timer;
let prevTime;
return function (...args) {
let currTime = Date.now();
let context = this;
if (!prevTime) prevTime = currTime;
clearTimeout(timer);

if (currTime - prevTime > delay) {
prevTime = currTime;
fn.apply(context, args);
clearTimeout(timer);
return;
}

timer = setTimeout(function () {
prevTime = Date.now();
timer = null;
fn.apply(context, args);
}, delay);
};
};
export default {
bind(el, binding) {
// data-src 储存真实src
el.setAttribute("data-src", binding.value);
// 设置src为loading图
el.setAttribute("src", defaultSrc);
},
inserted(el) {
// 兼容处理
if (IntersectionObserver) {
// 利用IntersectionObserver监听el
var io = new IntersectionObserver((entries) => {
let realSrc = el.dataset.src;
if (entries[0].isIntersecting) {
if (realSrc) {
el.src = realSrc;
el.removeAttribute("data-src");
}
}
});
io.observe(el);
} else {
// 监听scroll事件
let handler = throttle(load, 1000);
load(el);
window.addEventListener("scroll", () => {
handler(el);
});
}
},
};

使用

1
<img v-lazyLoad="'https://wang-z.gitee.io/img/author.jpg'" />

draggable 拖拽指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export default {
inserted: function (el) {
el.style.cursor = "move";
el.onmousedown = function (e) {
let disx = e.pageX - el.offsetLeft;
let disy = e.pageY - el.offsetTop;
document.onmousemove = function (e) {
let x = e.pageX - disx;
let y = e.pageY - disy;
let maxX =
document.body.clientWidth -
parseInt(window.getComputedStyle(el).width);
let maxY =
document.body.clientHeight -
parseInt(window.getComputedStyle(el).height);
if (x < 0) {
x = 0;
} else if (x > maxX) {
x = maxX;
}

if (y < 0) {
y = 0;
} else if (y > maxY) {
y = maxY;
}

el.style.left = x + "px";
el.style.top = y + "px";
};
document.onmouseup = function () {
document.onmousemove = document.onmouseup = null;
};
};
},
};

使用

需要设置需要拖拽的元素为相对定位,其父元素为绝对定位

1
2
3
<template>
<div class="el-dialog" v-draggable></div>
</template>

waterMarker添加背景水印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function addWaterMarker(str, parentNode, font, textColor) {
// 水印文字,父元素,字体,文字颜色
var can = document.createElement('canvas')
parentNode.appendChild(can)
can.width = 200
can.height = 150
can.style.display = 'none'
var cans = can.getContext('2d')
cans.rotate((-20 * Math.PI) / 180)
cans.font = font || '16px Microsoft JhengHei'
cans.fillStyle = textColor || 'rgba(180, 180, 180, 0.3)'
cans.textAlign = 'left'
cans.textBaseline = 'Middle'
cans.fillText(str, can.width / 10, can.height / 2)
parentNode.style.backgroundImage = 'url(' + can.toDataURL('image/png') + ')'
}

const waterMarker = {
bind: function (el, binding) {
addWaterMarker(binding.value.text, el, binding.value.font, binding.value.textColor)
},
}

export default waterMarker

使用
设置水印文案,颜色,字体大小即可

1
2
3
4
<template>
<div v-waterMarker="{text:'版权所有',textColor:'rgba(180, 180, 180, 0.4)'}"></div>
</template>

评论