怎么vue与element-ui项目中对dialog组件进行封装
创始人
2024-12-18 20:43:29
0次

1、问题起源

由于 Vue 基于组件化的设计,得益于这个思想,我们在 Vue 的项目中可以通过封装组件提高代码的复用性。根据我目前的使用心得,知道 Vue 拆分组件至少有两个优点:

1、代码复用。

2、代码拆分

在基于 element-ui 开发的项目中,可能我们要写出一个类似的调度弹窗功能,很容易编写出以下代码:


上述代码存在的问题非常多,首先当我们的弹窗越来越多的时候,我们会发现此时需要定义越来越多的变量去控制这个弹窗的显示或者隐藏。

由于当我们的弹窗的内部还有业务逻辑需要处理,那么此时会有相当多的业务处理代码夹杂在一起(比如我调用中国地图我需要用高德地图或者百度地图,而调用美国、英国地图我只能用谷歌地图,这会使得两套业务逻辑分别位于一个文件,严重加大了业务的耦合度)

我们按照分离业务,降低耦合度的原则,将代码按以下思路进行拆分:

1、View.vue


2、ChinaMapDialog.vue


3、由于此处仅仅展示伪代码,且和 ChinaMapDialog.vue 表达的含义一致, 为避免篇幅过长 USAMapDialog.vue 和 UKMapDialog.vue 已省略

2、问题分析

我们通过对这几个弹窗的分析,对刚才的设计进行抽象发现,这里面都有一个共同的部分,那就是我们对 dialog 的操作代码都是可以重用的代码,如果我们能够编写出一个抽象的弹窗,
然后在恰当的时候将其和业务代码进行组合,就可以实现 1+1=2 的效果。

3、设计

由于 Vue 在不改变默认的 mixin 原则(默认也最好不要改变,可能会给后来的维护人员带来困惑)的情况下,如果在混入过程中发生了命名冲突,默认会将方法合并(数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先),因此,mixin 无法改写本来的实现,而我们期望的是,父类提供一个比较抽象的实现,子类继承父类,若子类需要改表这个行为,子类可以重写父类的方法(多态的一种实现)。

因此我们决定使用 vue-class-component 这个库,以类的形式来编写这个抽象弹窗。

import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",
})
export default class AbstractDialog extends Vue {}

3.1 事件处理

查看 Element-UI 的官方网站,我们发现 ElDialog 对外抛出 4 个事件,因此,我们需要预先接管这 4 个事件。
因此需要在我们的抽象弹窗里预设这个 4 个事件的 handler(因为对于组件的行为的划分,而对于弹窗的处理本来就应该从属于弹窗本身,因此我并没有通过$listeners 去穿透外部调用时的监听方法)

import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",
})
export default class AbstractDialog extends Vue {
  open() {
    console.log("弹窗打开,我啥也不做");
  }

  close() {
    console.log("弹窗关闭,我啥也不做");
  }

  opened() {
    console.log("弹窗打开,我啥也不做");
  }

  closed() {
    console.log("弹窗关闭,我啥也不做");
  }
}

3.2 属性处理

dialog 有很多属性,默认我们只需要关注的是 before-close 和 title 两者,因为这两个属性从职责上划分是从属于弹窗本身的行为,所以我们会在抽象弹窗里面处理开关和 title 的任务

import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",
})
export default class AbstractDialog extends Vue {
  visible = false;

  t = "";

  loading = false;

  //定义这个属性的目的是为了实现既可以外界通过传入属性改变dialog的属性,也支持组件内部预设dialog的属性
  attrs = {};

  get title() {
    return this.t;
  }

  setTitle(title) {
    this.t = title;
  }
}

3.3 slots 的处理

查看 Element-UI 的官方网站,我们发现,ElDialog 有三个插槽,因此,我们需要接管这三个插槽

1、对 header 的处理

import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",
})
class AbstractDialog extends Vue {
  /*
   构建弹窗的Header
   */
  _createHeader(h) {
    // 判断在调用的时候,外界是否传入header的插槽,若有的话,则以外界传入的插槽为准
    var slotHeader = this.$scopedSlots["header"] || this.$slots["header"];
    if (typeof slotHeader === "function") {
      return slotHeader();
    }
    //若用户没有传入插槽,则判断用户是否想改写Header
    var renderHeader = this.renderHeader;
    if (typeof renderHeader === "function") {
      return {renderHeader(h)}
;     }     //如果都没有的话, 返回undefined,则dialog会使用我们预设好的title   } }

2、对 body 的处理

import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",
})
class AbstractDialog extends Vue {
  /**
   * 构建弹窗的Body部分
   */
  _createBody(h) {
    // 判断在调用的时候,外界是否传入default的插槽,若有的话,则以外界传入的插槽为准
    var slotBody = this.$scopedSlots["default"] || this.$slots["default"];
    if (typeof slotBody === "function") {
      return slotBody();
    }
    //若用户没有传入插槽,则判断用户想插入到body部分的内容
    var renderBody = this.renderBody;
    if (typeof renderBody === "function") {
      return renderBody(h);
    }
  }
}

3、对 footer 的处理

由于 dialog 的 footer 经常都有一些相似的业务,因此,我们需要把这些重复率高的代码封装在此,若在某种时候,用户需要改写 footer 的时候,再重写,否则使用默认行为

import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "BaseDialog",
})
export default class BaseDialog extends Vue {
  showLoading() {
    this.loading = true;
  }

  closeLoading() {
    this.loading = false;
  }

  onSubmit() {
    this.closeDialog();
  }

  onClose() {
    this.closeDialog();
  }

