Vue3防重復(fù)點(diǎn)擊指令clickOnce使用方法
一、問題背景
在實(shí)際的 Web 應(yīng)用開發(fā)中,我們經(jīng)常會遇到以下問題:
- 用戶快速多次點(diǎn)擊提交按鈕:導(dǎo)致重復(fù)提交表單,產(chǎn)生多條相同數(shù)據(jù)
- 異步請求未完成時再次點(diǎn)擊:可能導(dǎo)致數(shù)據(jù)不一致或服務(wù)器壓力增大
- 用戶體驗(yàn)不佳:沒有明確的加載狀態(tài)反饋,用戶不知道操作是否正在進(jìn)行
這些問題在以下場景中尤為常見:
- 表單提交(注冊、登錄、創(chuàng)建訂單等)
- 數(shù)據(jù)保存操作
- 文件上傳
- 支付操作
- API 調(diào)用
二、解決方案
clickOnce 指令通過以下機(jī)制解決上述問題:
1.節(jié)流機(jī)制
使用 @vueuse/core 的 useThrottleFn,在 1.5 秒內(nèi)只允許執(zhí)行一次點(diǎn)擊操作。
2.按鈕禁用
點(diǎn)擊后立即禁用按鈕,防止用戶再次點(diǎn)擊。
3.視覺反饋
自動添加 Element Plus 的 Loading 圖標(biāo),讓用戶明確知道操作正在進(jìn)行中。
4.智能恢復(fù)
- 如果綁定的函數(shù)返回 Promise(異步操作),則在 Promise 完成后自動恢復(fù)按鈕狀態(tài)
- 如果是同步操作,則立即恢復(fù)
三、核心特性
? 自動防重復(fù)點(diǎn)擊:1.5秒節(jié)流時間
? 自動 Loading 狀態(tài):無需手動管理 loading 變量
? 支持異步操作:自動檢測 Promise 并在完成后恢復(fù)
? 優(yōu)雅的清理機(jī)制:組件卸載時自動清理事件監(jiān)聽
? 類型安全:完整的 TypeScript 支持
四、技術(shù)實(shí)現(xiàn)
關(guān)鍵技術(shù)點(diǎn)
- Vue 3 自定義指令:使用
Directive類型定義 - VueUse 節(jié)流:
useThrottleFn提供穩(wěn)定的節(jié)流功能 - 動態(tài)組件渲染:使用
createVNode和render動態(tài)創(chuàng)建 Loading 圖標(biāo) - Promise 檢測:自動識別異步操作并在完成后恢復(fù)狀態(tài)
工作流程
用戶點(diǎn)擊按鈕
↓
節(jié)流檢查(1.5秒內(nèi)只執(zhí)行一次)
↓
禁用按鈕 + 添加 Loading 圖標(biāo)
↓
執(zhí)行綁定的函數(shù)
↓
檢測返回值是否為 Promise
↓
Promise 完成后(或同步函數(shù)執(zhí)行完)
↓
移除 Loading + 恢復(fù)按鈕狀態(tài)
五、使用方法
1. 注冊指令
// main.ts
import clickOnce from '@/directives/clickOnce'
app.directive('click-once', clickOnce)
2. 在組件中使用
<template>
<!-- 異步操作示例 -->
<el-button
type="primary"
v-click-once="handleSubmit">
提交表單
</el-button>
<!-- 帶參數(shù)的異步操作 -->
<el-button
type="success"
v-click-once="() => handleSave(formData)">
保存數(shù)據(jù)
</el-button>
</template>
<script setup lang="ts">
const handleSubmit = async () => {
// 模擬 API 調(diào)用
await api.submitForm(formData)
ElMessage.success('提交成功')
}
const handleSave = async (data: any) => {
await api.saveData(data)
ElMessage.success('保存成功')
}
</script>
六、優(yōu)勢對比
傳統(tǒng)方式
<template>
<el-button
type="primary"
:loading="loading"
:disabled="loading"
@click="handleSubmit">
提交
</el-button>
</template>
<script setup lang="ts">
const loading = ref(false)
const handleSubmit = async () => {
if (loading.value) return
loading.value = true
try {
await api.submit()
} finally {
loading.value = false
}
}
</script>
問題:
- 需要手動管理 loading 狀態(tài)
- 每個按鈕都要寫重復(fù)代碼
- 容易遺漏 finally 清理邏輯
使用 clickOnce 指令
<template>
<el-button
type="primary"
v-click-once="handleSubmit">
提交
</el-button>
</template>
<script setup lang="ts">
const handleSubmit = async () => {
await api.submit()
}
</script>
優(yōu)勢:
- 代碼簡潔,無需管理狀態(tài)
- 自動處理 loading 和禁用
- 統(tǒng)一的用戶體驗(yàn)
七、注意事項(xiàng)
- 僅用于異步操作:該指令主要為異步操作設(shè)計(jì),同步操作會立即恢復(fù)
- 綁定函數(shù)必須返回 Promise:對于異步操作,確保函數(shù)返回 Promise
- 節(jié)流時間固定:當(dāng)前節(jié)流時間為 1.5 秒,可根據(jù)需求調(diào)整
THROTTLE_TIME常量 - 依賴 Element Plus:使用了 Element Plus 的 Loading 圖標(biāo)和樣式
八、適用場景
? 適合使用:
- 表單提交按鈕
- 數(shù)據(jù)保存按鈕
- 文件上傳按鈕
- API 調(diào)用按鈕
- 支付確認(rèn)按鈕
? 不適合使用:
- 普通導(dǎo)航按鈕
- 切換/開關(guān)按鈕
- 需要快速連續(xù)點(diǎn)擊的場景(如計(jì)數(shù)器)
九、指令源碼
import type { Directive } from 'vue'
import { createVNode, render } from 'vue'
import { useThrottleFn } from '@vueuse/core'
import { Loading } from '@element-plus/icons-vue'
const THROTTLE_TIME = 1500
const clickOnce: Directive<HTMLButtonElement, () => Promise<unknown> | void> = {
mounted(el, binding) {
const handleClick = useThrottleFn(
() => {
// 如果元素已禁用,直接返回(雙重保險(xiǎn))
if (el.disabled) return
// 禁用按鈕
el.disabled = true
// 添加 loading 狀態(tài)
el.classList.add('is-loading')
// 創(chuàng)建 loading 圖標(biāo)容器
const loadingIconContainer = document.createElement('i')
loadingIconContainer.className = 'el-icon is-loading'
// 使用 Vue 的 createVNode 和 render 來渲染 Loading 組件
const vnode = createVNode(Loading)
render(vnode, loadingIconContainer)
// 將 loading 圖標(biāo)插入到按鈕開頭
el.insertBefore(loadingIconContainer, el.firstChild)
// 將 loading 圖標(biāo)存儲到元素上,以便后續(xù)移除
;(el as any)._loadingIcon = loadingIconContainer
;(el as any)._loadingVNode = vnode
// 執(zhí)行綁定的函數(shù)(應(yīng)返回 Promise 或普通函數(shù))
const result = binding.value?.()
const removeLoading = () => {
el.disabled = false
// 移除 loading 狀態(tài)
el.classList.remove('is-loading')
const icon = (el as any)._loadingIcon
if (icon && icon.parentNode === el) {
// 卸載 Vue 組件
render(null, icon)
el.removeChild(icon)
delete (el as any)._loadingIcon
delete (el as any)._loadingVNode
}
}
// 如果返回的是 Promise,則在完成時恢復(fù);否則立即恢復(fù)
if (result instanceof Promise) {
result.finally(removeLoading)
} else {
// 非異步操作,立即恢復(fù)(或根據(jù)需求決定是否恢復(fù))
// 通常建議只用于異步操作,所以這里也可以不處理,或給出警告
removeLoading()
}
},
THROTTLE_TIME,
)
// 將 throttled 函數(shù)存儲到元素上,以便在 unmount 時移除
;(el as any)._throttledClick = handleClick
el.addEventListener('click', handleClick)
},
beforeUnmount(el) {
const handleClick = (el as any)._throttledClick
if (handleClick) {
el.removeEventListener('click', handleClick)
// 取消可能還在等待的 throttle
handleClick.cancel?.()
delete (el as any)._throttledClick
}
},
}
export default clickOnce
十、總結(jié)
clickOnce 指令通過封裝防重復(fù)點(diǎn)擊邏輯,提供了一個開箱即用的解決方案,讓開發(fā)者可以專注于業(yè)務(wù)邏輯,而不用擔(dān)心重復(fù)點(diǎn)擊的問題。它結(jié)合了節(jié)流、狀態(tài)管理和視覺反饋,為用戶提供了更好的交互體驗(yàn)。
到此這篇關(guān)于Vue3防重復(fù)點(diǎn)擊指令clickOnce使用方法的文章就介紹到這了,更多相關(guān)Vue3防重復(fù)點(diǎn)擊指令clickOnce內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue與vue-i18n結(jié)合實(shí)現(xiàn)后臺數(shù)據(jù)的多語言切換方法
下面小編就為大家分享一篇vue與vue-i18n結(jié)合實(shí)現(xiàn)后臺數(shù)據(jù)的多語言切換方法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-03-03
vue.js element-ui tree樹形控件改iview的方法
這篇文章主要介紹了vue.js element-ui tree樹形控件改iview的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-03-03
Vue如何利用flex布局實(shí)現(xiàn)TV端城市列表功能
用vue開發(fā)了三四個組件了,都是H5的,現(xiàn)在來看看PC是如何玩轉(zhuǎn)組件的,下面這篇文章主要給大家介紹了關(guān)于Vue如何利用flex布局實(shí)現(xiàn)TV端城市列表功能的相關(guān)資料,需要的朋友可以參考下2023-01-01

