聊聊前端 UI 組件:組件體系

本文首發於 歐雷流。因爲我會時不時對文章進行補充、修正和潤色,爲了保證所看到的是最新版本,請閱讀 原文

本文是文章系列「聊聊前端 UI 組件」的第三篇。css

在本系列的上篇文章《聊聊前端 UI 組件:組件特徵》中,經過從關注點分離的角度進行前端 UI 組件的構成分析,並以較爲抽象的視角對 UI 組件分門別類,以及描述了讓組件間能夠表現複用的繼承關係,從而創建出前端 UI 組件的特徵模型。前端

本文將以上篇文章中所得出的特徵模型爲基礎,探討下如何設計並創建一個前端 UI 組件體系。vue

在作組件體系設計的時候,最重要的一點就是——要真真正正地想着把 UI 組件弄成可複用的,就像製造業生產時所用的物料同樣——構造可交換的 UI 組件。git

因爲 UI 組件構成元素的易變性對組件體系的設計有着很大的影響,爲了方便查看,將上篇文章中的易變性及其影響因素的表格搬過來:github

構成 易變性 影響因素
結構 視覺結構 不易變 內容結構、佈局類樣式
內容結構 較易變 生成 HTML 的 JS 庫/框架的源碼、平臺限定的視圖結構描述語言
表現 主題風格 很易變 GUI 設計人員的審美和想法、非佈局類樣式、圖標與圖片
行爲 交互邏輯 不易變 交互設計人員的想法
業務邏輯 很易變 業務規則

組件架構

表格中列出的 UI 組件構成元素均可以做爲單獨的組件存在。若是把 UI 組件看做是「最終產品」的話,那麼 UI 組件構成元素所對應的那些組件就是「中間產品」。web

在軟件工程中,「組件(component)」通常是指軟件的可複用塊,比如製造業所使用的「構件」。這是一個比較寬泛的概念,它能夠是軟件包,能夠是 web 服務,也能夠是模塊等。

但在前端眼裏,「組件」一般是指頁面上的視圖單元,即「UI 組件」。能夠說,「UI 組件」是「組件」的子集。json

——歐雷《聊聊前端 UI 組件:核心概念segmentfault

鑑於上述緣由,這裏須要特別說明下:上文所說的「做爲單獨的組件存在」中的「組件」是指「軟件的可複用塊」,而不是「UI 組件」。微信

風格組件

在上篇文章中提到了「虛擬組件」的概念——架構

在繼續往下以前,先引入一個「虛擬組件」的概念。正如它的名字所示,是一個虛擬的,實際不存在的,只是概念上的組件。它是幾個主題風格屬性的集合。

——歐雷《聊聊前端 UI 組件:組件特徵

與之類似,「風格組件」也是一些主題風格屬性的集合,大概包括:

風格組件構成

若是要用代碼來體現的話,能夠藉助 CSS 預處理器中的變量。這裏用 Sass 來舉例:

// 主題色
$sc--primary: #cce5ff !default;
$sc--secondary: #e2e3e5 !default;
$sc--info: #d1ecf1 !default;
$sc--success: #d4edda !default;
$sc--warning: #fff3cd !default;
$sc--danger: #f8d7da !default;

// 文本色
$sc--text-primary: #303133 !default;
$sc--text-secondary: #696c71 !default;
$sc--text-heading: #2c405a !default;
$sc--text-regular: #333 !default;
$sc--text-placeholder: #c0c4cc !default;

// 字體尺寸
$sc--font-size: 14px !default;
$sc--font-size-lg: 16px !default;
$sc--font-size-sm: 12px !default;

// 字體粗細
$sc--font-weight-light: 300 !default;
$sc--font-weight-normal: 400 !default;
$sc--font-weight-bold: 700 !default;

// 邊框粗細
$sc--border-width: 1px !default;

// 邊框顏色
$sc--border-color: #dcdfe6 !default;

// 邊框圓角
$sc--border-radius: 4px !default;
$sc--border-radius-lg: 6px !default;
$sc--border-radius-sm: 2px !default;

風格組件與表現複用的繼承密切相關——

輸入框組件、下拉列表組件等都屬於表單控件(form control),它們都繼承自「表單控件」這個虛擬組件,若是各自沒有指定顏色、字體、邊框等主題風格屬性的話,將會按照虛擬組件中所設定的來顯示。相似地,下拉列表組件、下拉菜單組件等都有彈出層(pop-up),它們都繼承了「彈出層」這個虛擬組件。

想必你已經發現了,下拉列表組件同時繼承了「表單控件」和「彈出層」這兩個虛擬組件,這就是上面提到的「多重繼承」。

