从0-1搭建一个UI组件库

211

一基础介绍

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)
      }
    }
  },