Vue3封裝組件完整實(shí)例(帶回調(diào)事件)
前言
這篇文章接著上一篇文章(Vue3封裝全局函數(shù)式組件)繼續(xù)分享vue3的組件封裝,上一篇文章是以Toast
提示為例子封裝的組件,此外還有Dialog
彈出框這種組件我們常常會封裝起來調(diào)用,它們之間區(qū)別不大,主要就是多了點(diǎn)擊按鈕能觸發(fā)回調(diào),所以這篇文章介紹一下函數(shù)式組件中回調(diào)事件的寫法,希望對大家有所啟發(fā)。
一、思路
首先組件調(diào)用需要滿足鏈?zhǔn)秸{(diào)用,想實(shí)現(xiàn)形如以下的寫法:
okToast.show() .then(res => { console.log('點(diǎn)擊了確認(rèn)'); }) .catch(err => { console.log('點(diǎn)擊了取消'); })
在 then 后面處理的是點(diǎn)擊確認(rèn)的邏輯,在 catch 后面處理的是點(diǎn)擊取消的邏輯,并且在 then 后面可以繼續(xù)調(diào)起第二個彈窗滿足鏈?zhǔn)秸{(diào)用。
那么很自然能想到的就是采用 Promise
實(shí)現(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: '確認(rèn)' }, 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(); }; //新增處理確認(rèn)的方法 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(); }); };
這里有個細(xì)節(jié),在 vue 文件里的 successHandle 方法里由于點(diǎn)擊后需要關(guān)閉彈窗,所以調(diào)用 hidden 方法,但是得先執(zhí)行 successBtn 方法,即 Promise 里的 resolve() ,然后處理完回調(diào)后的邏輯再進(jìn)行關(guān)閉卸載的邏輯,否則可能回調(diào)還沒執(zhí)行完彈窗就因?yàn)?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)用傳進(jìn)來的方法,這樣使得普通寫法即 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é)點(diǎn) 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ù)是根組件。第二個參數(shù)可選,它是要傳遞給根組件的 props) app = createApp(OkToast, { ...options, hide() { // 卸載已掛載的應(yīng)用實(shí)例 if (app) { app.unmount(); app = null; } // 刪除rootNode節(jié)點(diǎn) if (rootNode) { document.body.removeChild(rootNode); rootNode = null; } } }); // 將應(yīng)用實(shí)例掛載到創(chuàng)建的 DOM 元素上 return app.mount(rootNode); }; // 需要給options設(shè)默認(rèn)值,否則直接調(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)按鈕點(diǎn)擊后觸發(fā)此處事件那么就能改變 Promise 的狀態(tài)從而觸發(fā)回調(diào)了,very interesting!
三、使用方法及效果展示
傳入 type 為 “confirmAndcancel” 則有兩個按鈕,傳入 type 為 “confirm” 則只有一個,time 需要傳 -1 以便彈窗停留在窗口,詳細(xì)參數(shù)可看上面的 vue 文件里的props。同時由于 Promise 支持 finally 方法,所以作為示例也加上了。代碼如下(示例):
proxy .$okToast({ time: -1, width: 500, type: 'confirmAndcancel', content: '如果解決方法是丑陋的,那就肯定還有更好的解決方法,只是還沒有發(fā)現(xiàn)而已。', }) .then(res => { console.log('點(diǎn)擊了確認(rèn)1'); proxy .$okToast({ time: -1, width: 500, type: 'confirm', content: '再點(diǎn)一下彈窗就消失了', successText: 'OK' }) .then(res => { console.log('點(diǎn)擊了確認(rèn)2'); }); }) .catch(err => { console.log('點(diǎn)擊了取消'); proxy.$okToast({ time: -1, width: 500, type: 'confirm', content: '再點(diǎn)一下彈窗就消失了', successText: 'OK', }); }) .finally(() => { console.log('finally'); });
效果展示:
可以看到鏈?zhǔn)秸{(diào)用的回調(diào)是正常生效的。
四、加入一點(diǎn)細(xì)節(jié),完善效果
上面的效果大體上已經(jīng)實(shí)現(xiàn)了Dialog
的彈窗效果了,正當(dāng)我以為大功告成的時候,對比UI組件庫上的 Dialog ,我才意識到少了點(diǎn)東西。。。沒錯就是遮罩層,遮罩層除了樣式還有背景禁止?jié)L動的邏輯,好吧,趕緊補(bǔ)上去。
首先定義遮罩層的樣式,代碼如下(示例):
.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 的時候把遮罩層元素也添加進(jìn)去。一開始想法很簡單也實(shí)現(xiàn)了效果,就是直接監(jiān)聽元素的 “touchmove”
事件阻止?jié)L動,比如這樣:
let overlayNode = null; let rootNode = null; let app = null; // 阻止默認(rèn)事件 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)能滾動的情況時,遇到了移動端的滾動背景穿透問題,又得考慮兼容性問題,還真是一波三折。
解決辦法:
嘗試了各種寫法后,還好最終找到了一種比較簡單的寫法實(shí)現(xiàn)了效果,就是給同級的 Vue 創(chuàng)建的 app 元素 加 position = "fixed"
,彈窗出現(xiàn)的時候固定定位使得背景層頁面不能滾動, bottom
值定位在當(dāng)前頁面滾動距離免得頁面位置變化,最后彈窗消失的時候取消 fixed 定位,這個時候頁面會自動為于頂部,不過沒關(guān)系,再將頁面的 scrollTop
值還原回去就可以了,這種方法雖然不能保證100%成功,但是也經(jīng)過了我手上安卓及蘋果機(jī)的測試,如果遇到問題歡迎在評論區(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手機(jī)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é)點(diǎn) 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ù)默認(rèn)值 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ū)分開這兩個組件,然后單獨(dú)做配置項,這里只是為了兼容之前的Toast邏輯 // if (options.type === "confirmAndcancel" || options.type === "confirm") { if (options.time && options.time < 0) { createOverlay(); } // 創(chuàng)建元素節(jié)點(diǎn) 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)用實(shí)例掛載到創(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)驗(yàn)和技巧,因?yàn)橛行┢毡榇嬖诘膯栴}肯定有前人遇到過,那么站在巨人的肩膀上,加上自己的思考,問題就迎刃而解了。
到此這篇關(guān)于Vue3封裝組件的文章就介紹到這了,更多相關(guān)Vue3封裝組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用Vue3新特性構(gòu)建動態(tài)表單的方法詳解
傳統(tǒng)的表單開發(fā)通常需要編寫大量的重復(fù)代碼,例如處理用戶輸入、驗(yàn)證數(shù)據(jù)、更新 UI 等等,為了簡化開發(fā),我們可以借助 Vue 3 的新特性,例如組合式 API 和 ref 對象,所以本文我們將一起學(xué)習(xí)如何使用 Vue 3 的新特性構(gòu)建一個更加靈活、可擴(kuò)展的動態(tài)表單2024-06-06vue實(shí)現(xiàn)雙向數(shù)據(jù)綁定
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)雙向數(shù)據(jù)綁定,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-05-05vue watch偵聽器有無immediate的運(yùn)行順序問題
這篇文章主要介紹了vue watch偵聽器有無immediate的運(yùn)行順序問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08解決Vue.js Devtools未檢測到Vue實(shí)例的問題
在開發(fā)Vue.js應(yīng)用時,Vue.js Devtools是一個不可或缺的調(diào)試工具,然而,有時我們可能會遇到“Vue.js not detected”的提示,這意味著Vue.js Devtools未能成功識別和連接到我們的Vue應(yīng)用,本文將詳細(xì)解析這個問題,并提供相應(yīng)的解決步驟與代碼示例,需要的朋友可以參考下2024-01-01Vue 動態(tài)生成數(shù)據(jù)字段的實(shí)例
這篇文章主要介紹了Vue 動態(tài)生成數(shù)據(jù)字段的實(shí)例,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-04-04