组件库系列一:搭建自己的组件库

本文详细介绍了如何使用Vue CLI创建并构建一个简单的组件库,包括创建button和icon组件,实现type属性和slot功能,以及添加loading效果。此外,还涵盖了组件的单元测试设置,使用karma进行测试,并展示了如何处理按钮组。文章最后讨论了如何进行组件的打包发布和编写文档。
摘要由CSDN通过智能技术生成


组件库系列文章
组件库系列二:打包发布组件库_Palate的博客-CSDN博客_组件库如何发布

组件库系列三:编写组件库文档_Palate的博客-CSDN博客_组件库文档

组件库系列四:组件封装思路_Palate的博客-CSDN博客_组件库封装

组件库系列五:vuepress遇到的坑_Palate的博客-CSDN博客

创建简单组件库

1. 组件库使用思路

1.先导入组件库 import porUI from ‘porUI’
2.再通过全局方法Vue.use(porUI)加载插件
3.然后就可以通过标签去使用porUI中的组件了

2. 创建项目

​ 使用vue-cli3创建一个工程,选择配置

​ bable、CSS预处理器、单元测试

​ 这里单元测试选择 karma(Mocha + Chai),CSS预处理器选择 dark-scss

3. 创建组件

​ src下创建packages文件夹,在该文件夹中创建button.vue和icon.vue,如下:

<template>
	<button>button</button>
</template>

icon组件同上

4. 组件整合,导出install方法

在packages文件夹下,创建 index.js 文件

// 所有组件的入口,我们可以在这里进行扩展一些组件,并进行整合
import Button from './button.vue'
import Icon from './icon.vue'

// 在install方法里注册 全局组件
// 引入的时候,use这个方法
const install = (Vue) => {
  // 使用name获取到定义好的名字,方便修改
  Vue.component(Button.name, Button)
  Vue.component(Icon.name, Icon)
}


// 如果是script标签的方式引入并不会调用install方法,这里需要处理一下
// 当前全局window下有Vue实例的话,直接调用install把Vue传进去
if (typeof window.Vue !== 'undefined') {
  install(Vue)
}
// Vue只有用script标签的方式导入才会挂载到window上
// import的方式导入是挂载不到window上的,而是在当前的模块内


export default {
  install
}

5. 定义组件名

组件中添加name,后续要改名的话,在组件文件里改

<template>
	<button>button</button>
</template>
<script>
    export default {
        name: 'por-button' // 定义组件名
    }
</script>

6. 导入组件库

在main.js中导入我们的组件库

import Vue from 'vue'
import App from './App.vue'
import porUI from './packages/index' // 引入自己的ui库
Vue.use(porUI) // 挂载到全局

new Vue({
	render:h=>h(App),
}).$mount('#app')

App.vue中使用

<template>
	<div id='app'>
    	<por-button></por-button>
      <por-icon></por-icon>
  </div>
</template>

编写button组件

1. 样式文件

在src下新建styles文件夹,在里面新建_var.scss,用来作为组件的样式

$border-radius: 4px; //设置圆角

$primary: #409EFF; // 各种颜色
$success: #67C23A;
$warning: #E6A23C;
$danger: #F56C6C;
$info: #909399;


$primary-hover: #66b1ff; // 鼠标移入的颜色
$success-hover: #85ce61;
$warning-hover: #ebb563;
$danger-hover: #f78989;
$info-hover: #a6a9ad;

