Vue3封裝全局Dialog組件的實(shí)現(xiàn)方法
系列文章目錄
前言
前面的文章介紹了封裝 Dialog 彈出框的組件,同時實(shí)現(xiàn)了彈窗的鏈?zhǔn)秸{(diào)用。不過需求總是在變化的,這次需求要在顯示了彈窗的基礎(chǔ)上不斷往上面再顯示彈窗,所以來分享一下自己改善后的封裝方法,總結(jié)一下最新的心得體會。
一、使用方式
全局封裝的組件,在vue文件還有js文件當(dāng)然都是可以使用的,這一點(diǎn)在第一章已經(jīng)介紹了。下面展示在vue文件內(nèi)使用的方式,在需要彈窗的地方調(diào)用proxy.$popup()方法即可,隨便寫了寫,可以對照下面效果展示來看。
import { getCurrentInstance } from 'vue';
// 獲取當(dāng)前實(shí)例,相當(dāng)于 vue2 中的this
const { proxy } = getCurrentInstance();
const popupFn = () => {
proxy.$popup({
content:
'銀河廣袤寂靜,也孕育了無數(shù)美好。此刻,如果您仰望星空,看見那深邃黑暗中若隱若現(xiàn)的點(diǎn)點(diǎn)微芒,便會知曉,那高遠(yuǎn)無窮的璀璨正是我們共同的期冀。',
successText: '愿此行,終抵群星',
// 點(diǎn)擊遮罩層是否關(guān)閉彈窗,默認(rèn)false
closeByOverlay: true
});
};
const openPopup = () => {
proxy.$popup({
content: '生命遠(yuǎn)不止連軸轉(zhuǎn)和忙到極限,人類的體驗(yàn)遠(yuǎn)比這遼闊、豐富得多。',
holdOnFn: popupFn
});
};二、效果展示
點(diǎn)擊“彈窗”調(diào)用的是 openPopup 方法。

三、思路及實(shí)現(xiàn)方式

上面這個動圖展示了其運(yùn)行的原理,做出來之后我回看一下覺得其實(shí)細(xì)節(jié)還真不少,我將一步步介紹自己的思路及實(shí)現(xiàn)方法,完整代碼在最后貼上,可供大家參考。
1、保留原有彈窗基礎(chǔ)上加入新彈窗
vue文件組件里面主要區(qū)別在于點(diǎn)擊確認(rèn)按鈕時如果參數(shù)holdOnFn存在值的話就不執(zhí)行hidden,而是執(zhí)行傳進(jìn)來的方法,這樣舊彈窗就不會被關(guān)閉了,然后在彈窗上加個關(guān)閉按鈕讓其可以調(diào)用hidden方法去關(guān)閉。js文件主要區(qū)別就在于不能使用單例模式了,因?yàn)橐獎?chuàng)建多個彈窗。
// 省略無關(guān)代碼
const props = defineProps({
holdOnFn: {
type: Function
}
});
// 隱藏彈窗方法
const hidden = () => {
isShow.value = false;
props.hide();
};
// 成功按鈕
const successHandle = () => {
props.successBtn();
if (!props.holdOnFn) {
nextTick(() => {
hidden();
});
} else {
props.holdOnFn();
}
};2、彈窗之間的層級問題
這里給遮罩層及彈窗加入樣式,用fixed定位并且在外層記錄當(dāng)前z-index值,保證新彈窗的值永遠(yuǎn)大于舊彈窗,關(guān)鍵代碼節(jié)選:
// 這里數(shù)值大一些保證彈窗在所有組件最上方
let overlayNodeZIndex = 3000;
// 創(chuàng)建遮罩層
const createOverlay = () => {
const overlayNode = document.createElement('div');
overlayNode.className = 'my-overlay';
document.body.appendChild(overlayNode);
overlayNode.style.zIndex = overlayNodeZIndex;
return overlayNode;
};
const popup = (options = {}) => {
// 創(chuàng)建遮罩層
let overlayNode = null;
overlayNode = createOverlay();
// 創(chuàng)建彈窗元素節(jié)點(diǎn)
const rootNode = document.createElement('div');
// 在body標(biāo)簽內(nèi)部插入此元素
document.body.appendChild(rootNode);
rootNode.className = `my-dialog`;
rootNode.style.position = 'fixed';
rootNode.style.zIndex = overlayNodeZIndex;
overlayNodeZIndex += 1;
// 創(chuàng)建應(yīng)用實(shí)例(第一個參數(shù)是根組件。第二個參數(shù)可選,它是要傳遞給根組件的 props)
const app = createApp(Popup, {
...options
});
return app.mount(rootNode);
};
3、彈窗關(guān)閉所有
有這么一個場景,當(dāng)用戶點(diǎn)了第二個或以上的彈窗之后需要一次性把之前彈窗都關(guān)閉掉,所以有了這么一個hideAll方法。那么調(diào)用proxy.$popupHide()則可以關(guān)閉所有彈窗了。
// 定義臨時數(shù)組存放關(guān)閉彈窗的方法
let hideArr = [];
const popup = (options = {}) => {
// 省略無關(guān)代碼
const hide = function () {
// 關(guān)閉一個彈窗則從hideArr中取出一個
hideArr.pop();
};
// 每創(chuàng)建一個彈窗將其卸載方法存入hideArr數(shù)組中
hideArr.push(hide);
const app = createApp(Popup, {
...options,
hide
});
return app.mount(rootNode);
};
const okFun = (options = {}) => {
popup(options).show();
};
// 隱藏所有彈窗,遍歷然后調(diào)用
const hideAll = () => {
hideArr.forEach(e => {
e();
});
};
popup.install = app => {
app.config.globalProperties.$popup = options => okFun(options);
app.config.globalProperties.$popupHide = hideAll;
};
popup.show = options => okFun(options);
popup.hideAll = hideAll;
export default popup;
4、彈窗滾動相關(guān)問題
這個問題其實(shí)在第二章也有探討過,采取了文檔固定定位加記錄高度的方法實(shí)現(xiàn)固定背景層,避免滾動穿透問題,不過由于現(xiàn)在彈窗不止一個了,文檔上的fixed定位加上的時機(jī)以及去掉的時機(jī)就得考慮好,所以寫法要相應(yīng)改進(jìn)一下。正好上面hideAll方法存下來的數(shù)組里面方法的個數(shù)正好代表著目前彈窗的個數(shù),所以可以根據(jù)數(shù)組長度做出判斷。
let hideArr = [];
let appScrollTop = 0;
let bodyTop = 0;
// 創(chuàng)建彈窗調(diào)用的方法
const popup = (options = {}) => {
// 禁止app滾動邏輯
const appDOM = document.querySelector('#app');
// 此處hideArr用作彈窗計數(shù)
if (hideArr.length === 0) {
// 記錄滾動高度
appScrollTop = appDOM.scrollTop;
// 記錄文檔高度,兼容移動端瀏覽器,防止偏移
bodyTop = document.scrollingElement.scrollTop;
appDOM.style.position = 'fixed';
appDOM.style.bottom = appScrollTop + 'px';
// 兼容ios手機(jī)fixed定位不生效,得加overflow
appDOM.style.overflow = 'visible';
}
// 關(guān)閉彈窗的方法
const hide = function () {
// 解除app滾動邏輯
// 關(guān)閉一個彈窗則從hideArr中取出一個
hideArr.pop();
// 此處hideArr用作彈窗計數(shù)
if (hideArr.length === 0) {
// 解除app元素滾動,移除樣式
const appDOM = document.querySelector('#app');
appDOM.style.removeProperty('position');
appDOM.style.removeProperty('bottom');
appDOM.style.removeProperty('overflow');
// 恢復(fù)文檔滾動位置
appDOM.scrollTop = appScrollTop;
document.scrollingElement.scrollTop = bodyTop;
// 恢復(fù)默認(rèn)值
overlayNodeZIndex = 3000;
appScrollTop = 0;
bodyTop = 0;
}
};
// 每創(chuàng)建一個彈窗將其卸載方法存入hideArr數(shù)組中
hideArr.push(hide);
const app = createApp(Popup, {
...options,
hide
});
return app.mount(rootNode);
};
5、遮罩層加上動畫效果
①:創(chuàng)建遮罩層的時候
const createOverlay = () => {
const overlayNode = document.createElement('div');
overlayNode.className = 'my-overlay';
// 添加初始顯示的過渡效果
overlayNode.classList.add('my-overlay-enter-from');
// 在body標(biāo)簽內(nèi)部插入此元素
document.body.appendChild(overlayNode);
const remove = () => {
overlayNode.classList.remove('my-overlay-enter-from');
};
// requestAnimationFrame在瀏覽器下一次重繪之前執(zhí)行
requestAnimationFrame(remove);
overlayNode.style.zIndex = overlayNodeZIndex;
return overlayNode;
};②:關(guān)閉彈窗的時候
const hide = function () {
// 顯示移除動畫
overlayNode.classList.add('my-overlay-leave-to');
setTimeout(() => {
overlayNode.classList.remove('my-overlay-leave-to');
// 移除遮罩層
deleteOverlay(overlayNode);
}, 300);
}③:彈窗遮罩層樣式
.my-overlay {
position: fixed;
top: 0;
left: 0;
z-index: 100;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
transition: opacity 0.3s ease-out;
}
.my-overlay-enter-from,
.my-overlay-leave-to {
opacity: 0;
}6、配置點(diǎn)擊遮罩層關(guān)閉彈窗
根據(jù)傳入?yún)?shù)對象的closeByOverlay屬性,監(jiān)聽遮罩層點(diǎn)擊事件即可。
const props = defineProps({
// 默認(rèn)點(diǎn)擊遮罩層不關(guān)閉
closeByOverlay: {
type: Boolean,
default: false,
},
overlayNode: {
type: Object,
},
})
// 顯示彈窗方法
const show = () => {
isShow.value = true;
if (props.closeByOverlay) {
props.overlayNode.addEventListener('click', hidden);
}
};四、完整代碼環(huán)節(jié)
1、vue文件
<template>
<transition name="toast" @after-leave="onAfterLeave">
<div class="toast" v-if="isShow" :style="{ width: toastWidth }" :class="{ bgc: needBgc }">
<div v-if="showCancel || props.holdOnFn" class="cancel" @click="hidden"></div>
<div v-if="content" 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: ''
},
width: {
default: 640
},
textAlign: {
type: String,
default: 'center'
},
type: {
type: String,
default: 'confirm'
},
hide: {
type: Function
},
successText: {
type: String,
default: '確認(rèn)'
},
cancelText: {
type: String,
default: '取消'
},
successBtn: {
type: Function
},
cancelBtn: {
type: Function
},
// 是否需要右上角關(guān)閉按鈕
showCancel: {
type: Boolean,
default: false
},
// 是否需要背景
needBgc: {
type: Boolean,
default: true
},
// 傳入調(diào)用方法
holdOnFn: {
type: Function
},
// 默認(rèn)點(diǎn)擊遮罩層不關(guān)閉
closeByOverlay: {
type: Boolean,
default: false
},
overlayNode: {
type: Object
}
});
// 彈窗控制
const isShow = ref(false);
// 寬度控制
const toastWidth = computed(
() =>
((parseInt(props.width.toString()) / 750) * document.documentElement.clientWidth).toFixed(3) +
'px'
);
// 顯示彈窗方法
const show = () => {
isShow.value = true;
if (props.closeByOverlay) {
props.overlayNode.addEventListener('click', hidden);
}
};
// 將方法暴露出去
defineExpose({
show
});
// 隱藏彈窗方法
const hidden = () => {
isShow.value = false;
props.hide();
if (props.closeByOverlay) {
props.overlayNode.removeEventListener('click', hidden);
}
};
// 由于遮罩層需要消失動畫,兩個動畫效果必須同時觸發(fā),所以關(guān)閉時直接調(diào)用hide方法
const onAfterLeave = () => {
// props.hide();
};
const successHandle = () => {
props.successBtn();
if (!props.holdOnFn) {
nextTick(() => {
hidden();
});
} else {
props.holdOnFn();
}
};
const cancelHandle = () => {
props.cancelBtn();
nextTick(() => {
hidden();
});
};
</script>
<style lang="scss" scoped>
@mixin expand-btn {
&::before {
content: '';
position: absolute;
top: -10px;
right: -10px;
bottom: -10px;
left: -10px;
}
}
.toast {
position: fixed;
top: 45%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 99;
text-align: center;
.cancel {
background: url('../../assets/images/quxiao@2x.png') no-repeat center / contain;
position: absolute;
top: 10px;
right: 10px;
width: 15px;
height: 15px;
@include expand-btn;
}
.content {
color: #ffcc99;
max-height: 50vh;
overflow-y: scroll;
}
.operation {
position: relative;
display: flex;
justify-content: space-around;
align-items: center;
margin-top: 15px;
&::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
bottom: -50%;
left: -50%;
border-top: 1px solid #666;
transform: scale(0.5);
}
.close {
position: relative;
width: 100%;
padding: 10px 0;
color: rgba($color: #ffcc99, $alpha: 0.9);
&::after {
content: '';
position: absolute;
top: -50%;
right: -50%;
bottom: -50%;
left: -50%;
border-right: 1px solid #666;
transform: scale(0.5);
}
}
.confirm {
width: 100%;
padding: 10px 0;
color: rgba($color: #ffcc99, $alpha: 0.9);
}
}
}
.bgc {
background: #333333;
border-radius: 20px;
padding: 25px 20px 0;
}
.toast-enter-active,
.toast-leave-active {
transition: all 0.3s ease-out;
}
.toast-enter-from,
.toast-leave-to {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
}
</style>2、js文件
import { createApp } from 'vue';
import Popup from './Popup.vue';
let overlayNodeZIndex = 3000;
let hideArr = [];
let appScrollTop = 0;
let bodyTop = 0;
// 創(chuàng)建遮罩層
const createOverlay = () => {
const overlayNode = document.createElement('div');
overlayNode.className = 'my-overlay';
// 添加初始顯示的過渡效果
overlayNode.classList.add('my-overlay-enter-from');
// 在body標(biāo)簽內(nèi)部插入此元素
document.body.appendChild(overlayNode);
const remove = () => {
overlayNode.classList.remove('my-overlay-enter-from');
};
// requestAnimationFrame在瀏覽器下一次重繪之前執(zhí)行
requestAnimationFrame(remove);
overlayNode.style.zIndex = overlayNodeZIndex;
return overlayNode;
};
// 移除遮罩層
const deleteOverlay = overlayNode => {
if (overlayNode) {
document.body.removeChild(overlayNode);
}
};
const popup = (options = {}) => {
// 禁止app滾動
const appDOM = document.querySelector('#app');
// 此處hideArr用作彈窗計數(shù)
if (hideArr.length === 0) {
// 記錄滾動高度
appScrollTop = appDOM.scrollTop;
// 記錄文檔高度,兼容移動端瀏覽器,防止偏移
bodyTop = document.scrollingElement.scrollTop;
appDOM.style.position = 'fixed';
appDOM.style.bottom = appScrollTop + 'px';
// 兼容ios手機(jī)fixed定位不生效,得加overflow
appDOM.style.overflow = 'visible';
}
// 創(chuàng)建遮罩層
let overlayNode = createOverlay();
// 創(chuàng)建彈窗元素節(jié)點(diǎn)
const rootNode = document.createElement('div');
// 在body標(biāo)簽內(nèi)部插入此元素
document.body.appendChild(rootNode);
rootNode.className = `my-dialog`;
rootNode.style.position = 'fixed';
rootNode.style.zIndex = overlayNodeZIndex;
overlayNodeZIndex += 1;
const hide = function () {
// 顯示移除動畫
overlayNode.classList.add('my-overlay-leave-to');
setTimeout(() => {
overlayNode.classList.remove('my-overlay-leave-to');
deleteOverlay(overlayNode);
}, 300);
// 解除body滾動
// 關(guān)閉一個彈窗則從hideArr中取出一個
hideArr.pop();
// 此處hideArr用作彈窗計數(shù)
if (hideArr.length === 0) {
// 解除app元素滾動,移除樣式
const app = document.querySelector('#app');
app.style.removeProperty('position');
app.style.removeProperty('bottom');
app.style.removeProperty('overflow');
// 恢復(fù)文檔滾動位置
app.scrollTop = appScrollTop;
document.scrollingElement.scrollTop = bodyTop;
// 恢復(fù)默認(rèn)值
overlayNodeZIndex = 3000;
appScrollTop = 0;
bodyTop = 0;
}
// 卸載已掛載的應(yīng)用實(shí)例
setTimeout(() => {
app.unmount();
document.body.removeChild(rootNode);
}, 300);
};
// 每創(chuàng)建一個彈窗將其卸載方法存入hideArr數(shù)組中
hideArr.push(hide);
// 創(chuàng)建應(yīng)用實(shí)例(第一個參數(shù)是根組件。第二個參數(shù)可選,它是要傳遞給根組件的 props)
const app = createApp(Popup, {
...options,
hide,
overlayNode
});
// 將應(yīng)用實(shí)例掛載到創(chuàng)建的 DOM 元素上
return app.mount(rootNode);
};
// 顯示彈窗
const okFun = (options = {}) => {
return new Promise((resolve, reject) => {
options.successBtn = () => {
resolve();
};
options.cancelBtn = () => {
reject();
};
popup(options).show();
});
};
// 隱藏所有彈窗
const hideAll = () => {
hideArr.forEach(e => {
e();
});
};
popup.install = app => {
// 注冊全局屬性,相當(dāng)于 Vue2 的this
app.config.globalProperties.$popup = options => okFun(options);
app.config.globalProperties.$popupHide = hideAll;
};
// 定義兩個方法用于js文件直接調(diào)用
popup.show = options => okFun(options);
popup.hideAll = hideAll;
export default popup;
總結(jié)
以上就是全部內(nèi)容,本文通過封裝可多次彈出的 Dialog 組件進(jìn)一步探索了 Vue3 函數(shù)式組件的封裝方法。暫時我把自己遇到的問題都列舉出來了,但是可能還有些地方考慮不夠周全,代碼不夠整潔優(yōu)雅,我相信還有更好的實(shí)現(xiàn)方式,這也是我一直追求的,與君共勉!
到此這篇關(guān)于Vue3封裝全局Dialog組件的實(shí)現(xiàn)方法的文章就介紹到這了,更多相關(guān)Vue3封裝全局Dialog組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue使用Element組件時v-for循環(huán)里的表單項驗(yàn)證方法
這篇文章主要介紹了vue使用Element組件時v-for循環(huán)里的表單項驗(yàn)證方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-06-06
基于vue開發(fā)微信小程序mpvue-docs跳轉(zhuǎn)頁面功能
這篇文章主要介紹了基于vue寫微信小程序mpvue-docs跳轉(zhuǎn)頁面,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2019-04-04
如何使用VuePress搭建一個類型element ui文檔
這篇文章主要介紹了如何使用VuePress搭建一個類型element ui文檔,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02
vue3路由跳轉(zhuǎn)params傳參接收不到的解決辦法
這篇文章主要給大家介紹了關(guān)于vue3路由跳轉(zhuǎn)params傳參接收不到的解決辦法,Vue3是目前前端開發(fā)中非常流行的框架之一,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06
vue中v-for和v-if不能在同一個標(biāo)簽使用的最新解決方案
這篇文章主要介紹了vue中v-for和v-if不能在同一個標(biāo)簽的最新解決方案,這里描述了兩種解決方案,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-07-07
Vue+UpLoad實(shí)現(xiàn)上傳預(yù)覽和刪除圖片的實(shí)踐
本文主要介紹了Vue+UpLoad實(shí)現(xiàn)上傳預(yù)覽和刪除圖片的實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02

