【分享】Dialog业务组件函数式调用

143 阅读1分钟

问题

项目中存在较多 Dialog 组件跨页面使用的状况,带来的问题如下

<script>
...
import LogisticsInvoiceDialog from '@/views/fba/logisticsInvoice/components/LogisticsInvoiceDialog';
...
export default {
    ...
    components: {
        ...
        LogisticsInvoiceDialog,
        ...
    },
    data() {
        return {
            // 导出报关资料相关
            logisticsInvoiceProps: {
                logisticsInvoiceType: 'export'
            },
            logisticsInvoiceVisible: false,
            mountLogisticsInvoiceDialog: false
        };
    },
    ...
}
</script>

<template>
    ...
    <!-- 导出报关资料 -->
    <LogisticsInvoiceDialog
      v-if="mountLogisticsInvoiceDialog"
      ref="logisticsInvoiceDialog"
      exportApi="postLogisticsInvoiceTemplateShippingOrderExport"
      :logisticsInvoiceProps="logisticsInvoiceProps"
      :visible="logisticsInvoiceVisible"
      @close="switchLogisticsInvoiceDialog({ state: false })">
    </LogisticsInvoiceDialog>
    ...
</template>
  • 需要将 DOM 结构导入后注册
  • 需要将 DOM 结构添加到 template 上下文中
  • 需要将 dialog 组件依赖的数据模型声明在 data 中

当一个核心功能页面依赖非常多的 dialog 类组件后,导致源码量庞大,可维护性与拓展性都会大大降低, 当然使用 mixins 可以解决部分问题,但是随着 mixins 的数量增多也会随之带来很多副作用,可能会导致让你感到困惑的混乱,当前某个特定的功能是来自何处的时候,或是源自何种方法的时候。 所以采用函数式调用可能是目前比较好的一种解决方案。

函数式调用

<script>
...
import logisticsInvoiceDialog from '@/views/fba/logisticsInvoice/components/dialog';
...
export default {
    ...
    methods: {
        switchLogisticsInvoiceDialog({ dialogType, data, batch, selections }) {
            if (batch && selections.length === 0) this.$message.error('请至少选择一条数据');
            
            logisticsInvoiceDialog.open({
                logisticsInvoiceProps: {
                isEdit: true,
                logisticsInvoiceType: dialogType,
                exportIds: batch ? selections : [data.id]
                },
                exportApi: 'postLogisticsInvoiceTemplateShippingOrderExport'
            });
        }
    },
    ...
}
</script>

完全的依赖注入,无需在调用者上下文中在声明与之无关的业务逻辑

封装方案

通用版本

// index.js
import Vue from 'vue';
import store from '@/store';
import router from '@/router';
import DialogLogisticsInvoice from '@/views/fba/logisticsInvoice/components/TheLogisticsInvoiceDialog/DialogLogisticsInvoice';

const DialogLogisticsInvoiceComponent = Vue.extend(DialogLogisticsInvoice);
let app;
DialogLogisticsInvoice.open = (propsData = {}) => {
  if (!app) {
    app = new DialogLogisticsInvoiceComponent({
      propsData,
      router, // 内部组件依赖在注入
      store, // 内部组件依赖在注入
      watch: {
        $route() {
          this.$emit('destroyDialog');
        }
      }
    });
  }
  // 销毁 实例 DOM
  app.$on('destroyDialog', () => {
    app.$destroy();
    app.$el.remove();
    app = null;
  });
  app.$mount();
  document.body.appendChild(app.$el);
  // 延迟 保留动画效果
  Vue.nextTick(() => {
    app.open();
  });
};

export default DialogLogisticsInvoice;

keep-alive版本

用于开启keep-alive页面,离开页面只隐藏不销毁逻辑

也可以使用传递 this 实现

项目里多处使用无法单例化

import Vue from 'vue';
import store from '@/store';
import router from '@/router';
import DialogLogisticsInvoice from '@/views/fba/logisticsInvoice/components/TheLogisticsInvoiceDialog/DialogLogisticsInvoice';
import { mapState } from 'vuex';

const DialogLogisticsInvoiceComponent = Vue.extend(DialogLogisticsInvoice);

