@stella2/xj-ui
v0.2.6
Published
我们的划分以`elementUi`为基准分为
Downloads
3
Readme
从零搭建Vue
组件库
一.组件库的划分
我们的划分以elementUi
为基准分为
Basic
:Button
、Icon图标
、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
初始化项目
vue create xj-ui
? Check the features needed for your project:
(*) Babel # babel配置
( ) TypeScript
( ) Progressive Web App (PWA) Support
( ) Router
( ) Vuex
(*) CSS Pre-processors # css预处理器
( ) 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
三.目录结构配置
│ .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 xjUi from './packages';
Vue.use(xjUi)
我们可以通过插件的方式去引入我们的组件库
五.编写Button组件
实现功能规划
- [ ] 按钮的基本用法
- [ ] 图标按钮
- [ ] 按钮加载中状态
- [ ] 按钮组的实现
准备备用样式
$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).实现按钮的基本用法
使用type属性来定义 Button 的样式。
<template>
<button class="xj-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() { // 动态添加按钮样式
let classes = [];
if (this.type) {
classes.push(`xj-button-${this.type}`);
}
return classes;
}
},
name: "xj-button"
};
</script>
<style lang="scss">
@import '../styles/_var.scss';
$height: 42px;
$font-size: 16px;
$color: #606266;
$border-color: #dcdfe6;
$background: #ecf5ff;
$active-color: #3a8ee6;
.xj-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).图标按钮
带图标的按钮可增强辨识度(有文字)或节省空间(无文字)。
使用
iconfont
添加图标
创建图标组件:
<template>
<svg class="xj-icon" aria-hidden="true">
<use :xlink:href="`#icon-${icon}`" />
</svg>
</template>
<script>
import "../styles/icon";
export default {
props: {
icon: String
},
name: "xj-icon"
};
</script>
<style lang="scss">
.xj-icon {
width: 24px;
height: 24px;
vertical-align: middle;
}
</style>
<button class="xj-button" :class="btnClass">
<xj-icon
:icon="icon"
v-if="icon"
class="icon"
></xj-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).按钮加载中状态
要设置为 loading 状态,只要设置loading
属性为true
即可。
<template>
<button class="xj-button" :class="btnClass" :disabled="loading">
<xj-icon :icon="icon" v-if="icon && !loading" class="icon"></xj-icon>
<xj-icon icon="loading" v-if="loading" class="icon loading"></xj-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).按钮组的实现
以按钮组的方式出现,常用于多项类似操作。
<template>
<div class="xj-button-group">
<slot></slot>
</div>
</template>
<script>
export default {
name:'xj-button-group',
mounted () {
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";
.xj-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>
六.搭建测试环境
我们需要测试ui
渲染后的结果。需要在浏览器中测试,所有需要使用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: 'xj-ui'
}
})
expect(wrapper.text()).to.equal('xj-ui')
})
it('2.测试传入icon属性', () => {
const wrapper = shallowMount(Button, {
stubs: {
'xj-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: {
'xj-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.测试点击按钮', () => {
const wrapper = shallowMount(Button, {
stubs: ['xj-icon']
})
wrapper.find('button').trigger('click')
expect(wrapper.emitted('click').length).to.eq(1);
});
// 5.测试前后图标
it('5.测试前后图标', () => {
const wrapper = shallowMount(Button, {
stubs: {
'xj-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 xj-ui ./src/packages/index.js"
(2)配置运行入口
"main": "./dist/xj-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).配置导航
增加config.js
module.exports = {
title: 'xj-ui', // 设置网站标题
description: 'ui 库', //描述
dest: './build', // 设置输出目录
port: 1234, //端口
themeConfig: { //主题配置
nav: [{
text: '主页',
link: '/'
}, // 导航条
],
// 为以下路由添加侧边栏
sidebar: {
'/components/': [{
collapsable: true,
children: [
'button'
]
}
]
}
}
}
(5).初始化配置文件 .vuepress
enhanceApp.js
安装包
npm install element-ui highlight.js node-sass sass-loader --save
link组件库
npm link xj-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 xjUi from 'xj-ui' // 要编写对应的文档的包
import 'xj-ui/dist/xj-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(xjUi)
}
(6).覆盖默认样式
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).创建components目录
创建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": "在线运行",
"tooltip-text": "前往 jsfiddle.net 运行此示例"
}
};
},
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).编写对应组件的md
文件
# Button组件
常用的操作按钮。
## 基础用法
基础的按钮用法。
<demo-block>
::: slot source
<button-test1></button-test1>
:::
使用type属性来定义 Button 的样式。
::: slot highlight
```html
<div>
<xj-button>默认按钮</xj-button>
<xj-button type="primary">主要按钮</xj-button>
<xj-button type="success">成功按钮</xj-button>
<xj-button type="info">信息按钮</xj-button>
<xj-button type="warning">警告按钮</xj-button>
<xj-button type="danger">危险按钮</xj-button>
</div>
```
:::
</demo-block>
九.发布到npm
配置.npmignore
配置文件
npm addUser
npm publish
十.推送到git
添加npm
图标 https://badge.fury.io/for/js
git remote add origin
git push origin master