爲何有這個項目?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端版本
其實項目想在年底的時候開源,我多作一些功能,多作一點測試,多完善文檔,多修改接口保證更友好更簡單.但沒辦法,要找工做了,項目如今僅有一個雛形,如今提早把架構思路和項目最主要的特色分享出來,我會盡個人全力爭取在年底讓這個項目成爲一個合格的開源項目