基於VUE+TypeScript 一個快速開發的移動端UI組件庫

這是一篇求職文章 年齡21 目前級別p6+ 座標成都 找一份vue.js移動端H5工做
一份沒有任何包裝純真實的簡歷 簡歷戳這

求職文章一共有兩篇 另一篇請點這一個nuxt(vue)+mongoose全棧項目聊聊我粗淺的項目架構

項目簡介

爲何有這個項目?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和其餘有什麼不一樣

  • 1 webpack打包typescript 我引入了 ForkTsCheckerWebpackPlugin,感受最大的影響就是打包速度快了,並且這個插件高度適配vue,還提供了tslint,雖然我在這個項目沒引用,以後會提到
  • 2 我項目中有一個qymhui.config.js,這個文件是UI的配置項,是暴漏給開發者的,就相似於.babelrc postcss.config.js 同樣,我在webpack中讀取他,而後經過webpack.definePlugin寫入process.env,這個位置有一個大坑 1.暴漏給開發者的js只能用commonjs語法 2.我暴漏的js裏面開發者是能夠寫入函數的,然而JSON.stringify是直接忽略函數,以後我經過了對象深度拷貝解決了這個問題

架構分析

  • 1 第一步 在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>


複製代碼
  • 2 第二步引用並暴漏

我在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
}

複製代碼
  • 3 直接在引用其中的install,而後經過Vue.use 註冊插件就可使用了

項目特色

快速開發

思路

與其餘UI框架的不一樣在於,咱們在組件的佈局上進行了創新
日常咱們在項目時,會寫html,再寫css,html中存在大量複雜的命名,若是採用BEM命名準則,好比 .a_b_c .a-b_c 經過下劃線連接命名,剛纔的列子還只是測試,在真實的開發環境下長度是可怕的,因此咱們在佈局layout組件中,直接省去了元素命名,並將css書寫成本降到最低

架構

這個地方是用typesrcipt的繼承實現的

首先構造屬性vuets,下面的列子舉了一個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庫

架構

咱們提供了一個默認配置,而後暴漏給用戶一個配置,用戶的配置是經過webpacknode環境讀取的,最後合併兩個配置並傳向組件,下面就是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
  }
}

複製代碼

不止UI組件

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.jsrem佈局,這是有問題的,flexible.js官方也提到了,以後會經過vh重寫佈局

  • UI模塊須要增長,目前的UI框架是從咱們的項目中抽離出來的經常使用的模塊,但不表明是你們經常使用的,模塊量過少

  • 文檔如今只有移動端版,未來會支持到PC端版本

結語

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

相關文章
相關標籤/搜索