Vue3封裝組件完整實例(帶回調(diào)事件)
前言
這篇文章接著上一篇文章(Vue3封裝全局函數(shù)式組件)繼續(xù)分享vue3的組件封裝,上一篇文章是以Toast提示為例子封裝的組件,此外還有Dialog 彈出框這種組件我們常常會封裝起來調(diào)用,它們之間區(qū)別不大,主要就是多了點擊按鈕能觸發(fā)回調(diào),所以這篇文章介紹一下函數(shù)式組件中回調(diào)事件的寫法,希望對大家有所啟發(fā)。
一、思路
首先組件調(diào)用需要滿足鏈?zhǔn)秸{(diào)用,想實現(xiàn)形如以下的寫法:
okToast.show()
.then(res => {
console.log('點擊了確認');
})
.catch(err => {
console.log('點擊了取消');
})在 then 后面處理的是點擊確認的邏輯,在 catch 后面處理的是點擊取消的邏輯,并且在 then 后面可以繼續(xù)調(diào)起第二個彈窗滿足鏈?zhǔn)秸{(diào)用。
那么很自然能想到的就是采用 Promise 實現(xiàn),Promise 可以把異步操作執(zhí)行后的狀態(tài)及時傳遞回來使得回調(diào)函數(shù)能夠及時調(diào)用。
二、代碼示例
1. vue文件
代碼如下(示例):
<template>
<transition name="toast" @after-leave="onAfterLeave">
<div class="toast" v-if="isShow" :style="{ width: toastWidth }">
<div
v-if="time < 0 && type !== 'confirm' && type !== 'confirmAndcancel'"
class="cancel"
@click="hidden"
></div>
<img
v-if="type === 'success' || type === 'icon'"
class="img"
src="../../assets/images/7vip_web_toast_finish_icon_40x40@2x.png"
alt="success"
/>
<img
v-if="type === 'warn'"
class="img"
src="../../assets/images/7vip_web_toast_warn.png"
alt="warn"
/>
<div v-if="content && type !== 'icon'" class="content" :style="{ textAlign }">
{{ content }}
</div>
<!-- 主要在這里增加了兩種樣式 -->
<!-- 這是只有一個確定按鈕的 -->
<div class="operation" v-if="type === 'confirm'">
<div class="confirm" @click="successHandle">{{ successText }}</div>
</div>
<!-- 這是同時有確定與取消按鈕的 -->
<div class="operation" v-if="type === 'confirmAndcancel'">
<div class="close" @click="cancelHandle">{{ cancelText }}</div>
<div class="confirm" @click="successHandle">{{ successText }}</div>
</div>
</div>
</transition>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue';
const props = defineProps({
content: {
type: String,
default: 'success'
},
time: {
type: Number,
default: 2000
},
width: {
default: 310
},
textAlign: {
type: String,
default: 'center'
},
type: {
type: String,
default: 'success'
},
hide: {
type: Function,
default: () => {}
},
successText: {
type: String,
default: '確認'
},
cancelText: {
type: String,
default: '取消'
},
successBtn: {
type: Function,
default: () => {}
},
cancelBtn: {
type: Function,
default: () => {}
}
});
const isShow = ref(false);
const toastWidth = computed(
() => (parseInt(props.width.toString()) / 750) * document.documentElement.clientWidth + 'px'
);
const show = () => {
isShow.value = true;
if (props.time >= 0) {
setTimeout(() => {
// isShow.value = false;
successHandle();
}, props.time);
}
};
defineExpose({
show
});
const hidden = () => {
isShow.value = false;
};
const onAfterLeave = () => {
props.hide();
};
//新增處理確認的方法
const successHandle = () => {
props.successBtn();
nextTick(() => {
hidden();
});
};
//新增的處理取消的方法
const cancelHandle = () => {
props.cancelBtn();
nextTick(() => {
hidden();
});
};
</script>
<style lang="scss" scoped>
.toast-enter-active,
.toast-leave-active {
transition: opacity 0.3s ease-out;
}
.toast-enter-from,
.toast-leave-to {
opacity: 0;
}
.toast {
position: fixed;
top: 45%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 99;
background: #333333;
border-radius: 20px;
padding: 20px;
text-align: center;
.cancel {
background: url('../../assets/images/quxiao@2x.png') no-repeat center / contain;
position: absolute;
top: 10px;
right: 10px;
width: 20px;
height: 20px;
&::before {
content: '';
position: absolute;
top: -10px;
right: -10px;
bottom: -10px;
left: -10px;
}
}
.img {
width: 40px;
height: 40px;
}
.content {
margin-top: 10px;
font-size: 16px;
color: #ffcc99;
text-align: initial;
}
.operation {
display: flex;
justify-content: space-around;
align-items: center;
margin-top: 20px;
.confirm {
color: white;
opacity: 0.9;
}
.close {
color: #ffcc99;
opacity: 0.9;
}
}
}
</style>說明 ①:
const successHandle = () => {
props.successBtn();
nextTick(() => {
hidden();
});
};這里有個細節(jié),在 vue 文件里的 successHandle 方法里由于點擊后需要關(guān)閉彈窗,所以調(diào)用 hidden 方法,但是得先執(zhí)行 successBtn 方法,即 Promise 里的 resolve() ,然后處理完回調(diào)后的邏輯再進行關(guān)閉卸載的邏輯,否則可能回調(diào)還沒執(zhí)行完彈窗就因為 hidden 的原因被關(guān)掉了,所以這個執(zhí)行順序很重要,此處加入vue中的 nextTick 方法保證了 hidden 是在所有異步任務(wù)的最后再觸發(fā)。cancelHandle 方法同理。
說明 ②:
const show = () => {
isShow.value = true;
if (props.time >= 0) {
setTimeout(() => {
// isShow.value = false;
successHandle();
}, props.time);
}
};show方法里面不直接關(guān)閉彈窗了,而是調(diào)用傳進來的方法,這樣使得普通寫法即 proxy.$okToast() 也可以使用鏈?zhǔn)秸{(diào)用。
2. js文件
代碼如下(示例):
import { createApp } from 'vue';
import OkToast from './okToast.vue';
let rootNode = null;
let app = null;
const okToast = options => {
const dom = document.body.querySelector('.my-dialog');
if (!dom) {
// 創(chuàng)建元素節(jié)點
rootNode = document.createElement('div');
rootNode.className = `my-dialog`;
// 在body標(biāo)簽內(nèi)部插入此元素
document.body.appendChild(rootNode);
} else {
app.unmount();
}
// 創(chuàng)建應(yīng)用實例(第一個參數(shù)是根組件。第二個參數(shù)可選,它是要傳遞給根組件的 props)
app = createApp(OkToast, {
...options,
hide() {
// 卸載已掛載的應(yīng)用實例
if (app) {
app.unmount();
app = null;
}
// 刪除rootNode節(jié)點
if (rootNode) {
document.body.removeChild(rootNode);
rootNode = null;
}
}
});
// 將應(yīng)用實例掛載到創(chuàng)建的 DOM 元素上
return app.mount(rootNode);
};
// 需要給options設(shè)默認值,否則直接調(diào)用okToast()會出錯
const okFun = (options = {}) => {
return new Promise((resolve, reject) => {
options.successBtn = () => {
resolve();
};
options.cancelBtn = () => {
reject();
};
okToast(options).show();
});
};
okToast.install = app => {
// 注冊全局組件
// app.component("Toast", OkToast);
// 注冊全局屬性,類似于 Vue2 的 Vue.prototype
// app.config.globalProperties.$okToast = options => okToast(options).show();
app.config.globalProperties.$okToast = options => okFun(options);
};
// 定義show方法用于直接調(diào)用
// okToast.show = options => okToast(options).show();
okToast.show = options => okFun(options);
export default okToast;
說明 ①:
原本的寫法是okToast(options).show(),現(xiàn)在是調(diào)用okFun方法,在okFun方法里返回的是一個 Promise 對象,并且利用 createApp 方法其第二個參數(shù)是可以傳遞給根組件的,那么就能往參數(shù)上定義兩個方法,這兩個方法上分別綁定在確定及取消按鈕上,當(dāng)按鈕點擊后觸發(fā)此處事件那么就能改變 Promise 的狀態(tài)從而觸發(fā)回調(diào)了,very interesting!
三、使用方法及效果展示
傳入 type 為 “confirmAndcancel” 則有兩個按鈕,傳入 type 為 “confirm” 則只有一個,time 需要傳 -1 以便彈窗停留在窗口,詳細參數(shù)可看上面的 vue 文件里的props。同時由于 Promise 支持 finally 方法,所以作為示例也加上了。代碼如下(示例):
proxy
.$okToast({
time: -1,
width: 500,
type: 'confirmAndcancel',
content: '如果解決方法是丑陋的,那就肯定還有更好的解決方法,只是還沒有發(fā)現(xiàn)而已。',
})
.then(res => {
console.log('點擊了確認1');
proxy
.$okToast({
time: -1,
width: 500,
type: 'confirm',
content: '再點一下彈窗就消失了',
successText: 'OK'
})
.then(res => {
console.log('點擊了確認2');
});
})
.catch(err => {
console.log('點擊了取消');
proxy.$okToast({
time: -1,
width: 500,
type: 'confirm',
content: '再點一下彈窗就消失了',
successText: 'OK',
});
})
.finally(() => {
console.log('finally');
});效果展示:

可以看到鏈?zhǔn)秸{(diào)用的回調(diào)是正常生效的。
四、加入一點細節(jié),完善效果
上面的效果大體上已經(jīng)實現(xiàn)了Dialog 的彈窗效果了,正當(dāng)我以為大功告成的時候,對比UI組件庫上的 Dialog ,我才意識到少了點東西。。。沒錯就是遮罩層,遮罩層除了樣式還有背景禁止?jié)L動的邏輯,好吧,趕緊補上去。
首先定義遮罩層的樣式,代碼如下(示例):
.my-overlay {
position: fixed;
top: 0;
left: 0;
z-index: 99;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
}然后創(chuàng)建彈窗 DOM 的時候把遮罩層元素也添加進去。一開始想法很簡單也實現(xiàn)了效果,就是直接監(jiān)聽元素的 “touchmove” 事件阻止?jié)L動,比如這樣:
let overlayNode = null;
let rootNode = null;
let app = null;
// 阻止默認事件
let noScroll = e => {
e.preventDefault();
};
const okToast = options => {
const dom = document.body.querySelector(".my-dialog");
if (!dom) {
if (options.type === "confirmAndcancel" || options.type === "confirm") {
// 創(chuàng)建遮罩層
overlayNode = document.createElement("div");
overlayNode.className = `my-overlay`;
document.body.appendChild(overlayNode);
// 監(jiān)聽滾動事件
document.body.querySelector(".my-overlay").addEventListener("touchmove", noScroll);
}
rootNode = document.createElement("div");
rootNode.className = `my-dialog`;
document.body.appendChild(rootNode);
// 監(jiān)聽滾動事件
document.body.querySelector(".my-dialog").addEventListener("touchmove", noScroll);
} else {
app.unmount();
}
app = createApp(OkToast, {
...options,
hide() {
if (options.type === "confirmAndcancel" || options.type === "confirm") {
// 解除監(jiān)聽
document.body.querySelector(".my-overlay").removeEventListener("touchmove", noScroll);
document.body.querySelector(".my-dialog").removeEventListener("touchmove", noScroll);
if (overlayNode) {
document.body.removeChild(overlayNode);
overlayNode = null;
}
}
if (app) {
app.unmount();
app = null;
}
if (rootNode) {
document.body.removeChild(rootNode);
rootNode = null;
}
},
});
return app.mount(rootNode);
};存在的問題:
但是這樣的話,不僅把背景禁止?jié)L動了也把彈窗里的內(nèi)容給禁止?jié)L動了,如果彈窗文字內(nèi)容太多,那彈窗高度就需要限制,彈窗內(nèi)容做滾動處理,而在考慮這種背景不能滾動而彈窗內(nèi)能滾動的情況時,遇到了移動端的滾動背景穿透問題,又得考慮兼容性問題,還真是一波三折。
解決辦法:
嘗試了各種寫法后,還好最終找到了一種比較簡單的寫法實現(xiàn)了效果,就是給同級的 Vue 創(chuàng)建的 app 元素 加 position = "fixed",彈窗出現(xiàn)的時候固定定位使得背景層頁面不能滾動, bottom 值定位在當(dāng)前頁面滾動距離免得頁面位置變化,最后彈窗消失的時候取消 fixed 定位,這個時候頁面會自動為于頂部,不過沒關(guān)系,再將頁面的 scrollTop 值還原回去就可以了,這種方法雖然不能保證100%成功,但是也經(jīng)過了我手上安卓及蘋果機的測試,如果遇到問題歡迎在評論區(qū)指出。
代碼如下(示例):
let overlayNode = null;
let rootNode = null;
let app = null;
let scrollTop = 0;
const createOverlay = () => {
const app= document.querySelector('#app');
if (!scrollTop) {
scrollTop = app.scrollTop;
}
// 禁止app元素滾動
app.style.position = 'fixed';
app.style.bottom = scrollTop + 'px';
// 兼容ios手機fixed定位不生效,得加overflow
app.style.overflow = 'visible';
// 創(chuàng)建遮罩層
overlayNode = document.createElement('div');
overlayNode.className = `my-overlay`;
// // 在body標(biāo)簽內(nèi)部插入此元素
document.body.appendChild(overlayNode);
};
const deleteOverlay = () => {
// 刪除overlayNode節(jié)點
if (overlayNode) {
document.body.removeChild(overlayNode);
overlayNode = null;
}
// 解除app元素滾動,移除樣式
const app =document.querySelector('#app')
app.style.removeProperty('position')
app.style.removeProperty('bottom')
app.style.removeProperty('overflow')
// 恢復(fù)頁面滾動距離
app.scrollTop = scrollTop;
// 恢復(fù)默認值
scrollTop = 0;
};
const okToast = options => {
const dom = document.body.querySelector('.my-dialog');
if (!dom) {
// 將type與遮罩層關(guān)系解耦,根據(jù)傳入的time參數(shù)確定遮罩層顯示或隱藏,time小于0則是Dialog,大于0或者不傳time則是Toast,當(dāng)然更好的方式是區(qū)分開這兩個組件,然后單獨做配置項,這里只是為了兼容之前的Toast邏輯
// if (options.type === "confirmAndcancel" || options.type === "confirm") {
if (options.time && options.time < 0) {
createOverlay();
}
// 創(chuàng)建元素節(jié)點
rootNode = document.createElement('div');
rootNode.className = `my-dialog`;
document.body.appendChild(rootNode);
} else {
app.unmount();
// 根據(jù)傳入配置去掉遮罩層
if (!options.time || options.time > 0) {
deleteOverlay();
}
}
app = createApp(OkToast, {
...options,
hide() {
if (options.time && options.time < 0) {
deleteOverlay();
}
if (app) {
app.unmount();
app = null;
}
if (rootNode) {
document.body.removeChild(rootNode);
rootNode = null;
}
}
});
// 將應(yīng)用實例掛載到創(chuàng)建的 DOM 元素上
return app.mount(rootNode);
};另外 vue 文件也改一下樣式,滿足內(nèi)容區(qū)的滾動效果
.content {
margin-top: 10px;
font-size: 32px;
color: #ffcc99;
text-align: initial;
max-height: 50vh;
overflow-y: scroll;
}效果展示:

