標籤:

如何編寫易於拓展與維護的 CSS 代碼

如何編寫易於拓展與維護的 CSS 代碼

來自專欄 CSS10 人贊了文章

Replace "can you build this?" with "can you maintain this without losing your minds?"

這是前 Twitter 工程師 Nicolas 在 2013 年的一條 tweet,這句話可以應用在很多領域。兩年前看到這句話時,我已經寫了四年 CSS,簡單的項目複雜的項目都寫過,但是能讓我自豪地說出 「I can maintain this without losing my minds.」 的項目一個都沒有。所以就開始了一段關於 「如何編寫易於拓展與維護的 CSS 代碼」 的探索,選擇的方案是 OOCSS + BEM + ITCSS 這三個理論。

現在,兩年多的時間已經過去了,使用這三個理論完成了大大小小很多項目,也總結了一些經驗。我覺得現在是一個很好的時間做階段性總結,把經驗分享出來,所以有了這篇文章。在第一部分里會介紹一個好 CSS 代碼的三個特性,第二部分分別介紹 BEM,OOCSS,以及 ITCSS 這三個理論,以及它們對前面三個特性的影響,最後一部分是簡單的總結。

首先,根據多年的經驗,我認為一個易於拓展與維護的 CSS codebase 至少需要表現出三個特性:隔離性復用性,以及繼承性

隔離性

隔離性表現為每一部分樣式都要有明確的單一目的,不對目的之外的樣式造成干擾,比如 OOCSS 中的結構樣式不會去改變元素的顏色表現。由於 CSS 沒有作用域,任何樣式直接作用在全局生效,非常容易造成樣式衝突。隔離性做得不好的項目中可能會遇到這樣的問題:改動一處樣式,除了實現目的需求,也對其他樣式造成了破壞性干擾,之後不得不嘗試一些變通方法來縮小影響範圍,比如常見的調整屬性權重等。當這類問題不斷重現時,相信整個項目的 CSS 代碼已經盤根錯節,難以維護了。

目前 CSS Modules 等工具能夠給選擇器添加額外的標記限制作用域避免樣式衝突,在成員較多的開發團隊中這類工具也保障了新增代碼的安全性,不過這不在本篇文章的討論範圍之內,這篇文章試圖從理論層面介紹一些方法來規避上面提到的問題,增強對項目 CSS 代碼庫的規劃和管理,這與 CSS Modules 等工具並不衝突。

復用性

我們常說 「Dont repeat yourself」 那就不用再贅述這一條的重要性了。CSS 樣式的復用(或者說設計元素的復用)有個特點:很多情況下不是直接復用,而是在現有樣式的基礎上做出少許變化後的間接復用,關於這一點請大家留意下文中 BEM modifier 部分的介紹。

繼承性

這是 CSS 與生俱來的特性,當不同選擇器的樣式共同作用在同一個元素上時,這些樣式按照選擇器權重 (specificity) 的高低疊加生效,合理利用這個特性能夠大幅簡化代碼,令代碼庫條理清晰,而太過忽視這個特性很容易增加維護的複雜度,給自己製造不必要的麻煩。Harry Roberts 提出的 ITCSS 理論正是利用這個特性約束繼承順序,避免在開發過程中製造混亂。


介紹完這三個特性,還有三個理論,這三個理論與上面的三個特性並不是一一對應的關係,而是每個理論都對這三個特性有不同程度的輔助意義。

BEM

BEM 是 Yandex 團隊開發的一套 self-documenting 的命名規範,它的全稱是 「Block Element Modifier」 也就是用這三個部分組合命名 class 選擇器。BEM 因為過長的選擇器名以及 「醜陋的」 連字元 __-- 存在一些爭議,不過我認為這不足以掩蓋它的優點。不論是多人協作的大型項目,還是個人開發者維護自己之前的老項目,或者接管別人的項目,BEM 命名規範都能夠達成一種以最直觀的方式理解代碼意圖的默契,降低溝通成本。配合各種 CSS Preprocessors (Sass, LESS, Stylus 等) 的父選擇器 & 功能還可以使代碼更容易閱讀:

