使用vue仿照element-ui搭建组件库实战-笔记

167 阅读9分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 10 天

vue组件库搭建实战-笔记

1.课前

1.涉及知识点

image.png

2.组件库的使用

1.下载组件库 yarn add itcast-ui 2.在main.js中全局的引入组件库 import ItcastUI from 'itcast-ui'

3.使用Vue.use(itcast-ui)来使用组件

补充知识

1.Vue.use()的作用是通过全局方法 Vue.use() 使用插件

需要在你调用 new Vue() 启动应用之前完成。

2.Vue.config.productionTip = false 的作用:将productionTip设置成false可以阻止vue在启动时生成生产提示,减小体积=>j减小开销

3.new Vue():每个vue应用都是通过创建vue实例开始的,当vue实例被创建时,它的所有data的属性都会被添加进vue的相应式系统中,也就是说,如果在实例被创建之后的添加的data的属性将不会有响应式的变化

4.render函数:vue是虚拟DOM,所以在拿到template模板的时候需要将起转义为VNode函数,但是使用render函数就免去了转义的过程,render函数传入的参数h是构建虚拟DOM的工具,它的名字是createElement,h只是简写

2.项目初始化

1.为什么要用sass,因为项目中的字体图标库需要用到sass

2.项目的运行 使用yarn serve运行

3.清空所有的APP.vue内的东西和component的东西以及静态资源文件得到最干净的一个vue项目

补充知识

1.npm run dev和npm run serve的区别,具体看是npm run ***,看的是package.json配置文件中script里面写的是什么

image-20230131205957316

3.组件的封装

1.Button

步骤

1.搭架子

1.在文件component中创建单文件组件

2.在main.js中进行全局注册

首先进行导入,再通过Vue.component进行注册,第一个参数是组件的名字,第二个参数是导入的组件

3.在APP.vue中通过

<yf-button>......</yf-button>

标签来使用

4.在button.vue中用

<span><slot></slot></span>

在span里包裹一个slot插槽,这样可以用span来写样式,在app.vue中使用的时候也能直接通过标签里面的内容来改变button的名字

2.支持type属性

1.在app.vue中通过type属性来使用

<yf-button type="success">成功按钮</yf-button>

2.在button组件中,因为app.vue是button.vue的父组件,所以父向子传值,所以用props

props: {
    // type属性
    type: {
      // 数据类型为String
      type: String,
      //未传值的默认default类型
      default: 'default'
    }
  },

3.通过动态的class来生成按钮的type属性控制的样式

:class="[`yf-button-${type}`]"

4.在scss中根据不同的样式写css

3.plain属性(决定按钮是否是镂空的属性)

1.在app.vue中给按钮添加plain属性,该属性是一个布尔值

2.在button.vue中在props上添加plain属性

props: {
    //...
    // 朴素按钮属性
    plain: {
      type: Boolean,
      default: false
    }
  },

3.在class里面加上一个对象元素,键为'is-plain',值为props中的plain

:class="[`yf-button--${type}`,{'is-plain':plain}]
4.round属性

1.在button.vue中的scss中写圆角属性

2,在props中添加该属性

3.在class的对象中添加对round属性的控制

5.circle属性

1.在button.vue中的scss中写circle属性

2,在props中添加该属性

3.在class的对象中添加对circle属性的控制

4.将字体图标文件放在asset文件夹下

5.在main.js中全局导入字体图标文件

6.字体图标

1.通过props进行传值,在props中定义该属性的类型

props: {
//...
    icon: {
      type: String,
      default: ''
    }
}
​

2.在template中用i标签来存放字体图标,通过v-if中icon是否有值来控制字体图标是否显示,通过:class="one-icon-${icon}来控制字体图标

<i v-if="icon" :class="`one-icon-${icon}`"></i>

3.为了使即有图标又有文字的情况下更美观,将图标的margin-left控制为5px,通过[class*=one-icon-]来进行属性选则

.one-button [class*=one-icon-]+span{
  margin-left: 5px;
}

4.由于在i不显示时只要有span就会有margin,所以我们为了避免这种情况就用v-if来控制是否显示span,如果没有传入文字的话$slots.default为空

<span v-if="$slots.default"><slot></slot></span>
7.click事件

子组件click的时候会触发handleClick事件,在handleClick事件中通过$emit触发父组件的自定义事件click,父组件监听click事件触发自己的事件

1.在button.vue中的template中通过@click绑定一个点击事件

@click="handleClick"

2.在methods中写一个handleClick事件,通过$emit将父组件中的函数传进来

handleClick (e) {
      this.$emit('click', e)
    }
