在实际开发工程化vue项目时,基本都是使用单文件组件形式,即每个.vue文件都是一个组件。最基本的就是UI组件了,我们一般会根据自己的喜好和项目的需要选择一个成熟的组件库来使用,iView组件库、饿了么的Element组件库都是很优秀很全面的组件库,虽然都能满足我们的需求,可众口难调,总有一些特殊的需求不能满足,这个时候就需要我们自己去开发组件实现特殊的需求了。而且我们的目标不仅仅是使用轮子,更要有造轮子的能力。接下来我们就以常用的弹窗Modal组件为例一步一坑的搞一搞vue单文件组件开发。
一个vue组件除了HTML和css结构,最重要的三个部分是slot、props和events,所有的vue组件都逃不离这3个部分。
目标
Modal组件很常见,就是弹出一个带有遮罩的对话框,我们这一次的目标就以iView组件库的Modal为例吧:
1、第一步:基础-单文件组件模式
首先我们先创建一个vue工程,还不清楚的朋友可自行看一下vue官网教程搭建一个。单文件组件顾名思义一个.vue文件就是一个组件,所以我们新建一个Modal.vue
文件表示我们的目标Modal组件,再创建一个父组件HelloWorld.vue
来调用这个Modal组件,父组件是HelloWorld.vue
子组件是Modal.vue
代码分别为:
HelloWorld.vue
:
<template>
<div>
<button @click="toggleModal">打开Modal对话框</button>
<Modal v-show="showModal"></Modal>
</div>
</template>
<script>
import Modal from './Modal.vue'
export default {
data () {
return {
showModal:false
}
},
components:{
'Modal':Modal //声明组件
},
methods:{
toggleModal() {
this.showModal = !this.showModal; //切换showModal 的值来切换弹出与收起
},
}
}
</script>
<style>
</style>
Modal.vue
:
<template>
<div>
我是Modal里的内容
</div>
</template>
<style>
</style>
<script>
export default {
name: 'Modal',
props: { //props里面准备写上父组件HelloWorld要传进来的数据
},
data() {
return {
}
},
methods: {
}
}
</script>
我们这里为了更纯粹的介绍组件间的关系就把HelloWorld.vue通过 vue-router
配置成首页,小伙伴们大可根据自己工程的情况而定,反正就是一父一子的关系。运行命令npm run dev
(不熟悉的小伙伴可以再回头看看vue官网的教程把工程跑起来,可以看到组件间的调用关系就完成了,当然她还不是个真正的弹出框,但却是所有vue组件的基础:
2、第二步:组件个性化
有了第一步单文件组件结构的基础,接下来就可以在这基础上创造出各种组件来,想做Button组件就做Button组件、想做Loading组件就做Loading组件、想做Modal组件就做Modal组件,直到把所有用到的组件都做完了,就形成了自己的组件库,然后再打包托管到npm上...额慢慢来。接下来花一点时间让Modal.vue看上去有她该有的样子:
2.1Modal组件结构搭建
可以看到一个iView的Modal最基本的内容有
遮罩层、弹出框、header头部、body内容区和footer尾部
5个部分,各自都有自己的样式,接下来我们就要为Modal.vue组件加上这样的HTML结构和css样式,Modal.vue代码更新为:Modal.vue
:
<template>
<div class="modal-backdrop">
<div class="modal" :style="mainStyles">
<div class="modal-header">
<h3>我是一个Modal的标题</h3>
</div>
<div class="modal-body">
<p>我是一个Modal的内容</p>
</div>
<div class="modal-footer">
<button type="button" class="btn-close" @click="closeSelf">关闭</button>
<button type="button" class="btn-confirm" @click="confirm">确认</button>
</div>
</div>
</div>
</template>
<style>
.modal-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0,0,0,.3);
display: flex;
justify-content: center;
align-items: center;
}
.modal {
background-color: #fff;
box-shadow: 2px 2px 20px 1px;
overflow-x:auto;
display: flex;
flex-direction: column;
border-radius: 16px;
width: 700px;
}
.modal-header {
border-bottom: 1px solid #eee;
color: #313131;
justify-content: space-between;
padding: 15px;
display: flex;
}
.modal-footer {
border-top: 1px solid #eee;
justify-content: flex-end;
padding: 15px;
display: flex;
}
.modal-body {
position: relative;
padding: 20px 10px;
}
.btn-close, .btn-confirm {
border-radius: 8px;
margin-left:16px;
width:56px;
height: 36px;
border:none;
cursor: pointer;
}
.btn-close {
color: #313131;
background-color:transparent;
}
.btn-confirm {
color: #fff;
background-color: #2d8cf0;
}
</style>
<script>
export default {
name: 'Modal',
props: {
},
data() {
return {
}
},
methods: {
closeSelf() {
}
}
}
</script>
先看一下更新后Modal组件的样子:
2.2 组件通信
再仔细看一下Modal.vue组件的代码,发现有两个问题。
1、一是我们这里为弹出框写了一个width=700px,显然用户每打开一个Modal都是700宽,这样做组件的可重用性就太低了。接下来要做到宽度能由父组件来决定。
2、二是Modal组件里的关闭Button事件还没起作用,它的本意是点击它就关闭整个弹出框。接下来要做到实现此事件。
借由上面这两个问题,我们就要用到组件通信了,在vue组件中,父组件给子组件传数据是单向数据流,父组件通过定义属性的方式传递,子组件用props
接收,如父组件传一个width参数给子组件:
父组件:<Modal width="700" ></Modal>
子组件:props: { width:{ type:[Number,String], default:520 } }
而子组件向父组件传递数据的方式为事件传递,如要在子组件关闭Modal弹出框,则要传递一个双方约定的事件名给父组件,如:
父组件:<Modal @on-cancel="cancel" ></Modal>
子组件:this.$emit('on-cancel');
接下来就更新一下HelloWorld.vue和Modal.vue的代码,来解决这两个问题,实现父组件自定义弹出框宽度为200px,以及关闭弹出框功能
,更新代码如下:
HelloWorld.vue
:
<template>
<div>
<button @click="toggleModal">打开Modal对话框</button>
<!--定义width属性,和on-cancel事件-->
<Modal
v-show="showModal"
width="200"
@on-cancel="cancel"
></Modal>
</div>
</template>
<script>
import Modal from './Modal.vue'
export default {
data () {
return {
showModal:false
}
},
components:{
'Modal':Modal
},
methods:{
toggleModal() {
this.showModal = !this.showModal;
},
//响应on-cancel事件,来把弹出框关闭
cancel() {
this.showModal = false;
}
}
}
</script>
<style>
</style>
Modal.vue
:
<template>
<div class="modal-backdrop">
<div class="modal" :style="mainStyles">
<div class="modal-header">
<h3>我是一个Modal的标题</h3>
</div>
<div class="modal-body">
<p>我是一个Modal的内容</p>
</div>
<div class="modal-footer">
<button type="button" class="btn-close" @click="closeSelf">关闭</button>
<button type="button" class="btn-confirm" @click="confirm">确认</button>
</div>
</div>
</div>
</template>
<style>
.modal-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0,0,0,.3);
display: flex;
justify-content: center;
align-items: center;
}
.modal {
background-color: #fff;
box-shadow: 2px 2px 20px 1px;
overflow-x:auto;
display: flex;
flex-direction: column;
border-radius: 16px;
}
.modal-header {
border-bottom: 1px solid #eee;
color: #313131;
justify-content: space-between;
padding: 15px;
display: flex;
}
.modal-footer {
border-top: 1px solid #eee;
justify-content: flex-end;
padding: 15px;
display: flex;
}
.modal-body {
position: relative;
padding: 20px 10px;
}
.btn-close, .btn-confirm {
border-radius: 8px;
margin-left:16px;
width:56px;
height: 36px;
border:none;
cursor: pointer;
}
.btn-close {
color: #313131;
background-color:transparent;
}
.btn-confirm {
color: #fff;
background-color: #2d8cf0;
}
</style>
<script>
export default {
name: 'Modal',
//接收父组件传递的width属性
props: {
width:{
type:[Number,String],//类型检测
default:300 //父组件没传width时的默认值
}
},
data() {
return {
}
},
computed:{
//计算属性来响应width属性,实时绑定到相应DOM元素的style上
mainStyles() {
let style = {};
style.width = `${parseInt(this.width)}px`;
return style;
}
},
methods: {
//响应关闭按钮点击事件,通过$emit api通知父组件执行父组件的on-cancel方法
closeSelf() {
this.$emit('on-cancel');
}
}
}
</script>
新增的代码在图中都做了注释说明,此时就可看到效果:
可以看到iView的Modal组件定义了很多属性和事件,都是日积月累不断优化而来的,我们的例子只写了一个width属性和on-cancel事件,但其他的基本是大同小异,套路掌握了都可以一一实现的。
iView中Modal的props、events一览
:2.3 slot插槽
props、events都实现了,三部分中就剩slot了。slot的作用也是为了解决组件可重用的问题的。
props解决的是组件参数的传递、events解决的是组件事件的传递、slot解决的就是组件内容的传递。
首先发现问题,例子中Modal的标题和body里的内容是写死的一个<h3>一个<p>标签,但真正使用起来弹出框里的内容都是自定义的五花八门的,一个p标签是搞不定的。接下来我们再更新一下代码,Modal.vue的改动为把<h3>标签替换成了一个name=header
的<slot>插槽,body里的<p>标签替换成了一个name=body
的<slot>插槽(这里用的是具名slot插槽,单个插槽、作用于插槽等就不展开了还请小伙伴们查看vue官网文档):
<template>
<div class="modal-backdrop">
<div class="modal" :style="mainStyles">
<div class="modal-header">
<slot name="header">我是子组件定义的header</slot>
</div>
<div class="modal-body">
<slot name="body">我是子组件定义的body</slot>
</div>
<div class="modal-footer">
<button type="button" class="btn-close" @click="closeSelf">关闭</button>
<button type="button" class="btn-confirm" >确认</button>
</div>
</div>
</div>
</template>
HelloWorld.vue的改动为,在<Modal>里定义了一个具有属性slot
的div,slot的值为header
和<slot name="header">
对应,div里是一个图片和一个<h3>标题。并且这里特意只定义了一个具有slot的div:
<template>
<div>
<button @click="toggleModal">打开Modal对话框</button>
<Modal v-show="showModal" width="700" @on-cancel="cancel">
<div slot="header">
<div class="myHeader">
<img src="../assets/logo.png" width="40px" height="40px"/>
<h3>我是父组件定义的标题</h3>
</div>
</div>
</Modal>
</div>
</template>
前面说到slot解决的组件内容的传递,它就好像是子组件定义一个占位符,父组件有对应的内容传进来就替换掉它,没有传就默认显示子组件自己定义的内容,所以上面代码运行起来会是:
完整的HelloWorld.vue和Modal.vue代码如下:
HelloWorld.vue
:
<template>
<div>
<button @click="toggleModal">打开Modal对话框</button>
<Modal v-show="showModal" width="700" @on-cancel="cancel">
<div slot="header">
<div class="myHeader">
<img src="../assets/logo.png" width="40px" height="40px"/>
<h3>我是父组件定义的标题</h3>
</div>
</div>
</Modal>
</div>
</template>
<script>
import Modal from './Modal.vue'
export default {
data () {
return {
showModal:false
}
},
components:{
'Modal':Modal
},
methods:{
toggleModal() {
this.showModal = !this.showModal;
},
cancel() {
this.showModal = false;
}
}
}
</script>
<style>
.myHeader{
justify-content: flex-start;
padding: 15px;
display: flex;
}
</style>
Modal.vue
:
<template>
<div class="modal-backdrop">
<div class="modal" :style="mainStyles">
<div class="modal-header">
<slot name="header">我是子组件定义的header</slot>
</div>
<div class="modal-body">
<slot name="body">我是子组件定义的body</slot>
</div>
<div class="modal-footer">
<button type="button" class="btn-close" @click="closeSelf">关闭</button>
<button type="button" class="btn-confirm" >确认</button>
</div>
</div>
</div>
</template>
<style>
.modal-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0,0,0,.3);
display: flex;
justify-content: center;
align-items: center;
}
.modal {
background-color: #fff;
box-shadow: 2px 2px 20px 1px;
overflow-x:auto;
display: flex;
flex-direction: column;
border-radius: 16px;
}
.modal-header {
border-bottom: 1px solid #eee;
color: #313131;
justify-content: space-between;
padding: 15px;
display: flex;
}
.modal-footer {
border-top: 1px solid #eee;
justify-content: flex-end;
padding: 15px;
display: flex;
}
.modal-body {
position: relative;
padding: 20px 10px;
}
.btn-close, .btn-confirm {
border-radius: 8px;
margin-left:16px;
width:56px;
height: 36px;
border:none;
cursor: pointer;
}
.btn-close {
color: #313131;
background-color:transparent;
}
.btn-confirm {
color: #fff;
background-color: #2d8cf0;
}
</style>
<script>
export default {
name: 'Modal',
props: {
width:{
type:[Number,String],
default:300
}
},
data() {
return {
}
},
computed:{
mainStyles() {
let style = {};
style.width = `${parseInt(this.width)}px`;
return style;
}
},
methods: {
closeSelf() {
this.$emit('on-cancel');
}
}
}
</script>
3、后续
有几点需要说明一下。首先,毋庸置疑的是几乎所有的vue组件都是围绕着props、slot和events这三大件,每个部分都有不少的内容需要学习使用,就像前面说的slot还有单个插槽、作用域插槽等等,events的api也不止parent、dispatch等等针对各种情形应运而生的。其次,看到iViewUI组件库源码的小伙伴会觉得我们这个例子的代码和它的代码有出入,这是肯定的,例子总是单薄的。最后,我发现在写HTML结构和css时漏了一个transtions过渡效果,不过无伤大雅,周五了加班狗先闪为敬~~