问题
项目中存在较多 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 组件。