.block { // block styles &__element-1 { // element styles &--modifier { // modifier styles } } &__element-2 { // element styles }}

BEM Block

一個 BEM block 可以看做是一個組件的整體,使用 block 的名稱命名組件最外層元素的 class 選擇器(.block {}),每個 block 需要使用獨一無二的選擇簽名作為 namespace 避免與其他 blocks 命名重複而造成樣式衝突,也就是隔離樣式。創建 block 時要特別注意界定它的邊界以確保它始終作為獨立的整體復用。

BEM Element

BEM element 通過兩個下劃線 __ 與 BEM block 連接(.block__element {}),由於 block 名作為 namespace 不會出現樣式衝突,所以 element 的命名並沒有太多限制,唯一需要注意的是不可以將 element 視為 block 嵌套子元素,例如 .block__element__nested-block {}。此外,BEM element 不可以脫離 block 作為一個整體單獨復用。

BEM element 可以繼承來自 BEM block 的樣式,但是 BEM block 只可以繼承公共樣式,不可以繼承其他 blocks 的樣式,後面會有更詳細的解釋。

BEM Modifier

BEM modifier 是我最喜歡的部分,它用於標記樣式的變化。假設某個 UI 元素復用在其他地方時樣式需要發生少許變化,我們仍然要保持只寫一套樣式,並使用 modifier 標記出新樣式的變化。

舉個例子,假設 hero 部分有兩套設計,一套文本居左顯示,另一套文本居中顯示同時上下 padding 值也更小一些,除此之外它們的樣式完全相同。我們可以將第一種樣式作為默認樣式,然後創建一個 modifier 負責處理居中後的樣式變化:

.hero { padding-top: 60px; padding-bottom: 60px; // 其他樣式 &--centered { padding-top: 40px; padding-bottom: 40px; text-aligned: center; }}

使用時:

<div class="hero">...</div><div class="hero hero--centered">...</div>

CSS 中直接覆蓋樣式的成本非常低,所以有些開發者會比較隨意地對待這件事情,稍不注意就容易造成混亂,理不清楚。使用 modifier 就是為了隔離樣式,同時它也等同於通知其他團隊成員,或者提醒自己,這個部分有一處樣式變化,對後期維護有非常大的幫助,開發過程也會變得更加嚴謹。


OOCSS

Objected Oriented CSS 即 「面向對象的 CSS」,作者 Nicole Sullivan。它只有兩條原則,我認為也是提高 CSS 代碼復用性最重要的兩條原則。

1. Separate structure and skin

第一條,分離結構樣式與皮膚樣式。舉一個最典型的例子:Bootstrap 的按鈕樣式可以看做由兩類樣式共同組成:結構類樣式與皮膚類樣式。結構類樣式包含 .btn .btn-lg .btn-sm 等選擇器,皮膚類樣式包含 .btn-primary .btn-secondary .btn-success .btn-danger 等選擇器。這兩類樣式能夠按需組合實現出更多種不同的按鈕樣式,並且提升後期的拓展空間。當然能夠這樣組合的,並不僅僅只有按鈕,還有很多其他樣式。

分離出來的 object class 必須要保持它的樣式只有單一目的,才能實現它最大化的復用。我們也可以使用 BEM modifier 標記結構或者皮膚的不同狀態。

按照當下流行的組件化思想,以 React 項目為例,會將 .btn 做成一個按鈕組件,然後 .btn-lg .btn-sm .btn-primary .btn-secondary 等等都是這個按鈕組件的 props (modifier),不過在處理 CSS 樣式時,仍然將樣式按照結構與皮膚兩類分離,因為這是最實用的兩類。

我們再看一個例子:

這兩套組件擁有完全一致的皮膚樣式,區別只是組件的結構不同而已,所以我們可以只創建一套皮膚樣式,然後再創建兩套結構樣式組合使用。這裡我們可以選擇一種結構作為默認樣式,然後將另一種以 BEM modifier 標記僅包含樣式的變化,以便統一維護。如果後續有第三種結構變化,或者皮膚變化,處理辦法也顯而易見:

以上三張截圖均來自 Rob Turlinckx 的設計,已授權使用,在此表示感謝。

2. Separate container and content

第二條,分離容器樣式與內容樣式,這是一條樣式的解耦法則。還是先舉例子,以網格系統為例,沒有人會在網格的樣式中加入顏色屬性,因為對內容的破壞完全不受控制。這裡的網格就是容器,而嵌套在裡面的部分就是內容。現實中我們很容易能夠避免網格這類常用容器破壞內容樣式,而忽視項目中的自定義容器。當一個 BEM block 中嵌套其他 block 時,請注意不要讓外層容器干涉嵌套部分的樣式,而嵌套部分也不要依賴它外層 block 的樣式。為了處理好容器與內容的關係,建議:

  1. 界定好 BEM block 的邊界,block 需要使用的公共樣式通過 object class 獲得,而不是它的外層 block;
  2. 在設計容器時,盡量多考慮不同內容嵌套在其中的情況,並且避免使用可繼承屬性 (Inherited Properties);

此外,在重構樣式時,可能會重組一些 BEM block 或者說組件,遵循這條原則一定能夠提高重構後代碼的安全性。


ITCSS

ITCSS 的全拼 「Inverted Triangle CSS」,是一種將 CSS 代碼庫按照倒三角形狀管理的方法,作者 Harry Roberts。倒三角默認分為 7 層,每一層基於權重、復用性等因素考量放置不同的樣式:越基礎、權重越低、復用性越強的樣式越要往上層放置;反之,越特殊,權重越高,復用性越弱的樣式越要往下層放置。

圖片來源:speakerdeck.com/dafed/m

以 SCSS 為例,ITCSS 的文件結構如下所示:

scss/|-- _base.fonts.scss|-- _components.carousel.scss|-- _components.header.scss|-- _components.pagination.scss|-- _generic.box-sizing.scss|-- _generic.normalize.scss|-- _objects.buttons.scss|-- _objects.grids.scss|-- _objects.typography.scss|-- _settings.colors.scss|-- _tools.mixins.scss|-- _trumps.animations.scss`-- style.scss

"style.scss" 作為主文件,引用 partials 的順序為:

// Settings Layer@import settings.colors;// Tools Layer@import tools.mixins;// Generic Layer@import generic.box-sizing;@import generic.normalize;// Base Layer@import base.fonts;// Objects Layer@import objects.grids;@import objects.buttons;@import objects.typography;// Components Layer@import components.header;@import components.carousel;@import components.pagination;// Trumps Layer@import trumps.animations;

Settings Layer and Tools Layer

Settings Layer 與 Tools Layer 是倒三角最上方的兩層,適合放在一起介紹,前者存放全局變數,後者存放 mixins,functions,或者第三方 libraries。這兩層的特點是不會編譯輸出任何一行代碼到最終的 CSS 文件中,放在倒三角的最上方也是為了保障項目中任何一個地方都可以全局訪問這些代碼。

Generic Layer

這是最早開始編譯輸出 CSS 代碼的一層,主要用於統一不同瀏覽器默認樣式的差異,比如 Reset CSS (估計現在也沒人用了)與 Normalize.css,或者重置 box-sizing。這一層在項目初期添加後,基本不會再有任何改動。

Base Layer

有時也被稱作 Elements Layer,原則上只能出現 type 選擇器。這一層可以看作是只針對當前項目的 Reset CSS,例如根據設計稿給 h[1-6]inputbuttonulolpblockquote 等元素添加項目內的默認樣式。Web fonts 也添加在這一層。

Objects Layer

這一層僅包含基於 OOCSS 原則創建的 object class,只能出現 class 選擇器,這也是第一個開始出現 class 選擇器的一層。建議使用 o- 作為前綴以便與其他樣式分離,例如 .o-btn。也可以使用 BEM modifier 標記樣式的不同狀態,例如 .o-btn--primary。這些 object class 可以使用在項目中的任何地方,它們屬於全局樣式。

不過在 React 項目中,object 常常封裝成組件復用,如 Button, List, Container, Column/Grid 等等,不再適合統一在一個地方管理。

如果把根據 OOCSS 第一條原則分離出來的結構樣式作為公共的 object 樣式,例如 media object,可能會遇到這樣一個問題:沒有辦法單獨控制它們的響應式變化。假設一組 media object 被複用在了兩個不同的組件中,其中一個組件的結構需要比另一個更早地做出響應式變化,那這種復用方式容易將響應式變化隔離在兩個不同的地方(文件里),增加維護的難度。這裡有兩個解決辦法:一種不復用結構,結構樣式直接包含在組件樣式中;另一種將結構的不同變化藉助 BEM modifier 標記清楚,仍然統一在一個地方管理。當結構有特彆強的復用需求時建議使用第二種辦法。

Components Layer

從這一層開始,樣式不再具有復用性,只剩下組件級別的復用。這一層的樣式只針對某個特定的組件,每一個組件就是一個 BEM block,應該完全遵循 BEM 命名規範。

這一層已經有足夠多的樣式可以從前面各層繼承,所以在處理這一層的代碼時,儘可能多繼承公共樣式,在共用樣式的基礎上補充特殊樣式。以組件內(假設名為 Block)的一個按鈕為例:

<div class="block"> <button class="o-btn o-btn--large o-btn--primary block__btn">...</button></div>

先使用 button object 組合出按鈕的基礎樣式,再藉助 .block__btn 補充額外樣式,例如添加 margin 或者 float 等屬性。

React 項目中只是表象稍有不同而已,本質還是一樣的:

<div className="block"> <Button className="block__btn" size="large" color="primary">...</Button></div>

Pages Layer

這一層並不包含在 ITCSS 默認的 7 層裡面,不過 ITCSS 允許我們根據項目需要添加或刪除層。開發 marketing site 時常常有以頁面為單位的改動,放在這一層里非常合適。

Trumps Layer

這一層是倒三角結構的最下方,必須保障權重的絕對優勢,只有在這一層可以使用 !important 強制提升權重。比較常見的做法是把 hide/show 這類樣式放在這一層中。而 React 項目中,使用 inline style 就可以了,不需要保留這一層。

ITCSS 中的樣式繼承

ITCSS 的任何一層都會或多或少繼承它前面各層的樣式,然後在當前層對樣式做出補充。下圖展示了基於 ITCSS 管理的項目的選擇器權重變化,以往權重衝突的問題被很自然地規避掉了,而且是通過 CSS 自身的特性。

圖片來源:speakerdeck.com/dafed/m

Base Layer 繼承 Generic Layer 的樣式,保證各瀏覽器默認樣式統一的前提下補充各類元素的基礎樣式;Objects Layer 繼承前面兩層的樣式,補充能夠獨立復用的 object class;Components Layer 繼承前面各層的樣式,然後補充自身沒有復用性只針對某個組件的特殊樣式;以此類推。這樣的繼承過程能夠使項目的 CSS 代碼變得條理清晰不再混亂,也更安全。


拓展與維護

在 ITCSS 中,Objects Layer 以及之前的各層可以認為是項目的公共樣式,自 Components Layer 之後的各層不再是公共樣式。此外,我認為 Components Layer 中的 BEM modifier 也是它所修改的組件的公共樣式,因為 modifier 具有復用性。所以下面提到的公共樣式,主要指 Objects Layer 中的 object class 以及 Components Layer 中的 modifier 樣式。

項目迭代時,我們先判斷某個需求是屬於公共樣式的改動,還是非公共樣式的改動。對於公共樣式的改動,可以嘗試借鑒 「不可變對象」 (immutable object)的理念,團隊中的開發成員需要達成默契 object class 與 modifier 是不可變的,一旦創建後便不再允許改動或者刪除,除非有明確的目的需要全局改變這部分樣式,否則只能補充新的 object class 或者 modifier,只有這樣才能保障團隊協作的安全性。


總結

這套方案已經不斷改進、優化實施了兩年,對我個人的幫助非常大,主要體現在四個方面:

  • 良好的可拓展性與可維護性;
  • 最大限度地復用代碼,編譯生成的 CSS 文件體積極其小;
  • OOCSS,BEM,ITCSS 三個理論組合使用,保障代碼的健壯性;
  • 項目前期整理公共樣式雖然需要多花一點時間,但是之後的開發速度會變得飛快,也很順暢;

還有很多其他的 CSS 理論,如 SMACSS,Enduring CSS, Atomic CSS, RSCSS 等等,但目前我最喜歡的還是 OOCSS + BEM + ITCSS 這三條,簡單、實用、有效。

我試著把自己的經驗全部寫進這篇文章,但還有一些表達不盡人意的地方,如果有疑問歡迎討論。之後也想寫一套新的 CSS 框架,請保持關注 ;)


推薦閱讀:

在創建table表格時,當心「神秘人」改了你的代碼,而你卻不知道!
《Oli的前端一萬小時》之:(4)CSS 值和單位
動效是種浪漫-起點國際浪漫之旅
#02你認真學了css?(基本樣式2)
CSS選擇器、層疊、繼承的那些事

TAG:CSS |