從零搭建Vue
組件庫
一.組件庫的劃分
我們的劃分以elementUi
為基準(zhǔn)分為
Basic
:Button
、Icon圖標(biāo)
撵彻、Layout布局
徽龟、container布局容器
...Form
:Input
、Radio
沥割、checkbox
钦扭、DatePicker
纫版、Upload
...Data
:Table
、Tree
客情、Pagination
...Notice
:Alert
其弊、Loading
、Message
...Navigation
:Tabs
膀斋、Dropdown
梭伐、NavMenu
...Others
:Popover
,Dialog
、inifiniteScroll
仰担、Carousel
...
二.通過Vue-Cli
初始化項(xiàng)目
vue create m-ui
? Check the features needed for your project:
(*) Babel # babel配置
( ) TypeScript
( ) Progressive Web App (PWA) Support
( ) Router
( ) Vuex
(*) CSS Pre-processors # css預(yù)處理器
( ) Linter / Formatter
(*) Unit Testing # 單元測試
( ) E2E Testing
> Sass/SCSS (with dart-sass)
Sass/SCSS (with node-sass)
Less
Stylus
為什么選擇dart-sass?
? Pick a unit testing solution:
> Mocha + Chai # ui測試需要使用karma
Jest
三.目錄結(jié)構(gòu)配置
│ .browserslistrc # 兼容版本
│ .gitignore
│ babel.config.js # babel的配置文件
│ package-lock.json
│ package.json
│ README.md
├─public
│ favicon.ico
│ index.html
├─src
│ │ App.vue
│ │ main.js
│ │
│ ├─packages # 需要打包的組件
│ │ button.vue
│ │ icon.vue
│ │ index.js # 所有組件的入口
│ │
│ └─styles # 公共樣式
│ _var.scss
└─tests # 單元測試
└─unit
button.spec.js
四.編寫插件入口
import Button from './button.vue';
import Icon from './icon.vue';
const install = (Vue) =>{ // 對外暴露install方法
Vue.component(Button.name,Button);
Vue.component(Icon.name,Icon);
}
if(typeof window.Vue !== 'undefined'){
install(Vue);
}
export default {
install
}
import mUi from './packages';
Vue.use(mUi)
我們可以通過插件的方式去引入我們的組件庫
五.編寫B(tài)utton組件
實(shí)現(xiàn)功能規(guī)劃
- 按鈕的基本用法
- 圖標(biāo)按鈕
- 按鈕加載中狀態(tài)
- 按鈕組的實(shí)現(xiàn)
準(zhǔn)備備用樣式
$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;
}
(1).實(shí)現(xiàn)按鈕的基本用法
使用type屬性來定義 Button 的樣式糊识。
<template>
<button class="m-button" :class="btnClass">
<slot></slot>
</button>
</template>
<script>
export default {
props: {
type: {
type: String,
default: "",
validator(type) {
if (
type &&
!["warning", "success", "danger", "info", "primary"].includes(type)
) {
console.error(
"類型必須是:" + `'warning','success','danger','info','primary'`
);
}
return true;
}
}
},
computed: {
btnClass() { // 動態(tài)添加按鈕樣式
let classes = [];
if (this.type) {
classes.push(`m-button-${this.type}`);
}
return classes;
}
},
name: "m-button"
};
</script>
<style lang="scss">
@import '../styles/_var.scss';
$height: 42px;
$font-size: 16px;
$color: #606266;
$border-color: #dcdfe6;
$background: #ecf5ff;
$active-color: #3a8ee6;
.m-button {
border-radius: $border-radius;
border: 1px solid $border-color;
color: $color;
background: #fff;
height: 42px;
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;
}
@each $type,$color in (primary:$primary, success:$success, info:$info, warning:$warning, danger:$danger) {
&-#{$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>
(2).圖標(biāo)按鈕
帶圖標(biāo)的按鈕可增強(qiáng)辨識度(有文字)或節(jié)省空間(無文字)。
使用
iconfont
添加圖標(biāo)
創(chuàng)建圖標(biāo)組件:
<template>
<svg class="m-icon" aria-hidden="true">
<use :xlink:href="`#icon-${icon}`" />
</svg>
</template>
<script>
import "../styles/icon";
export default {
props: {
icon: String
},
name: "m-icon"
};
</script>
<style lang="scss">
.m-icon {
width: 24px;
height: 24px;
vertical-align: middle;
}
</style>
<button class="m-button" :class="btnClass">
<m-icon
:icon="icon"
v-if="icon"
class="icon"
></m-icon>
<span v-if="this.$slots.default">
<slot></slot>
</span>
</button>
<style>
.icon{
fill:#fff;
width: 16px;height:16px;
}
.icon + span {
margin-left: 5px;
}
span + .icon {
margin-right: 5px;
}
</style>
(3).按鈕加載中狀態(tài)
要設(shè)置為 loading 狀態(tài),只要設(shè)置loading
屬性為true
即可赂苗。
<template>
<button class="m-button" :class="btnClass" :disabled="loading">
<m-icon :icon="icon" v-if="icon && !loading" class="icon"></m-icon>
<m-icon icon="loading" v-if="loading" class="icon loading"></m-icon>
<span v-if="this.$slots.default">
<slot></slot>
</span>
</button>
</template>
<style>
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loading {
animation: spin 2s linear infinite;
}
</style>
(4).按鈕組的實(shí)現(xiàn)
以按鈕組的方式出現(xiàn)愉耙,常用于多項(xiàng)類似操作。
<template>
<div class="m-button-group">
<slot></slot>
</div>
</template>
<script>
export default {
name:'m-button-group',
mounted () {
let children = this.$el.children
for (let i = 0; i < children.length; i++) {
console.assert(children[i].tagName === 'BUTTON', '必須子節(jié)點(diǎn)是button')
}
}
}
</script>
<style lang="scss">
@import "../styles/_var.scss";
.m-button-group {
display: inline-flex;
vertical-align: middle;
button {
border-radius: 0;
position: relative;
&:not(first-child) {
margin-left: -1px;
}
&:first-child {
border-top-left-radius: $border-radius;
border-bottom-left-radius: $border-radius;
}
&:last-child {
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
}
}
button:hover {
z-index: 1;
}
button:focus {
z-index: 2;
}
}
</style>
六.搭建測試環(huán)境
我們需要測試ui
渲染后的結(jié)果拌滋。需要在瀏覽器中測試,所有需要使用Karma
Karma
配置
(1)安裝karma
npm install --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'],
preprocessors: {
'**/*.spec.js': ['webpack', 'sourcemap']
},
autoWatch: true,
webpack: webpackConfig,
reporters: ['spec'],
browsers: ['ChromeHeadless']
})
}
{
"scripts": {
"test": "karma start"
}
}
單元測試
import {
shallowMount
} from '@vue/test-utils';
import {
expect
} from 'chai'
import Button from '@/packages/button.vue'
import Icon from '@/packages/icon'
describe('button.vue', () => {
it('1.測試slot是否能正常顯示', () => {
const wrapper = shallowMount(Button, {
slots: {
default: 'm-ui'
}
})
expect(wrapper.text()).to.equal('m-ui')
})
it('2.測試傳入icon屬性', () => {
const wrapper = shallowMount(Button, {
stubs: {
'm-icon': Icon
},
propsData: {
icon: 'edit' // 傳入的是edit 測試一下 edit是否ok
}
})
expect(wrapper.find('use').attributes('href')).to.equal('#icon-edit')
})
it('3.測試傳入loading,是否能朴沿,控制loading屬性', () => {
const wrapper = shallowMount(Button, {
stubs: {
'm-icon': Icon
},
propsData: {
loading: true // 傳入的是edit 測試一下 edit是否ok
}
})
expect(wrapper.find('use').attributes('href')).to.eq('#icon-loading');
expect(wrapper.find('button').attributes('disabled')).to.eq('disabled');
})
it('4.測試點(diǎn)擊按鈕', () => {
const wrapper = shallowMount(Button, {
stubs: ['m-icon']
})
wrapper.find('button').trigger('click')
expect(wrapper.emitted('click').length).to.eq(1);
});
// 5.測試前后圖標(biāo)
it('5.測試前后圖標(biāo)', () => {
const wrapper = shallowMount(Button, {
stubs: {
'm-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');
});
});
})
七.打包組件
(1)配置打包命令
"lib": "vue-cli-service build --target lib --name m-ui ./src/packages/index.js"
(2)配置運(yùn)行入口
"main": "./dist/m-ui.umd.min.js"
(3)link到全局下
npm link
八.使用VuePress
搭建文檔
VuePress
基本配置:
(1).安裝
npm install vuepress -D
(2).配置scripts
{
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
}
(3).初始化docs
增加入口頁面README.MD
---
home: true
actionText: 歡迎 →
actionLink: /components/button
features:
- title: 搭建自己的組件庫
details: 從0搭建自己的組件庫
---
(4).配置導(dǎo)航
增加config.js
module.exports = {
title: 'm-ui', // 設(shè)置網(wǎng)站標(biāo)題
description: 'ui 庫', //描述
dest: './build', // 設(shè)置輸出目錄
port: 1234, //端口
themeConfig: { //主題配置
nav: [{
text: '主頁',
link: '/'
}, // 導(dǎo)航條
],
// 為以下路由添加側(cè)邊欄
sidebar: {
'/components/': [{
collapsable: true,
children: [
'button'
]
}
]
}
}
}
(5).初始化配置文件 .vuepress
enhanceApp.js
-
安裝包
npm install element-ui highlight.js node-sass sass-loader --save
-
link組件庫
npm link m-ui
import Vue from 'vue';
import Element from 'element-ui'; // 引入elementUi
import 'element-ui/lib/theme-chalk/index.css'
import hljs from 'highlight.js'
import 'highlight.js/styles/googlecode.css' //樣式文件
import mUi from 'm-ui' // 要編寫對應(yīng)的文檔的包
import 'm-ui/dist/m-ui.css'
Vue.directive('highlight',function (el) {
let blocks = el.querySelectorAll('pre code');
blocks.forEach((block)=>{
hljs.highlightBlock(block)
})
})
export default ({
Vue,
options,
router,
siteData
}) => {
Vue.use(Element);
Vue.use(mUi)
}
(6).覆蓋默認(rèn)樣式
styles/palette.styl
$codeBgColor = #fafafa // 代碼背景顏色
$accentColor = #3eaf7c
$textColor = #2c3e50
$borderColor = #eaecef
$arrowBgColor = #ccc
$badgeTipColor = #42b983
$badgeWarningColor = darken(#ffe564, 35%)
$badgeErrorColor = #DA5961
.content pre{ margin: 0!important;}
.theme-default-content:not(.custom){
max-width: 1000px !important;
}
(7).創(chuàng)建components目錄
創(chuàng)建demo-block
可收縮代碼塊
<template>
<div
class="demo-block"
:class="[blockClass, { 'hover': hovering }]"
@mouseenter="hovering = true"
@mouseleave="hovering = false">
<div style="padding:24px">
<slot name="source"></slot>
</div>
<div class="meta" ref="meta">
<div class="description" v-if="$slots.default">
<slot></slot>
</div>
<div class="highlight " v-highlight>
<slot name="highlight"></slot>
</div>
</div>
<div
class="demo-block-control"
ref="control"
@click="isExpanded = !isExpanded">
<transition name="arrow-slide">
<i :class="[iconClass, { 'hovering': hovering }]"></i>
</transition>
<transition name="text-slide">
<span v-show="hovering">{{ controlText }}</span>
</transition>
</div>
</div>
</template>
<style lang="scss">
.demo-block {
border: solid 1px #ebebeb;
border-radius: 3px;
transition: .2s;
&.hover {
box-shadow: 0 0 8px 0 rgba(232, 237, 250, .6), 0 2px 4px 0 rgba(232, 237, 250, .5);
}
code {
font-family: Menlo, Monaco, Consolas, Courier, monospace;
}
.demo-button {
float: right;
}
.source {
padding: 24px;
}
.meta {
background-color: #fafafa;
border-top: solid 1px #eaeefb;
overflow: hidden;
height: 0;
transition: height .2s;
}
.description {
padding: 20px;
box-sizing: border-box;
border: solid 1px #ebebeb;
border-radius: 3px;
font-size: 14px;
line-height: 22px;
color: #666;
word-break: break-word;
margin: 10px;
background-color: #fff;
p {
margin: 0;
line-height: 26px;
}
code {
color: #5e6d82;
background-color: #e6effb;
margin: 0 4px;
display: inline-block;
padding: 1px 5px;
font-size: 12px;
border-radius: 3px;
height: 18px;
line-height: 18px;
}
}
.highlight {
pre {
margin: 0;
}
code.hljs {
margin: 0;
border: none;
max-height: none;
border-radius: 0;
line-height: 1.8;
color:black;
&::before {
content: none;
}
}
}
.demo-block-control {
border-top: solid 1px #eaeefb;
height: 44px;
box-sizing: border-box;
background-color: #fff;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
text-align: center;
margin-top: -1px;
color: #d3dce6;
cursor: pointer;
position: relative;
&.is-fixed {
position: fixed;
bottom: 0;
width: 868px;
}
i {
font-size: 16px;
line-height: 44px;
transition: .3s;
&.hovering {
transform: translateX(-40px);
}
}
> span {
position: absolute;
transform: translateX(-30px);
font-size: 14px;
line-height: 44px;
transition: .3s;
display: inline-block;
}
&:hover {
color: #409EFF;
background-color: #f9fafc;
}
& .text-slide-enter,
& .text-slide-leave-active {
opacity: 0;
transform: translateX(10px);
}
.control-button {
line-height: 26px;
position: absolute;
top: 0;
right: 0;
font-size: 14px;
padding-left: 5px;
padding-right: 25px;
}
}
}
</style>
<script type="text/babel">
export default {
data() {
return {
hovering: false,
isExpanded: false,
fixedControl: false,
scrollParent: null,
langConfig: {
"hide-text": "隱藏代碼",
"show-text": "顯示代碼",
"button-text": "在線運(yùn)行",
"tooltip-text": "前往 jsfiddle.net 運(yùn)行此示例"
}
};
},
props: {
jsfiddle: Object,
default() {
return {};
}
},
methods: {
scrollHandler() {
const { top, bottom, left } = this.$refs.meta.getBoundingClientRect();
this.fixedControl = bottom > document.documentElement.clientHeight &&
top + 44 <= document.documentElement.clientHeight;
},
removeScrollHandler() {
this.scrollParent && this.scrollParent.removeEventListener('scroll', this.scrollHandler);
}
},
computed: {
lang() {
return this.$route.path.split('/')[1];
},
blockClass() {
return `demo-${ this.lang } demo-${ this.$router.currentRoute.path.split('/').pop() }`;
},
iconClass() {
return this.isExpanded ? 'el-icon-caret-top' : 'el-icon-caret-bottom';
},
controlText() {
return this.isExpanded ? this.langConfig['hide-text'] : this.langConfig['show-text'];
},
codeArea() {
return this.$el.getElementsByClassName('meta')[0];
},
codeAreaHeight() {
if (this.$el.getElementsByClassName('description').length > 0) {
return this.$el.getElementsByClassName('description')[0].clientHeight +
this.$el.getElementsByClassName('highlight')[0].clientHeight + 20;
}
return this.$el.getElementsByClassName('highlight')[0].clientHeight;
}
},
watch: {
isExpanded(val) {
this.codeArea.style.height = val ? `${ this.codeAreaHeight + 1 }px` : '0';
if (!val) {
this.fixedControl = false;
this.$refs.control.style.left = '0';
this.removeScrollHandler();
return;
}
setTimeout(() => {
this.scrollParent = document.querySelector('.page-component__scroll > .el-scrollbar__wrap');
this.scrollParent && this.scrollParent.addEventListener('scroll', this.scrollHandler);
this.scrollHandler();
}, 200);
}
},
mounted() {
this.$nextTick(() => {
let highlight = this.$el.getElementsByClassName('highlight')[0];
if (this.$el.getElementsByClassName('description').length === 0) {
highlight.style.width = '100%';
highlight.borderRight = 'none';
}
});
},
beforeDestroy() {
this.removeScrollHandler();
}
};
</script>
(8).編寫對應(yīng)組件的md
文件
# Button組件
常用的操作按鈕。
## 基礎(chǔ)用法
基礎(chǔ)的按鈕用法鸠真。
<demo-block>
::: slot source
<button-test1></button-test1>
:::
使用type屬性來定義 Button 的樣式悯仙。
::: slot highlight
?```html
<div>
<m-button>默認(rèn)按鈕</m-button>
<m-button type="primary">主要按鈕</m-button>
<m-button type="success">成功按鈕</m-button>
<m-button type="info">信息按鈕</m-button>
<m-button type="warning">警告按鈕</m-button>
<m-button type="danger">危險(xiǎn)按鈕</m-button>
</div>
?```
:::
</demo-block>
九.發(fā)布到npm
配置.npmignore
配置文件
npm addUser
npm publish
十.推送到git
添加npm
圖標(biāo) https://badge.fury.io/for/js
git remote add origin
git push origin master