<b>本文講的是一個健壯且可擴充的 CSS 架構所需的8個簡單規則,</b>
這是一份清單,裡面列出了在我多年的專業 Web 開發期間,在複雜的大型 Web 項目中學習到的有關管理 CSS 的事項。我多次被人問起這些東西,是以寫一份文檔記錄下來聽起來是個不錯的主意。
我已經盡力嘗試用簡短的語言去解釋它們了,然而這篇文章本質上還是長文慎入:
<a href="https://llp0574.github.io/2016/11/17/css-architecture/#1-always-prefer-classes">總是類名優先</a>
<a href="https://llp0574.github.io/2016/11/17/css-architecture/#2-co-locate-component-code">元件代碼放在一起</a>
<a href="https://llp0574.github.io/2016/11/17/css-architecture/#3-use-consistent-class-namespacing">使用一緻的類命名空間</a>
<a href="https://llp0574.github.io/2016/11/17/css-architecture/#4-maintain-a-strict-mapping-between-namespaces-and-filenames">維護命名空間和檔案名之間的嚴格映射</a>
<a href="https://llp0574.github.io/2016/11/17/css-architecture/#5-prevent-leaking-styles-outside-the-component">避免元件外的樣式洩露</a>
<a href="https://llp0574.github.io/2016/11/17/css-architecture/#6-prevent-leaking-styles-inside-the-component">避免元件内的樣式洩露</a>
<a href="https://llp0574.github.io/2016/11/17/css-architecture/#7-respect-component-boundaries">遵守元件邊界</a>
<a href="https://llp0574.github.io/2016/11/17/css-architecture/#8-integrate-external-styles-loosely">松散地整合外部樣式</a>
兩種方法間的抉擇不在本文過多贅述,并且像往常一樣,它們都有各自的支援者和反對者。說完這些,在下面的内容裡,我将會專注于第一種方法,是以如果你選擇了後者,那麼這篇文章可能就沒什麼吸引力了。
但更具體地說,怎樣才能被稱為健壯且可擴充呢?
友善 - 我們想要所有好的東西,并且還不想因它們而産生更多的工作。也就是說,我們不想因為采用這個架構而讓我們的開發者體驗變得更糟。可能的話,我們想(開發者體驗)變得更好。
安全性錯誤 - 結合之前的一點,我們想要所有東西都可以預設局部化,并且全局化隻是一個特例。工程師都是很懶的,是以為了得到最容易的方法往往都需要使用合适的解決方案。
這是顯而易見的。
是以在極少特例的情況下,你的樣式應該總是類名優先。
當使用一個元件的時候,如果所有群組件相關的資源(其 JavaScript 代碼,樣式,測試用例,文檔等等)都可以非常緊密地放在一起,那就更好了:
1
2
3
4
5
6
7
8
9
10
ui/
├── layout/
| ├── Header.js // component code
| ├── Header.scss // component styles
| ├── Header.spec.js // component-specific unit tests
| └── Header.fixtures.json // any mock data the component tests might need
├── utils/
| ├── Button.md // usage documentation for the component
| ├── Button.js // ...and so on, you get the idea
| └── Button.scss
比如,使用 <code>myapp-Header-link</code> 來當做一個類名,組成它的三個部分都有着特定的功能:
<code>myapp</code> 首先用來将我們的應用和其他可能運作在同一個 DOM 上的應用隔離開來
<code>Header</code> 用來将元件和應用裡其他的元件隔離開來
<code>link</code> 用來為局部樣式效果儲存一個局部名稱(在元件的命名空間内)
作為一個特殊的情況,<code>Header</code> 元件的根元素可以簡單地用 <code>myapp-Header</code> 類來标記。對于一個非常簡單的元件來說,這可能就是所需要做的全部了。
不管我們選擇怎樣的命名空間規範,我們都想要通過它保持一緻性。那三個類名組成部分除了有着特定功能,也同樣有着特定的含義。隻需要看一下類名,就可以知道它屬于哪裡了。這樣的命名空間将成為我們浏覽項目樣式的地圖。
目前為止我都假設命名空間的方案為 <code>app-Component-class</code>,這是我個人在工作當中發現确實好用的方案,當然你也可以琢磨出自己的一套來。
這隻是對之前兩條規則的邏輯組合(元件代碼放在一起以及類命名空間):所有影響一個特定元件的樣式都應該放到一個檔案裡,并以元件命名,沒有例外。
如果你正在使用浏覽器,然後發現一個元件表現異常,那麼你就可以點選右鍵檢查它,接着你就會看到:
<div class="myapp-Header">...</div>
注意到元件名稱,然後切換至你的編輯器,按下“快速打開檔案”的快捷鍵,然後開始輸入“head”,就可以看到:

這種來自 UI 元件關聯源代碼檔案的嚴格映射非常有用,特别是如果你新進入一個團隊并且還沒有完全熟悉代碼結構,通過這個方法你不需要熟悉就可以快速找到你應該寫代碼的地方了。
有一個對這種方法的自然推論(但或許不是那麼快變得明顯):一個單獨的樣式檔案應該隻包含屬于一個獨立命名空間的樣式。為什麼?假設我們有一個登入表單,隻在 <code>Header</code> 元件内使用。在 JavaScript 代碼層面,它被定義成一個名為<code>Header.js</code> 的輔助元件,并且沒有在任何地方被引入。你可能想聲明一個類名為 <code>myapp-LoginForm</code>,并在<code>Header.js</code> 和 <code>Header.scss</code> 裡使用。那麼假設團隊裡有一個新人被安排去修複登入表單上一個很小的布局問題,并想通過檢查元素發現在哪裡開始修改。然而并沒有 <code>LoginForm.js</code> 或者 <code>LoginForm.scss</code> 可以被發現,這時他就不得不憑借 <code>grep</code> (Linux 指令)或者靠猜去尋找相關聯的源代碼檔案。這也就是說,如果這個登入表單産生了一個獨立的命名空間,那麼就應該将其分割成一個獨立的元件。一緻性在大型項目裡是非常有價值的。
我們已經建立了自己的命名空間規範,并且現在想使用它們去沙箱化我們的 UI 元件。如果每個元件都隻使用加上它們唯一的命名空間字首的類名,那我們就可以确定它們的樣式不會洩露到其他元件中去。這是非常高效的(看後面的注意事項),但是不得不反複輸入命名空間也會變得越來越冗長乏味。
一個健壯,且仍然非常簡單的解決方案就是将整個樣式檔案包裝成一個字首。注意我們是怎樣做到隻需要重複一次應用群組件名稱:
11
12
.myapp-Header {
background: black;
color: white;
&-link {
color: blue;
}
&-signup {
border: 1px solid gray;
.myapp-Header-link {
.myapp-Header-signup {
display: block;
&-isScrolledDown &-signup {
display: none;
上面的編譯結果如下:
.myapp-Header-isScrolledDown .myapp-Header-signup {
隻要你的預編譯器支援冒泡(SASS、LESS、PostCSS 和 Stylus 都可以做到),甚至媒體查詢也可以很友善表示:
@media (max-width: 500px) {
上面的代碼就會變成:
上面的模式讓使用長且唯一的類名變得非常友善,因為你再也無需反複輸入它們了。友善性是強制的,因為如果不友善,那麼我們就會偷工減料了。
這篇文檔是關于樣式規範的,但樣式是不能憑空獨立存在的:我們在 JS 端也需要産生同樣的命名空間化類名,并且友善性也是強制的。
// Create a namespace-bound local copy of React:
var { React } = require('./config/css-ns')('Header');
// Create some elements:
<div className="signup">
<div className="intro">...</div>
<div className="link">...</div>
</div>
将渲染出的 DOM 如下所示:
<div class="myapp-Header-signup">
<div class="myapp-Header-intro">...</div>
<div class="myapp-Header-link">...</div>
這真的非常友善,并且上面所有的代碼讓 JS 端也變成了預設局部化。
但是我再次跑題了,回到 CSS 端。
還記得我說過給每個類名加上元件命名空間的字首時,這是對沙箱化樣式來說很高效的一種方式嗎?還記得我說過這裡有個“注意事項”嗎?
考慮下面的樣式:
a {
以及下面的元件層:
+-------------------------+
| Header |
| |
| [home] [blog] [kittens] | <-- 這些都是 <a> 元素
.myapp-Header a { color: blue; }
但是考慮布局在之後做一下變化:
+-----------------------------------------+
| Header +-----------+ |
| | LoginForm | |
| | | |
| [home] [blog] [kittens] | [info] | | <-- 這些是 <a> 元素
| +-----------+ |
選擇器 <code>.myapp-Header a</code> 同樣比對了 <code>LoginForm</code> 裡的 <code><a></code> 元素,是以我們搞砸了這裡的樣式隔離。事實證明,将所有樣式包裝到一個命名空間裡對于隔離元件及其鄰居元件來說,是一個高效的方式,但卻不能總是和其子元件隔離。
這個問題可以通過兩種方法修複:
絕不在樣式表中使用元素名稱選擇器。如果 <code>Header</code> 裡的 <code><a></code> 元素都使用 <code><a class="myapp-Header-link"></code>替代,那麼我們就不需要處理這個問題了。再往下看,有時候你會設定一些語義化标簽,像<code><article></code>、<code><aside></code> 以及 <code><th></code>,都放在了正确的位置上,并且你又不想用額外的類名來弄亂它們,這種情況下:
根據第二個方法來做調整,我們的樣式代碼就可以改寫如下:
> a {
這樣就可以確定隔離同樣作用于更深層次的元件樹,因為生成的選擇器變成了 <code>.myapp-Header > a</code>。
如果這聽起來有争議,那麼讓我通過下面這個同樣運作良好的例子更進一步地使你信服:
> nav > p > a {
層疊樣式最終會毀掉你的一天。要是嵌套越多的選擇器,那麼就有越高的機會造成一個元素比對上多于一個元件的情況。如果你讀到這裡,你就會知道我們已經消除了這種可能性了(使用嚴格的命名空間字首,并在需要的時候使用強關聯子元素選擇器)。
太多的特性會減少可複用性。寫給 <code>nav p a</code> 的樣式将不能在特定情況下之外的任意地方被複用。但其實我們從來沒想要它可複用,事實上,我們特意禁止這個可複用的方法,因為這種可複用性并不能在我們想實作元件隔離的目标上産生好的作用。
太多的特性會讓重構變得更加困難。這可以在現實中找到依據,假設你隻有一個 <code>.myapp-Header-link a</code>,你可以很自由地在元件的 HTML 中移動 <code><a></code> 元素,同樣的樣式總是會一直生效。然而如果使用 <code>> nav > p > a</code>,就需要更新選擇器去比對元件的 HTML 内這個連結的新位置。但考慮到我們想要 UI 是由一些小且隔離性好的元件組成,這個問題也不是相當重要。當然,如果你不得不在重構的時候考慮整個應用的 HTML 和 CSS,那麼這個問題可能就有點嚴重了。但是現在你是在一個隻有十行樣式代碼的小沙箱内進行操作,并且還知道沙箱外沒有其他東西需要考慮,那麼這種類型的變化就不是問題了。
通過這個例子,你應該很好的了解了規則,是以你知道什麼時候應該打破它們。在我們的架構裡,選擇器嵌套不僅僅隻是可以用,有時候它還是一件非常正确的事情。為之瘋狂吧。
是以我們是否已經實作了樣式的完美沙箱化,以至于每個元件的存在都可以和頁面的其他内容隔離開來呢?做一個快速回顧:
我們已經通過用元件的命名空間給每個類名加字首來避免元件向外洩露樣式:
+-------+
| |
| -----X--->
引申開來,這也意味着我們已經避免了元件間的洩露:
+-------+ +-------+
| | | |
| ------X------> |
而且我們還通過考慮子選擇器來避免洩露進入子元件:
+---------------------+
| +-------+ |
| | | |
| ----X------> | |
但更為關鍵的是,外部樣式仍然可以洩露進入元件當中:
----------> |
舉個例子,假設我們給元件寫了下面的樣式:
但是接着我們引入一個表現不好的第三方庫,有着下面的 CSS:
font-family: "Comic Sans";
沒有一個簡單的方法可以保護我們的元件不受外部樣式的污染,并且這是我們經常需要調整的地方:
幸好,對于你自己使用的依賴來說常常會有一個控制方式,并且也可以簡單地找一個表現更好的選擇。
最後,還有 <code><iframe></code>。它提供了 Web 運作環境所能提供的最強的隔離形式(既為 JS 也為 CSS),但同樣為運作成本(潛在因素)和維護(保留的記憶體)帶來了巨大的消耗。不過,通常代價是值得的,并且最著名的網絡嵌入(Facebook、Twitter、Disqus等等)事實上也是用 iframe 實作的。然而本文檔的目的是隔離成千上百個小元件,就此而言,這個方法将數以百倍地消耗我們的性能。
不管怎樣,這個題外話跑得有點遠了,回到我們的 CSS 規則。
就像我們賦予 <code>.myapp-Header > a</code> 的樣式,當嵌套元件的時候,我們可能還需要給子元件提供一些樣式(Web 元件類比再次完美,因為接下來 <code>> a</code> 和 <code>> my-custom-a</code> 的效果并沒有什麼差異)。考慮下面的布局:
+---------------------------------+
| Header +------------+ |
| | LoginForm | |
| | | |
| | +--------+ | |
| +--------+ | | Button | | |
| | Button | | +--------+ | |
| +--------+ +------------+ |
我們馬上可以看到用 <code>.myapp-Header .myapp-Button</code> 寫樣式不會是一個好主意,顯然應該用<code>.myapp-Header > .myapp-Button</code> 來替代。但是我們到底要給子元件提供什麼樣式呢?
注意到 <code>LoginForm</code> 靠在了 <code>Header</code> 的右邊界上。直覺看來,一個可能的樣式就是:
.myapp-LoginForm {
float: right;
我們沒有違反任何規則,但是我們讓 <code>LoginForm</code> 變得有點難以複用了:如果我們接下來的首頁想要這個<code>LoginForm</code>,但是不想要右浮動,那就不走運了。
這個問題實際的解決方案就是(局部地)放寬之前的規則,隻對目前檔案所屬的命名空間提供樣式。具體來說,我們希望用下面的代碼替換:
> .myapp-LoginForm {
這樣實際上已經很好了,隻要我們不允許随意地破壞子元件的沙箱:
padding: 20px;
// COUNTER-EXAMPLE; DON'T DO THIS
我們不允許這麼做,因為這樣做會失去局部變化沒有全局影響的安全性。使用上面代碼的話,當修改 <code>LoginForm</code> 元件表現的時候,<code>LoginForm.scss</code> 就不再是唯一需要檢查的地方了。發生變化再次變得可怕。是以可用與不可用之間的界限到底在哪裡?
我做的這個類比很糟糕,但我們繼續看:就像在一個國家内意味着在其實體邊界之内,我們建立了一個邊界,父元件隻可以在子元件邊界之外對(直接)子元件樣式産生影響。這意味着關系到位置和大小的屬性(如<code>position</code>、<code>margin</code>、<code>display</code>、<code>width</code>、<code>float</code>、<code>z-index</code> 等等)是可用的,而影響到内部邊界的屬性(如<code>border</code> 本身、<code>padding</code>、<code>color</code>、<code>font</code>等)是不可用的。
按照推論,下面這樣顯然也是禁止的:
> a { // relying on implementation details of LoginForm ;__;
有幾個有趣或者說無聊的邊界情況,比如:
<code>box-shadow</code> - 一個特定類型的 shadow 可以是一個元件外觀不可缺少的部分,是以元件應該自己包含這些樣式。話又說回來,這種視覺效果可以在邊界外清楚地渲染出來,是以它又可以回到父元件的作用域。
為了避免重複工作,有時可能需要在元件間共享樣式。為了避免全部工作,有時又可能想使用其他人建立的樣式。這兩種情況的實作都不應該建立出不必要的耦合到代碼庫中。
導出一大堆選擇器(版本 3.3.7 來說, 具體有 2481 個)到命名空間裡,不管你實際上是否使用它們。(有趣的一面:IE9 在預設忽略剩餘選擇器之前隻會處理 4095 個選擇器。我曾經聽說有人花了很多天來調試它們,鬼知道他們經曆了什麼。)
使用寫死的類名如 <code>.btn</code> 和 <code>.table</code>。不敢想象某些不小心複用了這些樣式的開發者或者項目。(諷刺臉)
不管了,我們希望使用 Bootstrap 作為 <code>Button</code> 元件的基礎。
用某段代碼替換下面的來整合到 HTML 端:
<button class="myapp-Button btn">
<button class="myapp-Button">
.myapp-Button {
@extend .btn; // from Bootstrap
這麼做有一個好處,那就是沒有給任何人(包括你自己)産生一種想法:在 HTML 元件上去依賴可笑地命名為 <code>btn</code> 的類。<code>Button</code> 所使用的樣式的來源是一個完全不需要顯示在外面的實作細節。是以,如果你決定放棄 Bootstrap 轉而支援另外的架構(或者隻是你自己去寫樣式),那麼這種改變無論如何都不會外部可見(呃,除非,這種可見變化是在于<code>Button</code> 本身長什麼樣子)。
同樣的原則适用于你自己的輔助類,并且你可以選擇使用更合理的類名:
@extend .myapp-utils-button; // defined elsewhere in your project
@extend %myapp-utils-button; // defined elsewhere in your project
@include myapp-generateCoolButton($padding: 15px, $withExplosions: true);
知曉所有規則,是以知道何時打破它們
最後,如前所述,當你了解了你所制定的規則(或者是從網上其他人那兒采取的),你就可以寫出對你有意義的特例。比如,如果你覺得直接使用一個輔助類是有附加價值的,那麼就可以這麼做:
<button class="myapp-Button myapp-utils-button">
這種附加價值可能是,比如說,你的測試架構之後可以更智能地自動找出什麼元素表現為按鈕,以及可以被點選。
或者你可能會在違背程度很小的情況下決定去打破元件隔離,并且分割元件的額外工作可能會變得更好。但我想要提醒的是這就像是個下坡路,而且不要忘了一緻性的重要性等等,隻要你的團隊保持一緻,并且你可以完成它們,那麼你就是在做對的事情。
<b></b>
<b>原文釋出時間為:2016年11月17日</b>
<b>本文來自雲栖社群合作夥伴掘金,了解相關資訊可以關注掘金網站。</b>