8.disabled

1.定义props中的disabled的属性

2.将disabled属性的属性值定为disabled,来使按钮禁用

3.设置禁用的样式

补充知识

1.所有的组件都是在component中创建单文件组件的,在main.js中进行全局的注册

2.user-select: none样式可以避免文字被选中

3.数组形式的props是比较宽松的,属于传值也可以,不传值也行,并且传过来什么就接受什么,不会进行props校验;一般情况下为了保证组件的严谨,我们都使用对象形式的props

4.我们可以给 :class (v-bind:class 的缩写) 传递一个对象来动态切换 class,“”里面可以是数组也可以是对象,props里面的值和data中的值一样可以直接作为变量使用。class是非常灵活的,涉及到布尔值的时候用对象会更方便一些

5.[class*=one-icon-]意思是class中是要有one-icon-的都会被选中

6.slots为所有的插槽,如果slot插槽里没有传东西的话,slots为所有的插槽,如果slot插槽里没有传东西的话,slots.default为空,$slots.default包含了所有没被包含在具名插槽内的节点

7.所有的vue的属性值都是要有引号的

:disabled="disabled"
'is-disabled':disabled//这里是一个对象的键值对,不是vue的属性与属性值

2.Dialog

步骤

1.搭架子

1.在component下创建dialog组件,在main.js中全局引入,在template中写好基础模板,在style中写好基础样式

main.js

import YfDialog from './components/dialog.vue'
Vue.component(YfDialog.name, YfDialog)
2.title

1.在app.vue中通过title属性,将title的值传入

2.在dialog.vue中添加props的title属性,在template标题处通过双花大括号修改标题

但是这样做的话仅仅能改变的是title的文本,如果我想让title是什么就是什么的话就需要用具名插槽

1.在dialog.vue中加上插槽,并通过name属性让其名字为title

<slot name="title">
            <span class="yf-dialog_title">{{title}}</span>
        </slot>

这样写还有一个好处是当使用的时候直接写title加title值时会调用默认的title样式

2.在app.vue中通过template和v-slot:title来使用

<template v-slot:title>
        <h3>我是标题</h3>
    </template>
3.宽和距离顶部的距离

1.使用props接受传递过来的宽高值

2.使用行内样式来动态控制宽高

:style="{width:width,marginTop:top}"
4.内容

使用默认插槽传递内容

5.底部

因为实际需求中可能是有些对话框需要底部有信息,有些不需要底部有信息。所以我们采用具名插槽的形式,为了消除掉不传底部信息时的插槽空间占用,我们采用v-if="$slots.footer"的方式

6.对话框的显示与隐藏

显示

1.使用props接受传递过来的visible 的值

2.通过v-show="visible"来控制显示与隐藏

隐藏

点击*可以隐藏,对话框,点击空白处也能隐藏对话框,点击取消按钮可以隐藏

1.在*按钮上绑定click事件,绑定的事件为handleClick,在整个对话框上通过.self修饰符绑定handleClick事件

<button class="yf-dialog_headerbtn" @click="handleClick">
         <i class="one-icon-close"></i>
       </button>

2.在handleClick事件上只需要改变visible的值就可以了,但是visible的值是不能直接改变的,因为visible的值是由父组件传递进来的,所以是不能直接修改props中的visible的

<div class="yf-dialog_wrapper" v-show="visible" @click.self="handleClick">

4.我们在handleClick事中通过$emit与父组件的事件建立连接,并将false传递给父组件

handleClick () {
      this.$emit('close', false)
    }

5.在父组件的close事件中改变visible的值

@close="close"
close (value) {
      this.visible = value
    }

6.在取消按钮上监听点击事件,在事件中使visible为0

使用.sync修饰符

1.在子组件dialog.vue中,将$emit的函数名字改了,改成了update:待修改属性名的形式

handleClick () {
      this.$emit('update:visible', false)
    }

2.在父组件app.vue中,在传值的属性后加上.sync修饰符

:visible.sync="visible"
7.动画

1.用transition标签把要实现动画效果的元素包起来

2.使用动画效果实现显示与隐藏的动画

.v-enter-active{
    animation: run .5s;
}
.v-leave-active{
    animation: run .5s reverse;
}
@keyframes run {
    0%{
        opacity: 0;
        transform: translateY(-20px);
    }
    100%{
        opacity: 1;
        transform: translateY(0px);
    }
}

transform: translateY处理了一下掉下来的感觉,使动画更逼真

补充知识

1.具名插槽的定义是slot标签加name属性来指定名字,具名插槽的使用是通过template标签加上v-slot来指定名字,这里name后面是等于,v-slot后面是冒号

