详解如何在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 $router和$route的区别详解
Dec 02 Vue.js
vuex Module将 store 分割成模块的操作
Dec 07 Vue.js
Vue实现随机验证码功能
Dec 29 Vue.js
vue中父子组件的参数传递和应用示例
Jan 04 Vue.js
Vue项目中使用mock.js的完整步骤
Jan 12 Vue.js
Vue如何实现变量表达式选择器
Feb 18 Vue.js
vue脚手架项目创建步骤详解
Mar 02 Vue.js
vue路由实现登录拦截
Mar 24 Vue.js
详解Vue的options
May 15 Vue.js
vue配置型表格基于el-table拓展之table-plus组件
Apr 12 Vue.js
vue3.0 数字翻牌组件的使用方法详解
Apr 20 Vue.js
vue实现登陆页面开发实践
May 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
常用PHP数组排序函数归纳
2016/08/08 PHP
php实现头像上传预览功能
2017/04/27 PHP
禁用页面部分JavaScript方法的具体实现
2013/07/31 Javascript
Javascript中克隆一个数组的实现代码
2013/12/06 Javascript
Bootstrap入门书籍之(五)导航条、分页导航
2016/02/17 Javascript
JavaScript仿聊天室聊天记录
2016/12/27 Javascript
微信小程序 Button 组件详解及简单实例
2017/01/10 Javascript
JavaScript 函数节流详解及方法总结
2017/02/09 Javascript
js实现无缝滚动图(可控制当前滚动的方向)
2017/02/22 Javascript
JS实现列表页面隔行变色效果
2017/03/25 Javascript
AngularJS实现自定义指令与控制器数据交互的方法示例
2017/06/19 Javascript
webpack学习笔记之代码分割和按需加载的实例详解
2017/07/20 Javascript
用jQuery将JavaScript对象转换为querystring查询字符串的方法
2018/11/12 jQuery
python list语法学习(带例子)
2013/11/01 Python
Windows系统下安装Python的SSH模块教程
2015/02/05 Python
Python 字符串与二进制串的相互转换示例
2018/07/23 Python
Python绘制的二项分布概率图示例
2018/08/22 Python
Python数据可视化:箱线图多种库画法
2019/11/06 Python
如何用 Python 制作 GitHub 消息助手
2021/02/20 Python
HTML5安全介绍之内容安全策略(CSP)简介
2012/07/10 HTML / CSS
英国现代家具和装饰网站:PN Home
2018/08/16 全球购物
英国户外装备商店:Ultimate Outdoors
2019/05/07 全球购物
英国Lookfantastic中文网站:护肤品美妆美发购物(英国直邮)
2020/04/27 全球购物
OnePlus加拿大官网:中国国际化手机品牌
2020/10/13 全球购物
交通专业个人自荐信格式
2013/09/23 职场文书
社团活动策划书范文
2014/01/09 职场文书
三个儿子教学反思
2014/02/03 职场文书
《雷雨》教学反思
2014/02/20 职场文书
学雷锋活动总结范文
2014/04/25 职场文书
优秀员工推荐信
2014/05/10 职场文书
领导视察通讯稿
2015/07/18 职场文书
2016年优秀团支部事迹材料
2016/02/26 职场文书
申论不会写怎么办?教您掌握这6点思维和原则
2019/07/17 职场文书
浅谈MySQL中的六种日志
2022/03/23 MySQL
Mac电脑OS系统下安装Nginx的详细教程
2022/04/14 Servers
SpringBoot Http远程调用的方法
2022/08/14 Java/Android