[譯] 什麼是模組化 CSS?

ssshooter發表於2018-10-05

模組化 CSS 是一組編寫程式碼的原則,基於這個原則編寫的程式碼具有高效能和可維護性。它起源於雅虎和 Yandex 的開發人員,目的是迎接維護大型程式碼庫帶來的挑戰。有些規則在提出之初稍有爭議,但後來被認為是最佳實踐。

目錄:

  1. 大規模 CSS 的處理難點
  2. 什麼是模組化
  3. 模組化框架
    1. OOCSS
    2. BEM
    3. SMACSS
  4. 共享模組化原則
  5. FAQ
  6. 總結,模組化 CSS 太美妙啦

(偷偷告訴你:如果你對這篇文章的篇幅感到不知所措,觀看視訊可能更適合你,這篇文章來源於此演講。)

大規模 CSS 的處理難點

模組化 CSS 使用的主要場景是棘手的大規模 CSS。正如 Nicholas Gallagher 所說的

“Replace ‘can you build this?’ with ‘can you maintain this without losing your minds?’” —Nicolas Gallagher

來源:Nicholas Gallagher,圖:dotCSS

這句話直指大規模 CSS 問題的核心。寫程式碼並不難,難的是在不讓你的程式碼隨著時間的推移成為拖累你的“技術債”。

難以理解

以下是 CSS Guidelines 中的一個示例,這個示例展示了一個問題:除了寫這段程式碼的人,沒有人知道這段程式碼是幹什麼的。

<div class="box profile pro-user">
  <img class="avatar image" />
  <p class="bio">...</p>
</div>
複製程式碼

boxprofile 有什麼關係?profileavatar 有什麼關係?或者他們之間真的有關係嗎?你應該在 bio 旁邊新增 pro-user 嗎?imageprofile 寫在同一部分 CSS 嗎?可以在其他地方使用 avatar 嗎?

光看程式碼無法回答這些問題,你必須在 CSS 程式碼中推理他們的作用。

難以複用

複用程式碼會非常棘手。假設你要在另一個頁面上覆用某個頁面上的樣式,但你想這麼做的時候,會發現那個樣式是專為第一個頁面而寫的。程式碼的作者認為它只用在某個特定元素中,或者它是從頁面繼承某些類,在其他環境中根本不起作用。你不想修改原來的內容,然後直接複製了程式碼。

現在你有兩個問題:一份原始程式碼,一份重複程式碼,你的維護負擔直接增加了一倍。

難以維護

大規模的 CSS 也難以維護。你改變了一個標籤,樣式就會像紙牌屋一樣崩潰。你想更新一個頁面上的樣式,卻破壞了另一個頁面的樣式。你試圖覆蓋其他頁面,但又深陷於優先度問題。

它讓我想起了我最喜歡的 CSS 笑話之一:

[譯] 什麼是模組化 CSS?

什麼是模組化

那麼我們如何解決這些問題呢?答案在於模組化這個概念,但這是什麼呢?我們先看看 Harry Roberts關注點分離的見解:

“Code which adheres to the separation of concerns can be much more confidently modified, edited, extended, and maintained because we know how far its responsibilities reach. We know that modifying layout, for example, will only ever modify layout—nothing else.” —Harry Roberts

來源:Harry Roberts,圖:CSSwizardry.com

這是一個常見程式設計習慣,但是許多 CSS 開發者不太熟悉。這個思想是確保你所寫的東西不會比你想要做的更多。

舉個例子,說明我在學習模組化 CSS 之前的工作方式。設計師給我這樣的草圖:

Illustration of a design comp for a bookstore website

圖:Yandex

我會覺得:“好吧,這是一個書店頁面,側邊欄中有一些小部件,右側列出了大概是書籍封面的清單,一個精選書評,下面還有其他的評論。”

我當時認為一個頁面是一個完整的單元,頁面裡的較小部分從屬於頁面。這是一種自上而下的思考方法,這導致大量只服務於單個頁面的一次性程式碼,不利於編寫可複用程式碼。

Illustration of a design comp for a bookstore with the components highlighted

圖:Yandex

模組化 CSS 需要你換一個角度看問題,不從頁面級別考慮,而是關注組成頁面的小塊。這不是一個頁面而是一個元件的集合。

你會發現頁面裡包含的是 logo,搜尋欄,導航,照片列表,輔助導航,標籤框,視訊播放器等。這些是可以網站的任何位置都可以獨立使用的內容。它們只是碰巧在這個特定頁面以這種方式組合。