那些所謂的「虛擬組件」,它們也遵循着一樣的繼承規則——若是自身沒有指定特定的主題風格屬性,則會按照父級所設定的顯示。那麼,虛擬組件的「父級」是啥呢?——是基礎風格。

——歐雷《聊聊前端 UI 組件:組件特徵

上面示例中所定義的 Sass 變量就是「基礎風格」。

以「表單控件」爲例,一個繼承了基礎風格的虛擬組件用代碼表示爲:

$sc--form-control-font-size: $sc--font-size !default;
$sc--form-control-font-size-lg: $sc--font-size-lg !default;
$sc--form-control-font-size-sm: $sc--font-size-sm !default;

$sc--form-control-height: 36px !default;
$sc--form-control-height-lg: 40px !default;
$sc--form-control-height-sm: 32px !default;

$sc--form-control-color: $sc--text-regular !default;
$sc--form-control-placeholder-color: $sc--text-placeholder !default;
$sc--form-control-bg: #fff !default;
$sc--form-control-box-shadow: none !default;

$sc--form-control-border-width: $sc--border-width !default;
$sc--form-control-border-color: $sc--border-color !default;
$sc--form-control-border-radius: $sc--border-radius !default;
$sc--form-control-border-radius-lg: $sc--border-radius-lg !default;
$sc--form-control-border-radius-sm: $sc--border-radius-sm !default;

相應地,輸入框組件的風格組件部分大體爲:

$sc--input-font-size: $sc--form-control-font-size !default;
$sc--input-font-size-lg: $sc--form-control-font-size-lg !default;
$sc--input-font-size-sm: $sc--form-control-font-size-sm !default;

$sc--input-height: $sc--form-control-height !default;
$sc--input-height-lg: $sc--form-control-height-lg !default;
$sc--input-height-sm: $sc--form-control-height-sm !default;

$sc--input-color: $sc--form-control-color !default;
$sc--input-placeholder-color: $sc--form-control-placeholder-color !default;
$sc--input-bg: $sc--form-control-bg !default;
$sc--input-box-shadow: $sc--form-control-box-shadow !default;

$sc--input-border-width: $sc--form-control-border-width !default;
$sc--input-border-color: $sc--form-control-border-color !default;
$sc--input-border-radius: $sc--form-control-border-radius !default;
$sc--input-border-radius-lg: $sc--form-control-border-radius-lg !default;
$sc--input-border-radius-sm: $sc--form-control-border-radius-sm !default;

視覺組件

雖然 UI 組件的最終呈現須要內容結構做爲骨架去支撐,但若僅僅是爲了勾勒出 UI 組件視覺結構的輪廓,只用 CSS 就能夠了。一系列模塊化、可複用、可組合的 CSS 規則構成了「視覺組件」,也可叫作「CSS 組件」。

在視覺組件中,得用 BEM 之類的命名法爲 CSS 類選擇器命名。推薦使用由 BEM 衍生出來的這種:

/* 組件 */
.ComponentName {}

/* 組件後代 */
.ComponentName-descendentName {}

/* 組件修飾符 */
.ComponentName--modifierName {}

/* 組件狀態 */
.ComponentName.is-stateOfComponent {}

一個完整的視覺組件中已經包含了風格組件。拿按鈕組件來舉例的話,它的視覺組件大致是這樣:

$sc--button-font-size: $sc--form-control-font-size !default;
$sc--button-font-size-lg: $sc--form-control-font-size-lg !default;
$sc--button-font-size-sm: $sc--form-control-font-size-sm !default;

$sc--button-padding-y: 10px !default;
$sc--button-padding-y-lg: 12px !default;
$sc--button-padding-y-sm: 9px !default;

$sc--button-padding-x: 20px !default;
$sc--button-padding-x-lg: 20px !default;
$sc--button-padding-x-sm: 15px !default;

$sc--button-color: $sc--form-control-color !default;
$sc--button-bg: $sc--form-control-bg !default;
$sc--button-box-shadow: $sc--form-control-box-shadow !default;

$sc--button-border-width: $sc--form-control-border-width !default;
$sc--button-border-color: $sc--form-control-border-color !default;
$sc--button-border-radius: $sc--form-control-border-radius !default;
$sc--button-border-radius-lg: $sc--form-control-border-radius-lg !default;
$sc--button-border-radius-sm: $sc--form-control-border-radius-sm !default;

$sc--button-disabled-color: $sc--text-placeholder !default;
$sc--button-disabled-bg: #eee !default;

/* ----- 以上爲風格組件部分 ----- */

