- 原文地址:What is Modular CSS?
- 原文作者:Scott Vandehey
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:ssshooter
- 校對者:Hopsken Park-ma
模組化 CSS 是一組編寫程式碼的原則,基於這個原則編寫的程式碼具有高效能和可維護性。它起源於雅虎和 Yandex 的開發人員,目的是迎接維護大型程式碼庫帶來的挑戰。有些規則在提出之初稍有爭議,但後來被認為是最佳實踐。
目錄:
(偷偷告訴你:如果你對這篇文章的篇幅感到不知所措,觀看視訊可能更適合你,這篇文章來源於此演講。)
大規模 CSS 的處理難點
模組化 CSS 使用的主要場景是棘手的大規模 CSS。正如 Nicholas Gallagher 所說的:
來源:Nicholas Gallagher,圖:dotCSS
這句話直指大規模 CSS 問題的核心。寫程式碼並不難,難的是在不讓你的程式碼隨著時間的推移成為拖累你的“技術債”。
難以理解
以下是 CSS Guidelines 中的一個示例,這個示例展示了一個問題:除了寫這段程式碼的人,沒有人知道這段程式碼是幹什麼的。
<div class="box profile pro-user">
<img class="avatar image" />
<p class="bio">...</p>
</div>
複製程式碼
box
和profile
有什麼關係?profile
和avatar
有什麼關係?或者他們之間真的有關係嗎?你應該在bio
旁邊新增pro-user
嗎?image
和profile
寫在同一部分 CSS 嗎?可以在其他地方使用avatar
嗎?
光看程式碼無法回答這些問題,你必須在 CSS 程式碼中推理他們的作用。
難以複用
複用程式碼會非常棘手。假設你要在另一個頁面上覆用某個頁面上的樣式,但你想這麼做的時候,會發現那個樣式是專為第一個頁面而寫的。程式碼的作者認為它只用在某個特定元素中,或者它是從頁面繼承某些類,在其他環境中根本不起作用。你不想修改原來的內容,然後直接複製了程式碼。
現在你有兩個問題:一份原始程式碼,一份重複程式碼,你的維護負擔直接增加了一倍。
難以維護
大規模的 CSS 也難以維護。你改變了一個標籤,樣式就會像紙牌屋一樣崩潰。你想更新一個頁面上的樣式,卻破壞了另一個頁面的樣式。你試圖覆蓋其他頁面,但又深陷於優先度問題。
它讓我想起了我最喜歡的 CSS 笑話之一:
什麼是模組化
那麼我們如何解決這些問題呢?答案在於模組化這個概念,但這是什麼呢?我們先看看 Harry Roberts 對關注點分離的見解:
來源:Harry Roberts,圖:CSSwizardry.com
這是一個常見程式設計習慣,但是許多 CSS 開發者不太熟悉。這個思想是確保你所寫的東西不會比你想要做的更多。
舉個例子,說明我在學習模組化 CSS 之前的工作方式。設計師給我這樣的草圖:
圖:Yandex
我會覺得:“好吧,這是一個書店頁面,側邊欄中有一些小部件,右側列出了大概是書籍封面的清單,一個精選書評,下面還有其他的評論。”
我當時認為一個頁面是一個完整的單元,頁面裡的較小部分從屬於頁面。這是一種自上而下的思考方法,這導致大量只服務於單個頁面的一次性程式碼,不利於編寫可複用程式碼。
圖:Yandex
模組化 CSS 需要你換一個角度看問題,不從頁面級別考慮,而是關注組成頁面的小塊。這不是一個頁面而是一個元件的集合。
你會發現頁面裡包含的是 logo,搜尋欄,導航,照片列表,輔助導航,標籤框,視訊播放器等。這些是可以網站的任何位置都可以獨立使用的內容。它們只是碰巧在這個特定頁面以這種方式組合。
模組化 CSS 是自下而上的思維,需要從構建整個站點的可複用構建模組開始。
這會讓你想起樂高?應該的!幾乎所有撰寫有關模組化 CSS 的人都使用樂高進行類比。使用標準化,易於理解,並且不依賴上下文的塊來構建 UI 的是一個很好的思路。
這樣的“塊”最著名的例子之一是由 Nicole Sullivan 定義的“媒體物件”,她認為這種物件是你將在任何網站上找到的最小的元件之一。
它將固定寬度的影像組合到靈活寬度的容器的一側,現在到處都可以看到這個模式。她撰寫了一篇名為 The Media Object Saves Hundreds of Lines of Code 的案例研究,談到將此模式應用於大型網站,最大的例子之一便是 Facebook:
這裡高亮顯示了 Facebook 流中的所有媒體物件。左上角個人資訊,右側導航元素,訂閱的每個帖子,甚至是廣告都是媒體物件。有時它們彼此巢狀。雖然使用目的不同,但它們都共享相同的基礎模式:固定寬度的影像,彈性寬度的文字。
她的觀點是,以 Facebook 的規模運營時,媒體物件就不止幾十個,這樣的頁面上有數百上千個。因此,可以想象如果為複用樣式作優化,可以節省大量程式碼,這可以帶來真正的高效能和低成本。
模組化框架
那麼,既然我們已經明確了模組化的概念,那麼讓我們看看這些年來推崇這一概念的三大框架:
OOCSS
物件導向的 CSS(Object-Oriented CSS)/ OOCSS 是模組化 CSS 的起源,由 Nicole Sullivan 於 2009 年提出,這基於她在雅虎的工作。這個框架的核心思想是 —— 物件是可重用的模式(pattern),其視覺外觀不由上下文決定。
- 有人質疑雅虎的能力,雅虎的前端團隊當時研發的 YUI library 是非常前沿的技術。在 2009 年,雅虎不是一家沒有前途的科技公司。
來源: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 的描述:
來源:Varya Stepanova,圖:ScotlandJS
BEM 由 3 部分組成:
塊(Block)
塊是網頁邏輯和功能的獨立元件。BEM 的發起人對其提出了更詳盡的定義:
首先,塊是可巢狀的。它們應該能被包含在另一個塊中,而不會破壞任何樣式。例如,可能在側欄中有一個標籤介面小部件的塊,該塊可能包含按鈕,這些按鈕也是一種單獨的塊。按鈕的樣式和選項卡式元素的樣式不會相互影響,一個巢狀在另一箇中,僅此而已。
其次,塊是可重複的。介面應該能夠包含同一塊的多個例項。就像 Nicole 所說的媒體物件一樣,複用塊可以節省大量程式碼。
元素(Element)
元素是塊的組成部分,它不能在塊之外使用。一個不錯的例子:一個導航選單,它包含的專案在選單的上下文之外沒有意義。你不會為選單項定義塊,選單本身應定義為塊,而選單項是其子元素。
修飾符(Modifier)
修飾符定義塊的外觀和行為。例如,選單塊的外觀的垂直或水平,取決於所使用的修飾符。
命名約定
BEM 所做的另一件事是定義了非常嚴格的命名約定:
.block-name__element--modifier
這看起來有點複雜,我來分解一下:
- 名稱以小寫字母書寫
- 名稱中的單詞用連字元(
-
)分隔 - 元素由雙下劃線(
__
)分隔 - 修飾符由雙連字元(
--
)分隔
這麼說也有點抽象,舉一個例子:
現在我們有一個標準的樂高 minifig。他是一個藍色的宇航員。我們將使用 .minifig
類來區分他。
可以看到 .minifig
塊由較小的元素組成,例如 .minifig__head
和 .minifig__legs
。現在我們新增一個修飾符:
通過新增 .minifig--red
修飾符,我們建立了標準藍色宇航員的紅色版本。
或者,我們可以使用 .minifig--yellow-new
修飾符將我們的宇航員改為新式黃制服版。
你可以使用同樣的方式進行更誇張的修改。通過使用 .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。
來源:Jonathan Snook,圖:Elida Arrizza
他在 OOCSS 和 BEM 的基礎上新增的關鍵概念是,不同類別的元件需要以不同的方式處理。
類別(Categories)
以下是他為 CSS 系統可能包含的規則定義的類別:
- 基礎(Base) 規則是HTML元素的預設樣式,如連結,段落和標題。
- 佈局(Layout) 規則將頁面分成幾個部分,並將一個或多個模組組合在一起。它們只定義佈局,而不管顏色或排版。
- 模組(Module)(又名“物件”或“塊”)是可重用的,設計中的一個模組。例如,按鈕,媒體物件,產品列表等。
- 狀態(State) 規則描述了模組或佈局在特定狀態下的外觀。通常使用 JavaScript 應用或刪除。例如,隱藏,擴充套件,啟用等。
- 主題(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 元素的預設樣式,如:
a
、li
和h1
- 佈局(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__arm
是 minifig
的一部分。然而,有時候他們處理 CSS 中的 DOM 結構時,會疑問如何作深層巢狀,比如 minifig__arm__hand
。
沒有必要這樣做。請記住,這個思路是要將樣式與標記分離。無論 hand
是 minifig
的直接子元素還是巢狀了多少層,都無關緊要。CSS 關心的只有 hand
是 minifig
的孩子。
.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
設定背景顏色和字型。
測試模組是否衝突的一種方法是以隨機順序載入它們。你可以將專案構建配置中設定為在構建時隨機交換樣式位置。如果看到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>
複製程式碼
box
和profile
有什麼關係?profile
和avatar
有什麼關係?或者他們之間有關係嗎?你應該在bio
旁邊新增pro-user
嗎?image
和profile
寫在同一部分 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 是可預測的,可維護的並且是高效能的。
現在我們可以重溫那個老笑話,結局發生了變化:
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。