在本系列文章《聊聊前端 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 的「引子」。
本文其他閲讀地址:個人網站|微信公眾號