开始
组内很多项目是Vue2写的,之前页面调用弹窗很麻烦,要引入弹窗组件,在data中声明showDialog,在methods中声明onShowDialog/onCloseDialog等至少两个方法,然后在template中还要写<Dialog v-if="showDialog" @onCloseDialog="onCloseDialog" :data="someData">。
可以看到引入一个弹窗要加这么多东西,既费劲又不容易维护。
原理
参考了Vant中Dialog的函数式的调用方法,this.$dialog.show({}).then(() => {}),在JS中完成弹窗的调用、回调等。具体实现方式是利用了Vue.extend,将弹窗组件作为构造器,生成一个子类。核心代码如下:
// dialog-handler.js
export default function DialogHandler({
dialog: VueDialog,
id,
customConfirm,
customCancel,
}) {
function initInstance() {
if (instance) {
instance.$destroy();
}
const dialogId = id || DEFAULT_ID;
const oldDialog = document.getElementById(dialogId);
if (oldDialog) {
document.body.removeChild(oldDialog);
}
const dialogRootDiv = document.createElement('div');
dialogRootDiv.id = dialogId;
document.body.appendChild(dialogRootDiv);
const inject = {
...INJECT_CONTENT,
};
if (customConfirm) inject.methods.CONFIRM = customConfirm;
if (customCancel) inject.methods.CANCEL = customCancel;
instance = new (Vue.extend(VueDialog))({
el: dialogRootDiv,
...inject,
});
}
function Dialog(options = {}) {
if (!instance || !isInDocument(instance.$el)) {
initInstance();
}
Object.assign(instance, Dialog.currentOptions, options);
}
Dialog.show = (options = {}) => {
Dialog({
...options,
});
return instance.SHOW_DIALOG().then((val) => {
instance = null;
return Promise.resolve(val);
})
.catch((err) => {
instance = null;
return Promise.reject(err);
});
};
Dialog.install = () => {
Vue.use(VueDialog);
};
Dialog.Component = VueDialog;
return Dialog;
}
使用
现在需要在原来的弹窗组件上改动两个地方:
- 外层增加
IS_DIALOG_SHOW - 方法替换,比如关闭用CANCEL,确认用CONFIRM,如果有其他逻辑,可以传入自定义的confirm方法
然后增加handler.js:
import Dialog from './index.vue';
import DialogHandler from 'xx/dialog-handler.js';
export default DialogHandler({
id: 'MATCH_INFO_LAYER',
dialog: Dialog,
});
最后页面使用的时候就能像Vant的dialog一样了:
import MatchIntroLayerHandler from 'xxx/dialog/handler.js'
MatchIntroLayerHandler.show({
data
})
优化
上述方法还要改之前的弹窗组件,属于侵入式的改动,需要进一步优化。
export function showComponentDialog(vueInstance, dialogComponent, dialogOptions) {
return new Promise((resolve) => {
if (typeof dialogComponent === 'function') {
dialogComponent().then((dialog) => {
const component = showDialog(vueInstance, dialog.default, dialogOptions);
if (component) {
resolve(component);
}
});
} else {
const component = showDialog(vueInstance, dialogComponent, dialogOptions);
if (component) {
resolve(component);
}
}
});
}
function showDialog(vueInstance, dialogComponent, dialogOptions) {
const dialogId = `dialog-id${new Date().getTime()}`;
if (document.getElementById(dialogId)) {
return;
}
const dialogRootDiv = document.createElement('div');
dialogRootDiv.id = dialogId;
document.body.appendChild(dialogRootDiv);
const VueComponent = Vue.extend(dialogComponent);
const component = new VueComponent({
propsData: dialogOptions,
}).$mount(dialogRootDiv);
if (vueInstance) {
function removeComponent() {
if (component) {
if (component.$destroy) {
component.$destroy();
}
if (document.body.contains(component.$el)) {
document.body.removeChild(component.$el);
}
}
}
vueInstance.$once('hook:deactivated', () => {
removeComponent();
});
vueInstance.$once('hook:destroyed', () => {
removeComponent();
});
}
return component;
}
使用方式:
showComponentDialog(this,
() => import('xxx.vue'),
{
show: true,
fn: () => {}
}).then(() => {
})
// or
showComponentDialog(this, comp, { /* */ })