我本沒有想着說要封裝一個彈窗組件,但有同行的朋友在問我,而且彈窗組件也確實在項目開發中用的比較多。思前想后,又本着樣式統一且修改起來方便的原則,還是再為大家分享一個我所封裝的彈窗組件吧。
其實,並不是所有封裝組件的方式都是一成不變的,你可以采用函數式組件這種能提高性能的方式,也可以使用帶有狀態和生命周期的普通組件的封裝方式。但像dialog這種包含很多點擊事件如確定或提交事件、取消或重置事件、右上角那個小叉叉的關閉事件等,又有可能包含嵌套其他組件如表格組件、表單組件、樹形組件、穿梭框組件等的公共組件,其成分略微復雜,功能不太單一,你若要采用函數式組件的方式來封裝也不是不可以,只是可能xue微要麻煩一些,我自己建議是不采用這種封裝方式,就采用普通的封裝方式就好。
至於普通組件的封裝方式,我想大家平時在開發的過程中對所接觸的普通組件即帶有狀態和生命周期,也能快樂地使用this關鍵詞的組件已經是非常熟悉了,所以這種封裝方式我就不會再做過多的介紹了。以下是具體的實現過程。
照例還是先來張效果圖:
1、所封裝的彈窗組件dialog.vue
<template>
<el-dialog
class="el-dialog-cus"
v-bind="attributes"
:visible="visible"
:before-close="beClose"
append-to-body
:close-on-click-modal="false"
v-on="on"
>
<slot v-if="visibleSlot"></slot>
<div slot="footer">
<el-button @click="cancel" plain>{{btnTxt[0]}}</el-button>
<el-button @click="confirm" type="primary" v-if="btnTxt[1]">{{btnTxt[1]}}</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
inheritAttrs: false,
props: {
config: Object,
autoClose: {
type: Boolean,
default: true,
},
beforeClose: {
type: Function,
default: () => {},
},
},
data() {
const { top = '20vh', width = '420px', title = '提示', center = false, btnTxt = ["取消", "確定"] } = this.config || {};
return {
visible: false,
attributes: {
top,
width,
title,
center,
...this.config,
},
btnTxt,
on: this.getDialogEvents(),
visibleSlot: false,
};
},
methods: {
open(ok) {
this.ok = ok;
this.visible = true;
this.visibleSlot = true;
return this.$nextTick();
},
cancel() {
this.visible = false;
},
confirm() {
let cancel = () => this.cancel();
this.ok(cancel);
this.autoClose && cancel();
},
beClose(done) {
done();
this.beforeClose();
this.cancel();
},
getDialogEvents(){
// closed: () => this.visibleSlot = false是為了防止彈窗中的內容先於彈窗消失而造成彈窗在關閉時有一個突然向上縮小的情況
let { close } = this.config || {}, events = {closed: () => this.visibleSlot = false};
if(close && typeof close == 'function'){
Object.assign(events, {
close,
})
}
return events
},
},
};
</script>
<style lang="scss">
.el-dialog-cus {
.el-dialog {
padding: 8px;
}
.el-dialog__title {
font-weight: bold;
}
.el-dialog__header {
padding: 20px 0 12px;
}
.el-dialog__headerbtn {
top: 8px;
right: 8px;
}
.el-dialog__body {
padding: 0 24px;
}
.el-dialog__footer {
padding: 20px;
.el-button {
padding: 8px 20px;
& + .el-button {
margin-left: 40px;
}
}
}
}
</style>
對於以上的一些代碼,我需要做一些特別的說明:
<slot v-if="visible"></slot>
這段代碼是彈窗的插槽,給彈窗中加入的主體內容都會出現在這里。為什么要在slot上加一個v-if的判斷呢?你猜... 哈哈哈哈哈哈,其實是為了防止在彈窗中嵌套一些其他組件時,那些組件的生命周期只會執行一次的問題出現。
open(ok) {
this.ok = ok;
this.visible = true;
this.visibleSlot = true;
return this.$nextTick();
}
這段代碼是彈出彈窗的方法,為的是在使用彈窗組件時,我們只需點擊一個按鈕並使用ref來獲取彈窗組件的這個方法即可打開彈窗,剩下的關閉彈窗的操作就交給彈窗的確定或取消按鈕來完成即可。我們不用再額外的寫關閉彈窗的方法並將關閉彈窗的props參數傳給彈窗組件。另外,在打開彈窗的方法中我還保存了一個ok事件,這個ok事件是用於在點擊了彈窗組件的確定或提交按鈕后所觸發的回調函數。比如我們點擊了彈窗的提交按鈕,我們需要調一個接口來完成數據的存儲或修改,那么這個ok事件就是為它實現的,畢竟彈窗組件充當的只是一個我們用於處理業務邏輯的中間橋梁。
另外,在這段代碼的最后返回了一個nextTick,nextTick是將回調函數延遲在下一次DOM更新數據后調用,簡單的理解就是當數據更新了,在DOM中渲染后,自動執行該函數。我們平時在操作DOM時用到它的情況就比較多,而我們的通常用法是:this.$nextTick(() => {})
,但其實這個方法還返回了一個Promise對象,所以我們還可以這么用:this.$nextTick().then(() => {})
。那么這里為什么要將這個方法返回呢?原因自然是需要在打開彈窗后去獲取彈窗中的DOM元素,因為當打開彈窗時,里邊的DOM元素還沒有渲染完成,此時我們是獲取不到里邊的DOM節點的。
confirm() {
let cancel = () => this.cancel();
this.ok(cancel);
this.autoClose && cancel();
}
這段代碼是在點擊彈窗的確定或提交按鈕時觸發的,但為什么要給一個之前保存的ok回調函數傳一個關閉的方法作為參數呢,這是因為有時我們在點擊了確定或提交的按鈕后並不想立即關閉這個彈窗,而是想在幾秒鍾的倒計時后再關閉這個彈窗並跳轉到其他頁面,亦或是在A彈窗的基礎上又彈出另外一個B彈窗,在B彈窗的基礎上又彈出一個C彈窗。關閉C彈窗時,還能看到B彈窗,而不用在A彈窗的基礎上通過點擊事件再彈出B彈窗。這個時候就需要把關閉的方法當作參數傳遞給ok回調函數,讓調用彈窗組件的人自行控制在什么時候關閉彈窗,這難道不香嗎?只不過這個時候可能需要多給彈窗組件傳一個參數autoClose來通知它是不是需要前端自行控制什么時候來關閉彈窗,畢竟彈窗組件在大多數情況下都是點擊了確定或提交按鈕后就直接被關閉了。
beClose(done) {
done();
this.beforeClose();
this.cancel();
}
這段代碼是彈窗組件的關閉前before-close
方法,element的官方解釋是“關閉前的回調,會暫停Dialog的關閉”,官方還給了一個特別的說明:
before-close
僅當用戶通過點擊關閉圖標或遮罩關閉Dialog
時起效。如果你在footer
具名slot
里添加了用於關閉Dialog
的按鈕,那么可以在按鈕的點擊回調函數里加入before-close
的相關邏輯。
它接收一個參數done,用於關閉Dialog。而this.beforeClose()
是用來自定義關閉前所要做的一些事情的方法。
還有一點需要注意的是:普通組件所有未聲明的屬性都會被解析到$attrs里面,並自動掛載到組件根元素上面。因為本次封裝的彈窗組件的外面已經沒有根元素了,也就是標簽el-dialog的外面沒有再包裹一層父標簽,所以前邊這句話的意義已經不大了。如果標簽el-dialog的外面又包裹了一層div,那么那句話就有意義了,也就是說這些未聲明的屬性也會出現在最外層的div上,如果不想讓這些未聲明的屬性也出現在最外層的div上,那么就可以用inheritAttrs:false
來禁止。但本次封裝的彈窗組件的外面沒有根元素,所以加不加這個inheritAttrs:false
都無所謂了。
2、彈窗組件的使用:
<template>
<div>
<el-button @click="open">點我打開</el-button>
<Dialog ref="dialog" :config="config" :beforeClose="beforeClose" @close="resetForm"><span ref="span">this is a dialog</span></Dialog>
</div>
</template>
<script>
import Dialog from "@/components/dialog";
export default {
components: {
Dialog,
},
data() {
return {
config: {
top: '20vh',
width: '500px',
title: '溫馨提示',
center: true,
btnTxt: ['取消', '提交'],
},
};
},
methods: {
open() {
this.$refs.dialog.open(cancel => {
// cancel();
console.log('點擊提交按鈕了')
})
.then(() => {console.log(this.$refs.span)}); //這里就充分利用了open方法中返回的nextTick
},
beforeClose(){
console.log('關閉前');
},
resetForm(){
// 這里可以寫重置表單的實現
},
}
};
</script>
以上具體的使用方法中:
open() {
this.$refs.dialog.open(cancel => {
// cancel();
console.log('點擊提交按鈕了')
});
}
這段代碼就是用來打開或彈出彈窗組件,采用的是ref獲取彈窗組件的open方法,並向彈窗組件的open方法傳一個回調函數,而這個回調函數的參數就是組件中ok事件觸發時所返回的函數參數cancel,如果不需要前端來自行控制彈窗的關閉,則不接收這個cancel參數即可。