2.通过事件修饰符.self实现点击遮罩触发隐藏对话框,点对话框本身不隐藏

3.sync修饰符是一个语法糖,用于子组件想修改父组件的值的缩写,子组件本来修改父组件的值时,父组件有一个传递给子组件的props, 和一个用于修改值的监听事件,但是现在可以将二者合并为一个,用:通过props传递的属性.sync="属性值"就可以了;

其在子组件并不会有什么优势,主要是父组件使用起来会简单很多,一般用于子组件想要修改父组件的值的情况下

4.用transition标签包裹元素后,这个元素在显示和隐藏的时候会自动添加一些类名,我们可以通过控制这些类名来实现特定的动画效果,总共有六个类名,分别为(这些都是在style中写的)

//进入前
.v-enter{
	opacity:0;
}
//进入后
.v-enter-to{
	opacity:1;
}
//进入时的过渡效果
.v-enter-active{
	transition: all 0.5s;
}
//离开前
.v-leave{
	opacity:1;
}
//离开后
.v-leave-to{
	opacity:0;
}
//离开时的过渡效果
.v-leave-active{
	transition: all 0.5s;
}

如果设置了name,那么将v改为name,如,name='yf',过渡时的类名就为yf-enter,以此类推

因为六个类名比较麻烦,所以我们可以简化处理

.v-enter-active{
	animation: run .5s;
}
.v-leave=active{
	animation: run .5s reverse;
}
@keyframes run{
	0% {
		opacity: 0;
	}
	100% {
		opacity: 1;
	}
}

5.scoped的作用:scoped会给当前也页面的所有元素都添加一个随机的属性选择器,同时也会给当前的所有样式添加对应的属性选择器,这样就保证了当前样式只在当前页面起作用

6.深度选择器,只要在样式前面加上深度选择器,scoped带来的作用就不会生效了,即不会给该元素添加这个随机属性了

深度选择器的写法

css:   >>>
less: /deep
scss:  ::v-deep

文档:vue-loader

3.input组件

步骤

1.搭架子

和之前的并无不同,在input.vue中搭框架,在main.js中进行组件的导入

2.基础属性

1.通过props将app.vue设置的placeholder传入,默认为空

2.通过props将app.vue设置的type属性传入,默认为文本框

3.通过props将app.vue设置的disabled属性传入,默认为false,并通过

:class="{
    'is-disabled':disabled
   }"

动态改变disabled的样式

3.v-model属性

1.在app.vue中使用v-model

2.根据v-model的原理在input组件中进行更新

3.通过props接受传递的value值

value: {
      type: String,
      default: ''
    }

4.在template中为value属性绑定value值

:value="value"

5.绑定input事件,为input事件绑定handelInput回调函数

@input="handelInput"

6.在回调函数中通过$emit修改父组件的值

handelInput (e) {
      this.$emit('input', e.target.value)
    }
4.清空value值

1.在app.vue中设置clearble属性

2.在input.vue中用props接收传递过来的clearble

clearble: {
      type: Boolean,
      default: false
    }

3.设置span标签,在标签中放clearble图标

<span class="yf-input_suffix" v-if="showSuffix">
   <i class="on-input_icon one-icon-cancel" v-if="clearble&&value" @click="clear"></i>
 </span>

4.在整个输入框上绑定类名yf-input_suffix,由showSuffix属性累控制该类名的显示与隐藏,

 computed: {
    showSuffix () {
      return this.clearble || this.showPassword
    }
  }

只要有clearble或者showPassword,showSuffix 不为空,则显示该类,该类的作用是在输入框里面留一个padding用来放清除内容图标和显示隐藏密码文本图标

5.为清除图标注册click事件,点击click的时候为其绑定回调函数clear,来使内容清空

clear () {
      this.$emit('input', '')
    }
5.显示密码文本

1.为显示按钮注册事件@click="handlePassword"

<i class="on-input_icon one-icon-visible" v-if="showPassword" @click="handlePassword" ></i>
handlePassword () {
      this.passwordVisible = !this.passwordVisible
    }

2.如果传了show-password,判断是否需要切换密码显示,如果show-password为真,则根据passwordVisible来判断让type为text还是,如果show-password为假,直接让type=type

:type="showPassword?(passwordVisible ? 'text':'password') :type"

本来直接点击事件中修改type就好了,但是type是父组件传过来的,不能修改props,所以我们另外定义了passwordVisible,通过点击事件修改passwordVisible,进而来决定type属性

