详解如何在vue+element-ui的项目中封装dialog组件


Posted in Vue.js onDecember 11, 2020

1、问题起源

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

1、代码复用。

2、代码拆分

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

<template>
  <div>
    <el-dialog :visible.sync="cnMapVisible">我是中国地图的弹窗</el-dialog>
    <el-dialog :visible.sync="usaMapVisible">我是美国地图的弹窗</el-dialog>
    <el-dialog :visible.sync="ukMapVisible">我是英国地图的弹窗</el-dialog>
    <el-button @click="openChina">打开中国地图</el-button>
    <el-button @click="openUSA">打开美国地图</el-button>
    <el-button @click="openUK">打开英国地图</el-button>
  </div>
</template>
<script>
export default {
  name: "View",
  data() {
    return {
      // 对百度地图和谷歌地图的一些业务处理代码 省略
      cnMapVisible: false,
      usaMapVisible: false,
      ukMapVisible: false,
    };
  },
  methods: {
    // 对百度地图和谷歌地图的一些业务处理代码 省略
    openChina() {},
    openUSA() {},
    openUK() {},
  },
};
</script>

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

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

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

1、View.vue

<template>
  <div>
    <china-map-dialog ref="china"></china-map-dialog>
    <usa-map-dialog ref="usa"></usa-map-dialog>
    <uk-map-dialog ref="uk"></uk-map-dialog>
    <el-button @click="openChina">打开中国地图</el-button>
    <el-button @click="openUSA">打开美国地图</el-button>
    <el-button @click="openUK">打开英国地图</el-button>
  </div>
</template>
<script>
export default {
  name: "View",
  data() {
    return {
      /**
       将地图的业务全部抽离到对应的dialog里面去,View只存放调度业务代码
      */
    };
  },
  methods: {
    openChina() {
      this.$refs.china && this.$refs.china.openDialog();
    },
    openUSA() {
      this.$refs.usa && this.$refs.usa.openDialog();
    },
    openUK() {
      this.$refs.uk && this.$refs.uk.openDialog();
    },
  },
};
</script>

2、ChinaMapDialog.vue

<template>
  <div>
    <el-dialog :visible.sync="baiduMapVisible">我是中国地图的弹窗</el-dialog>
  </div>
</template>
<script>
export default {
  name: "ChinaMapDialog",
  data() {
    return {
      // 对中国地图业务逻辑的封装处理 省略
      baiduMapVisible: false,
    };
  },
  methods: {
    // 对百度地图和谷歌地图的一些业务处理代码 省略
    openDialog() {
      this.baiduMapVisible = true;
    },
    closeDialog() {
      this.baiduMapVisible = false;
    },
  },
};
</script>

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 <div slot="header">{renderHeader(h)}</div>;
    }
    //如果都没有的话, 返回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 <div slot="footer">{renderFooter(h)}</div>;
    }

    return this.defaultFooter(h);
  }

  defaultFooter(h) {
    return (
      <div slot="footer">
        <el-button
          type="primary"
          loading={this.loading}
          on-click={() => {
            this.onSubmit();
          }}
        >
          保存
        </el-button>
        <el-button
          on-click={() => {
            this.onClose();
          }}
        >
          取消
        </el-button>
      </div>
    );
  }
}

最后,我们再通过 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 <div slot="header">{renderHeader(h)}</div>;
    }
  }

  /**
   * 构建弹窗的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 <div slot="footer">{renderFooter(h)}</div>;
    }

    return this.defaultFooter(h);
  }

  defaultFooter(h) {
    return (
      <div slot="footer">
        <el-button
          type="primary"
          loading={this.loading}
          on-click={() => {
            this.onSubmit();
          }}
        >
          保存
        </el-button>
        <el-button
          on-click={() => {
            this.onClose();
          }}
        >
          取消
        </el-button>
      </div>
    );
  }

  createContainer(h) {
    //防止外界误传参数影响弹窗本来的设计,因此,需要将某些参数过滤开来,有title beforeClose, visible
    var { title, beforeClose, visible, ...rest } = Object.assign({}, this.$attrs, this.attrs);
    return (
      <el-dialog
        {...{
          props: {
            ...rest,
            visible: this.visible,
            title: this.title || title || "弹窗",
            beforeClose: this.closeDialog,
          },
          on: {
            close: this.close,
            closed: this.closed,
            opened: this.opened,
            open: this.open,
          },
        }}
      >
        {/* 根据JSX的渲染规则 null、 undefined、 false、 '' 等内容将不会在页面显示,若createHeader返回undefined,将会使用默认的title */}
        {this._createHeader(h)}
        {this._createBody(h)}
        {this._createFooter(h)}
      </el-dialog>
    );
  }

  render(h) {
    return this.createContainer(h);
  }
}

4.应用

4.1组件调用

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

<script>
import Vue from "vue";
import AbstractDialog from "@/components/AbstractDialog.vue";
import Component from "vue-class-component";
@Component({
  name: "ChinaMapDialog",
})
class ChinaMapDialog extends AbstractDialog {
  get title() {
    return "这是中国地图";
  }
  
  attrs = {
   width: "600px",
  }

  //编写一些中国地图的处理业务逻辑代码

  //编写弹窗的内容部分
  renderBody(h) {
    return <div>我是中国地图,我讲为你呈现华夏最壮丽的美</div>;
  }
}
</script>

4.2 使用 Composition API

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

