在实际开发某个项目的时候,使用的是Element-UI库,因为该库的模态框默认颜色只有白色,而项目的需求是需要其他的颜色,在尝试修改原UI的颜色之后,发现在初始渲染的时候,会有颜色的闪动,放弃这个方法,又开始使用JS直接操作DOM结合setTimeout函数,实现了最终的效果,但是我们都知道JS操作DOM对于是很损耗性能的,而且使用定时器可能会造成内存泄露等一些列问题。
虽然效果已经展示,但是碍于自己无法忽视上面提到的 “代价”,所以自己动手实现了一个简易版的模态框,在其中也遇到了一些之前忽略的问题,下面根据代码一一道来。
先复一些这个模态框组件的github地址。

一、先定义一个组件
Vue.js的两大特点就是 双向数据绑定和 组件系统
所以这里插入一些组件的基本知识:
vue组件的核心选项:
-
1、
模板(template):模板声明了数据和最终展现给用户的DOM之间的映射关系。 -
2、
初始数据(data):一个组件的初始数据状态。对于可复用的组件来说,这通常是私有的状态。 -
3、
接受的外部参数(props):组件之间通过参数来进行数据的传递和共享。 -
4、
方法(methods):对数据的改动操作一般都在组件的方法内进行。 -
5、
生命周期钩子函数(lifecycle hooks):一个组件会触发多个生命周期钩子函数,最新2.0版本对于生命周期函数名称改动很大。 -
6、
私有资源(assets):Vue.js当中将用户自定义的指令、过滤器、组件等统称为资源。一个组件可以声明自己的私有资源。私有资源只有该组件和它的子组件可以调用。 等等。
如何编写一个可复用的组件?
因为组件的开发不只是为了代码解耦,使其独立于一部分,更重要的作用就是复用。
Vue.js组件 API 来自 三部分:prop、事件、slot:
-
prop允许外部环境传递数据给组件,在vue-cli工程中也可以使用vuex等传递数据。 -
事件允许组件触发外部环境的action -
slot允许外部环境将内容插入到组件的视图结构内。
html部分(基本布局)
<template>
<div class="dialog">
<div class="mask"></div>
<div class="dialog-content">
<!-- 标题 -->
<h3 class="title">{{ modal.title }}</h3>
<!-- 自定义内容 -->
<slot name="content"></slot>
<!-- 按钮组 -->
<div class="btn-group">
<div class="btn" @click="cancel">{{ modal.cancelButtonText }}</div>
<div class="btn" @click="submit">{{ modal.confirmButtonText }}</div>
</div>
</div>
</div>
</template>
内容部分使用了具名插槽,这样,就可以让外部环境将内容插入到组件的视图结构中(插入的内容可以是任何东西,如:表格。输入框。图片的)。这样就不会局限于组件中定义的内容了,使用起来更加方便以及更具有可造性。
css部分(样式)
布局之后,要想实现这一部分内容的实现,我们可以先忽略核心的部分,先呈现出基本的样式,下面的一段代码使用了less,使用的目的无非就是编写css更加具有逻辑,更加方便。
注意:如果你的vue显示使用vue-cli 3.x以下的版本安装的,则需要自己安装 less以及 less-loader,同时要在webpack中配置 less-loader,如果是vue-cli 3.x的,name就无需自手动安装了。
<style scoped lang="less">
.dialog {
position: relative;
.dialog-content {
position: fixed;
z-index: 2;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
box-sizing: border-box;
background: white;
border-radius: 5px;
padding: 20px;
min-height: 140px;
.title {
font-size: 16px;
font-weight: 600;
line-height: 30px;
}
.text {
font-size: 14px;
line-height: 30px;
color: #555;
}
.btn-group {
display: flex;
position: absolute;
right: 0;
bottom: 10px;
.btn {
padding: 10px 20px;
font-size: 14px;
&:last-child {
color: #76d49b;
}
}
}
}
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1;
}
}
上面的一整段代码,主要就是基本的布局,无可多说,为了使更多的读者更加快的读懂,挑出两小部分进行简单解释:
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
这段代码的作用就是让模态框垂直水平居中,如果不了解,这里笔者推荐一篇文章,讲解了常用的基本布局,非常详细。
z-index: 1;
该性指定了一个具有定位属性的元素及其子代元素的 z-order。 当元素之间重叠的时候,z-order 决定哪一个元素覆盖在其余元素的上方显示。 通常来说 z-index较大的元素会覆盖较小的一个。
JS部分
<script>
/* eslint-disable */
export default {
name: "MyModal",
props: {
dialogOption: Object
},
data() {
return {
resolve: "",
reject: "",
promise: "" //保存promise对象
};
},
computed: {
modal() {
let option = this.dialogOption;
return {
title: option.title || "提示",
text: option.text,
cancelButtonText: option.cancelButtonText
? option.cancelButtonText
: "取消",
confirmButtonText: option.confirmButtonText
? option.confirmButtonText
: "确定"
};
}
},
methods: {
//确定,将promise断定为完成态
submit() {
this.resolve();
},
// 取消,将promise断定为reject状态
cancel() {
this.reject();
},
//显示confirm弹出,并创建promise对象,给父组件调用
confirm() {
this.promise = new Promise((resolve, reject) => {
console.log(resolve, reject);
this.resolve = resolve;
this.reject = reject;
});
return this.promise; //返回promise对象,给父级组件调用
}
}
};
props就是接收父组件传递的参数,这里的参数为一个对象,对象中存有标题,按钮名称属性,目的就是为了更加方便的改变这些属性值。
重点是 methods中的三个方法,因为这里要结合父组件的部分内容实现,所以笔者先说明父组件中的使用,再来详细解释这一部分的功能。
二、 父组件的使用
<!-- 自定义模态框组件 -->
<my-modal :dialogOption="dialogOption" v-show="isShow" ref="dialog">
<div slot="content">
<label>用户名</label>
<input type="text" placeholder="请输入用户名" v-model="username">
<br>
<label>密码</label>
<input type="password" placeholder="请输入密码" v-model="password">
</div>
</my-modal>
<script>
/* eslint-disable */
import MyModal from "./components/MyModal.vue";
export default {
name: "app",
components: {
MyModal
},
data() {
return {
isShow: false,
dialogOption: {
title: "查看详情"
},
username: "",
password: ""
};
},
methods: {
open() {
this.isShow = true;
this.$refs.dialog
.confirm()
.then(() => {
this.isShow = false;
})
.catch(() => {
this.isShow = false;
});
}
}
};
</script>
首先看这一句 <my-modal :dialogOption="dialogOption" v-show="isShow" ref="dialog">,dialogOption这个属性值就是为子组件传递值,非常简单,v-show="isShow"这就是控制模态框的显示与隐藏。ref="dialog"获取组件实例。
注意:ref如果在普通的DOM元素上使用,引用的指向就是DOM元素;如果在子组件上,引用就指向组件实例。
所以当在下面调用open方法时,this.$ref.dialog就会获取模态框组件的实例,这时候就能访问该实例中的方法,此时调用实例中的 cofirm,因为组件中的cofirm返回的是一个promise对象,所以需要调用它的 then和catch两个方法分别来捕获resolve和reject两个结果。之后我们再来看子组件中的 data中定义了resolve和reject两个属性,这两个属性在 confirm中分别赋值(赋值为resolve和reject两个函数),所以再在 submit和cancel中使用这两个属性时,就需要执行这两个函数,实现自己的实际功能,最终关闭模态框。
上面就是模态框的核心内容,仔细理解下来是没有难度的。