  /**
   * 构建弹窗的Footer
   */
  _createFooter(h) {
    var footer = this.$scopedSlots.footer || this.$slots.footer;
    if (typeof footer == "function") {
      return footer();
    }
    var renderFooter = this.renderFooter;
    if (typeof renderFooter === "function") {
      return {renderFooter(h)}
;     }     return this.defaultFooter(h);   }   defaultFooter(h) {     return (                 {             this.onSubmit();           }}         >           保存                   {             this.onClose();           }}         >           取消                
    );   } }

最后,我们再通过 JSX 将我们编写的这些代码组织起来,就得到了我们最终想要的抽象弹窗
代码如下:

import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",
})
export default class AbstractDialog extends Vue {
  visible = false;

  t = "";

  loading = false;

  attrs = {};

  get title() {
    return this.t;
  }

  setTitle(title) {
    this.t = title;
  }

  open() {
    console.log("弹窗打开,我啥也不做");
  }

  close() {
    console.log("弹窗关闭,我啥也不做");
  }

  opened() {
    console.log("弹窗打开,我啥也不做");
  }

  closed() {
    console.log("弹窗关闭,我啥也不做");
  }

  showLoading() {
    this.loading = true;
  }

  closeLoading() {
    this.loading = false;
  }

  openDialog() {
    this.visible = true;
  }

  closeDialog() {
    if (this.loading) {
      this.$message.warning("请等待操作完成!");
      return;
    }
    this.visible = false;
  }

  onSubmit() {
    this.closeDialog();
  }

  onClose() {
    this.closeDialog();
  }

  /*
   构建弹窗的Header
   */
  _createHeader(h) {
    var slotHeader = this.$scopedSlots["header"] || this.$slots["header"];
    if (typeof slotHeader === "function") {
      return slotHeader();
    }
    var renderHeader = this.renderHeader;
    if (typeof renderHeader === "function") {
      return {renderHeader(h)}
;     }   }   /**    * 构建弹窗的Body部分    */   _createBody(h) {     var slotBody = this.$scopedSlots["default"] || this.$slots["default"];     if (typeof slotBody === "function") {       return slotBody();     }     var renderBody = this.renderBody;     if (typeof renderBody === "function") {       return renderBody(h);     }   }   /**    * 构建弹窗的Footer    */   _createFooter(h) {     var footer = this.$scopedSlots.footer || this.$slots.footer;     if (typeof footer == "function") {       return footer();     }     var renderFooter = this.renderFooter;     if (typeof renderFooter === "function") {       return {renderFooter(h)}
;     }     return this.defaultFooter(h);   }   defaultFooter(h) {     return (                 {             this.onSubmit();           }}         >           保存                   {             this.onClose();           }}         >           取消                
    );   }   createContainer(h) {     //防止外界误传参数影响弹窗本来的设计,因此,需要将某些参数过滤开来,有title beforeClose, visible     var { title, beforeClose, visible, ...rest } = Object.assign({}, this.$attrs, this.attrs);     return (                {/* 根据JSX的渲染规则 null、 undefined、 false、 '' 等内容将不会在页面显示,若createHeader返回undefined,将会使用默认的title */}         {this._createHeader(h)}         {this._createBody(h)}         {this._createFooter(h)}            );   }   render(h) {     return this.createContainer(h);   } }

4.应用

4.1组件调用

我们就以编写 ChinaMapDialog.vue 为例,将其进行改写

4.2 使用 Composition API

由于我们是通过组件的实例调用组件的方法,因此我们每次都需要获取当前组件的 refs 上面的属性,这样会使得我们的调用特别长,写起来也特别麻烦。
我们可以通过使用 Composition API 来简化这个写法


总结

开发这个弹窗所用到的知识点:
1、面向对象设计在前端开发中的应用;
2、如何编写基于类风格的组件(vue-class-component 或 vue-property-decorator);
3、JSX 在 vue 中的应用;
4、$attrs和$listeners 在开发高阶组件(个人叫法)中的应用;
5、slots 插槽,以及插槽在 JSX 中的用法;
6、在 Vue2.x 中使用 Composition API;

相关内容

热门资讯

空调连续工作多久需要停机休息 空调连续工作的时间取决于多个因素,包括空调的型号、功率、使用环境和维护情况等。一般来说,空调可以连续...
空调哪个符号是除湿的 空调除湿功能通常用一个水滴形状的符号表示。不同品牌和型号的空调遥控器可能会有所差异,但一般来说,除湿...
格力空调多长时间加一次氟利昂 1、一般情况下,空调对氟利昂的使用量是非常小的,所以在空调使用了四年左右的时候再考虑加也没问题。2、...
空调用多长时间需要加氟一次 一般情况下是五年。当出现以下这几种情况时,空调才需要加氟。1、空调的室内机和室外机各有两个接口,当接...
格调生活·C标思享荟:卡萨帝在... 9月12日的郑州龙湖金融岛,夜风轻拂,灯光璀璨。“C标时刻共传奇——卡萨帝思享荟暨20周年品牌盛典郑...
品牌价值破千亿、高端份额持续领... 9月12日,“C标时刻共传奇——卡萨帝思享荟暨20周年品牌盛典郑州站”在郑州龙湖金融岛举办。2026...
空调主板坏了修800还是换新的 空调主板坏了是修还是换新,需要考虑多个因素,以下是一些建议:1. 维修成本:如果维修费用较低,比如 ...
空调开多久需要休息多长时间 问题:空调开多久需要休息多长时间回答:1、在使用空调的过程中,一般空调开7~8个小时左右需要休息2~...
中央空调多长时间清洗一次 1、一般空调开机前清洗一次即可,空调开机中间时段再清洗一次,空调关机时清洗一次,这样比较合理。2、空...
中央空调氟利昂多久加一次 看空调制热和制冷程度,很多人的观点认为氟利昂必须一年放一次,其实这个观点是错误的,理解上面有点偏差,...