Vue UI組件庫開發經驗漫談

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

我在一個名為Admin UIVue UI組件庫(GitHub地址:https://github.com/BboyAwey/admin-ui)上投入了大量時間。該庫到目前版本發展到3.x,幾乎全部出自我手。它的優劣請先擱置不問,但至少,我從中積累和學習到的經驗足夠回答一個問題:如何開發一個Vue 組件庫。不過即使你是React的使用者,也可以參考本文給出的經驗,因為如果你打算編寫一個React UI組件庫,你將不得不面對幾乎完全一樣的問題。

這篇文章也是我在公司的一次技術分享的內容。我在這里主要只探討思路,盡量不去涉及具體實現。并且我對這些問題的解決思路也不盡然是完全合理的,如有錯漏請讀者斧正。

1 組織你的項目

當你開始著手組件庫的開發時,第一件事可能就是建立一個項目,因為是Vue 組件庫,你很可能會使用其官方推薦的vue-cli工具來生成一個項目。

1.1 合適的文件結構

當項目生成后,你很快就發現這個項目模版的文件結構用于業務開發非常合適,但并不那么適合組件庫的開發。這時你很可能會在其src文件夾下用你的組件庫名稱建立一個文件夾來存放你的組件庫代碼。但這個時候你還并不清楚所有需要做的事情,你并沒有繼續調整文件結構。

我們暫且將你的組件庫就命名為admin-ui,方便后續行文

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

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

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

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

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

1.3 組件庫本身開發文檔的管理

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

使用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都是非常歡迎的~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,582評論 25 707
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,775評論 18 139
  • 3月4日日精進:敬畏—進入—體驗—交給—持續 1,缺啥補啥,怕啥練啥; 2,一切為我所用,所用為團隊家; 3,我...
    54f0d725963c閱讀 161評論 0 0
  • 做一只優雅的狐貍精 說到狐貍精,似乎是個老話題了,在《封神榜》里,對九尾狐化身的蘇妲己并沒有太多的好感。這個禍國殃...
    夢影的花園閱讀 1,096評論 3 5
  • 現在怎么還有這么不懂事的人呢?是不知道警察叔叔有多辛苦么? 好吧,有這么多熱心市民貢獻素材,警察叔叔們的日常似乎也...
    青神科技客戶部閱讀 215評論 0 0