在日常的开发中,经常会遇到通过按钮点击来触发表单提交的情况。如果不加以控制,用户可能由于网络延迟或误操作而多次点击按钮,从而导致同一操作被执行多次,或发起多个相同的请求。为了避免这种情况,我们可以抽离一个通用如防抖、节流等的方法,或者实现一个自定义指令来处理重复点击的问题。
这篇文章将实现一个简单的全局自定义指令,在点击按钮时,使其进入loading状态,从而避免多次点击触发提交的情况
介绍
可点击官网查看详细介绍。
一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。
一个指令的定义对象可以提供几种钩子函数 (都是可选的):
const myDirective = {
// 在绑定元素的 attribute 前或事件监听器应用前调用
created(el, binding, vnode) {},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode) {},
// 在绑定元素的父组件及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode) {}
}
指令的钩子会传递以下几种参数:
-
el
:指令绑定到的元素。这可以用于直接操作 DOM。 -
binding
:一个对象,包含以下属性。value
:传递给指令的值。例如在v-my-directive="1 + 1"
中,值是2
。oldValue
:之前的值,仅在beforeUpdate
和updated
中可用。无论值是否更改,它都可用。arg
:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo
中,参数是"foo"
。modifiers
:一个包含修饰符的对象 (如果有的话)。例如在v-my-directive.foo.bar
中,修饰符对象是{ foo: true, bar: true }
。instance
:使用该指令的组件实例。dir
:指令的定义对象。
-
vnode
:代表绑定元素的底层 VNode。 -
prevVnode
:代表之前的渲染中指令所绑定元素的 VNode。仅在beforeUpdate
和updated
钩子中可用。
举例来说,像下面这样使用指令:
<div v-color:bgColor="'red'">自定义指令</div>
binding
参数会是一个这样的对象:
// 简化
{
"value": "red",
"arg": "bgColor",
"oldValue": undefined
}
用法
在 <script setup>
中,任何以 v
开头的驼峰式命名的变量都可以被用作一个自定义指令。
<script setup>
const vColor = {
mounted: () => {
el.style.backgroundColor = "#42b883";
},
};
</script>
<template>
<div v-color>自定义指令</div>
</template>
在没有使用 <script setup>
的情况下,自定义指令需要通过 directives
选项注册:
export default {
setup() {
},
directives: {
// 在模板中启用 v-focus
focus: {
}
}
}
在 main.js
中,通过 app.directive
可注册全局指令
app.directive('color', (el, binding) => {
el.style.color = binding.value
})
传值
固定值
通过 v-color="'red'"
的形式,传递一个固定值给指令
arg传值
通过 v-color:color="'red'"
的形式,给 binding.arg
赋值
<script setup>
const vColor = {
mounted: (el, binding) => {
el.style[binding.arg] = binding.value || "#FFFFFF";
el.style.backgroundColor = binding.value.backgroundColor || "#42b883";
},
};
</script>
<template>
<div v-color:color="'red'">arg传值</div>
</template>
js对象
如果指令需要多个值,可以向它传递一个 JavaScript 对象字面量
<script setup>
const vColor = {
mounted: (el, binding) => {
el.style.backgroundColor = binding.value.backgroundColor || "#42b883";
el.style.color = binding.value.color || "#FFFFFF";
},
};
</script>
<template>
<div
v-color="{ backgroundColor: 'red', color: 'white' }"
>
自定义指令
</div>
</template>
响应值
响应值需要在 updated
钩子中处理
<script setup>
// 设置一个响应值
const refValue = shallowRef({ backgroundColor: "red", color: "white" });
// 一秒后修改响应值
setTimeout(() => {
refValue.value = { backgroundColor: "blue", color: "yellow" };
}, 1000);
// 统一处理函数
function setColor(el, binding) {
el.style.backgroundColor = binding.value.backgroundColor || "#42b883";
el.style.color = binding.value.color || "#FFFFFF";
}
const vColor = {
mounted: setColor,
updated: setColor,
};
</script>
<template>
<div v-color="refValue">响应值</div>
</template>
<style scoped lang="scss"></style>
简单实现v-loading
在大致了解自定义指令的用法后,现在来实现一个简单的 v-loading
初始化
可以默认一个图标,在绑定的元素挂载时,判断是否加载 loading
,因为可能一来 v-loading='true'
<script setup>
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" data-v-d2e47025=""><path fill="currentColor" d="M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32m0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32m448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32m-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32M195.2 195.2a32 32 0 0 1 45.248 0L376.32 331.008a32 32 0 0 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248zm452.544 452.544a32 32 0 0 1 45.248 0L828.8 783.552a32 32 0 0 1-45.248 45.248L647.744 692.992a32 32 0 0 1 0-45.248zM828.8 195.264a32 32 0 0 1 0 45.184L692.992 376.32a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0m-452.544 452.48a32 32 0 0 1 0 45.248L240.448 828.8a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0z"></path></svg>`;
function createLoading(el, bingding) {
const loadingDiv = document.createElement("div");
loadingDiv.classList.add("loading-warp");
const parser = new DOMParser();
const svgDom = parser.parseFromString(svg, "image/svg+xml");
loadingDiv.appendChild(svgDom.documentElement);
el.appendChild(loadingDiv);
}
const vLoading = {
mounted(el, bingding) {
if (bingding.value) {
createLoading(el, bingding);
}
}
};
const isLoading = ref(true);
</script>
<template>
<button v-loading="isLoading">
v-loading指令
</button>
</template>
<style lang="scss">
button {
position: relative;
background: none;
border: none;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
padding: 8px;
margin-left: 8px;
background-color: aliceblue;
}
.loading-warp {
position: absolute;
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
top: 0;
left: 0;
height: 100%;
width: 100%;
color: #0cfb8f;
background-color: rgba(122, 122, 122, 0.8);
svg {
height: 1em;
width: 1em;
animation: loading 2s linear infinite;
}
}
@keyframes loading {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
自定义加载文字
我们可以通过给 v-loading
绑定js对象,但是如果需要自定义的东西太多,可能会导致模板变得过于复杂和难以维护,所以我们可以采用一种间接的方式,在各个钩子函数中,我们都是可以获取到绑定的 dom
元素的,我们可以为 dom
元素添加一些自定义属性来存储额外的信息,从而通过 el.getAttribute("自定义值")
来获取我们想要的数据。
<script setup>
const svg = ...;
function createLoading(el, bingding) {
...
const loadingText = document.createElement("span");
loadingText.innerHTML = el.getAttribute("loading-text") || "";
loadingDiv.appendChild(loadingText);
el.appendChild(loadingDiv);
}
const vLoading = {
mounted(el, bingding) {
if (bingding.value) {
createLoading(el, bingding);
}
}
};
const isLoading = ref(true);
</script>
<template>
<button v-loading="isLoading" loading-text="加载中">
v-loading指令
</button>
</template>
<style lang="scss">
...
</style>
自定义加载图标
方式就和文字一样,只需要给定一个自己想要替换的svg就可以了
<script setup>
const defaultSvg = ...;
function createLoading(el, bingding) {
...
const loadingDiv = document.createElement("div");
loadingDiv.classList.add("loading-warp");
const loadingSvg = el.getAttribute("loading-svg") || defaultSvg;
const parser = new DOMParser();
const svgDom = parser.parseFromString(loadingSvg, "image/svg+xml");
loadingDiv.appendChild(svgDom.documentElement);
...
el.appendChild(loadingDiv);
}
const vLoading = {
mounted(el, bingding) {
if (bingding.value) {
createLoading(el, bingding);
}
}
};
const isLoading = ref(true);
const loadingSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3a9 9 0 1 0 9 9"/></svg>`;
</script>
<template>
<button v-loading="isLoading" loading-text="自定义svg" :loading-svg="loadingSvg">
v-loading指令
</button>
</template>
<style lang="scss">
...
</style>
动态变化
现在一直是在默认加载 loading
状态,实际开发中应该是触发一个异步,或者耗时的操作以后,把按钮变成 loading
状态,且无法多次点击,涉及到数据变化的操作,就需要用到 updated
钩子函数来处理。
<script setup>
const defaultSvg = ...;
function createLoading(el, bingding) {
...
el.disabled = true;
el.appendChild(loadingDiv);
}
const vLoading = {
mounted(el, bingding) {
if (bingding.value) {
createLoading(el, bingding);
}
},
updated(el, bingding) {
if (bingding.value) {
createLoading(el, bingding);
} else {
el.removeChild(el.querySelector(".loading-warp"));
el.disabled = false;
}
},
};
const isLoading = ref(false);
async function doSomeSubmit() {
isLoading.value = true;
await new Promise((resolve) => setTimeout(resolve, 2000));
isLoading.value = false;
}
</script>
<template>
<button v-loading="isLoading" loading-text="动态变化" @click="doSomeSubmit">
v-loading指令
</button>
</template>
<style lang="scss">
...
</style>
全局指令
想把这个局部指令变成全局的,只需要把对应的方法抽离成一个 js
文件,导出给 app.directive
即可。
新建一个 loading.js
const defaultSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" data-v-d2e47025=""><path fill="currentColor" d="M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32m0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32m448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32m-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32M195.2 195.2a32 32 0 0 1 45.248 0L376.32 331.008a32 32 0 0 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248zm452.544 452.544a32 32 0 0 1 45.248 0L828.8 783.552a32 32 0 0 1-45.248 45.248L647.744 692.992a32 32 0 0 1 0-45.248zM828.8 195.264a32 32 0 0 1 0 45.184L692.992 376.32a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0m-452.544 452.48a32 32 0 0 1 0 45.248L240.448 828.8a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0z"></path></svg>`;
function createLoading(el, bingding) {
const loadingDiv = document.createElement("div");
loadingDiv.classList.add("loading-warp");
const loadingSvg = el.getAttribute("loading-svg") || defaultSvg;
const parser = new DOMParser();
const svgDom = parser.parseFromString(loadingSvg, "image/svg+xml");
loadingDiv.appendChild(svgDom.documentElement);
const loadingText = document.createElement("span");
loadingText.innerHTML = el.getAttribute("loading-text") || "";
loadingDiv.appendChild(loadingText);
el.disabled = true;
el.appendChild(loadingDiv);
}
const vLoading = {
mounted(el, bingding) {
if (bingding.value) {
createLoading(el, bingding);
}
},
updated(el, bingding) {
if (bingding.value) {
createLoading(el, bingding);
} else {
el.removeChild(el.querySelector(".loading-warp"));
el.disabled = false;
}
},
};
export default vLoading
在 main.js
中引入
import vLoading from "./directives/loading.js";
...
const app = createApp(App);
app.directive("loading", vLoading);
...
然后就可以直接使用了
<script setup>
const isLoading = ref(false);
async function doSomeSubmit() {
isLoading.value = true;
await new Promise((resolve) => setTimeout(resolve, 2000));
isLoading.value = false;
}
</script>
<template>
<button v-cloading="isLoading" loading-text="动态变化" @click="doSomeSubmit">
v-loading指令
</button>
</template>
总结
通过这篇文章,我们了解了 Vue 3 中自定义指令的基本概念和使用方法,并通过一个实际的例子实现了一个简单的 v-loading
指令。这个指令在开发中可以帮助我们更好地处理异步操作时的用户体验问题。希望这篇文章能对你有所帮助,在实际项目中,你可以根据需求进一步扩展和优化这个指令。