// 通过 init 方法实例化对象
DialogLogisticsInvoice.open = (propsData = {}) => {
   let app = new DialogLogisticsInvoiceComponent({
        propsData,
        router,
        store,
        data() {
            return {
                openRouterName: null,
            };
        },
        computed: {
            ...mapState('System', {
                keepAliveList: (state) => state.keepAliveList,
            }),
        },
        watch: {
            keepAliveList(nv) {
                const { openRouterName } = this;
                if (nv.includes(openRouterName)) {
                    console.log('reserve');
                } else {
                    console.log('destroy');
                }
            },
            $route: {
                handler({ name }) {
                    const { openRouterName } = this;
                    if (openRouterName === name) {
                        console.log('show');
                    } else {
                        console.log('hide');
                    }
                },
            },
        },
        mounted() {
            // First caller router name
            this.openRouterName = this.$route.name;
        },
    });
    // 销毁 实例 DOM
    app.$on('destroyDialog', () => {
        app.$destroy();
        app.$el.remove();
        app = null;
    });
    app.$mount();
    document.body.appendChild(app.$el);
    // 延迟 保留动画效果
    Vue.nextTick(() => {
        app.open();
    });
};

export default DialogLogisticsInvoice;
// TheLogisticsInvoiceDialog.vue
<script>
export default {
    name: 'DialogLogisticsInvoice',
    props: {
        logisticsInvoiceProps: {
            type: Object,
            default: () => {},
        },
        exportApi: {
            type: String,
            require: true,
            default: 'postLogisticsInvoiceTemplateSave',
        },
        submitCallBack: {
            type: Function,
            default: () => () => {
                // default function
            },
        },
    },

    data() {
        return {
            dialogVisible: false,
        };
    },
    computed: {
        logisticsInvoiceType() {
            return this.logisticsInvoiceProps.logisticsInvoiceType;
        },
        isEdit() {
            return this.logisticsInvoiceProps.isEdit;
        },
        isExport() {
            return this.logisticsInvoiceType === 'export';
        },
        logisticsInvoiceViewId() {
            return this.logisticsInvoiceProps.logisticsInvoiceViewId;
        },
        titlePrefix() {
            const prefixMap = {
                create: '创建导出模板 - ',
                view: '查看导出模板 - ',
                edit: '编辑导出模板 - ',
                export: '导出物流发票',
            };
            return prefixMap[this.logisticsInvoiceType] || '';
        },
    },
    mounted() {
        this.init();
    },
    methods: {
        ...
        open() {
            this.$nextTick(() => {
                this.dialogVisible = true;
            });
        },
        async init() {
            this.$store.dispatch('System/setLoading', { status: true });

            // 必要的await 网络异常会导致 echoDetailData处理异常
            await this.getLogisticsInvoiceTemplateConfig();

            const { logisticsInvoiceViewId: id, logisticsInvoiceType, logisticsInvoiceProps } = this;
            switch (logisticsInvoiceType) {
                case 'create':
                    this.templateName = logisticsInvoiceProps.templateName;
                    break;
                case 'view':
                case 'edit':
                    await this.echoDetailData(id);
                    break;
                case 'export':
                    this.templateName = '';
                    this.submitButtonText = '导 出';
                    await this.postLogisticsInvoiceTemplateList();
                    break;
                default:
                    break;
            }

            this.$store.dispatch('System/setLoading', { status: false });
        },
        ...
    },
};
</script>

<template>
    <el-dialog
        custom-class="dialog_middle"
        width="800px"
        :title="`${titlePrefix}${templateName}`"
        :visible.sync="dialogVisible"
        :modal="false"
        :close-on-click-modal="false"
        @closed="$emit('destroyDialog')"
    >
    ...
    // 正常的业务代码
    ...
    </el-dialog>
</template>

<style scoped lang="scss">
...
// 正常的style
</style>

在不改动业务逻辑的前提下,只需要在原有 methods 增加一个 open() 函数供 index.js 调用即可,组件所依赖的注入数据都可以在 DialogLogisticsInvoice.open() 中注入到组件,即可与调用者解耦,做到 dialog 组件的高内聚、低耦合,调用者无需关心使用 dailog 组件时内部的业务逻辑,只需要按照要求注入数据、绑定事件即可简单使用一个复杂的业务 dialog 组件。

扩展阅读

v2.vuejs.org/v2/api/#Vue…