詳解如何在vue+element-ui的項目中封裝dialog組件
1、問題起源
由于 Vue 基于組件化的設(shè)計,得益于這個思想,我們在 Vue 的項目中可以通過封裝組件提高代碼的復(fù)用性。根據(jù)我目前的使用心得,知道 Vue 拆分組件至少有兩個優(yōu)點:
1、代碼復(fù)用。
2、代碼拆分
在基于 element-ui 開發(fā)的項目中,可能我們要寫出一個類似的調(diào)度彈窗功能,很容易編寫出以下代碼:
<template>
<div>
<el-dialog :visible.sync="cnMapVisible">我是中國地圖的彈窗</el-dialog>
<el-dialog :visible.sync="usaMapVisible">我是美國地圖的彈窗</el-dialog>
<el-dialog :visible.sync="ukMapVisible">我是英國地圖的彈窗</el-dialog>
<el-button @click="openChina">打開中國地圖</el-button>
<el-button @click="openUSA">打開美國地圖</el-button>
<el-button @click="openUK">打開英國地圖</el-button>
</div>
</template>
<script>
export default {
name: "View",
data() {
return {
// 對百度地圖和谷歌地圖的一些業(yè)務(wù)處理代碼 省略
cnMapVisible: false,
usaMapVisible: false,
ukMapVisible: false,
};
},
methods: {
// 對百度地圖和谷歌地圖的一些業(yè)務(wù)處理代碼 省略
openChina() {},
openUSA() {},
openUK() {},
},
};
</script>
上述代碼存在的問題非常多,首先當(dāng)我們的彈窗越來越多的時候,我們會發(fā)現(xiàn)此時需要定義越來越多的變量去控制這個彈窗的顯示或者隱藏。
由于當(dāng)我們的彈窗的內(nèi)部還有業(yè)務(wù)邏輯需要處理,那么此時會有相當(dāng)多的業(yè)務(wù)處理代碼夾雜在一起(比如我調(diào)用中國地圖我需要用高德地圖或者百度地圖,而調(diào)用美國、英國地圖我只能用谷歌地圖,這會使得兩套業(yè)務(wù)邏輯分別位于一個文件,嚴(yán)重加大了業(yè)務(wù)的耦合度)
我們按照分離業(yè)務(wù),降低耦合度的原則,將代碼按以下思路進(jìn)行拆分:
1、View.vue
<template>
<div>
<china-map-dialog ref="china"></china-map-dialog>
<usa-map-dialog ref="usa"></usa-map-dialog>
<uk-map-dialog ref="uk"></uk-map-dialog>
<el-button @click="openChina">打開中國地圖</el-button>
<el-button @click="openUSA">打開美國地圖</el-button>
<el-button @click="openUK">打開英國地圖</el-button>
</div>
</template>
<script>
export default {
name: "View",
data() {
return {
/**
將地圖的業(yè)務(wù)全部抽離到對應(yīng)的dialog里面去,View只存放調(diào)度業(yè)務(wù)代碼
*/
};
},
methods: {
openChina() {
this.$refs.china && this.$refs.china.openDialog();
},
openUSA() {
this.$refs.usa && this.$refs.usa.openDialog();
},
openUK() {
this.$refs.uk && this.$refs.uk.openDialog();
},
},
};
</script>
2、ChinaMapDialog.vue
<template>
<div>
<el-dialog :visible.sync="baiduMapVisible">我是中國地圖的彈窗</el-dialog>
</div>
</template>
<script>
export default {
name: "ChinaMapDialog",
data() {
return {
// 對中國地圖業(yè)務(wù)邏輯的封裝處理 省略
baiduMapVisible: false,
};
},
methods: {
// 對百度地圖和谷歌地圖的一些業(yè)務(wù)處理代碼 省略
openDialog() {
this.baiduMapVisible = true;
},
closeDialog() {
this.baiduMapVisible = false;
},
},
};
</script>
3、由于此處僅僅展示偽代碼,且和 ChinaMapDialog.vue 表達(dá)的含義一致, 為避免篇幅過長 USAMapDialog.vue 和 UKMapDialog.vue 已省略
2、問題分析
我們通過對這幾個彈窗的分析,對剛才的設(shè)計進(jìn)行抽象發(fā)現(xiàn),這里面都有一個共同的部分,那就是我們對 dialog 的操作代碼都是可以重用的代碼,如果我們能夠編寫出一個抽象的彈窗,
然后在恰當(dāng)?shù)臅r候?qū)⑵浜蜆I(yè)務(wù)代碼進(jìn)行組合,就可以實現(xiàn) 1+1=2 的效果。
3、設(shè)計
由于 Vue 在不改變默認(rèn)的 mixin 原則(默認(rèn)也最好不要改變,可能會給后來的維護(hù)人員帶來困惑)的情況下,如果在混入過程中發(fā)生了命名沖突,默認(rèn)會將方法合并(數(shù)據(jù)對象在內(nèi)部會進(jìn)行遞歸合并,并在發(fā)生沖突時以組件數(shù)據(jù)優(yōu)先),因此,mixin 無法改寫本來的實現(xiàn),而我們期望的是,父類提供一個比較抽象的實現(xiàn),子類繼承父類,若子類需要改表這個行為,子類可以重寫父類的方法(多態(tài)的一種實現(xiàn))。
因此我們決定使用 vue-class-component 這個庫,以類的形式來編寫這個抽象彈窗。
import Vue from "vue";
import Component from "vue-class-component";
@Component({
name: "AbstractDialog",
})
export default class AbstractDialog extends Vue {}
3.1 事件處理
查看 Element-UI 的官方網(wǎng)站,我們發(fā)現(xiàn) ElDialog 對外拋出 4 個事件,因此,我們需要預(yù)先接管這 4 個事件。
因此需要在我們的抽象彈窗里預(yù)設(shè)這個 4 個事件的 handler(因為對于組件的行為的劃分,而對于彈窗的處理本來就應(yīng)該從屬于彈窗本身,因此我并沒有通過$listeners 去穿透外部調(diào)用時的監(jiān)聽方法)
import Vue from "vue";
import Component from "vue-class-component";
@Component({
name: "AbstractDialog",
})
export default class AbstractDialog extends Vue {
open() {
console.log("彈窗打開,我啥也不做");
}
close() {
console.log("彈窗關(guān)閉,我啥也不做");
}
opened() {
console.log("彈窗打開,我啥也不做");
}
closed() {
console.log("彈窗關(guān)閉,我啥也不做");
}
}
3.2 屬性處理
dialog 有很多屬性,默認(rèn)我們只需要關(guān)注的是 before-close 和 title 兩者,因為這兩個屬性從職責(zé)上劃分是從屬于彈窗本身的行為,所以我們會在抽象彈窗里面處理開關(guān)和 title 的任務(wù)
import Vue from "vue";
import Component from "vue-class-component";
@Component({
name: "AbstractDialog",
})
export default class AbstractDialog extends Vue {
visible = false;
t = "";
loading = false;
//定義這個屬性的目的是為了實現(xiàn)既可以外界通過傳入屬性改變dialog的屬性,也支持組件內(nèi)部預(yù)設(shè)dialog的屬性
attrs = {};
get title() {
return this.t;
}
setTitle(title) {
this.t = title;
}
}
3.3 slots 的處理
查看 Element-UI 的官方網(wǎng)站,我們發(fā)現(xiàn),ElDialog 有三個插槽,因此,我們需要接管這三個插槽
1、對 header 的處理
import Vue from "vue";
import Component from "vue-class-component";
@Component({
name: "AbstractDialog",
})
class AbstractDialog extends Vue {
/*
構(gòu)建彈窗的Header
*/
_createHeader(h) {
// 判斷在調(diào)用的時候,外界是否傳入header的插槽,若有的話,則以外界傳入的插槽為準(zhǔn)
var slotHeader = this.$scopedSlots["header"] || this.$slots["header"];
if (typeof slotHeader === "function") {
return slotHeader();
}
//若用戶沒有傳入插槽,則判斷用戶是否想改寫Header
var renderHeader = this.renderHeader;
if (typeof renderHeader === "function") {
return <div slot="header">{renderHeader(h)}</div>;
}
//如果都沒有的話, 返回undefined,則dialog會使用我們預(yù)設(shè)好的title
}
}
2、對 body 的處理
import Vue from "vue";
import Component from "vue-class-component";
@Component({
name: "AbstractDialog",
})
class AbstractDialog extends Vue {
/**
* 構(gòu)建彈窗的Body部分
*/
_createBody(h) {
// 判斷在調(diào)用的時候,外界是否傳入default的插槽,若有的話,則以外界傳入的插槽為準(zhǔn)
var slotBody = this.$scopedSlots["default"] || this.$slots["default"];
if (typeof slotBody === "function") {
return slotBody();
}
//若用戶沒有傳入插槽,則判斷用戶想插入到body部分的內(nèi)容
var renderBody = this.renderBody;
if (typeof renderBody === "function") {
return renderBody(h);
}
}
}
3、對 footer 的處理
由于 dialog 的 footer 經(jīng)常都有一些相似的業(yè)務(wù),因此,我們需要把這些重復(fù)率高的代碼封裝在此,若在某種時候,用戶需要改寫 footer 的時候,再重寫,否則使用默認(rèn)行為
import Vue from "vue";
import Component from "vue-class-component";
@Component({
name: "BaseDialog",
})
export default class BaseDialog extends Vue {
showLoading() {
this.loading = true;
}
closeLoading() {
this.loading = false;
}
onSubmit() {
this.closeDialog();
}
onClose() {
this.closeDialog();
}
/**
* 構(gòu)建彈窗的Footer
*/
_createFooter(h) {
var footer = this.$scopedSlots.footer || this.$slots.footer;
if (typeof footer == "function") {
return footer();
}
var renderFooter = this.renderFooter;
if (typeof renderFooter === "function") {
return <div slot="footer">{renderFooter(h)}</div>;
}
return this.defaultFooter(h);
}
defaultFooter(h) {
return (
<div slot="footer">
<el-button
type="primary"
loading={this.loading}
on-click={() => {
this.onSubmit();
}}
>
保存
</el-button>
<el-button
on-click={() => {
this.onClose();
}}
>
取消
</el-button>
</div>
);
}
}
最后,我們再通過 JSX 將我們編寫的這些代碼組織起來,就得到了我們最終想要的抽象彈窗
代碼如下:
import Vue from "vue";
import Component from "vue-class-component";
@Component({
name: "AbstractDialog",
})
export default class AbstractDialog extends Vue {
visible = false;
t = "";
loading = false;
attrs = {};
get title() {
return this.t;
}
setTitle(title) {
this.t = title;
}
open() {
console.log("彈窗打開,我啥也不做");
}
close() {
console.log("彈窗關(guān)閉,我啥也不做");
}
opened() {
console.log("彈窗打開,我啥也不做");
}
closed() {
console.log("彈窗關(guān)閉,我啥也不做");
}
showLoading() {
this.loading = true;
}
closeLoading() {
this.loading = false;
}
openDialog() {
this.visible = true;
}
closeDialog() {
if (this.loading) {
this.$message.warning("請等待操作完成!");
return;
}
this.visible = false;
}
onSubmit() {
this.closeDialog();
}
onClose() {
this.closeDialog();
}
/*
構(gòu)建彈窗的Header
*/
_createHeader(h) {
var slotHeader = this.$scopedSlots["header"] || this.$slots["header"];
if (typeof slotHeader === "function") {
return slotHeader();
}
var renderHeader = this.renderHeader;
if (typeof renderHeader === "function") {
return <div slot="header">{renderHeader(h)}</div>;
}
}
/**
* 構(gòu)建彈窗的Body部分
*/
_createBody(h) {
var slotBody = this.$scopedSlots["default"] || this.$slots["default"];
if (typeof slotBody === "function") {
return slotBody();
}
var renderBody = this.renderBody;
if (typeof renderBody === "function") {
return renderBody(h);
}
}
/**
* 構(gòu)建彈窗的Footer
*/
_createFooter(h) {
var footer = this.$scopedSlots.footer || this.$slots.footer;
if (typeof footer == "function") {
return footer();
}
var renderFooter = this.renderFooter;
if (typeof renderFooter === "function") {
return <div slot="footer">{renderFooter(h)}</div>;
}
return this.defaultFooter(h);
}
defaultFooter(h) {
return (
<div slot="footer">
<el-button
type="primary"
loading={this.loading}
on-click={() => {
this.onSubmit();
}}
>
保存
</el-button>
<el-button
on-click={() => {
this.onClose();
}}
>
取消
</el-button>
</div>
);
}
createContainer(h) {
//防止外界誤傳參數(shù)影響彈窗本來的設(shè)計,因此,需要將某些參數(shù)過濾開來,有title beforeClose, visible
var { title, beforeClose, visible, ...rest } = Object.assign({}, this.$attrs, this.attrs);
return (
<el-dialog
{...{
props: {
...rest,
visible: this.visible,
title: this.title || title || "彈窗",
beforeClose: this.closeDialog,
},
on: {
close: this.close,
closed: this.closed,
opened: this.opened,
open: this.open,
},
}}
>
{/* 根據(jù)JSX的渲染規(guī)則 null、 undefined、 false、 '' 等內(nèi)容將不會在頁面顯示,若createHeader返回undefined,將會使用默認(rèn)的title */}
{this._createHeader(h)}
{this._createBody(h)}
{this._createFooter(h)}
</el-dialog>
);
}
render(h) {
return this.createContainer(h);
}
}
4.應(yīng)用
4.1組件調(diào)用
我們就以編寫 ChinaMapDialog.vue 為例,將其進(jìn)行改寫
<script>
import Vue from "vue";
import AbstractDialog from "@/components/AbstractDialog.vue";
import Component from "vue-class-component";
@Component({
name: "ChinaMapDialog",
})
class ChinaMapDialog extends AbstractDialog {
get title() {
return "這是中國地圖";
}
attrs = {
width: "600px",
}
//編寫一些中國地圖的處理業(yè)務(wù)邏輯代碼
//編寫彈窗的內(nèi)容部分
renderBody(h) {
return <div>我是中國地圖,我講為你呈現(xiàn)華夏最壯麗的美</div>;
}
}
</script>
4.2 使用 Composition API
由于我們是通過組件的實例調(diào)用組件的方法,因此我們每次都需要獲取當(dāng)前組件的 refs 上面的屬性,這樣會使得我們的調(diào)用特別長,寫起來也特別麻煩。
我們可以通過使用 Composition API 來簡化這個寫法
<template>
<div>
<china-map-dialog ref="china"></china-map-dialog>
<usa-map-dialog ref="usa"></usa-map-dialog>
<uk-map-dialog ref="uk"></uk-map-dialog>
<el-button @click="openChina">打開中國地圖</el-button>
<el-button @click="openUSA">打開美國地圖</el-button>
<el-button @click="openUK">打開英國地圖</el-button>
</div>
</template>
<script>
import { ref } from "@vue/composition-api";
export default {
name: "View",
setup() {
const china = ref(null);
const usa = ref(null);
const uk = ref(null);
return {
china,
usa,
uk,
};
},
data() {
return {
/**
將地圖的業(yè)務(wù)全部抽離到對應(yīng)的dialog里面去,View只存放調(diào)度業(yè)務(wù)代碼
*/
};
},
methods: {
// 對百度地圖和谷歌地圖的一些業(yè)務(wù)處理代碼 省略
openChina() {
this.china && this.china.openDialog();
},
openUSA() {
this.usa && this.usa.openDialog();
},
openUK() {
this.uk && this.uk.openDialog();
},
},
};
</script>
總結(jié)
開發(fā)這個彈窗所用到的知識點:
1、面向?qū)ο笤O(shè)計在前端開發(fā)中的應(yīng)用;
2、如何編寫基于類風(fēng)格的組件(vue-class-component 或 vue-property-decorator);
3、JSX 在 vue 中的應(yīng)用;
4、$attrs和$listeners 在開發(fā)高階組件(個人叫法)中的應(yīng)用;
5、slots 插槽,以及插槽在 JSX 中的用法;
6、在 Vue2.x 中使用 Composition API;
到此這篇關(guān)于詳解如何在vue+element-ui的項目中封裝dialog組件的文章就介紹到這了,更多相關(guān)vue element封裝dialog內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于vue-admin-element中的動態(tài)加載路由
這篇文章主要介紹了關(guān)于vue-admin-element的動態(tài)加載路由,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08
一文詳解Vue如何整合Echarts實現(xiàn)可視化界面
ECharts,縮寫來自Enterprise Charts,商業(yè)級數(shù)據(jù)圖表,一個純Javascript的圖表庫,可以流暢的運行在PC和移動設(shè)備上。本文將在Vue中整合Echarts實現(xiàn)可視化界面,感興趣的可以了解一下2022-04-04
解決vue3中from表單嵌套el-table時填充el-input,v-model不唯一問題
這篇文章主要給大家介紹一下如何解決vue3中from表單嵌套el-table時填充el-input,v-model不唯一問題,文中有相關(guān)的解決方法,通過代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2023-07-07
Vue實現(xiàn)電梯樣式錨點導(dǎo)航效果流程詳解
這篇文章主要介紹了Vue實現(xiàn)電梯樣式錨點導(dǎo)航效果流程,這種導(dǎo)航效果廣泛應(yīng)用于商城點餐購物情景,文中通過示例代碼介紹的很詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-05-05
vue項目預(yù)覽excel表格功能(file-viewer插件)
這篇文章主要介紹了vue項目預(yù)覽excel表格功能(file-viewer插件),本文分步驟結(jié)合實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2023-10-10