这里讲一下笔者在实现时遇到的 坑:
控制模态框显示与隐藏的时候,笔者脑子一热使用了 v-if,先说一下这两者的区别吧:
v-if 和 v-show
共同点:
v-if 和 v-show 都是动态显示DOM元素。
区别:
-
1、编译过程:
v-if是 真正 的 条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。v-show的元素始终会被渲染并保留在 DOM 中。v-show只是简单地切换元素的 CSS 属性display。 -
2、编译条件:
v-if是惰性的:如果在初始渲染时条件为假,则什么也不做。直到条件第一次变为真时,才会开始渲染条件块。v-show不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。 -
3、性能消耗:
v-if有更高的切换消耗。v-show有更高的初始渲染消耗。 -
4、应用场景:
v-if适合运行时条件很少改变时使用。v-show适合频繁切换。
先不说最坑的地方,就两者的区别来看,在这里使用 v-show对于性能优化是更优的选择。
除此之外,如果我们使用了 v-if,我们会发现当我们使用 ref获取组件实例的时候会使一个空的对象,原因就是当调用open打开模态框的时候,此时模态框组件并没有真正的渲染到DOM节点上,以至于ref是访问不到组件实例的,所以接下来就会发生一系列的问题,相反的,如果我们使用 v-if,不管最初我们是不是看到了模态框组件,它都是已经渲染在了DOM节点上了,所以不会导致刚刚出现的问题。
所以,当我们在选择v-if或v-show的时候,除了考虑性能之外,我们更考虑它们的使用以及可能会造成的不可预料的错误。
到这里,模态框组件的实现、使用以及笔者遇到的问题都已经在上面说明了,希望阅读这篇文章的可爱的人儿们都有所收获~~~~
