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;
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。
更新日志
- 【雨果唱片】中国管弦乐《鹿回头》WAV
- APM亚流新世代《一起冒险》[FLAC/分轨][106.77MB]
- 崔健《飞狗》律冻文化[WAV+CUE][1.1G]
- 罗志祥《舞状元 (Explicit)》[320K/MP3][66.77MB]
- 尤雅.1997-幽雅精粹2CD【南方】【WAV+CUE】
- 张惠妹.2007-STAR(引进版)【EMI百代】【WAV+CUE】
- 群星.2008-LOVE情歌集VOL.8【正东】【WAV+CUE】
- 罗志祥《舞状元 (Explicit)》[FLAC/分轨][360.76MB]
- Tank《我不伟大,至少我能改变我。》[320K/MP3][160.41MB]
- Tank《我不伟大,至少我能改变我。》[FLAC/分轨][236.89MB]
- CD圣经推荐-夏韶声《谙2》SACD-ISO
- 钟镇涛-《百分百钟镇涛》首批限量版SACD-ISO
- 群星《继续微笑致敬许冠杰》[低速原抓WAV+CUE]
- 潘秀琼.2003-国语难忘金曲珍藏集【皇星全音】【WAV+CUE】
- 林东松.1997-2039玫瑰事件【宝丽金】【WAV+CUE】