弹窗的使用方式优化

345 阅读1分钟

开始

组内很多项目是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;
}

使用

现在需要在原来的弹窗组件上改动两个地方:

  1. 外层增加IS_DIALOG_SHOW
  2. 方法替换,比如关闭用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, { /* */ })