$primary-active: #3a8ee6; // 激活的颜色
$success-active: #5daf34;
$warning-active: #cf9236;
$danger-active: #dd6161;
$info-active: #82848a;
* { // 清除默认样式
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

2. 插槽接收标签内容

一般使用按钮都会在标签中间放入内容,使用插槽处理该位置的内容

APP.vue

<por-button>默认按钮</por-button>

button.vue

// 在组件里面就要用插槽接收
<template>
	<button class='por-button'>
    // 因为有可能插槽会有一些样式,这里用span多包一层,并且判断有插槽再创建span
    <span v-if='this.$slots.default'>
    	<slot></slot>
    </span>
    </button>
</template>
<script>
export default {
	name: 'por-button' // 定义组件名
}
</script>
<style lang='scss'>
@import '../styles/_var.scss'; // 导入公共样式
$height: 42px; // 设置一些公共变量
$font-size: 16px;
$color: #606266;
$border-color: #dcdfe6;
$background: #ecf5ff;
$active-color: #3a8ee6;
.por-button {
  border-radius: $border-radius;
  border: 1px solid $border-color;
  color: $color;
  background: #fff;
  height: $height;
  cursor: pointer;
  font-size: $font-size;
  line-height: 1;
  padding: 12px 20px;
  display: inline-flex;
  justify-content: center;
  vertical-align: middle;
  &:hover {
    border-color: $border-color;
    background-color: $background;
  }
  &:focus,&:active {
    color: $active-color;
    border-color: $active-color;
    background-color: $background;
    outline: none;
  }
</style>

3. 实现按钮的type属性

传入type属性

App.vue中:

<template>
	<div id='app'>
    	<!-- 默认按钮 --> 	
    	<por-button>默认按钮</por-button>
      	<!-- 带类型的按钮 -->
    	<por-button type='primary'>主要按钮</por-button>
    	<por-button type='warning'>警告按钮</por-button>
    	<por-button type='danger'>危险按钮</por-button>
    	<por-button type='success'>成功按钮</por-button>
    	<por-button type='info'>信息按钮</por-button>
    </div>
</template>

props接收属性

button.vue中:

class动态绑定类名

<template>
    <!-- class属性绑定计算属性的btnClass数组 -->
		<button class='por-button' :class='btnClass'>
      <span v-if='this.$slots.default'>
        <!-- 按钮文本等内容 -->
    	  <slot></slot>
      </span>
    </button>
</template>

props属性接收传入的参数

<script>
export default {
  name: 'por-button', // 定义组件名
  props:{
    // type,按钮颜色类型
    type: {
      String, // type为字符串
      // 默认为空字符串
      default:'',
      validator(type){ // 内容校验
          // 判断类型值不为空且在不在五个类型里面
          if(type&&!['primary','warning','danger','success','info'].includes(type) ){
            // 如果不在,则打印error一下
            console.error('type的类型必须为primary,warning,danger,success,info')
          }
          return true // 这里一定要返回true,false会报错
        }
      }
    },
  computed:{
    btnClass(){
      // 可能有多个类名,先定义一个数组
      let classes = []
      // 按钮颜色类名
      if(this.type){
        classes.push(`por-button-${this.type}`)
      }
      return classes
    }
  }
}
</script>

样式

<style lang='scss'>
@import '../styles/_var.scss'; // 导入公共样式
$height: 42px; // 设置一些公共变量
$font-size: 16px;
$color: #606266;
$border-color: #dcdfe6;
$background: #ecf5ff;
$active-color: #3a8ee6;
.por-button {
 //此处内容省略在前面查看
    
    $color-list: ( 
    // 类名:颜色对
      primary: $primary,
      success: $success,
      info: $info,
      warning: $warning,
      danger: $danger
    );
    // 循环颜色maps,两个参数第一个为键第二个为值
    // .$type为类名,$color为颜色
    @each $type,$color in $color-list {
      &-#{$type} {
        background: #{$color};
        border: 1px solid #{$color};
        color: #fff;
      }
    }
    // 鼠标经过
    @each $type,$color in (primary:$primary-hover, success:$success-hover, info:$info-hover, warning:$warning-hover, danger:$danger-hover) {
        &-#{$type}:hover {
            background: #{$color};
            border: 1px solid #{$color};
            color: #fff;
        }
    }
    // 点击
    @each $type,$color in (primary:$primary-active, success:$success-active, info:$info-active, warning:$warning-active, danger:$danger-active) {
        &-#{$type}:active, &-#{$type}:focus {
          background: #{$color};
          border: 1px solid #{$color};
          color: #fff;
        }
    }
}
</style>

编写icon组件

1. 下载icon

阿里图标库下载图标,以选择symbol(svg)的方式下载,目标文件为iconfont.js,然后后放入src/styles文件夹下,改名为icon.js。(或者直接点击页面链接,复制代码后放入iconjs中)

2. 在组件中使用icon

icon组件中

<template>
	<svg class="por-icon" aria-hidden="true">
      <use xlink:href="#${icon}" /> // #加上图标库里复制的名字
    </svg>
</template>
<script>
<script>
  import './../styles/icon'
  export default {
    name: 'PorIcon', // 定义组件名
    props:{
      icon:{
        type:String,
        require:true // 设置必传
      }
    }
  }
</script>
<style>
  .por-icon {
    width: 30px;
    height: 30px;
    vertical-align: -0.15em;
    fill: currentColor;
    overflow: hidden;
  }
</style>

App.vue中使用

    <por-icon icon='icon-shezhi'></por-icon>

带icon的button组件

1. button组件中icon

button组件中接收icon属性,并使用icon组件

