Vue3封裝全局Dialog組件的實(shí)現(xiàn)方法
系列文章目錄
前言
前面的文章介紹了封裝 Dialog
彈出框的組件,同時(shí)實(shí)現(xiàn)了彈窗的鏈?zhǔn)秸{(diào)用。不過需求總是在變化的,這次需求要在顯示了彈窗的基礎(chǔ)上不斷往上面再顯示彈窗,所以來分享一下自己改善后的封裝方法,總結(jié)一下最新的心得體會(huì)。
一、使用方式
全局封裝的組件,在vue文件還有js文件當(dāng)然都是可以使用的,這一點(diǎn)在第一章已經(jīng)介紹了。下面展示在vue文件內(nèi)使用的方式,在需要彈窗的地方調(diào)用proxy.$popup()
方法即可,隨便寫了寫,可以對(duì)照下面效果展示來看。
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)微芒,便會(huì)知曉,那高遠(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)方式
上面這個(gè)動(dòng)圖展示了其運(yùn)行的原理,做出來之后我回看一下覺得其實(shí)細(xì)節(jié)還真不少,我將一步步介紹自己的思路及實(shí)現(xiàn)方法,完整代碼在最后貼上,可供大家參考。
1、保留原有彈窗基礎(chǔ)上加入新彈窗
vue文件組件里面主要區(qū)別在于點(diǎn)擊確認(rèn)按鈕時(shí)如果參數(shù)holdOnFn存在值的話就不執(zhí)行hidden,而是執(zhí)行傳進(jìn)來的方法,這樣舊彈窗就不會(huì)被關(guān)閉了,然后在彈窗上加個(gè)關(guān)閉按鈕讓其可以調(diào)用hidden方法去關(guān)閉。js文件主要區(qū)別就在于不能使用單例模式了,因?yàn)橐獎(jiǎng)?chuàng)建多個(gè)彈窗。
// 省略無關(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、彈窗之間的層級(jí)問題
這里給遮罩層及彈窗加入樣式,用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í)例(第一個(gè)參數(shù)是根組件。第二個(gè)參數(shù)可選,它是要傳遞給根組件的 props) const app = createApp(Popup, { ...options }); return app.mount(rootNode); };
3、彈窗關(guān)閉所有
有這么一個(gè)場(chǎng)景,當(dāng)用戶點(diǎn)了第二個(gè)或以上的彈窗之后需要一次性把之前彈窗都關(guān)閉掉,所以有了這么一個(gè)hideAll方法。那么調(diào)用proxy.$popupHide()
則可以關(guān)閉所有彈窗了。
// 定義臨時(shí)數(shù)組存放關(guān)閉彈窗的方法 let hideArr = []; const popup = (options = {}) => { // 省略無關(guān)代碼 const hide = function () { // 關(guān)閉一個(gè)彈窗則從hideArr中取出一個(gè) hideArr.pop(); }; // 每創(chuàng)建一個(gè)彈窗將其卸載方法存入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、彈窗滾動(dòng)相關(guān)問題
這個(gè)問題其實(shí)在第二章也有探討過,采取了文檔固定定位加記錄高度的方法實(shí)現(xiàn)固定背景層,避免滾動(dòng)穿透問題,不過由于現(xiàn)在彈窗不止一個(gè)了,文檔上的fixed
定位加上的時(shí)機(jī)以及去掉的時(shí)機(jī)就得考慮好,所以寫法要相應(yīng)改進(jìn)一下。正好上面hideAll
方法存下來的數(shù)組里面方法的個(gè)數(shù)正好代表著目前彈窗的個(gè)數(shù),所以可以根據(jù)數(shù)組長(zhǎng)度做出判斷。
let hideArr = []; let appScrollTop = 0; let bodyTop = 0; // 創(chuàng)建彈窗調(diào)用的方法 const popup = (options = {}) => { // 禁止app滾動(dòng)邏輯 const appDOM = document.querySelector('#app'); // 此處hideArr用作彈窗計(jì)數(shù) if (hideArr.length === 0) { // 記錄滾動(dòng)高度 appScrollTop = appDOM.scrollTop; // 記錄文檔高度,兼容移動(dòng)端瀏覽器,防止偏移 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滾動(dòng)邏輯 // 關(guān)閉一個(gè)彈窗則從hideArr中取出一個(gè) hideArr.pop(); // 此處hideArr用作彈窗計(jì)數(shù) if (hideArr.length === 0) { // 解除app元素滾動(dòng),移除樣式 const appDOM = document.querySelector('#app'); appDOM.style.removeProperty('position'); appDOM.style.removeProperty('bottom'); appDOM.style.removeProperty('overflow'); // 恢復(fù)文檔滾動(dòng)位置 appDOM.scrollTop = appScrollTop; document.scrollingElement.scrollTop = bodyTop; // 恢復(fù)默認(rèn)值 overlayNodeZIndex = 3000; appScrollTop = 0; bodyTop = 0; } }; // 每創(chuàng)建一個(gè)彈窗將其卸載方法存入hideArr數(shù)組中 hideArr.push(hide); const app = createApp(Popup, { ...options, hide }); return app.mount(rootNode); };
5、遮罩層加上動(dòng)畫效果
①:創(chuàng)建遮罩層的時(shí)候
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)閉彈窗的時(shí)候
const hide = function () { // 顯示移除動(dòng)畫 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ù)對(duì)象的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); } }; // 由于遮罩層需要消失動(dòng)畫,兩個(gè)動(dòng)畫效果必須同時(shí)觸發(fā),所以關(guān)閉時(shí)直接調(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滾動(dòng) const appDOM = document.querySelector('#app'); // 此處hideArr用作彈窗計(jì)數(shù) if (hideArr.length === 0) { // 記錄滾動(dòng)高度 appScrollTop = appDOM.scrollTop; // 記錄文檔高度,兼容移動(dòng)端瀏覽器,防止偏移 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 () { // 顯示移除動(dòng)畫 overlayNode.classList.add('my-overlay-leave-to'); setTimeout(() => { overlayNode.classList.remove('my-overlay-leave-to'); deleteOverlay(overlayNode); }, 300); // 解除body滾動(dòng) // 關(guān)閉一個(gè)彈窗則從hideArr中取出一個(gè) hideArr.pop(); // 此處hideArr用作彈窗計(jì)數(shù) if (hideArr.length === 0) { // 解除app元素滾動(dòng),移除樣式 const app = document.querySelector('#app'); app.style.removeProperty('position'); app.style.removeProperty('bottom'); app.style.removeProperty('overflow'); // 恢復(fù)文檔滾動(dòng)位置 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)建一個(gè)彈窗將其卸載方法存入hideArr數(shù)組中 hideArr.push(hide); // 創(chuàng)建應(yīng)用實(shí)例(第一個(gè)參數(shù)是根組件。第二個(gè)參數(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 => { // 注冊(cè)全局屬性,相當(dāng)于 Vue2 的this app.config.globalProperties.$popup = options => okFun(options); app.config.globalProperties.$popupHide = hideAll; }; // 定義兩個(gè)方法用于js文件直接調(diào)用 popup.show = options => okFun(options); popup.hideAll = hideAll; export default popup;
總結(jié)
以上就是全部?jī)?nèi)容,本文通過封裝可多次彈出的 Dialog 組件進(jìn)一步探索了 Vue3 函數(shù)式組件的封裝方法。暫時(shí)我把自己遇到的問題都列舉出來了,但是可能還有些地方考慮不夠周全,代碼不夠整潔優(yōu)雅,我相信還有更好的實(shí)現(xiàn)方式,這也是我一直追求的,與君共勉!
到此這篇關(guān)于Vue3封裝全局Dialog組件的實(shí)現(xiàn)方法的文章就介紹到這了,更多相關(guān)Vue3封裝全局Dialog組件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue elementUI 表單嵌套驗(yàn)證的實(shí)例代碼
這篇文章主要介紹了vue + elementUI 表單嵌套驗(yàn)證,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-11-11在Vue中使用xlsx組件實(shí)現(xiàn)Excel導(dǎo)出功能的步驟詳解
在現(xiàn)代Web應(yīng)用程序中,數(shù)據(jù)導(dǎo)出到Excel格式是一項(xiàng)常見的需求,Vue.js是一種流行的JavaScript框架,允許我們構(gòu)建動(dòng)態(tài)的前端應(yīng)用程序,本文將介紹如何使用Vue.js和xlsx組件輕松實(shí)現(xiàn)Excel數(shù)據(jù)導(dǎo)出功能,需要的朋友可以參考下2023-10-10vue 使用localstorage實(shí)現(xiàn)面包屑的操作
這篇文章主要介紹了vue 使用localstorage實(shí)現(xiàn)面包屑的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-11-11Vue實(shí)現(xiàn)大屏頁(yè)面的屏幕自適應(yīng)
這篇文章主要為大家詳細(xì)介紹了Vue實(shí)現(xiàn)大屏頁(yè)面的屏幕自適應(yīng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10Element-ui Drawer抽屜按需引入基礎(chǔ)使用
這篇文章主要為大家介紹了Element-ui Drawer抽屜按需引入基礎(chǔ)使用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07詳解基于vue-cli配置移動(dòng)端自適應(yīng)
本篇文章主要介紹了詳解基于vue-cli配置移動(dòng)端自適應(yīng),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-01-01