- 原文作者:Jarno Rantanen
- 譯文出自:掘金翻譯計(jì)劃
- 譯者:linpu.li
- 校對者:galenyuan,StarCrew

這是一份清單,里面列出了在我多年的專業(yè) Web 開發(fā)期間,在復(fù)雜的大型 Web 項(xiàng)目中學(xué)習(xí)到的有關(guān)管理 CSS 的事項(xiàng)。我多次被人問起這些東西,所以寫一份文檔記錄下來聽起來是個不錯的主意。
我已經(jīng)盡力嘗試用簡短的語言去解釋它們了,然而這篇文章本質(zhì)上還是長文慎入:
- 總是類名優(yōu)先
- 組件代碼放在一起
- 使用一致的類命名空間
- 維護(hù)命名空間和文件名之間的嚴(yán)格映射
- 避免組件外的樣式泄露
- 避免組件內(nèi)的樣式泄露
- 遵守組件邊界
- 松散地整合外部樣式
介紹
如果你正在開發(fā)前端應(yīng)用,那么最后你肯定需要關(guān)心樣式方面的問題。盡管開發(fā)前端應(yīng)用的技術(shù)水平持續(xù)增長,CSS 仍然是給 Web 應(yīng)用賦予樣式的唯一方式(而且最近,在某些情況下,原生應(yīng)用也一樣)。目前在市面上有兩大類樣式解決方案,即:
- CSS 預(yù)編譯器,已經(jīng)存在很長時間了(如 SASS、LESS 及其他)
- CSS-in-JS 庫,一個相對較新的樣式解決方案(如 free-style 和很多其他的)
兩種方法間的抉擇不在本文過多贅述,并且像往常一樣,它們都有各自的支持者和反對者。說完這些,在下面的內(nèi)容里,我將會專注于第一種方法,所以如果你選擇了后者,那么這篇文章可能就沒什么吸引力了。
主要目標(biāo)
但更具體地說,怎樣才能被稱為健壯且可擴(kuò)展呢?
- 面向組件 - 處理 UI 復(fù)雜性的最佳實(shí)踐就是將 UI 分割成一個個的小組件。如果你正在使用一個合理的框架,JavaScript 方面就將原生支持(組件化)。舉個例子,React 就鼓勵高度組件化和分割。我們希望有一個 CSS 架構(gòu)去匹配。
- 沙箱化(Sandboxed) - 如果一個組件的樣式會對其他組件產(chǎn)生不必要以及意想不到的影響,那么將 UI 分割成組件并不會對我們的認(rèn)知負(fù)荷起到幫助作用。就這方面而言,CSS的基本功能,如層疊(cascade)以及一個針對標(biāo)識符的獨(dú)立全局命名空間,都會給你造成負(fù)擔(dān)。如果你熟悉 Web 組件規(guī)范的話,那么就可以認(rèn)為它(此架構(gòu))有著 Shadow DOM 的樣式隔離好處 ,而無需關(guān)心瀏覽器支持(或者規(guī)范是否經(jīng)過嚴(yán)格的推敲)。
- 方便 - 我們想要所有好的東西,并且還不想因它們而產(chǎn)生更多的工作。也就是說,我們不想因?yàn)椴捎眠@個架構(gòu)而讓我們的開發(fā)者體驗(yàn)變得更糟??赡艿脑?,我們想(開發(fā)者體驗(yàn))變得更好。
- 安全性錯誤 - 結(jié)合之前的一點(diǎn),我們想要所有東西都可以默認(rèn)局部化,并且全局化只是一個特例。工程師都是很懶的,所以為了得到最容易的方法往往都需要使用合適的解決方案。
具體的規(guī)則
1. 總是類名優(yōu)先
這是顯而易見的。
不要去使用 ID 選擇器 (如 #header),因?yàn)槊慨?dāng)你認(rèn)為某樣?xùn)|西只會有一個實(shí)例的時候,在無限的時間范圍內(nèi),你都將被證明是錯的。一個典型的例子就是,當(dāng)想要在我們構(gòu)建的大型應(yīng)用中修復(fù)任何數(shù)據(jù)綁定漏洞的時候(這種情況尤為明顯)。我們從為 UI 代碼創(chuàng)建兩個實(shí)例開始,它們并行在同一個 DOM,并都綁定到一個數(shù)據(jù)模型的共享實(shí)例上。這么做是為了保證所有數(shù)據(jù)模型的變化都可以正確體現(xiàn)到這兩個 UI 上。所以任何你可能假設(shè)總是唯一的組件,如一個頭部模板,就不再唯一了。順便一提,這對找出其他唯一性假設(shè)相關(guān)的細(xì)微漏洞來說,也是一個很好的基準(zhǔn)。我跑題了,但這個故事告訴我們的就是:沒有一種情況是使用 ID 選擇器會比使用類選擇器更好,所以只要不使用就行了。
同樣也不應(yīng)該直接使用元素選擇器(如 p)。通常對一個屬于組件的元素使用元素選擇器是可以的(看下面),但是對于元素本身來說,最終你將會為了一個不想使用它們的組件,而不得不將那些樣式給撤銷掉?;叵胍幌挛覀兊闹饕繕?biāo),這同樣也違背了它們(面向組件,避免折磨人的層疊(cascade),以及默認(rèn)局部化)。如果你這么選擇的話,那么在body上設(shè)置一些像字體,行高以及顏色的屬性(也叫可繼承屬性),對這個規(guī)則來說也可以是一個特例,但是如果你真正想做到組件隔離的話,那么放棄這些也完全是可行的(看下面關(guān)于使用外部樣式的部分)。
所以在極少特例的情況下,你的樣式應(yīng)該總是類名優(yōu)先。
2. 組件代碼放在一起
當(dāng)使用一個組件的時候,如果所有和組件相關(guān)的資源(其 JavaScript 代碼,樣式,測試用例,文檔等等)都可以非常緊密地放在一起,那就更好了:
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
當(dāng)你寫代碼的時候,只需要簡單地打開項(xiàng)目的瀏覽工具,組件的所有其他內(nèi)容都唾手可得了。樣式代碼和生成DOM的JavaScript之間有著天然的耦合性,而且我敢打賭你在修改完其中一個之后不久肯定會去修改另外一個。舉例來說,這同樣適用于組件及其測試代碼??梢哉J(rèn)為這就是 UI 組件的訪問局部性原理。我以前也會細(xì)致地去維護(hù)各種獨(dú)立的鏡像文件,它們各自存在 styles/、 tests/ 和 docs/ 等目錄下面,直到我意識到,實(shí)際上我一直這么做的唯一原因是因?yàn)槲揖褪且恢边@樣做的。
3. 使用一致的類命名空間
CSS 對類名及其他標(biāo)識符(如 ID、動畫名稱等)都有一個獨(dú)立扁平的命名空間。就像過去在 PHP 里,其社區(qū)想通過簡單地使用更長且具有結(jié)構(gòu)性的名稱來處理這個問題,因此就效仿了命名空間(BEM 就是個例子)。我們也想要選擇一個命名空間規(guī)范,并堅(jiān)持下去。
比如,使用 myapp-Header-link 來當(dāng)做一個類名,組成它的三個部分都有著特定的功能:
-
myapp首先用來將我們的應(yīng)用和其他可能運(yùn)行在同一個 DOM 上的應(yīng)用隔離開來 -
Header用來將組件和應(yīng)用里其他的組件隔離開來 -
link用來為局部樣式效果保存一個局部名稱(在組件的命名空間內(nèi))
作為一個特殊的情況,Header 組件的根元素可以簡單地用 myapp-Header 類來標(biāo)記。對于一個非常簡單的組件來說,這可能就是所需要做的全部了。
不管我們選擇怎樣的命名空間規(guī)范,我們都想要通過它保持一致性。那三個類名組成部分除了有著特定功能,也同樣有著特定的含義。只需要看一下類名,就可以知道它屬于哪里了。這樣的命名空間將成為我們?yōu)g覽項(xiàng)目樣式的地圖。
目前為止我都假設(shè)命名空間的方案為 app-Component-class,這是我個人在工作當(dāng)中發(fā)現(xiàn)確實(shí)好用的方案,當(dāng)然你也可以琢磨出自己的一套來。
4. 維護(hù)命名空間和文件名之間的嚴(yán)格映射
這只是對之前兩條規(guī)則的邏輯組合(組件代碼放在一起以及類命名空間):所有影響一個特定組件的樣式都應(yīng)該放到一個文件里,并以組件命名,沒有例外。
如果你正在使用瀏覽器,然后發(fā)現(xiàn)一個組件表現(xiàn)異常,那么你就可以點(diǎn)擊右鍵檢查它,接著你就會看到:
<div class="myapp-Header">...</div>
注意到組件名稱,然后切換至你的編輯器,按下“快速打開文件”的快捷鍵,然后開始輸入“head”,就可以看到:
這種來自 UI 組件關(guān)聯(lián)源代碼文件的嚴(yán)格映射非常有用,特別是如果你新進(jìn)入一個團(tuán)隊(duì)并且還沒有完全熟悉代碼結(jié)構(gòu),通過這個方法你不需要熟悉就可以快速找到你應(yīng)該寫代碼的地方了。
有一個對這種方法的自然推論(但或許不是那么快變得明顯):一個單獨(dú)的樣式文件應(yīng)該只包含屬于一個獨(dú)立命名空間的樣式。為什么?假設(shè)我們有一個登錄表單,只在 Header 組件內(nèi)使用。在 JavaScript 代碼層面,它被定義成一個名為 Header.js 的輔助組件,并且沒有在任何地方被引入。你可能想聲明一個類名為 myapp-LoginForm,并在 Header.js 和 Header.scss 里使用。那么假設(shè)團(tuán)隊(duì)里有一個新人被安排去修復(fù)登錄表單上一個很小的布局問題,并想通過檢查元素發(fā)現(xiàn)在哪里開始修改。然而并沒有 LoginForm.js 或者 LoginForm.scss 可以被發(fā)現(xiàn),這時他就不得不憑借 grep (Linux 命令)或者靠猜去尋找相關(guān)聯(lián)的源代碼文件。這也就是說,如果這個登錄表單產(chǎn)生了一個獨(dú)立的命名空間,那么就應(yīng)該將其分割成一個獨(dú)立的組件。一致性在大型項(xiàng)目里是非常有價(jià)值的。
5. 避免組件外的樣式泄露
我們已經(jīng)建立了自己的命名空間規(guī)范,并且現(xiàn)在想使用它們?nèi)ド诚浠覀兊?UI 組件。如果每個組件都只使用加上它們唯一的命名空間前綴的類名,那我們就可以確定它們的樣式不會泄露到其他組件中去。這是非常高效的(看后面的注意事項(xiàng)),但是不得不反復(fù)輸入命名空間也會變得越來越冗長乏味。
一個健壯,且仍然非常簡單的解決方案就是將整個樣式文件包裝成一個前綴。注意我們是怎樣做到只需要重復(fù)一次應(yīng)用和組件名稱:
.myapp-Header {
background: black;
color: white;
&-link {
color: blue;
}
&-signup {
border: 1px solid gray;
}
}
上面的例子是在 SASS 中實(shí)現(xiàn)的,但其中的 & 符號(或許讓人有點(diǎn)驚訝)在所有相關(guān)的 CSS 預(yù)處理器中都做著同樣的工作(SASS、PostCSS、LESS 以及 Stylus)。出于完整性,接下來給出上面 SASS 代碼編譯后的結(jié)果:
.myapp-Header {
background: black;
color: white;
}
.myapp-Header-link {
color: blue;
}
.myapp-Header-signup {
border: 1px solid gray;
}
所有常見的模式也可以使用它很好地表示出來,比如不同的組件狀態(tài)有著不同的樣式(想想 BEM 條件下的修飾符):
.myapp-Header {
&-signup {
display: block;
}
&-isScrolledDown &-signup {
display: none;
}
}
上面的編譯結(jié)果如下:
.myapp-Header-signup {
display: block;
}
.myapp-Header-isScrolledDown .myapp-Header-signup {
display: none;
}
只要你的預(yù)編譯器支持冒泡(SASS、LESS、PostCSS 和 Stylus 都可以做到),甚至媒體查詢也可以很方便表示:
.myapp-Header {
&-signup {
display: block;
@media (max-width: 500px) {
display: none;
}
}
}
上面的代碼就會變成:
.myapp-Header-signup {
display: block;
}
@media (max-width: 500px) {
.myapp-Header-signup {
display: none;
}
}
上面的模式讓使用長且唯一的類名變得非常方便,因?yàn)槟阍僖矡o需反復(fù)輸入它們了。方便性是強(qiáng)制的,因?yàn)槿绻环奖?,那么我們就會偷工減料了。
JS 端的快速一覽
這篇文檔是關(guān)于樣式規(guī)范的,但樣式是不能憑空獨(dú)立存在的:我們在 JS 端也需要產(chǎn)生同樣的命名空間化類名,并且方便性也是強(qiáng)制的。
厚著臉皮做個廣告,我恰好為此曾經(jīng)建立了一個非常簡單,無任何依賴的 JS 庫,叫做 css-ns。當(dāng)在框架層面編譯的時候(比如使用 React),它允許在一個特定文件內(nèi)強(qiáng)制建立一個特定的命名空間。
// 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>
</div>
這真的非常方便,并且上面所有的代碼讓 JS 端也變成了默認(rèn)局部化。
但是我再次跑題了,回到 CSS 端。
6. 避免組件內(nèi)的樣式泄露
還記得我說過給每個類名加上組件命名空間的前綴時,這是對沙箱化樣式來說很高效的一種方式嗎?還記得我說過這里有個“注意事項(xiàng)”嗎?
考慮下面的樣式:
.myapp-Header {
a {
color: blue;
}
}
以及下面的組件層:
+-------------------------+
| Header |
| |
| [home] [blog] [kittens] | <-- 這些都是 <a> 元素
+-------------------------+
這很酷,不是嗎?Header 里只有 <a> 元素會變成藍(lán)色,因?yàn)槲覀兩傻囊?guī)則如下:
.myapp-Header a { color: blue; }
但是考慮布局在之后做一下變化:
+-----------------------------------------+
| Header +-----------+ |
| | LoginForm | |
| | | |
| [home] [blog] [kittens] | [info] | | <-- 這些是 <a> 元素
| +-----------+ |
+-----------------------------------------+
選擇器 .myapp-Header a 同樣匹配了 LoginForm 里的 <a> 元素,所以我們搞砸了這里的樣式隔離。事實(shí)證明,將所有樣式包裝到一個命名空間里對于隔離組件及其鄰居組件來說,是一個高效的方式,但卻不能總是和其子組件隔離。
這個問題可以通過兩種方法修復(fù):
- 絕不在樣式表中使用元素名稱選擇器。如果
Header里的<a>元素都使用<a class="myapp-Header-link">替代,那么我們就不需要處理這個問題了。再往下看,有時候你會設(shè)置一些語義化標(biāo)簽,像<article>、<aside>以及<th>,都放在了正確的位置上,并且你又不想用額外的類名來弄亂它們,這種情況下: - 在你的命名空間之外只使用
>操作符 來選擇元素。
根據(jù)第二個方法來做調(diào)整,我們的樣式代碼就可以改寫如下:
.myapp-Header {
> a {
color: blue;
}
}
這樣就可以確保隔離同樣作用于更深層次的組件樹,因?yàn)樯傻倪x擇器變成了 .myapp-Header > a。
如果這聽起來有爭議,那么讓我通過下面這個同樣運(yùn)行良好的例子更進(jìn)一步地使你信服:
.myapp-Header {
> nav > p > a {
color: blue;
}
}
經(jīng)過多年的可靠建議,我們一直認(rèn)為要盡量避免選擇器嵌套(包括這個使用了 > 的強(qiáng)關(guān)聯(lián)形式)。但是為什么呢?這個引用的原因歸結(jié)為以下三個:
- 層疊樣式最終會毀掉你的一天。要是嵌套越多的選擇器,那么就有越高的機(jī)會造成一個元素匹配上多于一個組件的情況。如果你讀到這里,你就會知道我們已經(jīng)消除了這種可能性了(使用嚴(yán)格的命名空間前綴,并在需要的時候使用強(qiáng)關(guān)聯(lián)子元素選擇器)。
- 太多的特性會減少可復(fù)用性。寫給
nav p a的樣式將不能在特定情況下之外的任意地方被復(fù)用。但其實(shí)我們從來沒想要它可復(fù)用,事實(shí)上,我們特意禁止這個可復(fù)用的方法,因?yàn)檫@種可復(fù)用性并不能在我們想實(shí)現(xiàn)組件隔離的目標(biāo)上產(chǎn)生好的作用。 - 太多的特性會讓重構(gòu)變得更加困難。這可以在現(xiàn)實(shí)中找到依據(jù),假設(shè)你只有一個
.myapp-Header-link a,你可以很自由地在組件的 HTML 中移動<a>元素,同樣的樣式總是會一直生效。然而如果使用> nav > p > a,就需要更新選擇器去匹配組件的 HTML 內(nèi)這個鏈接的新位置。但考慮到我們想要 UI 是由一些小且隔離性好的組件組成,這個問題也不是相當(dāng)重要。當(dāng)然,如果你不得不在重構(gòu)的時候考慮整個應(yīng)用的 HTML 和 CSS,那么這個問題可能就有點(diǎn)嚴(yán)重了。但是現(xiàn)在你是在一個只有十行樣式代碼的小沙箱內(nèi)進(jìn)行操作,并且還知道沙箱外沒有其他東西需要考慮,那么這種類型的變化就不是問題了。
通過這個例子,你應(yīng)該很好的理解了規(guī)則,所以你知道什么時候應(yīng)該打破它們。在我們的架構(gòu)里,選擇器嵌套不僅僅只是可以用,有時候它還是一件非常正確的事情。為之瘋狂吧。
出于好奇的題外話:預(yù)防泄露樣式進(jìn)入組件
所以我們是否已經(jīng)實(shí)現(xiàn)了樣式的完美沙箱化,以至于每個組件的存在都可以和頁面的其他內(nèi)容隔離開來呢?做一個快速回顧:
-
我們已經(jīng)通過用組件的命名空間給每個類名加前綴來避免組件向外泄露樣式:
+-------+ | | | -----X---> | | +-------+ -
引申開來,這也意味著我們已經(jīng)避免了組件間的泄露:
+-------+ +-------+ | | | | | ------X------> | | | | | +-------+ +-------+ -
而且我們還通過考慮子選擇器來避免泄露進(jìn)入子組件:
+---------------------+ | +-------+ | | | | | | ----X------> | | | | | | | +-------+ | +---------------------+ -
但更為關(guān)鍵的是,外部樣式仍然可以泄露進(jìn)入組件當(dāng)中:
+-------+ | | ----------> | | | +-------+
舉個例子,假設(shè)我們給組件寫了下面的樣式:
.myapp-Header {
> a {
color: blue;
}
}
但是接著我們引入一個表現(xiàn)不好的第三方庫,有著下面的 CSS:
a {
font-family: "Comic Sans";
}
沒有一個簡單的方法可以保護(hù)我們的組件不受外部樣式的污染,并且這是我們經(jīng)常需要調(diào)整的地方:
幸好,對于你自己使用的依賴來說常常會有一個控制方式,并且也可以簡單地找一個表現(xiàn)更好的選擇。
而且,我說的是沒有一個簡單的的方法可以保護(hù)組件,并不意味著沒有方法。老兄,當(dāng)然是有方法的,它們只是有不同的取舍:
- 只需強(qiáng)制覆蓋它:如果你為每個組件的每個元素去引入一個 CSS 重置樣式,并且使用一個優(yōu)先級總是高于其他第三方庫的選擇器,那么就非常棒了。但是除非是一個小應(yīng)用(假設(shè)一個第三方“共享”按鈕可以嵌入到網(wǎng)站上那種),否則這種方法將會迅速失控。這不算是一個好主意,只是在這里列出來等待完善。
-
all: initial是一個很少人知道的新 CSS 屬性,它專門為了這個問題而設(shè)計(jì)。它可以阻止繼承屬性流入,并且只要它贏得了特性之爭(并且只要你為每個想保護(hù)的屬性重復(fù)使用它),還可以作為一個本地重置生效。它的實(shí)現(xiàn)有些錯綜復(fù)雜,而且還不是所有瀏覽器都支持,但是all: initial最后或許可以成為樣式隔離的有效方法。 - Shadow DOM 已經(jīng)被提到過,而它正是為你解決問題的一個工具,因?yàn)樗试S為 JS 和 CSS 聲明組件邊界。盡管最近有一絲希望的微光,Web 組件規(guī)范還是沒有在今年取得很大的進(jìn)步,并且除非你使用的是一些已知可支持的瀏覽器,否則還是不能將 Shadow DOM 列入考慮范圍。
- 最后,還有
<iframe>。它提供了 Web 運(yùn)行環(huán)境所能提供的最強(qiáng)的隔離形式(既為 JS 也為 CSS),但同樣為運(yùn)行成本(潛在因素)和維護(hù)(保留的內(nèi)存)帶來了巨大的消耗。不過,通常代價(jià)是值得的,并且最著名的網(wǎng)絡(luò)嵌入(Facebook、Twitter、Disqus等等)事實(shí)上也是用 iframe 實(shí)現(xiàn)的。然而本文檔的目的是隔離成千上百個小組件,就此而言,這個方法將數(shù)以百倍地消耗我們的性能。
不管怎樣,這個題外話跑得有點(diǎn)遠(yuǎn)了,回到我們的 CSS 規(guī)則。
7. 遵守組件邊界
就像我們賦予 .myapp-Header > a 的樣式,當(dāng)嵌套組件的時候,我們可能還需要給子組件提供一些樣式(Web 組件類比再次完美,因?yàn)榻酉聛?> a 和 > my-custom-a 的效果并沒有什么差異)??紤]下面的布局:
+---------------------------------+
| Header +------------+ |
| | LoginForm | |
| | | |
| | +--------+ | |
| +--------+ | | Button | | |
| | Button | | +--------+ | |
| +--------+ +------------+ |
+---------------------------------+
我們馬上可以看到用 .myapp-Header .myapp-Button 寫樣式不會是一個好主意,顯然應(yīng)該用 .myapp-Header > .myapp-Button 來替代。但是我們到底要給子組件提供什么樣式呢?
注意到 LoginForm 靠在了 Header 的右邊界上。直觀看來,一個可能的樣式就是:
.myapp-LoginForm {
float: right;
}
我們沒有違反任何規(guī)則,但是我們讓 LoginForm 變得有點(diǎn)難以復(fù)用了:如果我們接下來的主頁想要這個 LoginForm,但是不想要右浮動,那就不走運(yùn)了。
這個問題實(shí)際的解決方案就是(局部地)放寬之前的規(guī)則,只對當(dāng)前文件所屬的命名空間提供樣式。具體來說,我們希望用下面的代碼替換:
.myapp-Header {
> .myapp-LoginForm {
float: right;
}
}
這樣實(shí)際上已經(jīng)很好了,只要我們不允許隨意地破壞子組件的沙箱:
// COUNTER-EXAMPLE; DON'T DO THIS
.myapp-Header {
> .myapp-LoginForm {
color: blue;
padding: 20px;
}
}
我們不允許這么做,因?yàn)檫@樣做會失去局部變化沒有全局影響的安全性。使用上面代碼的話,當(dāng)修改 LoginForm 組件表現(xiàn)的時候,LoginForm.scss 就不再是唯一需要檢查的地方了。發(fā)生變化再次變得可怕。所以可用與不可用之間的界限到底在哪里?
我們希望遵守每個子組件內(nèi)部的沙箱,因?yàn)槲覀儾幌胍蕾嚻鋵?shí)現(xiàn)細(xì)節(jié)。它對于我們來說是個黑盒。相反地,在子組件外部的是父組件的沙箱,它占據(jù)著主要位置。區(qū)分內(nèi)部和外部正好引出了 CSS 中最基本的概念之一:盒模型。
我做的這個類比很糟糕,但我們繼續(xù)看:就像在一個國家內(nèi)意味著在其物理邊界之內(nèi),我們建立了一個邊界,父組件只可以在子組件邊界之外對(直接)子組件樣式產(chǎn)生影響。這意味著關(guān)系到位置和大小的屬性(如 position、margin、display、width、float、z-index 等等)是可用的,而影響到內(nèi)部邊界的屬性(如 border 本身、padding、color、font等)是不可用的。
按照推論,下面這樣顯然也是禁止的:
// COUNTER-EXAMPLE; DON'T DO THIS
.myapp-Header {
> .myapp-LoginForm {
> a { // relying on implementation details of LoginForm ;__;
color: blue;
}
}
}
有幾個有趣或者說無聊的邊界情況,比如:
-
box-shadow- 一個特定類型的 shadow 可以是一個組件外觀不可缺少的部分,因此組件應(yīng)該自己包含這些樣式。話又說回來,這種視覺效果可以在邊界外清楚地渲染出來,所以它又可以回到父組件的作用域。 -
color,font及其他可繼承屬性 -.myapp-Header > .myapp-LoginForm { color: red }這種寫法碰到了子組件內(nèi)部的屬性,但從另一方面來說,這又可以在功能上等同于.myapp-Header { color: red; },這種寫法根據(jù)其他規(guī)則又是可行的。 -
display- 如果子組件使用了 Flexbox 布局,那么它很可能依賴于其根元素上設(shè)置display: flex屬性。不過,父組件也可能選擇通過display: none來隱藏其子組件。
在這些邊界情況下要意識到一件重要的事情,你并不是在冒著打核戰(zhàn)爭的危險(xiǎn),而只是在引入少量的 CSS 層疊好回到自己的樣式。就和其他不好的做法一樣,適當(dāng)?shù)厥褂脤盈B是可以的。例如,再仔細(xì)看到最后的例子,特性優(yōu)先級比較正如你所想要的效果一樣:當(dāng)組件可見時,.myapp-LoginForm { display: flex } 的優(yōu)先級更高。而當(dāng)擁有者決定用 .myapp-Header-loginBoxHidden > .myapp-LoginBox { display: none } 隱藏組件時,這個樣式的優(yōu)先級更高。
8. 松散地整合外部樣式
為了避免重復(fù)工作,有時可能需要在組件間共享樣式。為了避免全部工作,有時又可能想使用其他人創(chuàng)建的樣式。這兩種情況的實(shí)現(xiàn)都不應(yīng)該創(chuàng)建出不必要的耦合到代碼庫中。
拿一個具體的例子來說,考慮使用一些來自 Bootstrap 的樣式,因?yàn)檫@對于使用惱人的框架來說是一個很好的例子。想想我們上面所討論到的所有事情,關(guān)于為樣式共享一個獨(dú)立的全局命名空間,以及不好的沖突,Bootstrap 會:
- 導(dǎo)出一大堆選擇器(版本 3.3.7 來說, 具體有 2481 個)到命名空間里,不管你實(shí)際上是否使用它們。(有趣的一面:IE9 在默認(rèn)忽略剩余選擇器之前只會處理 4095 個選擇器。我曾經(jīng)聽說有人花了很多天來調(diào)試它們,鬼知道他們經(jīng)歷了什么。)
- 使用寫死的類名如
.btn和.table。不敢想象某些不小心復(fù)用了這些樣式的開發(fā)者或者項(xiàng)目。(諷刺臉)
不管了,我們希望使用 Bootstrap 作為 Button 組件的基礎(chǔ)。
用某段代碼替換下面的來整合到 HTML 端:
<button class="myapp-Button btn">
考慮在樣式中擴(kuò)展這個類:
<button class="myapp-Button">
.myapp-Button {
@extend .btn; // from Bootstrap
}
這么做有一個好處,那就是沒有給任何人(包括你自己)產(chǎn)生一種想法:在 HTML 組件上去依賴可笑地命名為 btn 的類。Button 所使用的樣式的來源是一個完全不需要顯示在外面的實(shí)現(xiàn)細(xì)節(jié)。因此,如果你決定放棄 Bootstrap 轉(zhuǎn)而支持另外的框架(或者只是你自己去寫樣式),那么這種改變無論如何都不會外部可見(呃,除非,這種可見變化是在于 Button 本身長什么樣子)。
同樣的原則適用于你自己的輔助類,并且你可以選擇使用更合理的類名:
.myapp-Button {
@extend .myapp-utils-button; // defined elsewhere in your project
}
.myapp-Button {
@extend %myapp-utils-button; // defined elsewhere in your project
}
最后,所有的 CSS 預(yù)編譯器都支持 mixins 的概念,這可是一個強(qiáng)有力的工具:
.myapp-Button {
@include myapp-generateCoolButton($padding: 15px, $withExplosions: true);
}
應(yīng)該注意的是當(dāng)處理更友好的樣式框架時(如 Bourbon 或者 Foundation),它們實(shí)際上會這么做:定義一大堆 mixin 給你去在需要的時候使用,并且它們本身沒有放出任何樣式。如 Neat 框架。
在結(jié)束前
知曉所有規(guī)則,所以知道何時打破它們
最后,如前所述,當(dāng)你理解了你所制定的規(guī)則(或者是從網(wǎng)上其他人那兒采取的),你就可以寫出對你有意義的特例。比如,如果你覺得直接使用一個輔助類是有附加價(jià)值的,那么就可以這么做:
<button class="myapp-Button myapp-utils-button">
這種附加價(jià)值可能是,比如說,你的測試框架之后可以更智能地自動找出什么元素表現(xiàn)為按鈕,以及可以被點(diǎn)擊。
或者你可能會在違背程度很小的情況下決定去打破組件隔離,并且分割組件的額外工作可能會變得更好。但我想要提醒的是這就像是個下坡路,而且不要忘了一致性的重要性等等,只要你的團(tuán)隊(duì)保持一致,并且你可以完成它們,那么你就是在做對的事情。
結(jié)語
如果你喜歡這篇文章,你完全可以 tweet 關(guān)于它的內(nèi)容!或者不。