<template>
    <!-- class属性绑定计算属性的btnClass数组 -->
	<button class='por-button' :class='btnClass'>
        <!-- 字体图标 -->
        <por-icon :icon="icon" v-if="icon" class="icon"></por-icon>
        <span v-if='this.$slots.default'>
        <!-- 按钮文本等内容 -->
            <slot></slot>
        </span>
    </button>
</template>
<script>
export default {
  name: 'por-button', // 定义组件名
  props:{
    // type,按钮颜色类型
    type: {
      // 略
      },
    icon: { // 接收icon参数
       type: String
    }
  }

样式修改部分

  // 按钮基本样式
  .por-button {
   	// 省略内容
      
    // 设置按钮icon大小
    .icon {
      width: 16px;
      height: 16px;
    }
  }

App.vue中使用时带上icon属性

<!-- 带图标的按钮 -->
<por-button type="primary" icon='icon-shezhi'>设置</por-button>

2. 图标左右位置

类名+弹性布局,设置子元素顺序

button.vue中

<script>
export default {
  name: 'por-button', // 定义组件名
  props:{
    // type,按钮颜色类型
    type: {
	   // 略
      },
      icon: { // 接收icon参数
        type: String
      },
      iconPosition: {// icon位置参数,默认靠左,样式未设置
        type: String,
        default: 'left',
        validator (data) {
          let Arr = ['left', 'right']
          if (data && Arr.indexOf(data) == -1) {
            console.error('iconPosition类型必须为left,right')
            return true
          }
          return true
        }
      }
    },
  computed:{
    btnClass(){
      //略
      // 图标位置类型(左,右)
      if (this.iconPosition) {
        classes.push(`icon-${this.iconPosition}`)
      }
      return classes
    }
  }
}
</script>
<style lang='scss'>
 // 按钮基本样式
 .por-button {
   // 省略内容
 }
  // 设置子元素顺序的方式控制图标的位置
.icon-left {
  .icon {
    order: 1; // 顺序排第一
    margin-right: 5px;
  }
  span {
    order: 2; // 顺序排第二
  }
}
.icon-right {
  .icon {
    margin-left: 5px;
    order: 2;
  }
  span {
    order: 1;
  }
}
</style>

loading效果和点击事件

button.vue

<template>
    <!-- class属性绑定计算属性的btnClass数组 -->
    <!-- 使用$emit触发click事件,同时要把事件源$event传出去,效果是在外面一点击就把当前事件触发给click事件,同时在外面绑定的方法里,可以拿到事件源。-->
		<button class='por-button' :class='btnClass' :disabled='loading' @click="$emit('click',$event)">
      <!-- 字体图标 -->
      <por-icon :icon="icon" v-if="icon" class="icon"></por-icon>
      <!-- 因为有可能插槽会有一些样式,这里用span多包一层,并且判断有插槽再创建span -->
      <!-- 加载状态 -->
      <por-icon icon="icon-jiazai" v-if="loading" class="icon"></por-icon>
      <span v-if='this.$slots.default'>
        <!-- 按钮文本等内容 -->
    	  <slot></slot>
      </span>
    </button>
</template>
<script>
export default {
  name: 'por-button', // 定义组件名
  props:{
    // type,按钮颜色类型
 	// 接收icon参数
	// icon位置参数,默认靠左,样式未设置

      loading: {
        type: Boolean, // 布尔类型在传值的时候可以直接写不用赋值
        default: false
      }
    },
  computed:{
	// 略
  }
}
</script>
<style>
.por-button {
  // 略
  // loading 状态
  &[disabled]{ // 属性选择器
    cursor: not-allowed; // 禁止点击
  }
}
</style>

App.vue

<por-button type="primary" loading>加载中的按钮,不可点击</por-button>
<por-button type="success" @click='btn'>绑定了点击事件的按钮</por-button>

按钮组

在packages文件夹下创建button-group.vue

注意:按钮组里面只能放置按钮组件,得在mounted钩子里面校验一下

<template>
  <div class="button-group">
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: 'por-button-group',
  // 在钩子里面,校验内部的元素是否是我们的button组件,如果不是就报错
  mounted () {
    // 其实就是把当前原生的dom元素拿到 
    let children = this.$el.children
    // 遍历元素
    for (let i = 0; i < children.length; i++ ) {
      console.assert(children[i].tagName === 'BUTTON', '子元素必须为button') // 使用断言工具函数 
    }
  }
}
</script>

