动态

详情 返回 返回

聊聊前端 UI 組件:組件設計 - 动态 详情

在本系列文章《聊聊前端 UI 組件:組件體系》中初步説明了 UI 組件的架構設計,本文將在此基礎上進一步展開説説那篇文章中一筆帶過的部分,並闡述在設計一個 UI 組件時應該注意的點有哪些。

目錄結構

在《聊聊前端 UI 組件:組件體系》中列出的目錄結構的基礎上做了些許調整——

component
   ├── demo                       # 示例相關文件
   │   └── ...
   ├── test                       # 測試相關文件
   │   └── ...
   ├── style                      # 樣式相關文件
   │   ├── _functions.scss        # Sass 函數(可選)
   │   ├── _properties.scss       # CSS 自定義屬性(必需),風格組件的一部分,供外部運行時自定義主題風格
   │   ├── _variables.scss        # Sass 變量(必需),風格組件的一部分,供外部編輯時/編譯時自定義主題風格
   │   ├── _mixins.scss           # Sass 混入(可選)
   │   └── _rules.scss            # CSS 規則(必需),視覺組件,具有約束結構的作用
   ├── typing                     # 類型相關文件
   │   ├── custom-properties.ts   # CSS 自定義屬性配置項(必需),用於運行時生成 CSS 自定義屬性
   │   ├── aliases.ts             # 類型別名(可選)
   │   ├── interfaces.ts          # 結構組件接口(必需)
   │   └── index.ts               # 類型統一導出
   ├── HeadlessComponent.ts       # 無頭組件,UI 組件與結構無關的邏輯
   ├── Component.vue              # 結構組件,受生成 HTML 的 JS 庫/框架的源碼、平台限定的視圖結構描述語言影響
   ├── index.ts                   # 模塊統一導出
   ├── changelog.md               # 組件變更記錄
   ├── readme.md                  # 組件説明文檔
   ├── metadata.yml
   └── package.json

命名約定

HTML & CSS class

在基於組件開發(Component-based Development),即大家所説的「組件化」,在 web 前端領域普及之前,流行過一種神奇的 class 命名方式,可以説是一種方法論了——原子類(atomic classes)。

估計一入行就是 React、Vue 橫行的前端,壓根兒就沒聽過更沒見過「原子類」是個什麼東西——

<style>
.w-100 { width: 100px; }
.w-150 { width: 150px; }
.h-100 { height: 100px; }
.h-150 { height: 150px; }

.m-10 { margin: 10px; }
.m-20 { margin: 20px; }
.mt-10 { margin-top: 10px; }
.ml-15 { margin-left: 15px; }

.bgc-red { background-color: red; }
.bgc-greed { background-color: green; }

