聊聊前端 UI 元件:元件設計

發表於2023-09-19

本系列文章《聊聊前端 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 CSSNormalize.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 DesignElement 的做法是將其作為 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 的「引子」。


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

相關文章