.Button {
  padding: $sc--button-padding-y $sc--button-padding-x;
  font-size: $sc--button-font-size;
  color: $sc--button-color;
  background-color: $sc--button-bg;
  border: $sc--button-border-width solid $sc--button-border-color;
  border-radius: $sc--button-border-radius;
  box-shadow: $sc--button-box-shadow;

  &-icon,
  &-text {
    display: inline-block;
    vertical-align: middle;
  }

  &-icon + &-text {
    margin-left: 5px;
  }

  // 大按鈕
  &--large {
    padding: $sc--button-padding-y-lg $sc--button-padding-x-lg;
    font-size: $sc--button-font-size-lg;
    border-radius: $sc--button-border-radius-lg;
  }

  // 小按鈕
  &--small {
    padding: $sc--button-padding-y-sm $sc--button-padding-x-sm;
    font-size: $sc--button-font-size-sm;
    border-radius: $sc--button-border-radius-sm;
  }

  // 按鈕失效/禁用狀態
  &.is-disabled {
    color: $sc--button-disabled-color;
    background-color: $sc--button-disabled-bg;
  }
}

從上面的代碼示例中能夠看出,按鈕組件中包含了「圖標」和「文本」這兩個橫向排列且垂直居中的「後代」,並有「常規」、「大」和「小」三種「規格」,以及有「正常」和「失效/禁用」兩種「狀態」——經過 CSS 描繪出了 UI 組件視覺上的基本結構與特性。

無頭組件

「無頭」這個詞譯自「headless」,在計算機領域中表明硬件或軟件在使用或運行時不須要依賴 GUI 相關的設備或庫。在這裏,「無頭組件」是指 UI 組件的交互邏輯,以及與之相融合的業務邏輯。

無頭組件的職責是負責監聽並接收事件系統的通知,提供處理 UI 組件自身狀態、數據轉換邏輯的函數或方法,它不該該關注和處理除了交互邏輯以外的事情。

在無頭組件中,所監聽並接收的並不是是運行環境提供的真實事件,而是自定義的「代理事件」,它是真實事件的佔位符。這麼作的主要緣由是,同一個行爲雖然多是由不一樣的真實事件觸發的,但對 UI 組件而言其語義是相同的——經過代理事件來表達對 UI 組件有意義的真實語義。

就拿下拉菜單組件來講,它的彈出層的顯示能夠經過其所包含的按鈕的 clickmouseover 這兩個真實事件來觸發,但對 UI 組件的真實語義是「彈出」而非「點擊」或「懸停」,於是使用代理事件 pop-up 來替代。

上文說到無頭組件是「UI 組件的交互邏輯及與之相融合的業務邏輯」,又說「不該該關注和處理除了交互邏輯以外的事情」,這兩點乍看之下相互矛盾,然而並不——

業務邏輯對於一個網站、應用來講是十分必要且重要的,但對 UI 組件來講,它就沒那麼必要了,更談不上重要。在前端的 GUI 層面,業務邏輯理應是交互邏輯的延伸。

——歐雷《聊聊前端 UI 組件:組件特徵

在 UI 組件中,業務邏輯與事件是息息相關的,不只僅是 UI 事件,如點擊按鈕後發出 HTTP 請求;還有數據事件,如業務數據變化後更新顯示的文本。所以,業務邏輯是交互邏輯的延伸。這就須要無頭組件在處理交互邏輯時提供擴展點,好比「事件映射」,以使業務邏輯做爲無頭組件的擴展存在,而不是集成進去。

無頭組件會根據代理事件去調用事件處理函數。在未指定的狀況下,代理事件會默認指向一個真實事件,事件處理函數會執行一段默認的處理邏輯;事件映射的做用就是更改代理事件所指向的真實事件以及事件處理函數的邏輯。

無頭組件的接口定義大概長這樣:

// 代理事件
type EventBroker = string;
// 真實事件
type EventName = string;
// 事件處理函數
type EventHandler = (params: any) => void;
// 事件對象
type EventObject = { name: EventName; handler: EventHandler };
// 事件映射
type EventMap = { [key: string]: EventName | EventHandler | EventObject };

interface IHeadlessComponent {
  // 事件映射
  setEventMap(map: EventMap): void;
  // 獲取真實事件
  getEventName(broker: EventBroker): EventName;
  // 獲取事件處理函數
  getEventHandler(broker: EventBroker): EventHandler;
}

結構組件

顧名思義,「結構組件」是用來生成 UI 組件內容結構的,但它的做用不只如此,還會負責對接視覺組件與無頭組件。

