一基础介绍
1.1、内容:
- 封装常见的功能性组件(Button、Modal、From相关)
- 把组建封装成UI组件库并发布到NPM上
1.2、涉及知识点:
- Vue基础知识
- 组件基本语法
- 组件间通信
- 插槽的使用
- Props校验
- 过度与动画处理
- 计算属性和监听属性
- v-model语法糖
- vue插件机制
- npm发布
1.3、收获:
- 掌握组件封装的语法和技巧
- 学会造轮子,了解element-ui组件库的实现原理
- 搭建和积累自己的组件库
二、项目搭建
2.1 初始化项目
vue create itcast-ui
手动选择初始化配置
cd itcast-ui
npm run serve
2.2 删除app中默认内容,保留最干净的脚手架
<template>
<div id="app">
itcast
</div>
</template>
<script>
export default {
}
</script>
<style lang="scss">
</style>
2.3 封装Button组件
2.3.1 前置知识点
- 组件通讯
- 插槽
- props校验
2.3.1 button组件的基本结构(参数支持)
2.3.2 button基础的样式
<template>
<button class="vn-buttom">
<slot></slot>
</button>
</template>
<script>
export default {
name: 'VnButton'
}
</script>
<style lang="scss" scoped>
.vn-button{
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
background-color: #fff;
border:1px solid #dcdfe6;
color:#606266;
-webkit-appearance: none;
text-align: center;
outline: none;
margin: 0;
transition: 0.1s;
font-size: 500;
//禁止元素的文字被选择
-moz-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
padding: 12px 20px;
font-size: 14px;
border-radius: 4px;
&:hover,
&:focus {
color: #409eff;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
}
</style>
2.3.3 button的type属性
<button class="vn-buttom" :class="[`vm-button-${type}`]">
<slot></slot>
</button>
// 封装一个通用的组件,会对props做一个约束,props进行校验
props: {
type: {
type: String
}
},
.vm-button-success{
color: #fff;
background-color: #67c23a;
border-color: #67c23a;
&:hover,
&:focus {
color: #85ce61;
border-color: #85ce61;
background-color: #fff;
}
}
2.3.4 button的plain属性
plain属性是一个布尔值,默认为false
<button class="vn-buttom" :class="[`vn-button-${type}`,{'is-plain':plain}]>
</button>
组件中定义样式,支持不同type对应的plain属性
.vn-button-primary.is-plain{
color: #409eff;
background-color: #ecf5ff;
border-color: #409eff;
&:hover,
&:focus {
color: #fff;
border-color: #66b1ff;
background-color: #fff;
}
}
.vn-button-success.is-plain{
color: #67c23a;
background-color: #f0f8eb;
border-color: #c2e7b0;
&:hover,
&:focus {
color: #85ce61;
border-color: #85ce61;
background-color: #fff;
}
}
2.3.5 button的icon
这里我直接使用了iconfont的代码下载文件。其中iconfont.css中,我们对代码进行改造。
使用这种匹配类型,可以是的我们在引用样式的时候,默认使用该样式。vn-icon-开头的class属性默认拥有上述属性。
注意,当我们发现icon和旁边的文字距离太近的时候,需要给文字加一个边距,但是当没有传入文字的时候,这个span的边距也是存在的,所以我们可以通过判断有没有传递插槽来决定span需不需要展示。 利用this.$slots.default,当不存在的时候表示我们没有传入插槽。
<button class="vn-button" :class="[`vn-button-${type}`,{'is-plain':plain,'is-round':round,'is-circle':circle}]"
@click="handleClick"
>
<i :class="icon"></i>
<!-- 当我们没有传入插槽的时候 -->
<span v-if="$slots.default">
<slot></slot>
</span>
</button>
2.4 封装Button组件
2.4.1 前置知识
- vue的过渡和动画
- sync修饰符
- 具名插槽与v-slot指令
2.4.2 dialog组件的基本结构(参数支持)
HTML
<!-- 对话框的遮罩 --><!--频繁隐藏显示就使用show-->
<transition name="dialog-fade">
<div class="vn-dialog_wrapper" v-show="visible" @click="handleClose">
<!-- 真正的对话框 -->
<div class="vn-dialog" :style="{width:width,marginTop:top}">
<div class="vn-dialog_header">
<slot name="title">
<span class="vn-dialog_title">{{title}}</span>
</slot>
<button class="vn-dialog_headerbtn" @click="handleClose">
<i class="vn-icon-close"></i>
</button>
</div>
<div class="vn-dialog_body">
<slot></slot>
</div>
<div class="vn-dialog_footer" v-if="$slots.footer">
<slot name="footer">
</slot>
</div>
</div>
</div>
</transition>
SCSS
.vn-dialog_wrapper{
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
overflow: auto;
margin: 0;
z-index: 3003;
background-color: rgba(0,0,0, 0.5);
.vn-dialog{
position: relative;
margin: 15vh auto 50px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 1px 3px rgba(0,0,0, 0.3);
box-sizing: border-box;
width: 30%;
&_header {
padding: 20px 20px 10px;
.vn-dialog_title{
line-height: 24px;
font-size: 18px;
color: #303133;
}
.vn-dialog_headerbtn{
position: absolute;
top: 20px;
right: 20px;
padding: 0;
background-color: transparent;
border: none;
outline: none;
cursor: pointer;
font-size: 16px;
.vn-icon-close{
color: #909399;
}
}
}
&_body{
padding: 30px 20px;
color: #606266;
font-size: 14px;
word-break: break-all;
}
&_footer{
padding: 10px 20px 20px;
text-align: right;
box-sizing: border-box;
// 组件中的央视覆盖不了的时候用深度选择器
// 深度选择器 scss ::v-deep less /deep/
.vn-button:first-child {
margin-right: 20px;
}
}
}
}
.dialog-fade-enter-active{
animation: fade 1s
}
.dialog-fade-leave-active{
animation: fade 1s reverse
}
@keyframes fade {
0%{
opacity: 0;
transform: translateY(-20px);
}
100%{
opacity: 1;
transform: translateY(0);
}
}
2.4.3 dialog组件的设计的知识点
- 知识点1:具名插槽与v-slot指令
当我们需要多个插槽的时候,例如上述用到的title、footer我们需要定义额外的插槽,<slot name="title">
以及<slot name="footer">
内容部分我们使用不带name
的插槽,实际上等于带有隐藏的名字default
。
其实个人理解上,插槽就是子组件的扩展,通过<slot>
插槽向组件内部指定位置可以传递内容,这样父组件就可以很方便的在子组件中插入内容了,所以插槽显不显示是有父组件传不传来控制的,但是在哪里显示是子组件自己定义的。
父组件可以通过props向子组件传递属性或者方法,但是父组件不能通过属性传递带有标签的内容,甚至组件,而插槽可以。
如下,子组件定义一个footer插槽,便于父组件给子组件传递内容(例如弹窗底部的操作按钮,弹窗组件不能写死,按钮种类很多,可以让父组件自由传递)
<div>
<slot name="footer">
</slot>
</div>
父组件向子组件传递一个取消按钮还有一个确认按钮。
在向具名插槽提供内容的时候,我们可以在一个 <template>
元素上使用 v-slot
指令,并以 v-slot 的参数的形式提供其名称。
<template v-slot:footer>
<vn-button @click="visible=false">取消</vn-button>
<vn-button type="primary" @click="visible=false">确认</vn-button>
</template>
- 知识点2 sync修饰符
在某些情况下我们需要对一个props进行双向绑定。但是真正的双向绑定会带来维护上的问题。
子组件中中我们听过 this.$emit('update:title', newTitle)
告诉父组件你要监听update:title
事件并且更新本地的一个数据。
父组件通过@update:title="doc.title = $event"
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>
为了更加简化我们可以使用缩写模式。像下面这么写,不代表我们不遵循子传父 ,只是触发的事件比较奇怪就是@update:title事件 :title.sync是一个语法糖,相当于写了:title="title" 注册了@update:title事件,并且会接受参数,把值给改掉 其实就是一种缩写,参考官网
<text-document :title.sync="doc.title"></text-document>
- 知识点3 vue的过渡和动画
// vue里面实现动画可以使用六种状态,但是也可以使用下面这种方式
@keyframes run {
0%{
opacity: 0;
}
100%{
opacity: 1;
transform: translateY(0px);
}
}
.aa-enter-active{
animation: run .5s;
}
.aa-leave-active{
animation: run .5s reverse;
}
2.4 封装Input组件
2.4.1 前置知识
- v-model语法糖,:value仅仅有单项数据绑定,只能控制输入框的值,但是在输入框中输入值,username值不会变。配合input事件的时候,就等价于v-model。
<!-- v-model就是一个语法糖 -->
<!-- <input type="text" v-model="username"> -->
<input type="text" :value="username" @input="username=$event.target.value">
2.4.2 参数支持
2.5 封装radio组件
<labe class="vn-radio " :class="{'is-checked':checked}">
<span class="vn-radio_input">
<span class="vn-radio_inner"></span>
<!--下面这个input是不想看到的,上面的是需要长得像radio-->
<!-- 不能直接双向绑定value,value是父组件传递过来的,model取决于value,所以利用计算属性 -->
<input type="radio" class="vn-radio_original" :value="label" :name="name" v-model="model"/>
</span>
<span class="vn-radio_label">
<slot></slot>
<!--如果不传内容,就把label当成内容 -->
<template v-if="!$slots.default">{{label}}</template>
</span>
</labe>
.vn-radio {
color: #606266;
font-weight: 500;
line-height: 1;
position: relative;
cursor: pointer;
display: inline-block;
white-space: nowrap;
outline: none;
font-size: 14px;
margin-right: 30px;
-moz-user-select: none;
.vn-radio_input {
white-space: nowrap;
cursor: pointer;
outline: none;
display: inline-block;
line-height: 1;
position: relative;
vertical-align: middle;
.vn-radio_inner {
border: 1px solid #dcdfe6;
border-radius: 100%;
width: 14px;
height: 14px;
background-color: #fff;
position: relative;
cursor: pointer;
display: inline-block;
box-sizing: border-box;
&:after {
width: 4px;
height: 4px;
border-radius: 100%;
background-color: #fff;
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0);
transition: transform 0.15s ease-in;
}
}
.vn-radio_original {
opacity: 0;
outline: none;
position: absolute;
z-index: -1;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
}
.vn-radio_label {
font-size: 14px;
padding-left: 10px;
}
}
}
.vn-radio.is-checked {
.vn-radio_inner {
border-color: #409eff;
background: #409eff;
&:after {
transform: translate(-50%, -50%) scale(1);
}
}
.vn-radio_label {
color: #409eff;
}
}
2.5.1 分析
首先先让vn-radio_original原始的radio隐藏看不见,其次让vn-radio_inner看起来和radio一样。然后radio要被选中,所以加一个选中的样式,vn-radio.is-checked把圆圈改成蓝色的,接下来我们radio基本结构有了,我们开始开发他的功能,
<vn-radio label='1' v-model="gender">男</vn-radio>
<vn-radio label='0' v-model="gender">女</vn-radio>
我们利用v-model都绑定同一个值,组件要接受label,父组件还会将绑定的值传给组件v-model,那么组件就接受value,value就是绑定的值。作为表单元素,还会传递name给组件。
props: {
label: {
type: [String, Number, Boolean],
defalue: ''
},
value: null,
name: {
type: String,
defalue: ''
}
}
我们每个radio都有个value值。首先,我们要给input框加value值,value就是我们接收到的label的值,我们想要控制哪个input框选中,我们还要加v-model。我们先给一下name属性,v-model是双向绑定value,但是value是父组件传进来的,子组件不能违反单向数据流,我们应该双向绑定我们自己的数据model,这个model就是取决于value的值。
<input type="radio" class="vn-radio_original" :value="label" :name="name" v-model="model"/>
所以我们提供一个计算属性,model的值其实就是父组件传进来的value,所以获取值就是获取value,我们如果要改值,我们就触发父组件的input事件(为什么是input事件呢?因为给一个组件v-model,相当于又一个value还有一个input事件)。计算属性在set的时候能拿到value。
computed: {
model: {
get () {
// 获取父组件的value
return this.value
},
set (value) {
// 触发父组件给当前组件组册的input事件
this.$emit('input', value)
}
}
},