模組化 CSS 是自下而上的思維,需要從構建整個站點的可複用構建模組開始。

Image of workers building with Lego bricks

圖:BEM Method

這會讓你想起樂高?應該的!幾乎所有撰寫有關模組化 CSS 的人都使用樂高進行類比。使用標準化,易於理解,並且不依賴上下文的塊來構建 UI 的是一個很好的思路。

這樣的“塊”最著名的例子之一是由 Nicole Sullivan 定義的“媒體物件”,她認為這種物件是你將在任何網站上找到的最小的元件之一。

An example of the media object

它將固定寬度的影象組合到靈活寬度的容器的一側,現在到處都可以看到這個模式。她撰寫了一篇名為 The Media Object Saves Hundreds of Lines of Code 的案例研究,談到將此模式應用於大型網站,最大的例子之一便是 Facebook:

The media object highlighted in red on the facebook homepage

圖:Nicole Sullivan

這裡高亮顯示了 Facebook 流中的所有媒體物件。左上角個人資訊,右側導航元素,訂閱的每個帖子,甚至是廣告都是媒體物件。有時它們彼此巢狀。雖然使用目的不同,但它們都共享相同的基礎模式:固定寬度的影象,彈性寬度的文字。

她的觀點是,以 Facebook 的規模運營時,媒體物件就不止幾十個,這樣的頁面上有數百上千個。因此,可以想象如果為複用樣式作優化,可以節省大量程式碼,這可以帶來真正的高效能和低成本。

模組化框架

那麼,既然我們已經明確了模組化的概念,那麼讓我們看看這些年來推崇這一概念的三大框架:

OOCSS

物件導向的 CSS(Object-Oriented CSS)/ OOCSS 是模組化 CSS 的起源,由 Nicole Sullivan 於 2009 年提出,這基於她在雅虎的工作。這個框架的核心思想是 —— 物件是可重用的模式(pattern),其視覺外觀不由上下文決定。

  • 有人質疑雅虎的能力,雅虎的前端團隊當時研發的 YUI library 是非常前沿的技術。在 2009 年,雅虎不是一家沒有前途的科技公司。

“a CSS ‘object’ is a repeating visual pattern, that can be abstracted into an independent snippet of HTML, CSS, and possibly JavaScript. That object can then be reused throughout a site.” —Nicole Sullivan

來源:Nicole Sullivan,圖:John Morrison

正如她在 2009 年所定義的那樣,這就是模組化 CSS 的起源。除此之外,OOCSS 可歸結為幾個核心原則:

上下文無關

首先,無論你把它放在哪裡,一個物件都應該看起來無差別,不應根據物件的上下文設定物件的樣式。

例如,不是將側邊欄中的所有按鈕都設定為橙色,將主區域中的所有按鈕設定為藍色,而是應該建立一個藍色的按鈕類,以及一個橙色的 modifier。這樣做橙色按鈕可以在任何地方使用,它們沒有被繫結在側邊欄上,它們只是你的按鈕樣式之一。

皮膚(主題)

她談到的另一個概念,是如何從正在應用的皮膚中抽象出物件的結構

我們可以回到媒體物件的例子。它的樣式與標籤結構無關。有一個容器,一個固定寬度的影象和內容。你可以應用不同的樣式,但是無論樣式如何改變,標籤結構都是一樣的。

她建議的其他方法是為常見的視覺模式建立可複用的類。她給出了一個例子,在 2009 年的亞馬遜網站幾乎所有的東西都有陰影,但因為它們由不同設計師創作,所以相似卻不相同。通過標準化這些元素陰影,可以優化程式碼並使網站更高效。

使用 Class

當時她提出了一個非常具有爭議性的規則,但後來被廣為接受:使用 class 來命名物件及其子元素,這樣可以在不影響樣式的情況下修改 HTML 標籤。

她不希望 CSS 由 HTML 標籤來確定,這樣的話如果將標題從“h1”更改為“h4”,則不必更新 CSS。無論選擇哪個標籤,該標題應該有一個固有的 class。例如,你的導航應該類似於 .site-nav 而不是 #header ul

不使用 ID

既然建議“總是使用 class”,那麼自然禁止使用 ID 選擇器。這與當時使用 ID 作為名稱空間的常見實踐相違背,直接引用巢狀在其中的元素。