.c-fff { color: #fff; }
.c-000 { color: #000; }

.f-l { float: left; }
.f-r { float: right; }
</style>

<div class="w-150 h-150 f-l mt-10 ml-15 bgc-red c-000">
  <div class="w-100 h-100 f-r m-20 bgc-green c-fff">Atomic classes</div>
</div>

看到了吧,這種方法論強調的就是儘可能將 CSS 的每個屬性和值的組合拆成 class,命名方式也基本是「屬性名 + 屬性值」的形式,並且屬性名和屬性值是否進行「簡寫」以及中間有沒有 -_ 等分隔符就看編寫的人的素養和心情了。

原子類的「優點」是,它把 class 拆分到足夠細,很好很「原子」;原子化帶來的特點就是可組合性很強,這樣任何頁面都可以通過原子類的有機組合去實現,只有想不到,沒有做不到!哪天設計師説要把按鈕距離左邊的 15 像素改為 10 像素——沒問題!把 <button>.ml-15 換成 .ml-10 就好!小菜一碟!

為什麼上面説的「優點」是加了引號的?我就想知道,原子類除了寫的時候字符數可能會稍微少些,跟寫內聯樣式(inline style)有什麼區別?有更語義化嗎?可讀性有變更好嗎?人腦負擔有降低嗎?中、大型項目維護起來更方便嗎?

隨着基於組件開發在 web 前端領域的普及,原子類的身影逐漸消失;但最近因為某個 CSS 框架人氣走高的原因,原子類再度死灰復燃……

那麼,原子類或者説樣式原子化是錯的嗎?不是,都是時臣的錯!啊,不!都是 utility-first 思想的錯!

class 應該是語義化的,尤其是在基於組件開發時,讓在視圖結構中一眼看到 class 後,就知道它是個什麼東西,而不是它長什麼樣。

另外,基於組件開發的特點之一就是封裝,對外屏蔽內部細節;而 utility-first 思想恰恰是暴露細節,這與基於組件開發的理念「三觀不合」。

在基於組件開發的體系下,class 理應是 component-first,即應用 CSS 組件(CSS component),那些 utility class 作為輔助存在。也就是説,當 CSS 組件自帶樣式與實際需求有些許不符時,利用 utility class 進行「微調」,而不是在外部重寫 CSS 組件的樣式——這也是一種組合方式。

比如,按鈕 CSS 組件本身是不會在水平方向撐滿容器的,但設計師想讓它佔滿一行——

<style>
.Button {
  display: inline-block;
  text-align: center;
}

.u-block {
  display: block !important;
}
</style>

<div>
  <button class="Button u-block">CSS component</button>
</div>

CSS 組件在本系列文章所闡述的 UI 組件體系中,叫做「視覺組件」,class 的命名遵循 BEM 的變體——SUIT CSS 命名約定。

SUIT CSS 是 Normalize.css 的作者 Nicolas Gallagher 於 2013 年左右時創立,雖然現在已經處於基本不維護的狀態了,但它基於組件開發的思想仍發揮着餘熱。

SUIT CSS 命名約定我從 2014 年用到現在,並且會繼續用下去。本系列文章 CSS 相關的示例代碼中 class 的命名皆遵循此命名約定。在基於組件開發的體系下,強烈建議 class 命名遵循 SUIT CSS 命名約定——

/* 組件 */
.ComponentName {}

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

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

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

/* 輔助工具 */
.u-utilityName {}

組件基類 .ComponentName 及其後代 .ComponentName-descendentName 很好理解,它們天然具有層級關係,共同描述了一個 UI 組件的結構——

<!-- 用語義化 HTML 標籤 -->
<article class="Article">
  <header class="Article-header">
    <h1 class="Article-title">文章標題</h1>
  </header>
  <section class="Article-section">
    <h2>章節標題</h2>
    <p>章節段落</p>
  </section>
  <footer class="Article-footer">一些其他信息</footer>
</article>

<!-- 用非語義化 HTML 標籤,更能凸顯出 class 命名語義化的作用 -->
<div class="Article">
  <div class="Article-header">
    <h1 class="Article-title">文章標題</h1>
  </div>
  <div class="Article-section">
    <h2>章節標題</h2>
    <p>章節段落</p>
  </div>
  <div class="Article-footer">一些其他信息</div>
</div>

而組件修飾符 .ComponentName--modifierName 和組件狀態 .ComponentName.is-stateOfComponent 有時就不能很好地區分何時該用哪個了。就拿按鈕 CSS 組件來説,它的顏色、是否可用與尺寸,哪個該用修飾符?哪個算是狀態?

我給出一個比較簡單的判斷標準:如果是 UI 組件的特性,即不會因為什麼條件而改變的,用修飾符;倘若會因某個條件滿足與否而變化,那就是狀態——

<!-- 用語義化 HTML 標籤,大號(尺寸)的主要(功能色)操作按鈕 -->
<button class="Button Button--primary Button--large">新增</button>

<!-- 用非語義化 HTML 標籤,不可用(狀態)的危險(功能色)操作按鈕 -->
<span class="Button Button--danger is-disabled">批量刪除</span>

應該注意的是,組件修飾符和組件狀態都是直接加在 UI 組件的根節點上的,也就是要跟在組件基類的後面,不能用於組件後代上。假如一個組件後代需要程序化地改變它本身的樣式,要用輔助工具類而不是狀態類。當一個組件後代的結構、功能等變得複雜時,要將其封裝成一個新的組件。

Sass 變量與 CSS 自定義屬性

在本系列文章所闡述的 UI 組件體系中,Sass 變量和 CSS 自定義屬性合稱為「風格組件」,它們負責主題風格的定製,是與設計體系(Design System)的結合點。其中,Sass 變量是在編輯時/編譯時,CSS 自定義屬性則是在運行時。

在這裏,Sass 變量與 CSS 自定義屬性的命名方式比較類似,它們大概都是 <namespace>-<component-name>[-descendent-name|-modifier-name][-state]-(variable-name|property-name) 的形式。

由於我在基於本系列文章所闡述的思想做一套叫做「Petals」的半成品 UI 組件,因此之後的示例代碼中涉及到的 <namespace> 部分基本都會用 petals

Sass 變量是以 $__petals$petals 開頭,與組件名之間用 -- 連接,前者是內部使用(私有)的,上層開發者無需關心,後者是供外部在編輯時/編譯時定製用;CSS 自定義屬性則用 --petals 開頭,以 - 與組件名相連——

/* 實際形式:<namespace>-<component-name>-(variable-name|property-name) */
$__petals--button-font-size: --petals-button-font-size;
$__petals--button-line-height: --petals-button-line-height;

/* 實際形式:<namespace>-<component-name>-<modifier-name>-<state>-(variable-name|property-name) */
$petals--button-primary-focus-color: var($__petals--primary-active-color, $petals--primary-active-color) !default;
$petals--button-primary-focus-bg: var($__petals--primary-active-bg, $petals--primary-active-bg) !default;

上文所説的 CSS 組件,即視覺組件,它是將樣式進行封裝,對外屏蔽細節;而風格組件相反,通過將視覺組件所用到的 CSS 屬性值動態化的方式達到樣式可定製化的目的,這就變得像 utility-first 的原子類一樣暴露了樣式細節。

但與 utility-first 的 CSS 框架不同的是,風格組件只給進行主題風格定製的人帶來了心智負擔,對其他的上層開發者並無影響。

業務無關

本系列文章主要討論的對象是業務無關的 UI 組件,在單説「UI 組件」或「組件」時也是指這個;而業務相關的 UI 組件,在本系列文章所闡述的 UI 組件體系中叫做「部件」。

根據 UI 組件的通用性,可分為「通用組件」和「專用組件」。「通用組件」是能夠滿足大部分常規場景的 UI 組件,它們的集合通常會作為「組件庫」整體打包發佈為一個軟件包;「專用組件」是為了解決某些特殊場景需求而存在的,像數據網格、各種編輯器等,這類一般都是單獨發包。

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

上面提到的「通用組件」和「專用組件」都是業務無關的 UI 組件。

UI 組件是什麼?可以認為它是一個返回視圖結構的函數,而 UI 組件的屬性(prop)和事件(event)就是這個「函數」的參數。屬性是 UI 組件的外部與其內部進行主動通信的數據,事件則是進行被動通信的回調函數。

一個封裝得好的函數,它的參數應儘可能少,要想明白每個參數的語義,且必須確實有其存在的意義——UI 組件的屬性和事件的設計也該如此。

在設計 UI 組件的屬性時,先思考下要加的這個屬性是不是屬於這個 UI 組件本身的特性?若不是,那要加的屬性的值所對應的 UI 組件的特性是什麼?如果這兩個問題都沒有得到答案,那麼這個屬性可以不用加了。

UI 組件的屬性只應與其本身的特性有關,與業務意義無關——自身特性是自然特性,業務意義是附加特性。

比如,一個按鈕組件通常會有「主要」、「次要」和「危險」這幾種多少與業務沾邊的語義,那麼組件的屬性該如何設計來滿足這種需求呢?

Ant Design 和 Element 的做法是將其作為 type 屬性的值或獨立成一個屬性——

<Button type="primary">Ant Design 中的主要按鈕</Button>
<Button>Ant Design 中的次要(默認)按鈕</Button>
<Button danger>Ant Design 中的危險按鈕</Button>

<el-button type="primary">Element 中的主要按鈕</el-button>
<el-button>Element 中的次要(默認)按鈕</el-button>
<el-button type="danger">Element 中的危險按鈕</el-button>

按照上面説的 UI 組件屬性設計原則來看,「主要」、「次要」和「危險」作用到按鈕組件上的表現主要是顏色發生了變化,所以應該去用表示按鈕的自然特性「顏色」的 color 屬性來滿足同樣的需求——

<button color="primary">主要按鈕</button>
<button>次要(默認)按鈕</button>
<button color="danger">危險按鈕</button>

<!-- 還可以擴展出其他任意多顏色的按鈕 -->
<button color="f00">紅色按鈕</button>
<button color="yellow">黃色按鈕</button>
<button color="blue">藍色按鈕</button>

若 UI 組件的某組特性是二元對立的,如「禁用」與「啓用」,則選擇默認不生效的那個作為屬性,且屬性值是布爾型,默認值為 false

還是拿按鈕組件來舉例:如果默認是「禁用」,那就設計一個代表「啓用」的 enabled 屬性,其默認值是 false,只要組件在被使用時傳入了 enabled,就變成了「啓用」狀態;反之亦然。

另外,UI 組件的屬性值儘可能是簡單數據類型,也就是數字、字符串等。

業務相關

業務相關的 UI 組件,即上文所説的「部件」,因其關注點與業務無關的 UI 組件不同,所以在設計時所遵守的原則和考慮的事情也不盡相同,甚至會大相徑庭。一般來説,會用到上下文與依賴注入等技術。

由於業務相關的 UI 組件不是本系列文章主要討論的對象,在此就不展開説了。

總結

前幾天在朋友圈立了個 flag——

立旗

本文就是該 flag 的「引子」。


本文其他閲讀地址:個人網站|微信公眾號

user avatar dirackeeko 头像 dunizb 头像 imba97 头像 yuhuashi_584a46acea21f 头像 yayujs 头像 compose_hub 头像 liulhf 头像 fromin 头像 changlina 头像 zhishaofei3_586768cab32fd 头像 humi 头像 xingxingshangdelizhi 头像
点赞 16 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.