为何有这个项目?javascript
以前重构完公司的项目后将项目的组件进行抽离而后构成了这个项目,UI库基于项目以后维护也比较方便css
项目地址html
学生机服务器ui.qymh.org.cn,阿里云当时提供了一个0.9元的cdn,服务器虽然差了点但我挂了cdn,访问应该不会卡
注意在pc端下查看,请按f12调至移动端视角
一样要注意的是在掘金app中打开这个项目,点我项目中的返回箭头是无效的.我也不知道为何,须要点掘金app提供的箭头返回路由vue
githubjava
项目github地址QymhUInode
项目截图webpack
项目目录仿element-ui
,先来看张图片ios
目录分析git
component
打包后的组件js
dist
列子打包后的文件
docs
挂载的静态github page
examples
列子目录
packages
组件目录
src
资源目录
typings
构建的命名空间
webpack
webpack目录github
组件目录
构造了这么多组件,这个地方的目录是仿的element-ui的架构目录
webpack配置
webpack
这里是一个大的知识点,叙述起来太麻烦了,这里提一下这个项目的webpack
和其余有什么不一样
webpack
打包typescript
我引入了 ForkTsCheckerWebpackPlugin
,感受最大的影响就是打包速度快了,并且这个插件高度适配vue
,还提供了tslint
,虽然我在这个项目没引用,以后会提到qymhui.config.js
,这个文件是UI的配置项,是暴漏给开发者的,就相似于.babelrc
postcss.config.js
同样,我在webpack
中读取他,而后经过webpack.definePlugin
写入process.env
,这个位置有一个大坑 1.暴漏给开发者的js只能用commonjs
语法 2.我暴漏的js里面开发者是能够写入函数的,然而JSON.stringify
是直接忽略函数,以后我经过了对象深度拷贝解决了这个问题架构分析
packages
中建立组件目录,下面的步骤会以q-radio
这个按钮组件进行举列,咱们来看看他的目录结构pug
,vue
中写typescript
我使用了vue-property-decorator
,预处理器用的scss
packages/radio/index.ts
import Radio from './src/main.vue'
export default Radio
复制代码
packages/radio/src/main.vue
<template lang="pug">
.q-radio(:style="computedOuterStyle")
//- 方形选择器
.q-radio-rect(
v-if="type==='rect'"
@click="change(!active)"
:style="computedStyle")
span(v-show="active")
i.q-icon.icon-check(:style="{color:active?activeColor:''}")
//- 圆形选择器
.q-radio-circle(
v-if="type==='circle'"
@click="change(!active)"
:style="computedStyle")
span.q-radio-circle-value(
v-show="active")
i.q-icon.icon-check(:style="{color:active?activeColor:''}")
</template>
<script lang="ts">
import { Vue, Component, Prop, Emit } from 'vue-property-decorator'
import Proto from '../../proto/tag/main.vue'
import createStyle from '../../proto/tag'
const config = require('../../../src/qymhui.config').default.qradio
@Component({})
export default class QRadio extends Proto {
// 激活状态
private active: boolean = false
// 类型
@Prop({ default: config.type })
private type: radio.type
// 是否有边框
@Prop({ default: config.hasBorder })
private hasBorder: boolean
// 边框颜色
@Prop({ default: config.borderColor })
private borderColor: string
// 激活下的颜色
@Prop({ default: config.activeColor })
private activeColor: string
// 激活下的背景颜色
@Prop({ default: config.activeBkColor })
private activeBkColor: string
// 激活下的border颜色
@Prop({ default: config.activeBorderColor })
private activeBorderColor: string
private get computedStyle() {
let style = Object.create(null)
if (this.hasBorder) {
style.borderStyle = 'solid'
style.borderWidth = '1px'
if (this.active) {
style.borderColor = this.activeBorderColor
} else {
style.borderColor = this.borderColor
}
}
if (this.active && this.activeBkColor && this.type === 'circle') {
style.backgroundColor = this.activeBkColor
}
return style
}
private get computedOuterStyle() {
let style = createStyle(this)
return style
}
@Emit()
private change(active: boolean) {
this.active = !this.active
}
}
</script>
<style lang="scss" scoped>
.q-radio {
display: inline-block;
height: 0.5rem;
width: 0.5rem;
position: relative;
}
.q-radio-rect {
position: absolute;
top: 0;
left: 0;
height: 0.5rem;
width: 0.5rem;
line-height: 0.5rem;
border-radius: 0.05rem;
display: inline-block;
font-size: 10px;
text-align: center;
> span {
display: inline-block;
height: 100%;
width: 100%;
> i {
font-size: 14px;
}
}
}
.q-radio-circle {
position: absolute;
top: 0;
left: 0;
height: 0.5rem;
width: 0.5rem;
line-height: 0.5rem;
border-radius: 50%;
display: inline-block;
font-size: 10px;
text-align: center;
&-value {
color: #fff;
}
> span {
display: inline-block;
height: 100%;
width: 100%;
> i {
font-size: 14px;
}
}
}
</style>
复制代码
我在src/index.ts
中引入这个组件,并暴漏注册组件的方法,这个位置的写法也仿的element-ui
不过这个地方有一个坑,element-ui
注册组件直接用的component.name
就能够拿到组件的名字,但ts打包组件的名字会被压缩,不知道这算不算一个Bug,因此咱们得单独把每一个组件的名字用数组保存,咱们来看看代码
import './fonts/iconfont.css'
import './style/highLight.scss'
import './style/widget.scss'
import './style/animate.scss'
import './style/mescroll.scss'
import 'swiper/dist/css/swiper.min.css'
import 'mobile-select/mobile-select.css'
import Vue from 'vue'
import lazyLoad from 'vue-lazyload'
import CONFIG from './qymhui.config'
Vue.use(lazyLoad, CONFIG.qimage)
import '../packages/widget'
import QRow from '../packages/row'
import QCol from '../packages/col'
import QText from '../packages/text'
import QCell from '../packages/cell'
import QHeadBar from '../packages/headBar'
import QSearchBar from '../packages/searchBar'
import QTabBar from '../packages/tabBar'
import QTag from '../packages/tag'
import QCode from '../packages/code'
import QForm from '../packages/form'
import QInput from '../packages/input'
import QRadio from '../packages/radio'
import QStepper from '../packages/stepper'
import QTable from '../packages/table'
import QOverlay from '../packages/overlay'
import QFiles from '../packages/files'
import QImage from '../packages/image'
import QSwiper from '../packages/swiper'
import QPhoto from '../packages/photo'
import QSelect from '../packages/select'
import QScroll from '../packages/scroll'
const components = [
QRow,
QCol,
QText,
QCell,
QHeadBar,
QSearchBar,
QTabBar,
QTag,
QCode,
QForm,
QInput,
QRadio,
QStepper,
QTable,
QOverlay,
QFiles,
QImage,
QSwiper,
QPhoto,
QSelect,
QScroll
]
const componentsName: string[] = [
'QRow',
'QCol',
'QText',
'QCell',
'QHeadBar',
'QSearchBar',
'QTabBar',
'QTag',
'QCode',
'QForm',
'QInput',
'QRadio',
'QStepper',
'QTable',
'QOverlay',
'QFiles',
'QImage',
'QSwiper',
'QPhoto',
'QSelect',
'QScroll'
]
const install = function(Vue: any, opts: any) {
components.map((component: any, i) => {
Vue.component(componentsName[i], component)
})
}
export default {
install,
QRow,
QCol,
QText,
QCell,
QHeadBar,
QSearchBar,
QTabBar,
QTag,
QCode,
QForm,
QInput,
QRadio,
QStepper,
QTable,
QOverlay,
QFiles,
QImage,
QSwiper,
QPhoto,
QSelect,
QScroll
}
复制代码
思路
与其余UI框架的不一样在于,咱们在组件的布局上进行了创新
日常咱们在项目时,会写html
,再写css
,html
中存在大量复杂的命名,若是采用BEM命名准则
,好比 .a_b_c
.a-b_c
经过下划线连接命名,刚才的列子还只是测试,在真实的开发环境下长度是可怕的,因此咱们在布局layout组件中,直接省去了元素命名,并将css
书写成本降到最低
架构
这个地方是用typesrcipt
的继承实现的
首先构造属性vue
和ts
,下面的列子举了一个q-row
的列子,我把经常使用的css样式直接放在了q-row
组建的prop
中
packages/proto/row/main.vue
<script lang="tsx">
import { Vue, Component, Prop } from 'vue-property-decorator'
@Component
export default class Proto extends Vue {
// 高
@Prop({ default: -1 })
public h: string
// 行高
@Prop({ default: -1 })
public lh: string
// 宽
@Prop({ default: -1 })
public w: string
// 高度百分比
@Prop({ default: -1 })
public row: string
// 宽度百分比
@Prop({ default: -1 })
public col: string
// margin-top
@Prop({ default: 0 })
public mt: string
// margin-right
@Prop({ default: 0 })
public mr: string
// margin-bottom
@Prop({ default: 0 })
public mb: string
// margin-left
@Prop({ default: 0 })
public ml: string
// padding-top
@Prop({ default: 0 })
public pt: string
// padding-right
@Prop({ default: 0 })
public pr: string
// padding-bottom
@Prop({ default: 0 })
public pb: string
// padding-left
@Prop({ default: 0 })
public pl: string
// 定位
@Prop({ default: 'static' })
public position: common.position
// top
@Prop({ default: -1 })
public t: number | string
// right
@Prop({ default: -1 })
public r: number | string
// bottom
@Prop({ default: -1 })
public b: number | string
// left
@Prop({ default: -1 })
public l: number | string
// 字体大小
@Prop({ default: -1 })
public fontSize: string
// 字体颜色
@Prop({ default: '' })
public color: string
// 背景颜色
@Prop({ default: '' })
public bkColor: string
// text-align
@Prop({ default: '' })
public textAlign: common.textAlign
// z-index
@Prop({ default: 'auto' })
public zIndex: string
// display
@Prop({ default: '' })
public display: common.display
// vertical-align
@Prop({ default: 'baseline' })
public vertical: common.vertical
// overflow
@Prop({ default: 'visible' })
public overflow: common.overflow
// text-decoration
@Prop({ default: 'none' })
public decoration: common.decoration
// border-radius
@Prop({ default: -1 })
public radius: number | string
// word-break
@Prop({ default: 'normal' })
public wordBreak: common.wordBreak
// text-indent
@Prop({ default: -1 })
public indent: string
// border
@Prop({ default: '' })
public border: string
// border-top
@Prop({ default: '' })
public borderTop: string
// border-right
@Prop({ default: '' })
public borderRight: string
// border-bottom
@Prop({ default: '' })
public borderBottom: string
// border-left
@Prop({ default: '' })
public borderLeft: string
}
</script>
复制代码
packages/proto/row/index.ts
// 构造全局样式
export default function createStyle(vm: any) {
const style: any = {
// 可选属性为auto
// 高
height:
vm.h === -1 && vm.row === -1
? 'auto'
: vm.h !== -1
? `${vm.h / 10}rem`
: `${vm.row}%`,
// 行高
lineHeight: vm.lh === -1 ? 'auto' : `${vm.lh / 10}rem`,
// 宽
width:
vm.w === -1 && vm.col === -1
? 'normal'
: vm.w !== -1
? `${vm.w / 10}rem`
: `${vm.col}%`,
// 定位
position: vm.position,
// top
top:
vm.t === -1
? 'auto'
: typeof vm.t === 'number'
? `${vm.t / 10}rem`
: `${vm.t}%`,
// right
right:
vm.r === -1
? 'auto'
: typeof vm.r === 'number'
? `${vm.r / 10}rem`
: `${vm.r}%`,
// bottom
bottom:
vm.b === -1
? 'auto'
: typeof vm.b === 'number'
? `${vm.b / 10}rem`
: `${vm.b}%`,
// left
left:
vm.l === -1
? 'auto'
: typeof vm.l === 'number'
? `${vm.l / 10}rem`
: `${vm.l}%`,
// 字体
fontSize: vm.fontSize === -1 ? 'inherit' : `${vm.fontSize}px`,
// 可选属性为空
// margin-top
marginTop: vm.mt === 0 ? '' : `${vm.mt / 10}rem`,
// margin-right
marginRight: vm.mr === 0 ? '' : `${vm.mr / 10}rem`,
// margin-bottom
marginBottom: vm.mb === 0 ? '' : `${vm.mb / 10}rem`,
// margin-left
marginLeft: vm.ml === 0 ? '' : `${vm.ml / 10}rem`,
// padding-top
paddingTop: vm.pt === 0 ? '' : `${vm.pt / 10}rem`,
// padding-right
paddingRight: vm.pr === 0 ? '' : `${vm.pr / 10}rem`,
// padding-bottom
paddingBottom: vm.pb === 0 ? '' : `${vm.pb / 10}rem`,
// padding-left
paddingLeft: vm.pl === 0 ? '' : `${vm.pl / 10}rem`,
// border-radius
borderRadius:
vm.radius === -1
? ''
: typeof vm.radius === 'number'
? `${vm.radius / 10}rem`
: `${vm.radius}%`,
// color
color: vm.color,
// 背景颜色
backgroundColor: vm.bkColor,
// text-align
textAlign: vm.textAlign,
// z-index
zIndex: vm.zIndex,
// display
display: vm.display,
// vertical-align
verticalAlign: vm.vertical,
// overflow
overflow: vm.overflow,
// word-break
wordBreak: vm.wordBreak,
// text-indent
textIndent: vm.indent === -1 ? '' : `${vm.indent / 10}rem`,
// text-decoration
textDecoration: vm.decoration === 'none' ? '' : vm.decoration,
// border
border: vm.border || '',
// border-top
borderTop: vm.borderTop || '',
// border-right
borderRight: vm.borderRight || '',
// border-bottom
borderBottom: vm.borderBottom || '',
// border-left
borderLeft: vm.borderLeft || ''
}
for (const i in style) {
if (style.hasOwnProperty(i)) {
const item: string = style[i]
if (
item === '' ||
(item === 'auto' && i !== 'overflow') ||
item === 'inherit' ||
item === 'static' ||
item === 'normal' ||
item === 'baseline' ||
item === 'visible' ||
(item === 'none' && i === 'textDecoration')
) {
delete style[i]
}
// 更符合移动端overflow auto的标准
if (i === 'overflow' && (item === 'auto' || item === 'scroll')) {
style['-webkit-overflow-scrolling'] = 'touch'
}
}
}
return style
}
复制代码
思路
与其余UI框架不一样,咱们提供了config
去改变默认的UI布局.你的项目的组件大小可能和UI库提供的不同,不要紧,咱们内置了基础的UI布局,但你能够经过 qymhui.config.js
去修改咱们的默认配置,打造一个属于本身项目的UI库
架构
咱们提供了一个默认配置,而后暴漏给用户一个配置,用户的配置是经过webpack
在node
环境读取的,最后合并两个配置并传向组件,下面就是qymhui.config.js
的默认配置
// q-cell
export const qcell = {
bkColor: '',
hasPadding: true,
borderTop: false,
borderBottom: false,
borderColor: '#d6d7dc',
leftIcon: '',
leftIconColor: '',
leftText: '',
leftTextColor: '#333',
leftWidth: '',
title: '',
titleColor: '',
rightText: '',
rightTextColor: '',
rightArrow: false,
rightArrowColor: '#a1a1a1',
baseHeight: 1.2
}
// q-head-bar
export const qheadbar = {
color: '',
bkColor: '',
bothWidth: 1,
hasPadding: true,
padding: 0.2,
borderTop: false,
borderBottom: false,
borderColor: '#d6d7dc',
leftEmpty: false,
leftArrow: false,
centerEmpty: false,
centerText: '',
centerTextColor: '',
rightEmpty: false,
rightArrow: false,
rightText: '',
rightTextColor: '',
baseHeight: 1.2
}
// q-search-bar
export const qsearchbar = {
color: '',
bkColor: '',
hasPadding: true,
padding: 0.2,
bothWidth: 1,
borderTop: false,
borderBottom: false,
borderColor: '#d6d7dc',
value: '',
leftArrow: false,
leftText: '',
leftTextColor: '',
searchBkColor: 'white',
placeholder: '请输入...',
clearable: false,
rightText: '搜索',
rightTextColor: '',
baseHeight: 1.2
}
// q-tabbar
export const qtabbar = {
bkColor: '',
borderTop: '',
borderBottom: '',
borderColor: '#d6d7dc',
baseHeight: 1.2
}
// q-text
export const qtext = {
lines: 0
}
// q-tag
export const qtag = {
bkColor: '#d6d7dc',
color: 'white',
fontSize: 12,
value: '',
hasBorder: false,
hasRadius: true,
borderColor: '#d6d7dc',
active: false,
activeBkColor: '',
activeColor: 'white'
}
// q-input
export const qinput = {
hasBorder: false,
borderBottom: true,
borderColor: '#d6d7dc',
bkColor: '',
color: '',
type: 'text',
fix: 4,
placeholder: ''
}
// q-radio
export const qradio = {
type: 'rect',
hasBorder: true,
borderColor: '#a1a1a1',
activeColor: '',
activeBkColor: '',
activeBorderColor: 'transparent'
}
// q-stepper
export const qstepper = {
color: '#F65A44',
min: 0,
max: '',
fix: 4
}
// q-overlay
export const qoverlay = {
position: '',
opacity: 0.3,
bkColor: 'white',
minHeight: 10,
maxHeight: 13,
show: false
}
// q-files
export const qfiles = {
multiple: true,
maxCount: 3,
maxSize: 4,
value: '点击上传',
hasBorder: true,
borderColor: '#a1a1a1'
}
// q-image
export const qimage = {
preLoad: 1.3,
loading: '',
attemp: 1,
bkSize: 'contain',
bkRepeat: 'no-repeat',
bkPosition: '50%'
}
// q-scroll
export const qscroll = {
// 下拉刷新
down: (vm) => {
return {
// 是否启用
use: true,
// 是否初次调用
auto: false,
// 回调
callback(mescroll) {
vm.$emit('refresh')
}
}
},
// 上拉加载
up: (vm) => {
return {
// 是否启用
use: true,
// 是否初次调用
auto: true,
// 是否启用滚动条
scrollbar: {
use: true
},
// 回调
callback: (page, mescroll) => {
vm.$emit('load', page)
},
// 无数据时的提示
htmlNodata: '<p class="upwarp-nodata">-- 没有更多的数据 --</p>'
}
}
}
// $notice
export const $notice = {
// 提醒
toast: {
position: 'bottom',
timeout: 1500
},
// 弹窗
confirm: {
text: '请输入文字',
btnLeft: '肯定',
btnRight: '取消'
}
}
// $cookie
export const $cookie = {
// 过时时间
enpireDays: 7
}
// $axios
export const $axios = {
// 是否输入日志
log: true,
// 超时
timeout: 20000,
// 请求拦截器
requestFn: (config) => {
return config
},
// 响应拦截器
responseFn: (response) => {
return response
}
}
复制代码
Widget
咱们在项目中提供了除了UI组件的widget经常使用方法并将他们直接挂载在vue
的原型上,你能够在vue
环境中直接引用
好比
$cookie
设置 cookie
$storage
设置 storage
$toast
提醒插件
$axios
ajax封装
下面贴一下$cookie
的封装
packages/widget/cookie/index.ts
import Vue from 'vue'
const Cookie = Object.create(null)
const config = require('../../../src/qymhui.config').default.$notice
Cookie.install = (Vue: any) => {
Vue.prototype.$cookie = {
/**
* 获取cookie
* @param key 键
*/
get(key: string): string | number {
let bool = document.cookie.indexOf(key) > -1
if (bool) {
let start: number = document.cookie.indexOf(key) + key.length + 1
let end: number = document.cookie.indexOf(';', start)
if (end === -1) {
end = document.cookie.length
}
let value: any = document.cookie.slice(start, end)
return escape(value)
}
return ''
},
/**
* 设置cookie
* @param key 键
* @param value 值
* @param expireDays 保留日期
*/
set(key: string, value: any, expireDays: number = config.enpireDays) {
let now = new Date()
now.setDate(now.getDate() + expireDays)
document.cookie = `${key}=${escape(value)};expires=${now.toUTCString}`
},
/**
* 删除Cookie
* @param key 键
*/
delete(key: string | string[]) {
let now = new Date()
now.setDate(now.getDate() - 1)
if (Array.isArray(key)) {
for (let i in key) {
let item: string = key[i]
let value: any = this.get(item)
document.cookie = `${item}=${escape(
value
)};expires=${now.toUTCString()}`
}
} else {
let value = this.get(key)
document.cookie = `${key}=${escape(value)};expires=${now.toUTCString()}`
}
},
/**
* 直接删除全部cookie
*/
deleteAll() {
let cookie = document.cookie
let arr = cookie.split(';')
let later = ''
let now = new Date()
now.setDate(now.getDate() - 1)
for (let i in arr) {
let item = arr[i]
later = item + `;expires=${now.toUTCString()}`
document.cookie = later
}
}
}
}
Vue.use(Cookie)
复制代码
移动端适配,目前仅支持flexible.js
的rem
布局,这是有问题的,flexible.js
官方也提到了,以后会经过vh
重写布局
UI模块须要增长,目前的UI框架是从咱们的项目中抽离出来的经常使用的模块,但不表明是你们经常使用的,模块量过少
文档如今只有移动端版,未来会支持到PC端版本
其实项目想在年底的时候开源,我多作一些功能,多作一点测试,多完善文档,多修改接口保证更友好更简单.但没办法,要找工做了,项目如今仅有一个雏形,如今提早把架构思路和项目最主要的特色分享出来,我会尽个人全力争取在年底让这个项目成为一个合格的开源项目