vue页面中处理大量对话框

3,870 阅读2分钟

背景

最近项目中一个页面要用到很多个对话框(7-8个左右), 而且每个对话框的结构是不一样的, 有的底部需要确定-取消按钮,有的只要确定按钮, 有的啥都不要, 样式都不同, 对话框也有自己的内部逻辑, 要控制对话框的显示隐藏visible.sync,一个对话框就要声明一个控制显示的变量,管理这些变量会增多不少脏代码。某些场景对话框要顺序弹出关闭,比如dialog1填完一些表单数据后,拿到接口数据后,关闭当前对话框,弹出dialog2,并获取dialog1的接口数据。

*按照一开始在data声明变量控制对话框的显示隐藏, A组件这样写:

<template>
    <div>
        <el-dialog :visible.sync="dialogVisible.dialog1" >...html结构</el-dialog>
        <el-dialog :visible.sync="dialogVisible.dialog2" >...html结构</el-dialog>
        <el-dialog :visible.sync="dialogVisible.dialog3" >...html结构</el-dialog>
        <el-dialog :visible.sync="dialogVisible.dialog4" >...html结构</el-dialog>
        <el-dialog :visible.sync="dialogVisible.dialog5" >...html结构</el-dialog>   
        ...other code
    </div>
</template>
<script>
export default {
  data() {
    return {
      dialogVisible: {
        dialog1: false,
        dialog2: false,
        dialog3: false,
        dialog4: false,
        dialog5: false,
        dialog6: false,
        dialog7: false,
        dialog8: false,
        dialog9: false,
      }
    },
    method: {
        showDialog1() {
            this.dialog1 = true;
        },
        showDialog2() {
            this.dialog2 = true;
        },
    }
  }
}
</script>
// 然后整个组件加起来1000多行, 有时候一个el-dialog单html结构就100多行, 维护是十分痛苦的

要解决的问题

  • 不需要关注对话框显示隐藏
  • 通过指令的方式唤起一个对话框
  • 对话框按顺序显示和数据传递

解决过程

我的做法是基于el-dialog组件把对话框封装成全局弹窗组件, 提供了一个方法invoke呼出对话框并返回promise, 在对话框的内部可以resolverejectpromise, 这样就可以链式then呼出下一个对话框,并且通过promise传值。

下面是根据我的业务场景实现的全局对话框:

目录结构
.dialog
|____dialog.js
|____dialog.vue


// dialog.vue
<template>
  <el-dialog :visible.sync="visible" :title="title" @close="close" @open="open":width="width">
    <functionalComponent :params="params" :render="render" ></functionalComponent>
  </el-dialog>
</template>
<script>
const functionalComponent = {
  functional: true,
  props: {
    render: Function,
    params: Object
  },
  render(h, ctx) {
    const params = { ...ctx.props.params };
    return ctx.props.render(h, params);
  }
}
const noop = () => {};
export default {
  data() {
    return {
      title: '',
      width: '50%',
      render: noop,
      cancel: noop,
      open: noop, // '目前业务只用到了el-dialog中的close&open事件,可以扩展
      close: noop,
      visible: false,
      resolver: {},
      params: {},
    };
  },
  methods: {
    invoke(config) {
      this.initParams(config);
      return new Promise((resolve, reject) => {
        this.resolver = { resolve, reject };
      })
    },
    initParams(config) {
      this.visible = true;
      this.title = config.title;
      this.width = config.width;
      this.render = config.render;
      this.open = config.open;
      this.close = () => {
          this.close();
          this.hide();
      };
      this.cancel = () => {
          this.canel();
          this.hide();
      }
      this.params = config.params;
    }
    hide() {
      this.visible = false;
    },
    resolve(v) {
      this.resolver.resolve(v);
    },
    reject(err) {
      this.resolver.reject(new Error(err));
    },
  },
  components: {
      functionalComponent,
  }
}
</script>

// dialog.js
import Dialog from './dialog.vue';
import router from '@/router';
import store from '@/store'
import Vue from 'vue';
const noop = () => {};
let dialogInstance = null;
// 挂载Dialog并获得实例
const newInstance = properties => {
    const props = properties || {};
    const Instance = new Vue({
        data: props,
        store, // 因为这个是一个单独的Vue实例,跟root实例是隔离的,所以这里需要把store和router上去
        router,
        render: h => h(Dialog, {
            props
        })
    });
    const component = Instance.$mount();
    document.body.appendChild(component.$el);
    return Instance.$children[0];
};
// 全局只有一个实例, 每次invoke的时候只是在更换functionalComponent的内容,且返回新的promise(pending态)
const getDialogInstance = () => {
    dialogInstance = dialogInstance || newInstance();
    return dialogInstance;
}