<template>
  <div>
    <china-map-dialog ref="china"></china-map-dialog>
    <usa-map-dialog ref="usa"></usa-map-dialog>
    <uk-map-dialog ref="uk"></uk-map-dialog>
    <el-button @click="openChina">打开中国地图</el-button>
    <el-button @click="openUSA">打开美国地图</el-button>
    <el-button @click="openUK">打开英国地图</el-button>
  </div>
</template>
<script>
import { ref } from "@vue/composition-api";
export default {
  name: "View",
  setup() {
    const china = ref(null);
    const usa = ref(null);
    const uk = ref(null);
    return {
      china,
      usa,
      uk,
    };
  },
  data() {
    return {
      /**
       将地图的业务全部抽离到对应的dialog里面去,View只存放调度业务代码
      */
    };
  },
  methods: {
    // 对百度地图和谷歌地图的一些业务处理代码 省略
    openChina() {
      this.china && this.china.openDialog();
    },
    openUSA() {
      this.usa && this.usa.openDialog();
    },
    openUK() {
      this.uk && this.uk.openDialog();
    },
  },
};
</script>

总结

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

到此这篇关于详解如何在vue+element-ui的项目中封装dialog组件的文章就介绍到这了,更多相关vue element封装dialog内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Vue.js 相关文章推荐
Vue +WebSocket + WaveSurferJS 实现H5聊天对话交互的实例
Nov 18 Vue.js
详解vue 组件注册
Nov 20 Vue.js
Vue实现购物小球抛物线的方法实例
Nov 22 Vue.js
vue单元格多列合并的实现
Nov 26 Vue.js
vue祖孙组件之间的数据传递案例
Dec 07 Vue.js
vue 在服务器端直接修改请求的接口地址
Dec 19 Vue.js
vue项目中openlayers绘制行政区划
Dec 24 Vue.js
关于Vue Router的10条高级技巧总结
May 06 Vue.js
vue如何批量引入组件、注册和使用详解
May 12 Vue.js
详细聊聊vue中组件的props属性
Nov 02 Vue.js
Vue全局事件总线你了解吗
Feb 24 Vue.js
解决vue自定义组件@click点击失效问题
Apr 30 Vue.js
vue使用exif获取图片经纬度的示例代码
Dec 11 #Vue.js
vue使用exif获取图片旋转,压缩的示例代码
Dec 11 #Vue.js
Vue 实现一个简单的鼠标拖拽滚动效果插件
Dec 10 #Vue.js
vuex页面刷新导致数据丢失的解决方案
Dec 10 #Vue.js
详解vue-cli项目在IE浏览器打开报错解决方法
Dec 10 #Vue.js
Vue+element-ui添加自定义右键菜单的方法示例
Dec 08 #Vue.js
vue添加自定义右键菜单的完整实例
Dec 08 #Vue.js
You might like
一个程序下载的管理程序(一)
2006/10/09 PHP
在WordPress中使用PHP脚本来判断访客来自什么国家
2015/12/10 PHP
PHP5.6新增加的可变函数参数用法分析
2017/08/25 PHP
原生PHP实现导出csv格式Excel文件的方法示例【附源码下载】
2019/03/07 PHP
prototype1.4中文手册
2006/09/22 Javascript
Jquery设置attr的disabled属性控制某行显示或者隐藏
2014/09/25 Javascript
解决JS组件bootstrap table分页实现过程中遇到的问题
2016/04/21 Javascript
WebSocket+node.js创建即时通信的Web聊天服务器
2016/08/08 Javascript
servlet+jquery实现文件上传进度条示例代码
2017/01/25 Javascript
jQuery实现获取form表单内容及绑定数据到form表单操作分析
2018/07/03 jQuery
layui问题之模拟select点击事件的实例讲解
2018/08/15 Javascript
[01:53]DOTA2超级联赛专访Zhou 五年职业青春成长
2013/05/29 DOTA
[09:22]2014DOTA2西雅图国际邀请赛 主赛事第二日TOPPLAY
2014/07/21 DOTA
python实现html转ubb代码(html2ubb)
2014/07/03 Python
使用Python的Twisted框架实现一个简单的服务器
2015/04/16 Python
Python中获取对象信息的方法
2015/04/27 Python
Python基于回溯法子集树模板解决野人与传教士问题示例
2017/09/11 Python
Python列表元素常见操作简单示例
2019/10/25 Python
parser.add_argument中的action使用
2020/04/20 Python
python在一个范围内取随机数的简单实例
2020/08/16 Python
css3利用transform变形结合事件完成扇形导航
2020/10/26 HTML / CSS
Nisbets法国:英国最大的厨房和餐饮设备供应商
2019/03/18 全球购物
初中生物教学反思
2014/01/10 职场文书
村官工作鉴定评语
2014/01/27 职场文书
年会搞笑主持词
2014/03/27 职场文书
团代会宣传工作方案
2014/05/08 职场文书
2014世界杯球队球队口号
2014/06/05 职场文书
法学求职信
2014/06/22 职场文书
2014高中生入党思想汇报范文
2014/09/13 职场文书
2014领导班子专题民主生活会对照检查材料思想汇报
2014/09/23 职场文书
2014年计划生育协会工作总结
2014/11/14 职场文书
员工家属慰问信
2015/03/24 职场文书
焦裕禄纪念馆观后感
2015/06/09 职场文书
红歌会主持词
2015/07/02 职场文书
Nginx禁止ip访问或非法域名访问
2022/04/07 Servers
Windows Server 2012 修改远程默认端口3389的方法
2022/04/28 Servers