文章目录
组件库系列文章
组件库系列二:打包发布组件库_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