3.让小眼睛亮一下,增加一个one-icon-visible-active类,在类中设置一个颜色,并通过passwordVisible动态的去控制类名

:class="{'one-icon-visible-active':passwordVisible}"
.one-icon-visible-active{
    color: #409eff;
  }

补充知识

1.v-model就是一个语法糖

相当于

:value="username" @input="username=$event.target.value"

一个value加一个input事件

4.swich开关

步骤

1.开关自定义颜色

1.通过click事件来改变v-model的值

app.vue

<yf-swich v-model="value"></yf-swich>

value: false

swich.vue

@click="handleClick"
props: {
    value: {
      type: Boolean,
      default: false
    }
  }
handleClick () {
      this.$emit('input', !this.value)
    }

2.在scss中写选中样式

3.通过value的值动态改变样式

:class="{
    'is-checked':value
  }"

4.在app.vue中通过属性active-color、inactive-color控制开关颜色

 <yf-swich v-model="value" active-color="pink" inactive-color="skyBlue"></yf-swich>

5.用props传值,在子组件中接受参数

props:{
//...
	activeColor: {
      type: String,
      default: ''
    },
    inactiveColor: {
      type: String,
      default: ''
    }
}

6.封装改变颜色的函数setcolor

setColor () {
//如果传入的有颜色就往下执行
      if (this.activeColor || this.inactiveColor) {//根据value值对应应该传递的颜色
        const color = this.value ? this.activeColor : this.inactiveColor
        //根据$refs来修改颜色
        this.$refs.core.style.borderColor = color
        this.$refs.core.style.backgroundColor = color
      }
    }

7.在mounted的和click的回调函数中改变调用改变颜色按钮

mounted () {
    this.setColor()
  }
handleClick () {
      //...
      this.$nextTick(function () {
        this.setColor()
      })
    }
2.添加input框

用户在使用switch组件的时候,实质上是当成表单元素来使用的。因此可能会用到组件的name属性。所以需要在switch组件中添加一个checkbox,并且当值改变的时候,也需要设置checkbox的value值。

1.在app.vue中添加name属性

<yf-switch v-model="value" active-color="pink" inactive-color="skyBlue" name="username"></yf-switch>
data () {
    return {
    //...
      username: 'zs',
      
    }
  }

2.在switch组件中通过input type="checkbox添加了一个复选框",使用refs来操作DOM

<input type="checkbox" class="yf-switch_input" :name="name" ref="input">

3.隐藏checkbox

.yf-switch_input{
    position:absolute;
    width: 0;
    height: 0;
    opacity: 0;
    margin: 0;
  }

4.将switch的值和checkbox的值统一

mounted () {
    //...
    this.$refs.input.checked = this.value
  }
 handleClick () {
      //...
      this.$nextTick(function () {
        //...
        this.$refs.input.checked = this.value
      })
    }

补充知识

1.如果要控制DOM,需要用到ref,ref="core"定义,$refs.core来表示该DOM

2.要等到父组件的value改变后在调用函数setColor,所以我们需要使用this.$nextTick

5.radio组件

步骤

1.基本结构

1.搭建基本结构

2.在app.vue中写理想的单选效果

<yf-radio label="1" v-model="gender">男</yf-radio>
  <yf-radio label="0" v-model="gender">女</yf-radio>

3.在props接受父组件传递进来的值

props: {
    label: {
    //label的类型是以下数组中的类型选择其一
      type: [String, Boolean, Number],
      default: ''
    },
    value: null,
    name: {
      type: String,
      default: ''
    }
  }

4.使用插槽来显示label的文本,如果不传文本的话默认将label属性作为文本来显示

<span class="yf-radio_label">
        <slot></slot>
        <template v-if="!$slots.default">{{label}}</template>
    </span>
2.控制选中

1.radio.vue中为每一个input框加一个value属性,值为传进来的label,添加name属性,值为name

:value="label"
 :name="name"

2.应该有一个v-model用于双向绑定,但是我们不能直接双向绑定父元素传进来的值,所以我们双向绑定当前组件的值,我们绑定一个计算属性model

v-model="model"
computed: {
    model: {
      get () {
        return this.value
      },
      set (value) {
      //触发父组件传递进来的input事件来修改value值
        this.$emit('input', value)
      }
    }
  }

3.根据label值是否等于value值来选中样式

:class="{
 'is-checked':label==value
  }"
3.radio-group组件封装

为了避免让每一个radio去双向绑定,所以我们提供了radio-group组件

1.创建radio-group组件并全局注册

2.在app.vue中写自己想要的效果