總結(jié)
以上就是全部內(nèi)容,本文通過封裝 Dialog 彈出框組件探索了 Vue3 函數(shù)式組件的封裝方法,同時解決了移動端彈出框背景滾動的相關(guān)問題。在此次探索學(xué)習(xí)的過程中,我深刻體會到在遇到問題的時候,可以多轉(zhuǎn)換下思路,多嘗試,同時找找資料,學(xué)習(xí)一下別人的經(jīng)驗和技巧,因為有些普遍存在的問題肯定有前人遇到過,那么站在巨人的肩膀上,加上自己的思考,問題就迎刃而解了。
到此這篇關(guān)于Vue3封裝組件的文章就介紹到這了,更多相關(guān)Vue3封裝組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用Vue3新特性構(gòu)建動態(tài)表單的方法詳解
傳統(tǒng)的表單開發(fā)通常需要編寫大量的重復(fù)代碼,例如處理用戶輸入、驗證數(shù)據(jù)、更新 UI 等等,為了簡化開發(fā),我們可以借助 Vue 3 的新特性,例如組合式 API 和 ref 對象,所以本文我們將一起學(xué)習(xí)如何使用 Vue 3 的新特性構(gòu)建一個更加靈活、可擴展的動態(tài)表單2024-06-06
vue watch偵聽器有無immediate的運行順序問題
這篇文章主要介紹了vue watch偵聽器有無immediate的運行順序問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08

