Vue UI組件庫開發經驗漫談

UI組件是對一組相關的交互和樣式的封裝,提供了簡單的調用方式和接口,讓開發者能很便捷地使用組件提供的功能來實現業務需求。css

我在一個名爲Admin UIVue UI組件庫(GitHub地址:github.com/BboyAwey/ad…Vue 組件庫。不過即便你是React的使用者,也能夠參考本文給出的經驗,由於若是你打算編寫一個React UI組件庫,你將不得不面對幾乎徹底同樣的問題。html

這篇文章也是我在公司的一次技術分享的內容。我在這裏主要只探討思路,儘可能不去涉及具體實現。而且我對這些問題的解決思路也不盡然是徹底合理的,若有錯漏請讀者斧正。vue

1 組織你的項目

當你開始着手組件庫的開發時,第一件事可能就是創建一個項目,由於是Vue 組件庫,你極可能會使用其官方推薦的vue-cli工具來生成一個項目。node

1.1 合適的文件結構

當項目生成後,你很快就發現這個項目模版的文件結構用於業務開發很是合適,但並不那麼適合組件庫的開發。這時你極可能會在其src文件夾下用你的組件庫名稱創建一個文件夾來存放你的組件庫代碼。但這個時候你還並不清楚全部須要作的事情,你並無繼續調整文件結構。react

咱們暫且將你的組件庫就命名爲admin-ui,方便後續行文jquery

當你真正開始編寫第一個組件時,你確定會首先編寫一個用來展現正在開發中的組件的頁面,並在其上對其進行測試。因此你又在src文件夾中新建了一個examples文件夾用來存放你的示例代碼。webpack

這時你的文件結構看起來就像這樣:ios

組件庫文件夾和示例頁面文件夾

1.2 爲組件庫編寫或生成使用文檔

極可能一段時間後,你爲每一個組件都編寫了一個示例頁,甚至其中一些示例頁自己已經作的很棒了。若是有一天組件庫主要開發完成了,這堆實例頁也就沒什麼用了。因而你可能會對這些示例頁進行完善,將每一個組件的特性和接口列表甚至使用示例代碼放到它們的示例頁上,而後將其部署在一臺服務器上方便你的用戶隨時查看。它們很幸運地沒有被浪費,而是都變成了組件庫的使用文檔了!git

1.3 組件庫自己開發文檔的管理

而後你可能意識到一我的的力量之薄弱,你會邀請其它開發者參與到你的項目中,然而儘管你在使用vue-cli生成項目時已經開啓了ESlint,但多人協同開發一套完整的UI庫僅僅依靠代碼風格的統一是遠遠不夠的。大家可能須要創建開發文檔,將各類約定和設計,以及須要共享的其它信息發佈在其中。因此你又在項目的根目錄中新建了一個documentation目錄,並在其中使用GitBook生成了一個文檔,同時同步到了GitBook服務器,以便你的夥伴們即便沒有同步它們也能在線查看。github

使用gitbook創建的組件開發者文檔

1.4 各類安裝方式的支持

你如今已經能夠開始開發你的組件了,在編寫第一個組件的時候,你意識到你如今編寫的這個項目本質上是你的組件庫的使用文檔,若是這算是一個業務項目,那麼它是直接將你的組件庫源碼放置到了本身的源碼中來使用。但若是其它業務項目組使用了你的組件庫,他們目前只能像你如今這樣把源碼拷貝到他們的項目中使用,而且每次你的組件庫升級了,他們都須要經過再次進行拷貝來進行升級,這種狀況顯然是你所不能接受的。你開始考慮你的用戶們怎麼安裝你的組件庫了。

你首先想到的是最流行的安裝方式:npm。若是直接將你的組件庫發佈到npm上,你的用戶將能經過它或者yarn很是方便地進行安裝和升級。但目前你的組件庫剛剛起步,你並不但願立刻開源。因而你向公司申請了一個大家公司本身搭建的gitlab中的倉庫,而後在你的組件庫所在的那個文件夾git init 初始化了一個git項目,並將其同步到你申請的那個倉庫中。這時,公司的同事們已經能夠經過

npm install admin-ui git+ssh://admin-ui-git-location.git --save
複製代碼

來安裝你的組件庫了。

而後你能想到的第二種安裝方式就是CDN。你的用戶們經過在他們的頁面內聯

<script src="admin/ui/cdn/location"></script>
複製代碼

來使用你的組件庫,這時就涉及到如何打包你的組件庫了。在這種場景下,你須要將你的組件庫打包爲一整個admin-ui.js這樣的js文件來使用。關於打包咱們將在下一節繼續討論。

固然,最後一種安裝方式就是直接使用源碼了,將你的組件庫直接放到項目源碼中進行引用。

1.5 打包和發佈你的組件庫

肯定了須要支持的安裝方式,你可能已經意識到,如今你的項目中有兩部分須要打包和發佈:

  • 你的組件庫(這是重點)
  • 你的組件庫的使用文檔(也就是項目自己)

你首先想到的是使用文檔打包起來很方便,由於這個項目目前的打包配置直接就能將你編寫的全部示例頁面打包好,你惟一要作的就是運行

npm run build
複製代碼

但關鍵問題在於你的組件庫admin-ui。它目前是做爲你這個項目的一部分源碼存在的。因此你不得不開始思考如何對這部分代碼進行單獨打包。

固然,你也能夠不對你的組件庫進行打包,而是直接將其源碼發佈爲一個npm包,但那樣的話,用戶在使用它的時候就須要依賴打包工具來對你的代碼進行打包。而相似vue-cli這類工具生成的項目,默認狀況下是不會打包來自node_modules文件夾下的代碼,用戶必須修改構建配置手動指定須要打包的代碼位置,這很不方便

因而你在對照着build.jswebpack.prod.conf.jsprod.env.js,在項目根目錄下的buildconfig文件夾中分別新增了publish.jswebpack.publish.conf.jspublish.env.js文件,並查閱了webpack文檔,去掉了不須要的一些功能配置,設置好了對你的組件庫進行打包的配置。

你指望將打包後的代碼就放在你的組件庫文件夾內,並命名爲dist,這時你的組件庫的源文件就須要移動到src目錄下。

組件庫源碼與打包後的代碼並存

webpack在對代碼進行打包時須要指定入口文件,這時你發現你的組件庫自己尚未出口文件。

1.6 全量加載和按需加載

你在組件庫的src文件夾下新建了一個index.js文件,它引入並輸出了全部的組件。

import Button from './components/button'
import Icon from './components/icon'
// ...省略的代碼...
export {
  Button,
  Icon
  // ...省略的代碼...
}
複製代碼

到這裏,你或許會乾脆將組件庫自己的文件結構也一併規劃好:

組件庫自己文件結構

在這種輸出格式下,你的用戶能夠經過

import { Button } from 'admin-ui'
複製代碼

來從組件庫中得到Button組件。然而這僅僅是對這種格式的支持(這並非按需加載),用戶還須要可以進行全量加載,也就是一次引入全部組件並所有自動註冊好。因此你在index.js中將全部的組件都掛載到adminUi對象上,而後再在該對象上掛載install()方法用於支持Vue.use(),最後直接輸出這個對象。如今你的index.js看起來像這樣:

import Button from './components/button'
import Icon from './components/icon'
// ...省略的代碼...
export {
  Button,
  Icon
  // ...省略的代碼...
}

const adminUi = {
  Button,
  Icon,
  // ...省略的代碼...
}

adminUi.install = function (Vue, options = {}) {
  Vue.component('cu-button', Button)
  Vue.component('cu-icon', Icon)
  // ...省略的代碼,你也能夠用循環來寫...
}
export default adminUi
複製代碼

install()方法中能夠作不少事情,除了註冊組件,極可能你也會在其中進行一些實例方法的掛載

這時你的用戶能夠經過

import adminUi from 'adminUi'
Vue.use(adminUi)
複製代碼

來進行全量加載。

接下來就是按需加載。你發現若是僅僅是經過你的index.js入口文件去加載某個組件,其它組件雖然沒有被用戶引入,但仍舊被編譯到了用戶的代碼中去了。因此你不得不考慮新的方式。既然不能從單一入口加載,是否能夠爲每一個組件指定一個加載點呢?你但願你的用戶可以經過相似

import Button from 'admin-ui/button'
複製代碼

這樣的方式來加載單個組件,這樣就不存在多餘的組件了。因此你意識到,每一個組件還須要單獨進行打包。以每一個組件的出口文件(可能也是個index.js,這裏你應該意識到每一個組件的文件結構保持一致能帶來好處)爲打包入口,將每一個組件都打包爲一個單獨的模塊放置到dist中的lib文件夾下。這時,按需加載就被支持了。

打包後的組件庫的文件結構

我並未討論具體的webpack配置,一是由於本文主要討論思路而不是具體實現,二是這個話題若是要深刻討論須要更多篇幅,三是webpack自己配置很是複雜而我並不算熟練。

而後你愉快地嘗試了一下打包,但沮喪地發現,無論是以組件庫自己的出口文件爲入口,仍是在對每一個組件進行單獨打包的時候,結果除了一個.js文件,還會有一個.css文件。你的用戶無論是全量加載,仍是按需加載,在引入.js文件時還要引入對應的.css文件。在全量加載時,因爲只加載一次,這彷佛不是什麼大問題。但若是是按需加載,由於要引入屢次,這就有些麻煩了。

分離的css文件是出於性能考慮,css文件能夠被瀏覽器緩存,同時組件自己渲染時不須要再生成css了

解決方案有兩種,一種是推薦用戶使用babel-plugin-component,另外一種是打包後的組件自己再也不提供css文件,而是全局引入全量加載的那個css文件。兩種作法均可以,但我使用的是後者。

有兩個緣由,首先組件們的樣式集合起來體積並不大,壓縮打包後控制在60KB之內(這其中絕大部分都是font-awesome的樣式代碼,組件的全部樣式不超過5kb);其次因爲使用了font-awesome,若是每一個組件單獨引入本身的樣式,依賴了font-awesome的組件們就會出現重複的樣式。

2 設計一個主題系統

當你的組件庫被用於不一樣的項目中,或者某個項目須要換膚功能時,不可避免地,你須要在你的組件庫中設計一個主題系統。

2.1 肯定主題系統功能邊界

首先須要明確,你的主題系統功能的邊界。在我看來,影響一個管理類後臺系統風格的因素主要有三種:

  • 顏色(這是最主要的)
  • 陰影
  • 圓角

因此不妨先將你的主題系統邊界就設定爲這三種因素。

2.2 選擇合適的實現方案

而後你開始思考可行的主題系統實現思路:

  • 特殊格式的字符串替換
  • 主題文件預編譯
  • 樣式類

特殊格式的字符串替換無疑是最簡便的,開發時當遇到須要被主題系統控制的樣式時,在css中直接使用特使格式的字符串,在運行時進行替換便可。好比:

div {
  color: $$primary$$;
}
複製代碼

運行時被你的腳本替換成:

div {
  color: #00f;
}
複製代碼

這種方案的優勢是開發時很是便捷,基本不影響開發體驗,甚至還有提高。在傳統的jquery時代問題不大,但就Vue項目而言,存在「替換時機」問題。你大能夠在項目初始化後將頁面中全部<style>標籤中的特殊字符替換掉,但當頁面變化,新的組件的style被插入到head中時,你還須要再次替換,很難找到合適的時機來作這件事。

主題文件預編譯是目前市面上主流的主題實現方案。即UI庫自己提供生成不一樣主題的css文件的工具,事先編譯好幾套不一樣的主題樣式文件。優勢是簡單直接,方便好用。但缺陷也顯而易見:運行時的主題替換變得很是粗暴(粗粒度)——你只能一整套一整套地替換。

樣式類則是設計好樣式規則,在須要的元素上應用樣式類便可:

.au-theme-font-color--primary {
  color: #00f;
}
複製代碼
<p class="au-theme-font-color--primary">主色</p>
複製代碼

樣式類一樣有它的很是明顯的缺陷:首先你須要有很是清晰的樣式類規則設計,而後對開發的影響也很是重大:全部的主題系統涵蓋的樣式都只能用樣式類來書寫,不能直接寫在css中。這兩點給使用者帶來必定認知和使用負擔。但優勢也一樣明顯:控制粒度能夠很是細,不存在替換時機問題,同時,不只僅能夠控制組件庫自己的主題,也能夠直接用於整個項目。

帶有實驗性質地,我選擇了樣式類,因此假定你也作出了一樣的選擇。

2.3 使用樣式類來設計和實現你的主題系統

若是你不知道從何下手,不妨試着從你的主題系統的使用者的角度入手。你指望你的使用者可以經過一個簡單的函數來控制主題:

adminUi.theme(config)
複製代碼

那麼很天然地你就會去定義好config的結構。根據前面界定好的主題系統功能,你會將其作以下定義:

const config = {
  colors: {
    'base-0': '#000',
    'base-1': '#232a29',
    'base-2': '#2f3938',
    'base-3': '#44514f ',
    'base-4': '#616e6c',
    'base-5': '#7c8886',
    'base-6': '#b1bbba',
    'base-7': '#d9dedd',
    'base-8': '#eaf0ef',
    'base-9': '#f3f7f6',
    'base-10': '#f7fcfb',
    'base-11': '#fdfdfd',
    'base-12': '#fff',

    'primary-top': '#01241d',
    'primary-up': '#3fd5b8',
    'primary': '#19bc9d',
    'primary-down': '#169f85',
    'primary-bottom': '#e7f7f4',

    'info-top': '#011725',
    'info-up': '#f0faf8',
    'info': '#3498db',
    'info-down': '#2d82ba',
    'info-bottom': '#e6f3fc',

    'warning-top': '#251800',
    'warning-up': '#fec564',
    'warning': '#ffb433',
    'warning-down': '#e99b14',
    'warning-bottom': '#fbf3e5',

    'danger-top': '#220401',
    'danger-up': '#f56354',
    'danger': '#e74c3c',
    'danger-down': '#c33a2c',
    'danger-bottom': '#fae7e5',

    'success-top': '#012401',
    'success-up': '#7fcb7f',
    'success': '#5cb95c',
    'success-down': '#3da63d',
    'success-bottom': '#e7fae7'
  },
  shadows: {
    'base': '0 1px 4px rgba(0, 0, 0, .2)',
    'primary': '0 0 4px rgba(25, 188, 157, .6)',
    'info': '0 0 4px rgba(52, 152, 219, .6)',
    'warning': '0 0 4px rgba(255, 180, 51, .6)',
    'danger': '0 0 4px rgba(231, 76, 60, .6)',
    'success': '0 0 4px rgba(92, 185, 92, .6)'
  },
  radiuses: {
    'small': '2px',
    'large': '5px'
  }
}
複製代碼
  • primarywarningdangerinfosuccess爲主要顏色
  • [COLOR]-up[COLOR]-down 爲明度較接近主要顏色的次要顏色
  • [COLOR]-top[COLOR]-bottom 爲明度與主要顏色相差較大的輔助顏色
  • base-0base-12 爲最暗無彩色和最亮無彩色
  • base-[1~11]爲按明度排列的無彩色(灰色)

不使用帶有顏色信息的詞(好比light、dark-primary等)而是使用數字和方向來做爲顏色名稱的緣由是爲了方便用戶在某個名稱上定義任意的顏色,假如你將純黑色的名稱定義爲了dark,但用戶配置時卻使用的是#fff純白色,這個名稱就會帶來誤解。在非彩色上,咱們使用數字來做爲名稱,而在彩色上,使用方向來做爲名稱,既能契合彩色的層次設計,又能規避歧義。

你的這套配置規則指望用戶可以按照明度配置顏色,每一個種類顏色明度排列都是一致的。這是爲了方便色彩之間的明暗搭配,好比應該在深色背景上使用淺色文字。但有這個限制的同時,帶來的好處即是用戶可以配置一些自定義的顏色。

同時,爲了進一步精簡顏色配置,你決定在陰影、非主要顏色和無彩色缺省的狀況下,基於primary顏色和一些輔助配置來自動計算它們。因而用戶的實際配置能夠進一步簡化:

export default {
  theme: {
    colors: { // 彩色配置
      primary: '#1c86e2',
      info: '#68217a',
      warning: '#f5ae08',
      danger: '#ea3a46',
      success: '#0cb470'
    },
    shadows: { // 陰影配置
      // primary: '0 0 4px #1c86e2',
      // info: '0 0 4px #68217a',
      // warning: '0 0 4px #f5ae08',
      // danger: '0 0 4px #ea3a46',
      // success: '0 0 4px #0cb470'
    },
    radiuses: {
      small: '3px',
      large: '5px'
    }
  },
  lightnessReverse: false, // 反轉lightness排序(黑白主題)
  colorTopBottom: 5, // top和bottom顏色距離純黑和純白的lightness的距離,越小越接近純黑純白
  colorUpDown: 10, // 彩色上下接近色與正色的lightness距離
  baseColorLeve: 12, // 無彩色分級數量
  baseColorHue: '20%', // 無彩色飽和度
  baseShadowOpacity: 0.2, // 無彩色陰影不透明度
  colorShadowOpacity: 0.6 // 彩色陰影不透明度
}

複製代碼

主題系統的文件結構以下:

主題系統文件結構

接下來,思考用戶配置完了主題系統後,他們如何將其應用到元素上。你的主題系統提供的樣式類,須要一個便於記憶的語法,來方便用戶使用,這時你可能會設計出相似下面這樣的語法規則:

前綴 [-僞類名] -屬性名 --屬性值 [-權重]
複製代碼
  • 前綴:主題樣式類前綴
  • 僞類名:可選的,若是主題是應用在當前元素的僞類上的,則能夠在類名中鏈接僞類名
  • 屬性名:樣式的屬性名
  • 屬性值:樣式的屬性值,即在config中配置好的名稱
  • 權重:可選的,可以使用其爲該主題樣式添加!important後綴

在這套語法規則下,用戶用起來就像下面這樣:

<div class=" au-theme-background-color--base-12 au-theme-border-color--primary au-theme-font-color--base-3 au-theme-box-shadow--base au-theme-radius--small"></div>
複製代碼

最後,你的主題系統將用戶傳入的配置根據你的語法規則轉換爲具體的樣式類代碼,並利用<style>標籤將其插入了頁面。

3 提供一套表單組件

任何一個UI組件庫,尤爲是管理系統的UI組件庫,都不可避免地須要提供一套表單組件。緣由很簡單,首先各家瀏覽器提供的默認表單控件不只風格不一還醜到天際;其次表單的排版、驗證等等功能都是剛需,沒理由不抽象出來。

因此首先你會列舉出經常使用的表單組件:文本輸入框、多選、單選、開關、下拉、級聯、日期、時間、文件上傳,這些組件都被你放到TODO LIST中了。

3.1 統一表單接口

你會發現不少表單組件的行爲方式是一致的,好比都須要value接口,都得支持v-model,都有input或者change事件等等。這些統一的行爲最好放置到一塊兒去,因此你使用Vue的mixin功能來提取這些統一的行爲到一塊兒,一方面便於管理,另外一方面可以使表單組件在功能與行爲上儘量保持一致,以此來下降用戶的認知成本。

3.2 統一驗證方式

其實驗證這部分功能嚴格來說,也算是統一的表單接口,因此也能夠一併放在上面的文件中,但驗證部分的邏輯其實相對獨立一些,因此你極可能會將其獨立出來另作一個minxin來管理。

若是你常常編寫表單,不難發現實際上關於驗證,只有兩種狀況:

  • 交互驗證:用戶在填寫某個表單元素時觸發了該元素的驗證
  • 提交驗證:用戶在提交時觸發了整個表單全部元素的驗證

要支持交互驗證其實很簡單,簡單利用事件便可實現。而要支持提交驗證,則須要每一個表單組件提供具體的驗證方法供外部調用,好比:this.$refs.$userNameInput.validate(),外部調用該函數便可得到驗證結果;在提交表單時將全部表單組件的驗證方法調用一遍便可。

而你的程序在運行驗證代碼時,也有兩種狀況:

  • 同步驗證
  • 異步驗證

支持同步驗證很是簡單,正常調用外部給定的驗證器函數,而後返回其結果便可。但若是是異步驗證就會比較麻煩。咱們稍微深刻一點,假設目前用戶像下面這樣指定了<au-input/>的驗證器:

<au-input :validatiors="[ { validator (v) { return v > 0 }, warning: '必須大於0' } ]"/>
複製代碼

當你獲取到這個驗證器後,你並不能知道其是同步仍是異步驗證,因此你可能會要求用戶指明是同步仍是異步:

<au-input :validatiors="validators"/>
複製代碼
export default {
  data () {
    return {
      validators: [
        {
          validator (v) { return v && v.length && !/^\s*$/g.test(v) },
          warning: '不能爲空'
        },
        {
          validator () {
            return new Promise(resolve => {
              axios.get('is-duplicated-name')
                .then(data => resolve(data.result))
            })
          },
          warning: '已經有重複的名字了',
          async: true
        }
      ]
    }
  }
}
複製代碼

當用戶像上面同樣指明瞭同步仍是異步驗證,而且其驗證函數返回的是一個promise後,你就能夠事先將全部的驗證器分紅兩類:同步驗證和異步驗證,而且首先驗證同步函數,若是有任意未經過的驗證,則能夠不驗證異步函數來減少開支。下面是我在Admin UI中的驗證邏輯,放出來供你們參考:

// the common validation logic of enhanced form components
export default {
  // ... 省略的代碼 ...
  methods: {
    validate () {
      let vm = this
      if (vm.warnings && vm.warnings.length) return false
      if (!vm.validators) return false
      // separate async and sync
      let syncStack = []
      let asyncStack = []
      vm.validators.forEach((v) => {
        if (v.async) {
          asyncStack.push(v)
        } else {
          syncStack.push(v)
        }
      })

      // handler warnings
      let handleWarnings = (res, i, warning) => {
        if (!res) {
          vm.$set(vm.localWarnings, i, warning)
        } else {
          vm.$delete(vm.localWarnings, i)
        }
      }

      return new Promise((resolve) => {
        let asyncCount = asyncStack.length
        // execute sync validation first
        syncStack.forEach((v, i) => {
          handleWarnings(v.validator(vm.value), i, v.warning)
        })
        // if sync validation passed, execute async validationg
        if (!vm.hasLocalWarnings) {
          if (asyncCount <= 0) { // no async
            resolve(!vm.hasLocalWarnings)
          } else {
            Promise.all(asyncStack.map((av, i) => {
              return av.validator(vm.value).then(res => {
                handleWarnings(res, i, av.warning)
              })
            })).then(results => {
              if (results.includes(false)) resolve(!vm.hasLocalWarnings)
              else resolve(!vm.hasLocalWarnings)
            })
          }
        } else { // if sync validation failed
          resolve(!vm.hasLocalWarnings)
        }
      })
    }
  }
}

複製代碼

表單組件的驗證方法返回的是一個promise,在其resolve方法中返回了具體的驗證結果,好處是在提交驗證時,用戶不須要區分同步仍是異步,所有統一對待,簡單方便:

export default {
  validateAllFormitems () {
    Promise.all([
      this.$refs.name.validate(),
      this.$refs.age.validate(),
      this.$refs.gender.validate()
    ]).then(results => {
      if (!results.includes(false)) this.submit()
    })
  }
}
複製代碼

3.3 封裝排版

常見的表單排班有兩種,一種是label與表單元素上下排列,另外一種是左右排列。你的表單組件們的排版方式應當保持統一,因此你可能會建立一個表單組件的容器組件來作這件事。固然,你的表單組件接口中,也應當有對應的控制排版的接口。

上下排列時,除了常規寬度,像文本輸入框、下拉選擇框一類的組件還要考慮100%寬度的情形。這時你可能須要另外一個full-width接口來讓用戶選擇是否全寬度佔滿。

左右排列時,則要考慮label的對齊。通常的作法是,規定全部表單元素的label到一個合適的寬度,以後label中的文字向右對齊。因爲各個組件之間自己是相互獨立的,你應該會指望使用者來告訴你label的合適寬度,因此你每一個表單組件都提供了label-width接口。

你將這些特性都封裝在一個叫form-item的容器組件中,並在每一個表單組件中使用它。

3.4 日期、時間和日期時間範圍

關於日期、時間及日期時間範圍這三個功能對應的組件,我但願你可以思考的問題是:如何劃分組件的功能。

常見的劃分是

  • 日期選擇器:能夠選擇單點日期,也能夠選擇日期範圍
  • 時間選擇器:能夠選擇單點時間,也能夠選擇時間範圍
  • 日期時間選擇器:能夠選擇單點日期和時間,也能夠選擇日期+時間的範圍

然而我更推薦的劃分是

  • 日期選擇器:僅能選擇單點日期
  • 時間選擇器:僅能選擇單點時間
  • 日期時間範圍選擇器:僅能選擇範圍,但能夠只是日期範圍,也能夠只是時間範圍,也能夠是日期+時間的範圍

這麼劃分的好處是,你的日期選擇器和時間選擇器可以最大程度被複用,而且三個組件在實現上相對前一種劃分中的三個組件要簡單不少。這並很差理解,須要你仔細體會。

4 提供一套圖標庫

絕大部分UI庫都提供了圖標組件。緣由很簡單:沒有人喜歡那煩人的字體文件路徑問題。將字體文件經過一個固定的組件進行引入可以避免你的使用者爲其所困。

Admin UI早期版本使用的是一個更漂亮的Ionicons圖標庫,但其圖標種類略少,後續的版本更換成了Font Awesome圖標庫。

選擇何種圖標影響並非很大,你甚至能夠不使用地第三方的圖標庫而是僅提供你的組件庫所須要的最小圖標集,轉而將使用何種圖標庫的選擇權交給你的使用者——圖標組件應當支持第三方圖標的使用。

5 必要的網格系統

市面上幾乎絕大部分UI庫都帶着網格系統,來方便開發者快速自適應佈局頁面。早期的技術,例如Bootstrap等UI庫,使用floatwidth等CSS屬性及CSS媒體查詢來實現網格系統。而現代的UI庫則大部分使用flex來實現網格。

你可能會想要使用現代的技術來實現一個相似Bootstrap中的網格系統。然而在Bootstrap中,網格的使用是依靠樣式類來進行的,同時它要求一個父元素及若干子元素來造成要求的結構。你可能對此並不滿意。樣式類的應用可使用props來替代,而固定的父級元素,你卻可能並不但願用戶關心。爲此,你考慮只用一個grid組件來實現整個網格系統,因此在初始化的時候,你須要處理好父元素,尤爲是須要處理父元素涉及使用display屬性的狀況,由於你須要老是在父元素上使用flex屬性。

在不考慮格子之間間距的狀況下,徹底可使用媒體查詢和flex屬性來完成網格系統。但若是涉及到間距,要使用CSS來實現就會比較麻煩。flex屬性可以實現網格橫向排列的特性,但網格自己寬度,由於涉及到間距的計算,你可能會使用JavaScript來作。

使用者經過props傳入一個相似widthLg的屬性,告知組件在大屏下所佔的網格數量,經過space屬性告知組件與下一組件的間距,這時候須要引入行的概念,你須要計算哪些組件處在一行中,由於行末尾的最後一個grid右側不能有間距,不然會由於超出一行總寬度而被擠到下一行。

網格的實現並不困難,主要面對的需求點就是網格本身的特性:不一樣屏寬下所佔網格數量、偏移距離、間距。你可能看明白了,我並不打算展開講具體實現,可是你能夠去看看源碼一探究竟。說實話,這部分的實現並不算有優雅,主要是一些實驗性質的作法。歡迎你來重構!

6 單元測試和國際化

說實話,這一部分我並不打算講。但你得知道,你的組件庫將來若是要開源,這兩部分是必不可少的。

7 結語

若是以具體實現細節做爲考量,這篇文章基本上沒有什麼乾貨。我僅僅是把編寫一個組件庫須要面對的問題進行了羅列,並泛泛地談了談解決方案而已。無論怎樣,但願可以對你有所啓發。

最後,即便是造輪子,也有它其中的樂趣所在。這裏先挖一個坑,將來我可能會從組件中挑選一些比較有意思的,另寫文章來分享具體的組件實現細節,敬請期待~

同時,也歡迎你去fork這個充滿了私貨的UI庫,提Issues或者Pull Request都是很是歡迎的~

相關文章
相關標籤/搜索