ID 會擾亂 CSS 優先度,這是其次,物件必須是可複用的。根據定義,ID 是唯一的。因此,如果在物件上設定 ID,則無法在同一頁面上重複使用它,缺少了模組化物件的要點。

BEM

接下來介紹下一個弘揚模組化 CSS 精神的框架。BEM,三個字母分別代表 Block、Element、Modifier,BEM 也是在 2009 年提出,起源於 Yandex(可以說是俄語版的 Google),除搜尋業務外還運營網路郵件程式,因此在程式設計上他們也需要解決與雅虎相同規模的難題。

他們提出了一套非常類似的程式碼原則。他們的核心概念是 —— 塊(block)(Nicole 稱之為“物體(object)”)由子元素(element)構成,並且可以修改(modified)(或“主題化”)。

以下是其中一位負責 BEM 的開發者 Varya Stepanova 對 BEM 的描述:

“BEM is a way to modularize development of web pages. By breaking your web interface into components… you can have your interface divided into independent parts, each one with its own development cycle.” —Varya Stepanova

來源:Varya Stepanova,圖:ScotlandJS

BEM 由 3 部分組成:

塊(Block)

塊是網頁邏輯和功能的獨立元件。BEM 的發起人對其提出了更詳盡的定義:

首先,塊是可巢狀的。它們應該能被包含在另一個塊中,而不會破壞任何樣式。例如,可能在側欄中有一個標籤介面小部件的塊,該塊可能包含按鈕,這些按鈕也是一種單獨的塊。按鈕的樣式和選項卡式元素的樣式不會相互影響,一個巢狀在另一箇中,僅此而已。

其次,塊是可重複的。介面應該能夠包含同一塊的多個例項。就像 Nicole 所說的媒體物件一樣,複用塊可以節省大量程式碼。

元素(Element)

元素是塊的組成部分,它不能在塊之外使用。一個不錯的例子:一個導航選單,它包含的專案在選單的上下文之外沒有意義。你不會為選單項定義塊,選單本身應定義為塊,而選單項是其子元素。

修飾符(Modifier)

修飾符定義塊的外觀和行為。例如,選單塊的外觀的垂直或水平,取決於所使用的修飾符。

命名約定

BEM 所做的另一件事是定義了非常嚴格的命名約定:

.block-name__element--modifier

這看起來有點複雜,我來分解一下:

  • 名稱以小寫字母書寫
  • 名稱中的單詞用連字元(-)分隔
  • 元素由雙下劃線(__)分隔
  • 修飾符由雙連字元(--)分隔

這麼說也有點抽象,舉一個例子:

Example of .minifig to indicate a lego minifig

現在我們有一個標準的樂高 minifig。他是一個藍色的宇航員。我們將使用 .minifig 類來區分他。

Example of .minifig module with child elements such as .minifig__head and .minifig__legs

可以看到 .minifig 塊由較小的元素組成,例如 .minifig__head.minifig__legs。現在我們新增一個修飾符:

Example of .minifig--red module modifier, turning the minifig red

通過新增 .minifig--red 修飾符,我們建立了標準藍色宇航員的紅色版本。

Example of a .minifig--yellow-new module modifier, turning the minifig yellow

或者,我們可以使用 .minifig--yellow-new 修飾符將我們的宇航員改為新式黃制服版。

Example of a .minifig--batman module modifier making a drastic change in the appearance of the minifig

你可以使用同樣的方式進行更誇張的修改。通過使用 .minifig--batman 修飾符,我們只用一個類就改變了 minifig 的每個部分的外觀。

這是實踐中的 BEM 語法例子:

<button class="btn btn--big btn--orange">
  <span class="btn__price">$9.99</span>
  <span class="btn__text">Subscribe</span>
</button>
複製程式碼

即使不看樣式程式碼,你也可以一眼就看出這段程式碼會建立一個既大又橙的價格按鈕。無論你是否喜歡帶有連字元和下劃線的這種風格,擁有嚴格的命名約定是模組化 CSS 向前邁出的一大步,這讓程式碼帶有自文件的效果!

不巢狀 CSS

就像 OOCSS 建議使用 class 而不使用 ID 一樣,BEM 也為程式碼風格作了一些限制。最值得注意的是,他們認為不應該巢狀 CSS 選擇器。巢狀選擇器擾亂了優先度,使得重用程式碼變得更加困難。例如,只需使用 .btn__price 而不是 .btn .btn__price

注意:這裡的巢狀指實踐中在 Sass 或 Less 巢狀選擇器的做法,但即使你沒有使用前處理器也適用,因為這關乎選擇器優先度問題。