若是單純從最終的 HTML 結構上來看,它也算是不易變的,但在現代前端開發中,HTML 的結構基本是動態生成的,而且強依賴於 React、Vue 這類沒有統一標準的 JS 庫/框架。另外,還存在着像 WXML、AXML 這類平臺限定的視圖結構描述語言。因爲寫法不一致,這就使頁面內容結構變得不那麼穩定。

——歐雷《聊聊前端 UI 組件:組件特徵

如上所述,UI 組件的內容結構依賴於視圖結構描述語法,視圖結構描述語法又取決於平臺或運行環境,這就致使告終構組件沒法像風格組件、視覺組件和無頭組件同樣將變化隔離在外部,於是結構組件是構成 UI 組件的幾個組件中易變性最強的,最容易被替換的。

用 Vue 2.x 版本的類組件寫法來舉例,下拉菜單組件的結構組件大體爲:

<template>
  <div :class="$style.Dropdown">
    <button :class="$style['Dropdown-trigger']" @[popUpEventName]="handlePopUp">顯示彈出層</button>
    <div :class="[$style['Dropdown-popup'], { [$style['is-shown']]: isPopUpShown }]">我是彈出層</div>
  </div>
</template>

<!-- 視覺組件 -->
<style lang="scss" src="./style.scss" module></style>

<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator';

// 無頭組件
import DropdownHeadlessComponent from './headless';

@Component
export default class DropdownComponent extends Vue {
  // 事件映射配置外置
  @Prop({ type: Object, default: () => ({}) })
  private readonly eventMap!: { [key: string]: any };

  private popUpEventName: string = '';

  private isPopUpShown: boolean = false;

  private handlePopUp(): void {
    this.isPopUpShown = true;
  }

  // 初始化無頭組件實例及與其相關的
  private initHeadless(): void {
    const hc = new DropdownHeadlessComponent();

    hc.setEventMap(this.eventMap);

    this.popUpEventName = hc.getEventName('pop-up');
  }

  public created(): void {
    this.initHeadless();
  }
}
</script>

假若要支持多技術棧、多平臺,當前流行的主要有兩種策略:在各技術棧、各平臺下分別實現結構組件;利用 Tarouni-app 這類工具進行轉譯。

可定製性

上文闡述的組件架構將一個本來很容易實現得魚龍混雜的 UI 組件根據關注點拆分紅了風格組件、視覺組件、無頭組件和結構組件,這種架構會使各部分的可複用性獲得很大的提高。除了易變性較強的結構組件以外,其餘組件在達到必定成熟度以後就基本不會變更。假如須要更換技術棧或新支持一個平臺,只需實現一遍結構組件便可,其餘組件能夠拿來就用。

不單是可複用性有所改善,可定製性也有所增強。根據定製代碼/配置與程序結合時所處的程序生命週期階段,將可定製性整理爲下表:

可定製點 編輯時/編譯時 運行時
主題風格
視覺結構
觸發事件
業務邏輯
內容結構

若是風格組件的代碼是像示例代碼中寫的那樣,是不支持運行時定製的,得稍微改造一下:

// 未經改造
$sc--font-size: 14px !default;

// 利用 CSS 自定義屬性改造後
$sc--font-size: var(--sc-font-size, 14px) !default;

組件規範

每一個 UI 組件都應當被視做是獨立的軟件包、模塊,因此它的各方面應該是完備的——除了實現 UI 組件的代碼,還應有詳盡的使用說明文檔、可交互的在線 demo、完善的測試代碼以及用來作一些自動化處理的元數據等。

仍是如下拉菜單組件爲例,上述材料相關文件的目錄結構大致以下:

dropdown
   ├── demo
   │   └── ...
   ├── test
   │   └── ...
   ├── changelog.md
   ├── headless.ts
   ├── index.ts
   ├── metadata.yml
   ├── package.json
   ├── readme.md
   ├── structure.vue
   └── style.scss

代碼編寫方面能夠參考我總結並整理的代碼風格指南:https://ntks.ourai.ws/guides/coding-style/

另外,在結構組件中對接視覺組件時,要用 CSS Modules,以免外部的樣式代碼所引發的非預期效果。

總結

本文基於本系列的上篇文章中得出的特徵模型提出了一個以「構造可交換的 UI 組件」爲目標的組件架構,主要由風格組件、視覺組件、無頭組件和結構組件所構成,除告終構組件以外的組件可複用性都很高。

當一個 UI 組件是可交換的時,就能夠圍繞它作一些比較有趣且有價值的事情了。

最後的最後,文中的示例代碼是爲了幫助理解而寫,僅供參考。;-)


歡迎關注微信公衆號【Coding as Hobby】(微信中搜「coding-as-hobby」)以及時閱讀最新的技術文章~ ;-)

相關文章
相關標籤/搜索