<style lang='scss'>
@import "./../styles/_var.scss"; // 导入公共样式
// 通过样式让整个按钮组只有四个角是圆角,并且两个按钮之间只有1px边框
.button-group {
  display: inline-flex; // 设置不要独占一行
  vertical-align: middle; // 上下居中
  //先覆盖掉原来按钮的圆角
  .por-button {
    position: relative; // 设置个定位,让各种hover状态可以控制层级
    border-radius: 0;
    // 让第一个按钮左边和最后一个按钮右边有圆角
    &:first-child {
      border-radius: $border-radius 0 0 $border-radius;
    }
    &:last-child {
      border-radius: 0 $border-radius $border-radius 0;
    }
    // 让按钮与按钮中间的边框只有一像素,可以让后面的盒子左移1px
    &:not(:first-child) {
      margin-left: -1px;
    }
    &:hover{
        z-index: 1;
    }
    &:focus{
        z-index: 1;
    }
  }
}
</style>

index.js内导入

import ButtonGroup from './button-group.vue'
const install = (Vue) => {
    Vue.component(ButtonGroup.name,ButtonGroup)
}

在App.vue中使用

<por-button-group>
      <por-button icon='icon-back' type="primary" >上一页</por-button>
      <por-button icon='icon-more' type="primary" icon-position='right'>下一页</por-button>
</por-button-group>

完成组件单元测试

1. 安装karma

yarn add --save-dev @vue/test-utils karma karma-chrome-launcher karma-mocha karma-sourcemap-loader karma-spec-reporter karma-webpack mocha karma-chai

2. 配置karma文件

在项目根目录创建karma.conf.js

var webpackConfig = require('@vue/cli-service/webpack.config')

module.exports = function(config) {
  config.set({
    frameworks: ['mocha'],
    files: ['tests/**/*.spec.js'], // 去监控tests文件夹下以.spec.js结尾的所有文件
    preprocessors: {
      '**/*.spec.js': ['webpack', 'sourcemap']
    },
    autoWatch: true, // 自动观察文件变化
    webpack: webpackConfig,
    reporters: ['spec'],
    browsers: ['ChromeHeadless'] //启动一个看不到界面的浏览器
  })
}

3. 测试用例文件

src下tests文件夹下的unit文件夹下创建button.spec.js

import {
  shallowMount
} from '@vue/test-utils'; //vue提供的快速测试的方法
import {
  expect
} from 'chai' // 引入chai
import Button from '@/packages/button.vue'
import Icon from '@/packages/icon' // 引入自己的组件

// 描述测试button组件
describe('button.vue', () => {
  // it后面是测试用例
  it('1.测试slot是否能正常显示', () => {
    const wrapper = shallowMount(Button, {
      slots: {
        default: 'por-ui'
      }
    })
    expect(wrapper.text()).to.equal('por-ui')
  })
  it('2.测试传入icon属性', () => {
    const wrapper = shallowMount(Button, {
      stubs: {
        'por-icon': Icon
      },
      propsData: {
        icon: 'edit' // 传入的是edit 测试一下 edit是否ok
      }
    })
    expect(wrapper.find('use').attributes('href')).to.equal('#edit')
  })
  it('3.测试传入loading,是否能,控制loading属性', () => {
    const wrapper = shallowMount(Button, {
      stubs: {
        'por-icon': Icon
      },
      propsData: {
        loading: true // 传入的是edit 测试一下 edit是否ok
      }
    })
    expect(wrapper.find('use').attributes('href')).to.eq('#icon-jiazai');
    expect(wrapper.find('button').attributes('disabled')).to.eq('disabled');
  })
  it('4.测试点击按钮', () => {
    const wrapper = shallowMount(Button, {
      stubs: ['por-icon']
    })
    wrapper.find('button').trigger('click')
    expect(wrapper.emitted('click').length).to.eq(1);
  });
  // 5.测试前后图标
  it('5.测试前后图标', () => {
    const wrapper = shallowMount(Button, {
      stubs: {
        'por-icon': Icon
      },
      slots: {
        default: 'hello'
      },
      attachToDocument: true,
      propsData: {
        iconPosition: 'left',
        icon: 'edit'
      }
    });
    let ele = wrapper.vm.$el.querySelector('span');
    expect(getComputedStyle(ele, null).order).to.eq('2');
    wrapper.setProps({
      iconPosition: 'right'
    });
    return wrapper.vm.$nextTick().then(() => {
      expect(getComputedStyle(ele, null).order).to.eq('1');
    });
  });
})

4. 查看测试效果

运行npm run test

参考:
vue组件库及文档开发 - 掘金 (juejin.cn)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值