這個原則不出問題是因為嚴格的命名約定。我們曾經使用巢狀選擇器將它們隔離在名稱空間的上下文中。而 BEM 的命名約定本身就提供了名稱空間,因此我們不再需要巢狀。即使 CSS 的根級別的所有內容都是單個類,但這些名稱的具體程度足以避免衝突。

一般來說,選擇器可以在沒有巢狀的情況下生效,就不要巢狀它。 BEM 允許此規則的唯一例外是基於塊狀態或其修飾符的樣式元素。例如,可以使用 .btn__text 然後用 .btn--orange .btn__text 來覆蓋應用了修飾符按鈕的文字顏色。

SMACSS

我們最後要討論的框架是 SMACSS,含義是 CSS 的可擴充套件性和模組化架構(Scalable & Modular Architecture)。Jonathan Snook 於 2011 年提出了 SMACSS,當時他在雅虎工作,為 Yahoo Mail 編寫 CSS。

“At the very core of SMACSS is categorization. By categorizing CSS rules, we begin to see patterns and can define better practices around each of these patterns.” —Jonathan Snook

來源:Jonathan Snook,圖:Elida Arrizza

他在 OOCSS 和 BEM 的基礎上新增的關鍵概念是,不同類別的元件需要以不同的方式處理。

類別(Categories)

以下是他為 CSS 系統可能包含的規則定義的類別:

  1. 基礎(Base) 規則是HTML元素的預設樣式,如連結,段落和標題。
  2. 佈局(Layout) 規則將頁面分成幾個部分,並將一個或多個模組組合在一起。它們只定義佈局,而不管顏色或排版。
  3. 模組(Module)(又名“物件”或“塊”)是可重用的,設計中的一個模組。例如,按鈕,媒體物件,產品列表等。
  4. 狀態(State) 規則描述了模組或佈局在特定狀態下的外觀。通常使用 JavaScript 應用或刪除。例如,隱藏,擴充套件,啟用等。
  5. 主題(Theme) 規則描述了模組或佈局在主題應用時的外觀,例如,在 Yahoo Mail 中,可以使用使用者主題,這會影響頁面上的每個模組。(這非常適用於像雅虎這樣的應用程式,但大多數網站都不會使用此類別。)

命名約定字首

下一個原則是使用字首來區分類別,他喜歡 BEM 明確的命名約定,但他還希望能夠一目瞭然地看出模組的型別。

  • l- 用作佈局規則的字首:l-inline
  • m- 用作模組規則的字首:m-callout
  • is- 用作狀態規則的字首:is-collapsed

(基礎規則沒有字首,因為它們直接應用於 HTML 元素而不使用類。)

共享模組化原則

這些框架的相同之處遠勝於其不同之處。我看到從 OOCSS 到 BEM 再到 SMACSS 的明確發展。它們的發展代表了我們行業在效能和大規模 CSS 領域不斷增長的經驗。

你不必選擇其中一個框架,相反,我們可以嘗試定義模組化 CSS 的通用規則。讓我們看看這些框架共用和保留的最佳部分。

模組化元素

模組化系統由以下元素組成:

  • 模組(Module):(又名物件,塊或元件)一種可複用且自成一體的模式。如媒體物件,導航和頁首。
  • 子元素(Child Element): 一個不能獨立存在的小塊,屬於模組的一部分。如媒體物件中的影象,導航選項卡和頁首 logo。
  • 模組修改器(Module Modifier):(又名皮膚或主題)改變模組的視覺外觀。如左/右對齊的媒體物件,垂直/水平導航。

模組化類別

模組化系統中的樣式可以分為以下幾類:

  • 基礎(Base) 規則是 HTML 元素的預設樣式,如:alih1
  • 佈局(Layout) 規則控制模組的佈局方式,但不控制視覺外觀,如:.l-centered.l-grid.l-fixed-top
  • 模組(Modules) 是可複用的,獨立的 UI 元件視覺樣式,如:.m-profile.m-card.m-modal
  • 狀態(State) 規則由 JavaScript 新增,如:.is-hidden.is-collapsed.is-active
  • 助手(Helper)(又名功能)規則適用範圍小,獨立於模組,如:.h-uppercase.h-nowrap.h-muted

模組化規則

在模組化系統中編寫樣式時,請遵循以下規則:

  • 不要使用 ID
  • CSS 巢狀不要超過一層
  • 為子元素新增類名
  • 遵循命名約定
  • 為類名新增字首