<yf-radio-group v-model="gender">
    <yf-radio label="0" >女</yf-radio>
   <yf-radio label="1" ></yf-radio>
  </yf-radio-group>

3.在radio-group.vue中搭建框架

4.如果radioGroup和radio中嵌套了很多层,我们就不能通过props来传值了,我们可以通过provide和inject进行传值与接受。在radioGroup中通过provide将radioGroup自己传给子组件,子组件通过inject来接受,并设置一个默认值为空,以便在不传值的时候为空值

radio-group.vue

provide () {
    return {
      radioGroup: this
    }
  }

radio.vue

inject: {
    radioGroup: {
      default: ''
    }
  }

5.我们在radio.vue中添加一个计算属性,该计算属性用于判断radio是否被radioGroup包裹

isGroup () {
      // 判断组件是否被包裹
      return !!this.radioGroup
    }

6.根据是否被包裹来确定获取的value值和触发的input事件

model: {
      get () {
        return this.isGroup ? this.radioGroup.value : this.value
      },
      set (value) {
        this.isGroup ? this.radioGroup.$emit('input', value) : this.$emit('input', value)
      }
    }

如果被包裹,获取的value值是radioGroup的值,如果没有被包裹,获取的value值是当前组件的value值;如果被包裹,触发的事件是radioGroup的事件,如果没有被包裹,触发的事件是radio的事件

7.将原来的value改为model

'is-checked':label==model

补充知识

1.表单元素都有可能传name属性,所以我们在props校验的时候记得写上对name的校验

2.如果要双向绑定一个计算属性的话,的提供他的get和set

3.provide 与inject:祖先组件中用provide提供值,子组件中用inject接受值;如果我们需要提供依赖当前组件实例的状态 (比如那些由 data() 定义的数据属性),那么可以以函数形式使用 provide,比如,如果我们要传递this,那么provide必须要用函数式

6.checkbox组件

与radio很像,不一样的是使用的时候双向绑定 的是一个数组

显示样式是根据ischecked来控制的

 ischecked () {
      return this.isGroup ? this.model.includes(this.label) : this.model
    }

7.form组件

步骤

1.创建form组件和form-item组件,并全局注册

2.在app.vue中写处理想情况

<template>
  <div id="app">
    <yf-form :model="model">
        <yf-form-item label="用户名">
            <yf-input placeholder="请输入用户名"></yf-input>
        </yf-form-item>
    </yf-form>
  </div>
</template>
data () {
    return {
      model: {

      }
    }
  }

3.在form.vue中用props接受app.vue的属性

props: {
    model: {
      type: Object,
      required: true
    }
  }

4.搭建form-item基本结构

5.在props中传入label

props: {
    label: String
  }

6.app.vue中完善表单的运用

<template>
  <div id="app">
    <yf-form :model="model" label-width="100px">
        <yf-form-item label="用户名">
            <yf-input placeholder="请输入用户名" v-model="model.username"></yf-input>
        </yf-form-item>
        <yf-form-item label="选择">
            <yf-switch v-model="model.active"></yf-switch>
        </yf-form-item>
    </yf-form>
  </div>
</template>
 model: {
        username: '',
        active: true
      }

7.在form.vue中通过provide将form传递给子组件

provide () {
    return {
      form: this
    }
  }

8.在form-item.vue中通过inject接受form

inject: {
    form: {
      default: ''
    }
  }

在template中动态改变label的宽度

<label  class="yf-form-item_label" :style="{width: this.form.labelWidth}">{{label}}</label>

补充知识

4.封装并发布

1.把packages处理成vue插件

1.用脚手架新建一个项目

2.新增examples文件夹和packages文件夹

3.修改vue.config.js文件

const path = require('path')
module.exports = {
  pages:{
    index:{
      // 修改项目入口文件
      entry:'examples/main.js',
      template:'public/index.html',
      filename:'index.html'
    }
  },
  // 扩展webpack配置,使webpages加入编译
  chainWebpack: config => {
    config.module
    .rule('js')
    .include.add(path.resolve(__dirname,'packages')).end()
    .use('babel')
    .loader('babel-loader')
    .tap(options => {
      return options
    })
  }
}

4.将所有封装的组件和font文件夹都复制到packages文件夹里

5.配置vue.config.js

6.在packages目录下新建index.js文件,用于声明install对象

7.在pacakges.json中的script下加入打包组件的命令

8.在控制台运行npm run lib进行打包

9.在github上创建仓库

10.在本地初始化git,然后使用命令提交代码至git仓库

2.发布到npm上

1.修改package.json

2.根目录下增加.npmigore文件

3.上传到npm