详解如何在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使用Elementui修改默认的最快方法
Dec 05 Vue.js
vue 动态创建组件的两种方法
Dec 31 Vue.js
Vue+scss白天和夜间模式切换功能的实现方法
Jan 05 Vue.js
Vue实现图书管理案例
Jan 20 Vue.js
Vue SPA 首屏优化方案
Feb 26 Vue.js
Vue项目中如何封装axios(统一管理http请求)
May 02 Vue.js
Vue+Element UI实现概要小弹窗的全过程
May 30 Vue.js
vue实现书本翻页动画效果实例详解
Apr 08 Vue.js
vue中的可拖拽宽度div的实现示例
Apr 08 Vue.js
vue @ ~ 相对路径 路径别名设置方式
Jun 05 Vue.js
vue递归实现树形组件
Jul 15 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
解析PHP中intval()等int转换时的意外异常情况
2013/06/21 PHP
PHP+jQuery 注册模块的改进(三):更新到Smarty3.1
2014/10/14 PHP
php实现mysql数据库分表分段备份
2015/06/18 PHP
js压缩利器
2007/02/20 Javascript
JS判断元素为数字的奇异写法分享
2012/08/01 Javascript
Jquery 改变radio/checkbox选中状态,获取选中的值(示例代码)
2013/12/12 Javascript
中止javascript执行的方法
2014/02/14 Javascript
javascript arguments使用示例
2014/12/16 Javascript
JS实现的简洁二级导航菜单雏形效果
2015/10/13 Javascript
详解AngularJS实现表单验证
2015/12/10 Javascript
JavaScript中的ajax功能的概念和示例详解
2016/10/17 Javascript
JS实现购物车特效
2017/02/02 Javascript
angularjs select 赋值 ng-options配置方法
2018/02/28 Javascript
javaScript强制保留两位小数的输入数校验和小数保留问题
2018/05/09 Javascript
使用D3.js构建实时图形的示例代码
2018/08/28 Javascript
Angular4 Select选择改变事件的方法
2018/10/09 Javascript
JavaScript数值类型知识汇总
2019/11/17 Javascript
原生JavaScript创建不可变对象的方法简单示例
2020/05/07 Javascript
python中zip和unzip数据的方法
2015/05/27 Python
python3 实现的人人影视网站自动签到
2016/06/19 Python
python3+PyQt5实现自定义流体混合窗口部件
2018/04/24 Python
Python unittest 简单实现参数化的方法
2018/11/30 Python
pandas ix &amp;iloc &amp;loc的区别
2019/01/10 Python
利用CSS3实现文本框的清除按钮相关的一些效果
2015/06/23 HTML / CSS
日本运动品牌美津浓官方购物网站:MIZUNO SHOP
2016/08/21 全球购物
Move Free官方海外旗舰店:美国骨关节健康专业品牌
2017/12/06 全球购物
ZWILLING双立人法国网上商店:德国刀具锅具厨具品牌
2019/08/28 全球购物
求职简历的自我评价怎样写好
2013/10/07 职场文书
银行内勤岗位职责
2014/04/09 职场文书
《观舞记》教学反思
2014/04/16 职场文书
应聘护士求职信
2014/07/21 职场文书
党委班子剖析材料
2014/08/21 职场文书
素质教育学习心得体会
2016/01/19 职场文书
读《庄子》有感:美而不自知
2019/11/06 职场文书
mysql知识点整理
2021/04/05 MySQL
记一次Mysql不走日期字段索引的原因小结
2021/10/24 MySQL