FAQ

這麼做 HTML 不會有很多類嗎?

模組化 CSS 最常見的反對意見就是,它會在 HTML 中產生許多類。我認為這是因為長期以來 CSS 的最佳實踐都認為應該避免大量 class 使用。早在 2011 年,Nicole Sullivan 就寫了一篇很棒的博文 Our (CSS) Best Practices are Killing Us,明確駁斥了這個想法。

我看到一些開發人員提倡使用前處理器的 extend 函式將多個樣式連線成一個類名。我建議不要這樣做,因為它會使你的程式碼不那麼靈活。他們不能讓其他開發者以新的方式組合你的樂高積木,而是固定了你定義的幾種組合。

BEM 的類名又長又醜!

不要因為類名太長而害怕,他們是自文件的!當我看到 BEM 風格的類名(或任何其他模組化命名約定)時,我會覺得很愉悅,因為只要看一眼就能知道這些類的含義。你可以在 HTML 中清晰理解它們。

孫元素的命名如何約定?

長話短說:沒這回事。

模組化 CSS 初學者可以快速掌握子元素的概念:minifig__armminifig 的一部分。然而,有時候他們處理 CSS 中的 DOM 結構時,會疑問如何作深層巢狀,比如 minifig__arm__hand

沒有必要這樣做。請記住,這個思路是要將樣式與標記分離。無論 handminifig 的直接子元素還是巢狀了多少層,都無關緊要。CSS 關心的只有 handminifig 的孩子。

.minifig {}
  .minifig__arm {}
      .minifig__arm__hand {} /* don't do this */
  .minifig__hand {} /* do this instead */
複製程式碼

模組衝突怎麼辦?

模組化 CSS 初學者比較關注的另一件事是模組之間的衝突。例如,如果我將 l-card 模組和 m-author-profile 模組同時應用於同一個元素,是否會導致問題?

答案是:理想情況下,模組不應該重疊太多。在這個例子中,l-card 模組關注佈局,而 m-author-profile 模組關注樣式,你可能會看到 l-card 設定寬度和邊距,而 m-author-profile 設定背景顏色和字型。

[譯] 什麼是模組化 CSS?

測試模組是否衝突的一種方法是以隨機順序載入它們。你可以將專案構建配置中設定為在構建時隨機交換樣式位置。如果看到bug,就證明你的 CSS 需要以特定順序載入。

如果你發現需要將兩個模組應用於同一個元素並且它們存在衝突,請考慮它們是否真的是兩個獨立的模組。也許它們可以用一個修飾符組合成一個模組?

該規則的最後一個例外是“helper”或“utility”類可能會發生衝突,在這些情況下,你可以安全地考慮使用 !important。我知道,你曾被告知 !important 不是什麼好東西,永遠不應該被使用,但我們的做法有細微的差別:主動使用它來確保 helper 類總是優先還是不錯的。 (Harry Roberts has more to say on this topic in the CSS Guidelines。)

總結,模組化 CSS 太美妙啦

我們來簡要回顧一下,還記得這段程式碼嗎?

<div class="box profile pro-user">
  <img class="avatar image" />
  <p class="bio">...</p>
</div>
複製程式碼

boxprofile 有什麼關係?profileavatar 有什麼關係?或者他們之間有關係嗎?你應該在 bio 旁邊新增 pro-user 嗎?imageprofile 寫在同一部分 CSS 嗎?可以在其他地方使用 avatar 嗎?

現在我們知道如何解決這些問題了。通過編寫模組化 CSS 並使用適當的命名約定,我們可以編寫自文件的程式碼:

<div class="l-box m-profile m-profile--is-pro-user">
  <img class="m-avatar m-profile__image" />
  <p class="m-profile__bio">...</p>
</div>
複製程式碼

我們可以看到哪些類彼此相關,哪些類彼此不相關,以及如何相關。我們知道在這個元件的範圍之外我們不能使用哪些類,當然,我們還知道哪些類可以在其他地方複用。

模組化 CSS 簡化了程式碼並推進了重構,產出自文件的程式碼,這樣的程式碼不影響外部作用域且可複用。

或者換句話說,模組化 CSS 是可預測的,可維護的並且是高效能的。

現在我們可以重溫那個老笑話,結局發生了變化:

Two CSS properties walk into a bar. Everything is fine, thanks to modular code and proper namespacing.

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章