我們在vue項目開發過程中總是遇到各種個樣的全局彈窗(權限提醒,alert提示,繳費提醒),這些全局彈窗全站很多地方在調用,調用調用很蠻煩,將這些組件部署到全局位置,又無法做到按需加載,而且部署到全局的組件數據回傳(比如檢測繳費是否完成)又是一個問題,我們又得借助vuex或者event來傳遞,又回造成vuex的濫用。
我們舉一個簡單的場景:
如果我們要在vue里實現一個類似原生alert的彈出框,大概長這個樣子。
<template>
<div class="popup_background">
<div>
<div v-html="title"></div>
<button @click="ok">確定</button>
<button @click="cancel">否定</button>
</div>
</div>
</template>
<script>
export default {
props: ['text'],
data() {
return {};
},
methods: {
ok() {
this.$emit('ok');
},
cancel() {
this.$emit('cancel');
}
}
}
</script>
如果有一個頁面,需要使用這個組件,即使是空頁面,也要做下面代碼里描述的所有事情
<template>
<div>
<button @click="showAlert">打開alert</button>
<ui-alert v-if="isShow" title="這是標題" @ok="fine" @cancel="cancel"></ui-alert>
</div>
</template>
<script>
import uiAlert from '******.vue'; // 引用組件
export default {
data() {
return {
isShow:false //控制彈窗是否顯示的開關
};
},
methods: {
showAlert() { //控制彈窗顯示
this.isShow = true;
},
fine() { //彈窗打開后點擊ok的事件綁定
this.isShow = false;
// 點擊ok觸發的事情
},
cancel() { //彈窗打開后點擊cancel的事件綁定
this.isShow = false;
// 點擊cancel觸發的事情
}
},
components:{uiAlert}
}
</script>
匯總一下,做的事情有
1、引用組件
2、設置控制顯示的變量開關
3、設置彈出組件的函數
4、設置多個回調事件,監聽返回值
但是我們想一下,假如我們可以像調用原生組件類似的形式,一個函數搞定,會更方便。獲取返回值的方式比如promise、回調。比如我們封裝成返回promise的一個函數,代碼可能像下面這樣。
<template>
<div>
<button @click="showAlert">打開alert</button>
</div>
</template>
<script>
import popAlert from '*******';
export default {
methods: {
showAlert() {
popAlert('這是標題').then(res=>{
// res 就是true或者false,代表點擊了ok還是cancel
})
}
}
}
</script>
這樣調用起來很方便,我們可以看看類似alert這樣的彈出框,有一些特點,就是過程化,它從被創建到使用,再到最后銷毀,這個過程相對調用的頁面是獨立的,交互過程是高度過程化的。不像其他組件和頁面直接可能多次交互。簡單來說,就是“用完即刪”。再有就是布局獨立,布局層面跟調用者不存在占位歸屬,更像是全局的組件。
只要符合這種特點的組件,封裝成函數,是更方便的策略。
那么如何封裝出一個類似剛才例子中popAlert這樣的函數呢?我們要保留組件要使用vue的開發方式,也就是說,我們要把已經存在的vue組件不進行任何改動,在組件外對它通過包裝的形式形成一個函數。這樣vue組件可以向前兼容,并且組件繼續使用vue的開發方式。
全局按需動態加載
我想到的實現方式,就是利用render函數。我們在一個“全局”的地方,所有頁面都會加載的最外層的模板,比如跟路由配置的那個App.vue。在這里添加我們要開發的,可以動態改變狀態的組件。
<template>
<div id="app">
<router-view/>
<!--全局的地方添加一個組件-->
<dynamic-vue></dynamic-vue>
</div>
</template>
.........
我們看一下dynamic-vue的實現
<!--一個用于創建臨時vue對象的容器,例如選擇彈窗,提示窗,或其他場景下使用的"用完即刪"型需求-->
<script>
export default {
data() {
return {
globleVue: null // 臨時動態添加的窗體,例如彈窗等
/*
{
vueObj: 'vue組件',
attr:{ //初始化組件的props
},
on:{ //組件的emit回調綁定
}
}
*/
}
},
render(createElement) {
if(this.globleVue){
return createElement(this.globleVue.vueObj, {
props: this.globleVue.attr,
on: this.globleVue.event
});
}
else {
return createElement('div');
}
}
}
</script>
接下來我們只要能夠通過某些方法把globleVue賦值成包含vue組件的對象,初始化的參數,回調方法的對象,全局就會彈出這個組件。我的實現方式是使用自帶的events對globleVue進行修改。我們在created里面增加event的監聽。
<script>
import { dynamicVueEvent } from 'dynamicVueObject.js';
export default {
created() {
// 支持全局調用臨時添加,用完即刪型vue組件
dynamicVueEvent.on('addVueObj', (vueObj, attr, event) => {
this.globleVue = {
vueObj,
attr,
event
};
});
dynamicVueEvent.on('deleteVueObj', () => {
this.globleVue = null;
});
},
data() {
return {
globleVue: null
}
},
render(createElement) {
if(this.globleVue){
return createElement(this.globleVue.vueObj, {
props: this.globleVue.attr,
on: this.globleVue.event
});
}
else {
return createElement('div');
}
}
}
</script>
觸發 dynamicVueEvent 事件的代碼。
// dynamicVueObject.js
import Vue from 'vue';
var events = require('events');
var eventEmitter = new events.EventEmitter();
export function createVueObj(vueClass, attr, event) {
let vueObj = Vue.extend(vueClass);
dynamicVueEvent.emit('addVueObj', vueObj, attr, event);
}
export function deleteVueObj() {
dynamicVueEvent.emit('deleteVueObj');
}
dynamicVueObject.js暴露了兩個函數,一個動態創建組件createVueObj,一個清空組件deleteVueObj。我們就有了在全局動態控制彈出組件的這個功能。我們可以在任何地方通過這兩個函數控制全局彈窗了。
接下來我們就可以對已經存在的alert.vue組件,不進行修改的基礎上,單獨封裝一個獨立的函數。
我們在原始的alert.vue旁邊創建一個alert.js,這樣調用者很容易發現和調用。
/*
alert.js
alert.vue組件函數化
*/
// 引入全局創建組件,和全局刪除組件兩個方法
import { createVueObj, deleteVueObj } from '../dynamicVueObject/dynamicVueObject';
import alertVue from './alert.vue'; // 引入要使用的組件
export function alert(text) {
return new Promise((resolve, reject) => {
let dyVueObj = createVueObj(alertVue, {
text
}, {
ok() {
resolve(true);
deleteVueObj(dyVueObj); // 刪除掉彈出的組件
},
cancel() {
resolve(false);
deleteVueObj(dyVueObj); // 刪除掉彈出的組件
}
});
});
}
這樣alert函數封裝完畢了。不到20行就可以針對alert.vue封裝一個promise方式的alert,很方便,接下來我們就可以調用一下這個alert函數
<template>
<div>
<button @click="showAlert">打開alert</button>
</div>
</template>
<script>
import popAlert from '****/alert.js';
export default {
methods: {
showAlert() {
popAlert('這是標題').then(res=>{
// res 就是true或者false,代表點擊了ok還是cancel
})
}
}
}
</script>
這樣調用就方便多了!!!
大概實現邏輯就是如此了。
后續做的優化
1、不再使用手動部署dynamicVueObject.vue到一個全局的地方。
不再使用手動部署dynamicVueObject.vue到一個全局的地方,而是在dynamicVueObject.js第一次執行的時候,在body上添加一個動態創建的新div上。
let dyVueObj = Vue.extend({
/*****dynamicVueObject.vue******/
});
// 添加到一個動態的div上
let instance = new dyVueObj({
el: document.createElement('div'),
});
// 添加到body上
document.body.appendChild(instance.$el);
export function createVueObj(vueClass, attr, event) {
let vueObj = Vue.extend(vueClass);
dynamicVueEvent.emit('addVueObj', vueObj, attr, event);
return vueObj;
}
export function deleteVueObj(obj) {
dynamicVueEvent.emit('deleteVueObj', obj);
}
2、支持同時彈出多個組件
allGlobleVue改為了一個數組,支持添加多個彈出組件createVueObj方法的返回值,作為deleteVueObj刪除方法的參數。
let dyVueObj = Vue.extend({
created() {
// 支持全局調用臨時添加,用完即刪型vue組件
dynamicVueEvent.on('addVueObj', (vueObj, attr, event) => {
this.allGlobleVue.push({
vueObj, attr, event
});
});
dynamicVueEvent.on('deleteVueObj', (vueObj) => {
this.allGlobleVue.splice(this.allGlobleVue.findIndex(item => {
return item.vueObj === vueObj
}), 1);
});
},
data() {
return {
allGlobleVue: [] // 臨時動態添加的窗體,例如彈窗等
}
},
render(createElement) {
return createElement('div', {}, this.allGlobleVue.map(item => {
return createElement(item.vueObj, {
props: item.attr,
on: item.event
})
}));
}
});
完整代碼的git倉庫是
https://github.com/13601313270/dynamicVueObject
完整的功能,支持同時彈出多個組件,git倉庫里包含一個可以跑起來的demo。