export default {
    invoke({
        title = '',
        render = noop,
        close = noop,
        open = noop,
        params = {},
        width = '50%'
    } = {}) {
        return getDialogInstance().invoke({title, render, open, close, params, width})
    },
    hide() {
        getDialogInstance().hide();
    },
    resolve(v) {
        getDialogInstance().resolve(v);
    },
    reject(err) {
        getDialogInstance().reject(err);
    },
}

使用方式

全局引入:
import Dialog from '@/components/dialog';
Vue.prototype.$dialog = Dialog;

在views文件目录下:
.dialog
|____dialog1.vue
|____dialog2.vue
|____dialog3.vue
|____index.vue
// dialog1.vue
<template>
  <div>
    ...假设很多行代码
    <button @click="showNextDialog">显示下一个对话框并传递数据</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      name: 'jiaxin'
    }
  },
  methods: {
    showNextDialog() {
    // 使用定时器模拟http请求,2秒后显示下一个对话框
      setTimeout(() => {
        this.$dialog.resolve(this.name);
      }, 2000);
    }
  },
}
</script>

// dialog2.vue
<template>
  <div>
    ...假设很多行代码
    <button @click="showNextDialog">显示下一个对话框并传递数据</button>
  </div>
</template>
<script>
export default {
  props: {
    name: String
  },
  methods: {
    showNextDialog() {
    // 使用定时器模拟http请求,2秒后显示下一个对话框
      setTimeout(() => {
        this.$dialog.resolve(10);
      }, 2000);
    }
  },
}
</script>

// dialog3.vue
<template>
  <div>
    ...假设很多行代码
    <button @click="closeDialog">关闭对话框</button>
  </div>
</template>
<script>
  export default {
    props: {
      hobby: String,
      age: String,
    },
    methods: {
      closeDialog () {
        setTimeout(() => {
          console.log(this.body);
          this.$dialog.hide(); // 关闭对话框
        }, 1000);
      }
    },
  }
</script>

// index.vue
<template>
    <div>
        ...假设这里有几百行结构
        <button @click="showDialog1">显示对话框1</button>
    </div>
</template>
<script>
import Dialog1 from './dialog1';
import Dialog2 from './dialog2';
import Dialog3 from './dialog3';
  export default {
    data() {
      return {
        hobby: 'eat'
      }
    },
    methods: {
        async showDialog1() {
            const name = await this.$dialog.invoke({
                title: '对话框1',
                render: (h) => h(Dialog1),
            });
            const age = await this.$dialog.invoke({
                title: '对话框2',
                render: (h) => <Dialog2 name={name}></Dialog2>,
            });

            this.$dialog.invoke({
                title: '对话框3',
                params: {
                    hobby: this.hobby,
                },
                render: (h, { hobby }) => <Dialog3 hobby={hobby} age={age} />
            });
        },
    }
  }
</script>
// 顺序是显示对话框1,对话框2,对话框3,通过promise的then链式调用

使用自定义指令美化this.$dialog.resolve()

当我们在http请求完成成功后才会显示下一个对话框,并且把请求数据带给下一个对话框,如果返回一个promise, this.$dialog.resolve()在promise成功后才会执行, 否则直接把binding的值传递给下个对话框

Vue.directive('dialog', {
  inserted: (el, binding, vnode) => {
    el.onclick = () => {
      const bind = binding.value;
      const bindingVal = typeof bind === 'function' ? bind() : bind;
      const resolve = vnode.context.$dialog.resolve;
      if (bindingVal instanceof Promise) {
        bindingVal.then(resolve);
      } else {
        resolve(bindingVal);
      }
    }
  }
})

// dialog1.vue
<template>
  <div>
    ...假设很多行代码
    <button v-dialog="showNextDialog">显示下一个对话框并传递数据</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      name: 'jiaxin'
    }
  },
  methods: {
      showNextDialog() {
        return new Promise(rs => setTimeout(rs, 2000));
      }
  },
}
// 此时效果是一样, 当然可以对this.$dialog.hide()做同样的处理,这里就不说了
</script>

总结

上面封装的dialog全局方法并不适用于所有场景,主要是针对我这边的业务,不过方向是对的,主要是用promise来实现实现异步呼出对话框和数据之间的通信。解决了上面所说的问题。

demo地址