CSS通用類(lèi)和“關(guān)注點(diǎn)分離” - 眾成翻譯
http://www.zcfy.cc/article/css-utility-classes-and-quote-separation-of-concerns-quote-4149.html
CSS通用類(lèi)和“關(guān)注點(diǎn)分離”
過(guò)去的幾年里,我編寫(xiě)CSS的方式已經(jīng)從“語(yǔ)義化”轉(zhuǎn)變?yōu)椤昂瘮?shù)式”(經(jīng)常被這樣稱呼)了。
用“函數(shù)式”方式編寫(xiě)css可以使許多開(kāi)發(fā)者的內(nèi)心激動(dòng)起來(lái)。所以我想介紹一下我是如何做到的,并且和大家分享一些經(jīng)驗(yàn)和見(jiàn)解。
第 1 階段: "語(yǔ)義化" CSS
當(dāng)你正努力學(xué)習(xí)如何把CSS寫(xiě)的更好地時(shí)候,會(huì)有人告訴你最好的方法是“關(guān)注點(diǎn)分離”。
這種方式的主旨是,你的HTML只能包含與內(nèi)容相關(guān)的信息,所有與樣式相關(guān)的信息都應(yīng)寫(xiě)在CSS里。
看下面這段HTML:
<p>
Hello there!
</p>
看到.text-center類(lèi)了嗎?文本居中屬于樣式,因此這段代碼違背了“關(guān)注點(diǎn)分離”原則,因?yàn)槲覀儼褬邮叫畔⒒烊際TML了。
那么推薦的方法是,根據(jù)內(nèi)容來(lái)給元素定義類(lèi)名。然后這些類(lèi)名就如同一個(gè)鉤子,你可以在CSS中用這些類(lèi)名給標(biāo)簽增加樣式:
<style>
.greeting {
text-align: center;
}
</style>
<p>
Hello there!
</p>
這種方法的典范是禪意花園。如果使用“關(guān)注點(diǎn)分離”原則,那么想要改變一個(gè)網(wǎng)站的樣式只需要更換樣式表就可以了。
我寫(xiě)碼的過(guò)程大致如下:
- 根據(jù)設(shè)計(jì)稿把需要的標(biāo)簽寫(xiě)好(這里拿一個(gè)作者簡(jiǎn)介卡作為例子):
<div>
<img src="http://p0.qhimg.com/t01e2c15da7b6fa9aba.jpg" alt>
<div>
<h2>Adam Wathan</h2>
<p>
Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
</p>
</div>
</div>
- 根據(jù)內(nèi)容添加一兩個(gè)具有描述性的類(lèi)名:
- <div>
+ <div>
<img src="http://p0.qhimg.com/t01e2c15da7b6fa9aba.jpg" alt>
<div>
<h2>Adam Wathan</h2>
<p>
Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
</p>
</div>
</div>
- 將這些類(lèi)名作為“鉤子”,在我們的CSS/Less/Sass中給新標(biāo)簽添加樣式:
.author-bio {
background-color: white;
border: 1px solid hsl(0,0%,85%);
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
> img {
display: block;
width: 100%;
height: auto;
}
> div {
padding: 1rem;
> h2 {
font-size: 1.25rem;
color: rgba(0,0,0,0.8);
}
> p {
font-size: 1rem;
color: rgba(0,0,0,0.75);
line-height: 1.5;
}
}
}
下面是結(jié)果演示:
See the Pen
"Semantic" mapping layer (terrible idea!)
by Adam Wathan (@adamwathan) on
我認(rèn)為這很有道理,所以很長(zhǎng)一段時(shí)間都是這么寫(xiě)HTML和CSS的。
但后來(lái),我感覺(jué)有點(diǎn)兒不對(duì)勁。
雖然我將“關(guān)注點(diǎn)分離”了,但HTML和CSS還是有很明顯的耦合。大多數(shù)時(shí)候我的CSS看起來(lái)就像是HTML標(biāo)簽的鏡子,嵌套的CSS選擇器將HTML結(jié)構(gòu)完全映射出來(lái)了。
我的標(biāo)簽確實(shí)與樣式分離了,但我的CSS卻與HTML結(jié)構(gòu)有很強(qiáng)的聯(lián)系。
也許是我的分離做的不夠徹底。
第 2 階段: 讓樣式與結(jié)構(gòu)解耦
在尋找解決辦法的過(guò)程中,我發(fā)現(xiàn)大家更傾向于給標(biāo)簽添加更多的類(lèi)名,這樣定義起來(lái)就會(huì)更直觀。能夠保持較低的優(yōu)先級(jí),并使CSS更少的依賴DOM結(jié)構(gòu)。
倡導(dǎo)這種思想并且最廣為人知的技術(shù)是
Block Element Modifer, 簡(jiǎn)稱
BEM.
用類(lèi)似BEM的方法,我們的作者簡(jiǎn)介標(biāo)簽看上去就會(huì)是這樣:
<div>
<img src="http://p0.qhimg.com/t01e2c15da7b6fa9aba.jpg" alt>
<div>
<h2>Adam Wathan</h2>
<p>
Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
</p>
</div>
</div>
...CSS是這樣:
.author-bio {
background-color: white;
border: 1px solid hsl(0,0%,85%);
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.author-bio__image {
display: block;
width: 100%;
height: auto;
}
.author-bio__content {
padding: 1rem;
}
.author-bio__name {
font-size: 1.25rem;
color: rgba(0,0,0,0.8);
}
.author-bio__body {
font-size: 1rem;
color: rgba(0,0,0,0.75);
line-height: 1.5;
}
對(duì)我來(lái)說(shuō)這已經(jīng)是很大的進(jìn)步了。我的標(biāo)簽看上去仍然很“語(yǔ)義化”且與樣式分離,而且現(xiàn)在CSS也與標(biāo)簽結(jié)構(gòu)解耦合了。額外的好處是避免了不必要的選擇器嵌套,保持了低優(yōu)先級(jí)。
但是很快我又陷入了兩難境地。
處理相似的組件
話說(shuō)我需要給網(wǎng)站加個(gè)新的內(nèi)容:用一個(gè)卡片布局來(lái)展示文章預(yù)覽。
話說(shuō)這個(gè)文章預(yù)覽卡片的頭部是一張照片,下邊是內(nèi)容部分,包括一個(gè)粗體標(biāo)題和一些較小的正文文本。
話說(shuō)它看起來(lái)跟作者簡(jiǎn)介非常像。
[圖片上傳失敗...(image-f0f4c9-1513407635278)]
如果用“關(guān)注點(diǎn)分離”的原則,如何處理最好呢?
我們不能將
.author-bio的類(lèi)名用在文章預(yù)覽卡中,因?yàn)檫@不符合語(yǔ)義化原則。所以我們肯定要用
.article-preview來(lái)給組件命名。
下邊將是標(biāo)簽的樣子:
<div>
<img src="http://p0.qhimg.com/t01f3a22fad3306d5cf.webp" alt>
<div>
<h2>Stubbing Eloquent Relations for Faster Tests</h2>
<p>
In this quick blog post and screencast, I share a trick I use to speed up tests that use Eloquent relationships but don't really depend on database functionality.
</p>
</div>
</div>
但是我們應(yīng)該如何處理CSS呢?
選擇 1: 復(fù)制樣式
一種方法是直接復(fù)制.author-bio的樣式,然后重命名一下:
.article-preview {
background-color: white;
border: 1px solid hsl(0,0%,85%);
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.article-preview__image {
display: block;
width: 100%;
height: auto;
}
.article-preview__content {
padding: 1rem;
}
.article-preview__title {
font-size: 1.25rem;
color: rgba(0,0,0,0.8);
}
.article-preview__body {
font-size: 1rem;
color: rgba(0,0,0,0.75);
line-height: 1.5;
}
這樣沒(méi)問(wèn)題,但很明顯不夠簡(jiǎn)潔。如果這個(gè)組件在樣式上與.author-bio只有稍微的不一樣(可能是不同的填充或字體顏色),改起來(lái)也會(huì)非常容易。
選擇 2:用
@extend
拓展作者簡(jiǎn)介卡
另一種選擇是使用預(yù)處理器的
@extend屬性。使你可以借用已經(jīng)在
.author-bio
組件定義好的樣式:
.article-preview {
@extend .author-bio;
}
.article-preview__image {
@extend .author-bio__image;
}
.article-preview__content {
@extend .author-bio__content;
}
.article-preview__title {
@extend .author-bio__name;
}
.article-preview__body {
@extend .author-bio__body;
}
一般不建議使用
@extend屬性。但撇開(kāi)這件事, 這樣可以解決我們的問(wèn)題對(duì)嗎?
我們移除了CSS中重復(fù)的部分,并且標(biāo)簽與樣式仍然是分離的。
但是讓我們?cè)倏匆粋€(gè)選項(xiàng)...
Option 3: 創(chuàng)建與內(nèi)容無(wú)關(guān)的組件
從“語(yǔ)義化”角度來(lái)看,.author-bio
和
.article-preview
組件并沒(méi)有共同處。一個(gè)是作者簡(jiǎn)介卡, 一個(gè)是文章預(yù)覽卡。
但是正如我們已經(jīng)看到的,他們從設(shè)計(jì)的角度來(lái)看有很多共同點(diǎn)。
因此,我們可以這樣做。創(chuàng)建一個(gè)新的組件,根據(jù)他們的共同點(diǎn)起類(lèi)名,然后在兩個(gè)內(nèi)容中復(fù)用。
我們給它起名叫
.media-card。
下面是CSS:
.media-card {
background-color: white;
border: 1px solid hsl(0,0%,85%);
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.media-card__image {
display: block;
width: 100%;
height: auto;
}
.media-card__content {
padding: 1rem;
}
.media-card__title {
font-size: 1.25rem;
color: rgba(0,0,0,0.8);
}
.media-card__body {
font-size: 1rem;
color: rgba(0,0,0,0.75);
line-height: 1.5;
}
...我們的作者簡(jiǎn)介標(biāo)簽看起來(lái)會(huì)像下邊這樣:
<div>
<img src="http://p0.qhimg.com/t01e2c15da7b6fa9aba.jpg" alt>
<div>
<h2>Adam Wathan</h2>
<p>
Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
</p>
</div>
</div>
...我們的文章預(yù)覽看起來(lái)會(huì)像下邊這樣:
<div>
<img src="http://p0.qhimg.com/t01f3a22fad3306d5cf.webp" alt>
<div>
<h2>Stubbing Eloquent Relations for Faster Tests</h2>
<p>
In this quick blog post and screencast, I share a trick I use to speed up tests that use Eloquent relationships but don't really depend on database functionality.
</p>
</div>
</div>
這種方法也不會(huì)有重復(fù)的CSS,但現(xiàn)在是不是“結(jié)構(gòu)與樣式混合”了?
我們希望兩個(gè)文本片段都使用.media-card做樣式。那如果我們想改變作者簡(jiǎn)介卡的樣式,而不改變文章預(yù)覽的,那該怎么辦呢?
之前,我們只需要打開(kāi)樣式表,給需要改變的組件隨便添加樣式就可以了。但現(xiàn)在我們需要編輯HTML!這很糟!
但讓我們先花幾分鐘換個(gè)角度想一想。
如果我們要添加一個(gè)有著相同樣式的新內(nèi)容,該怎么辦呢?
如果用“語(yǔ)義化”方法,我們需要寫(xiě)一段新的HTML,添加一些根據(jù)內(nèi)容起的類(lèi)名,打開(kāi)樣式表,為新的內(nèi)容添加一個(gè)新的CSS樣式組件,并通過(guò)復(fù)制、@extend、mixin來(lái)借用已經(jīng)存在的樣式。
如果用與內(nèi)容無(wú)關(guān)的
.media-card
類(lèi)名, 我們所需要做的只是寫(xiě)一段新的HTML,不需要修改樣式表。
如果我們真的將“結(jié)構(gòu)與樣式混合”了,那么無(wú)論HTML還是CSS,不都得修改嗎? (譯者:作者想表達(dá)的是,其實(shí)結(jié)構(gòu)與樣式還是分離的。)
“關(guān)注點(diǎn)分離”是個(gè)稻草人
當(dāng)你用"關(guān)注點(diǎn)分離"的原則來(lái)思考HTML和CSS的關(guān)系時(shí),就會(huì)是非黑即白的。
分離了(好!)或者沒(méi)分離(糟糕?。?/p>
這并不是思考HTML與CSS關(guān)系的正確方式。
相反,
要從依賴的角度來(lái)思考
有兩種編寫(xiě)HTML和CSS方式:
-
"關(guān)注點(diǎn)分離"CSS依賴HTML。
根據(jù)內(nèi)容給類(lèi)起名(例如
.author-bio),將CSS視為HTML的附屬品。
HTML是獨(dú)立的。他并不在乎你是怎么定義它的,它只暴露像.author-bio這樣的鉤子。
另一方面,你的CSS并不是獨(dú)立的。他需要知道你的HTML暴露了哪些類(lèi)名,然后通過(guò)這些類(lèi)名給HTML添加樣式。
在這個(gè)模型中,你可以任意編寫(xiě)HTML,但CSS卻不能被復(fù)用。
-
"結(jié)構(gòu)與樣式混合"HTML依賴CSS
根據(jù)設(shè)計(jì)稿提煉出樣式相同的部分,并用與內(nèi)容無(wú)關(guān)的名字作為類(lèi)名,就是將HTML作為CSS的附屬品。
CSS是獨(dú)立的。它并不關(guān)注自己被應(yīng)用的地方內(nèi)容是什么。他只是提供一系列代碼塊,哪個(gè)標(biāo)簽需要就添加在哪。
HTML不是獨(dú)立的。它需要使用由CSS提供的類(lèi), 它需要知道有哪些類(lèi)是定義好的,并且將這些類(lèi)結(jié)合起來(lái),來(lái)完成跟設(shè)計(jì)稿一樣的樣式。
在這個(gè)模型中,你的CSS是可復(fù)用的,但你的HTML不能隨意編寫(xiě)。
CSS禪意花園采用第一種方式,而UI框架如Bootstrap
或
則用的第二種方式。
兩種方法本質(zhì)上都不是“錯(cuò)誤的”。只是你要弄清楚你所在的語(yǔ)境中,什么才是最重要的?;谶@點(diǎn),再?zèng)Q定你要用哪種方法。
對(duì)于你正在編寫(xiě)的項(xiàng)目, 什么會(huì)更有價(jià)值: 可以隨意編寫(xiě)的HTML,還是可復(fù)用的CSS?
選擇可重用性
當(dāng)我閱讀了Nicolas Gallagher的
關(guān)于HTML語(yǔ)義化和前端架構(gòu)一文后,我改變了自己的觀點(diǎn)。
我不會(huì)在這里重申他的所有觀點(diǎn)。但毫無(wú)疑問(wèn),這個(gè)博客給了我深刻印象。我完全相信,對(duì)于我所編寫(xiě)的各種項(xiàng)目來(lái)說(shuō),選擇可復(fù)用的CSS是最正確的選擇。
第3階段: 與內(nèi)容無(wú)關(guān)的CSS組件
此時(shí)我的目標(biāo)很清楚,就是避免根據(jù)內(nèi)容來(lái)創(chuàng)建類(lèi)名。而是盡可能使起的類(lèi)名復(fù)用性更高。
比如類(lèi)名看上去如下:
.card-
.btn,.btn--primary,.btn--secondary .badge-
.card-list,.card-list-item .img--round-
.modal-form,.modal-form-section
...等等等等.
當(dāng)我開(kāi)始專(zhuān)注于創(chuàng)建可復(fù)用的類(lèi)名時(shí),我注意到一些事情:
組件做的事情越多,組件細(xì)節(jié)越多,越難以服用。
下面是一個(gè)直觀的例子。
話說(shuō)我們要建立一個(gè)表單,包括幾個(gè)表單部分和一個(gè)在底部的提交按鈕。
如果我們將所有表單內(nèi)容都視為.stacked-form
組件的一部分,我們就會(huì)把按鈕命名成
.stacked-form__button:
<form class="stacked-form" action="#">
<div>
</div>
<div>
</div>
<div>
<button class="stacked-form__button">Submit</button>
</div>
</form>
但也許在我們的網(wǎng)站中還有另外一個(gè)按鈕,它并不是表單的一部分,但是卻跟表單按鈕看上去一樣。
給這個(gè)按鈕使用
.stacked-form__button
類(lèi)名就不太合適了,它不是.stacked-form組件的一部分。
這兩個(gè)按鈕在各自的頁(yè)面上都是主要行為,那么如果我們根據(jù)組件的共同特征來(lái)給按鈕命名的話,就是.btn--primary,完全去掉
.stacked-form__
前綴。
<form class="stacked-form" action="#">
<div>
- <button class="stacked-form__button">Submit</button>
+ <button class="btn btn--primary">Submit</button>
</div>
</form>
話說(shuō)現(xiàn)在我們想讓.stacked-form組件跟前文中的作者簡(jiǎn)介、文章預(yù)覽一樣,使用卡片樣式的布局。
一種方法是創(chuàng)建一個(gè)修飾符并將其應(yīng)用于此表單:
- <form class="stacked-form" action="#">
+ <form class="stacked-form stacked-form--card" action="#">
</form>
但是如果我們已經(jīng)有了.card類(lèi),那我們?yōu)楹尾挥靡呀?jīng)存在的.card和.stacked-form組合起來(lái),實(shí)現(xiàn)設(shè)計(jì)稿的樣式呢?
+ <div>
<form class="stacked-form" action="#">
</form>
+ </div>
用這個(gè)方法,我們可以用.card
類(lèi)作為任何內(nèi)容的容器,然后.stacked-form類(lèi)則可以用在任何容器內(nèi)。
我們已經(jīng)從組件中提取出了更多的可復(fù)用部分,
并且我們不需要編寫(xiě)任何新的CSS
構(gòu)建子組件
話說(shuō)我們需要在表單的底部添加另一個(gè)按鈕,并且和之前的按鈕之間有一點(diǎn)兒距離:
<form class="stacked-form" action="#">
<div>
<button class="btn btn--secondary">Cancel</button>
<button class="btn btn--primary">Submit</button>
</div>
</form>
一種方法是創(chuàng)建一個(gè)新的子組件,例如.stacked-form__footer,為每個(gè)按鈕添加一個(gè)類(lèi),例如.stacked-form__footer-item,并使用子選擇器添加一些邊距:
<form class="stacked-form" action="#">
- <div>
+ <div>
- <button class="btn btn--secondary">Cancel</button>
- <button class="btn btn--primary">Submit</button>
+ <button class="stacked-form__footer-item btn btn--secondary">Cancel</button>
+ <button class="stacked-form__footer-item btn btn--primary">Submit</button>
</div>
</form>
CSS看上去就像下邊這樣:
.stacked-form__footer {
text-align: right;
}
.stacked-form__footer-item {
margin-right: 1rem;
&:last-child {
margin-right: 0;
}
}
那如果我們?cè)谧訉?dǎo)航或者h(yuǎn)eader中遇到相同的問(wèn)題時(shí),怎么辦?
我們不能在.stacked-form組件之外復(fù)用.stacked-form__footer, 因此也許我們會(huì)在header里新建一個(gè)新的子組件:
<header>
<h2>New Product</h2>
+ <div>
+ <button class="header-bar__action btn btn--secondary">Cancel</button>
+ <button class="header-bar__action btn btn--primary">Save</button>
+ </div>
</header>
...但現(xiàn)在我們不得不把已經(jīng)寫(xiě)好的.stacked-form__footer類(lèi),復(fù)制到新的.header-bar__actions子組件中。
這非常像剛開(kāi)始我們根據(jù)內(nèi)容來(lái)給類(lèi)命名時(shí)遇到的問(wèn)題,對(duì)不對(duì)?
解決這個(gè)問(wèn)題的一個(gè)方法是,想出一個(gè)全新的組件,這個(gè)組件易于復(fù)用,并且易于組合。
例如我們可以給它起名
.actions-list:
.actions-list {
text-align: right;
}
.actions-list__item {
margin-right: 1rem;
&:last-child {
margin-right: 0;
}
}
現(xiàn)在我們可以完全擺脫
.stacked-form__footer
和.header-bar__actions
子組件, 而是用.actions-list
在各自的場(chǎng)景中替換他們:
<form class="stacked-form" action="#">
<div>
<div>
<button class="actions-list__item btn btn--secondary">Cancel</button>
<button class="actions-list__item btn btn--primary">Submit</button>
</div>
</div>
</form>
<header>
<h2>New Product</h2>
<div>
<button class="actions-list__item btn btn--secondary">Cancel</button>
<button class="actions-list__item btn btn--primary">Save</button>
</div>
</header>
但如果其中一個(gè).actions-list支持左對(duì)齊,而另一個(gè)支持右對(duì)齊,我們應(yīng)該怎么辦呢?難道要用.actions-list--left
和
.actions-list--right
來(lái)作修飾嗎?
第4階段: 內(nèi)容無(wú)關(guān)組件 + 通用類(lèi)
給這些組件起名字總是很耗費(fèi)時(shí)間。
當(dāng)你創(chuàng)建例如.actions-list--left這樣的修飾類(lèi)名時(shí), 你完全就是在創(chuàng)建一個(gè)新的組件,這個(gè)組件只是為了適配一個(gè)CSS屬性。 因?yàn)轭?lèi)名里已經(jīng)有了left,因此你不可能欺騙任何人說(shuō)這是“語(yǔ)義化”的(譯者:作者的意思是結(jié)構(gòu)與樣式在這里是混合的)。
如果我們有一個(gè)組件需要使用left-align和right-align來(lái)修飾,那我們用這個(gè)當(dāng)做新的組件類(lèi)名來(lái)用,合適嗎?
這個(gè)就回到了當(dāng)初我們面臨的一個(gè)相同問(wèn)題,那個(gè)問(wèn)題我們是用.actions-list子組件替代
.stacked-form__footer
和
.header-bar__actions來(lái)解決的:
我們傾向于組合,而不是復(fù)制
那么如果我有兩個(gè)地方用到.actions-list組件,一個(gè)需要左對(duì)齊,而另一個(gè)需要右對(duì)齊,那我們?cè)趺从媒M合的方式來(lái)解決這個(gè)問(wèn)題呢?
創(chuàng)建好通用類(lèi)
要用組合的方法解決這個(gè)問(wèn)題,我們就需要給我們的組件添加一個(gè)新的可復(fù)用的類(lèi)名,以便達(dá)到我們想要的效果:
我們已經(jīng)想用
.actions-list--left
和
.actions-list--right來(lái)當(dāng)做類(lèi)名了,那么給這些新的類(lèi)起名為.align-left
和
.align-right也是合情合理的:
.align-left {
text-align: left;
}
.align-right {
text-align: right;
}
現(xiàn)在我們可以用組合的方式使我們的表單按鈕左對(duì)齊了:
<form class="stacked-form" action="#">
<div>
<div>
<button class="actions-list__item btn btn--secondary">Cancel</button>
<button class="actions-list__item btn btn--primary">Submit</button>
</div>
</div>
</form>
...在header標(biāo)簽里的按鈕右對(duì)齊:
<header>
<h2>New Product</h2>
<div>
<button class="actions-list__item btn btn--secondary">Cancel</button>
<button class="actions-list__item btn btn--primary">Save</button>
</div>
</header>
不要害怕
如果在HTML的類(lèi)名中見(jiàn)到"left" 和 "right"會(huì)使你感到不舒服,請(qǐng)記住,我們已經(jīng)不是在使用“關(guān)注點(diǎn)分離”方法給類(lèi)命名了,我們是根據(jù)設(shè)計(jì)稿提煉出通用的部分來(lái)給類(lèi)命名的。所有類(lèi)名中帶有樣式的描述是很自然的。
不要在假裝.stacked-form比.align-right更“語(yǔ)義化”了。他們都是在決定如何給標(biāo)簽添加樣式之后命名的。我們?cè)跇?biāo)簽中使用這些類(lèi)名是為了達(dá)到特殊的樣式效果。
我們正在寫(xiě)依賴于CSS的HTML。如果我們想把類(lèi)名從.stacked-form改為.horizontal-form的話,那我們就是在改變標(biāo)簽,而不是CSS。
刪除無(wú)用的抽象
解決了這個(gè)問(wèn)題后,一件有趣的事情出現(xiàn)了?,F(xiàn)在我們的.actions-list組件基本上沒(méi)用了。它之前的功能只有一個(gè)就是把內(nèi)容右對(duì)齊。
讓我們把它刪除:
- .actions-list {
- text-align: right;
- }
.actions-list__item {
margin-right: 1rem;
&:last-child {
margin-right: 0;
}
}
但現(xiàn)在,沒(méi)有了.actions-list類(lèi),而只有.actions-list__item類(lèi),看上去有些奇怪。有沒(méi)有一個(gè)方法,可以使我們?cè)跊](méi)有創(chuàng)建.actions-list__item組件的情況下,解決我們最初的問(wèn)題?
如果你回想一下,我們創(chuàng)建這個(gè)組件的原因,只是為了在兩個(gè)按鈕之間增加一些間距。對(duì)于一組按鈕來(lái)說(shuō).actions-list是非常恰當(dāng)?shù)拿?,因?yàn)樗芡ㄓ貌⑶铱梢院芎玫谋粡?fù)用。但肯定也會(huì)有這種情況,就是我們需要在兩個(gè)元素之間設(shè)置相同的間距,但這個(gè)元素并不屬于.actions組件,對(duì)不?
也許我們可以創(chuàng)建一個(gè)更適合復(fù)用的名字,比如
.spaced-horizontal-list怎么樣?我們已經(jīng)刪除了
.actions-list,因?yàn)樗且粋€(gè)不需要任何樣式的子組件。
間距通用類(lèi)
如果只是子元素需要樣式,也許只給子元素添加樣式會(huì)比使用偽選擇器把它們當(dāng)做一個(gè)組件來(lái)寫(xiě)樣式更簡(jiǎn)單呢?
給元素添加間距的,復(fù)用性更好的方法也許是給它取這樣一個(gè)名字,一看便知“這個(gè)元素應(yīng)該與它周?chē)脑赜幸恍╅g距”。
我們已經(jīng)添加了諸如.align-left
和
.align-right的通用類(lèi), 那我們?cè)偌右粋€(gè)只能添加右邊距的新通用類(lèi)怎么樣?
讓我們來(lái)創(chuàng)建一個(gè)通用類(lèi),例如
.mar-r-sm。功能是在一個(gè)元素的右邊添加一段距離的間距:
- .actions-list__item {
- margin-right: 1rem;
- &:last-child {
- margin-right: 0;
- }
- }
+ .mar-r-sm {
+ margin-right: 1rem;
+ }
下面是我們的表單和header現(xiàn)在的樣子:
<form class="stacked-form" action="#">
<div>
<button class="btn btn--secondary mar-r-sm">Cancel</button>
<button class="btn btn--primary">Submit</button>
</div>
</form>
<header>
<h2>New Product</h2>
<div>
<button class="btn btn--secondary mar-r-sm">Cancel</button>
<button class="btn btn--primary">Save</button>
</div>
</header>
.actions-list
組件已經(jīng)沒(méi)有了, 我們的CSS量更小, 類(lèi)更利于復(fù)用。
第5階段: 實(shí)用性第一的 CSS
在達(dá)到這個(gè)階段的時(shí)候,我剛剛建立了一整套實(shí)用類(lèi),用于常見(jiàn)的視覺(jué)微調(diào),例如:
文字的 大小、顏色、粗細(xì)
邊框的 顏色、寬度、位置
背景的 顏色
Flexbox 通用類(lèi)
Padding 和 margin 的助手
你甚至可以在不寫(xiě)新的CSS的情況下,創(chuàng)建一個(gè)全新的UI組件,這真是件令人興奮的事情。
從我的一個(gè)項(xiàng)目中來(lái)看下這個(gè)“產(chǎn)品卡”組件:
[圖片上傳失敗...(image-381772-1513407635276)]
以下是我的標(biāo)記:
<div>
<a href>
<img src>
</a>
<div>
<div>
<a href>
Test-Driven Laravel
</a>
</div>
<a href>
@icon('link')
</a>
</div>
<div>
<div>
@icon('currency-dollar', 'icon-sm text-dark-softest mr-4')
<span>$3,475</span>
</div>
<div>
@icon('user', 'icon-sm text-dark-softest mr-4')
<span>25</span>
</div>
</div>
</div>
第一眼看到這么多類(lèi)名,也許會(huì)使你感到失望。但是說(shuō)我們確實(shí)想使它成為一個(gè)真正的CSS組件,而不是從通用類(lèi)庫(kù)里挑出需要的類(lèi)名進(jìn)行組合。那么我們應(yīng)該怎么稱呼它呢?
我們不打算使用根據(jù)具體內(nèi)容而起的類(lèi)名,因?yàn)槟菢拥脑捨覀兊慕M件只能在特定情境中使用一次。
難道是向下邊這樣命名?
.image-card-with-a-full-width-section-and-a-split-section
當(dāng)然不是,這看上去太荒謬了。 我們更傾向于將它組合成小一些的組件,就像我們之前討論過(guò)的那樣。
那么這些小一些的組件會(huì)是什么?
它可能被放在.card容器里。 并不是所有卡片都有陰影,所以我們可以用.card--shadowed來(lái)修飾作為類(lèi)名。或者我們可以創(chuàng)建一個(gè)新的通用類(lèi)
.shadow
。這個(gè)類(lèi)可以用在任何元素上。這樣看起來(lái)更加利于復(fù)用,那我們就這么做吧!
我們的網(wǎng)站大部分卡片樣式?jīng)]有圓角,但是這個(gè)有。我們可以給他起名.card--rounded。但我們的網(wǎng)站會(huì)有一些別的元素也有相同幅度的圓角樣式,但他們不是卡片。一個(gè)
.rounded
通用類(lèi)的復(fù)用率也許更高。
那位于上邊的圖片呢?因?yàn)樗3峙c父元素寬度一樣,所以給它起名叫.img--fitted合適嗎? 這個(gè)網(wǎng)站上有些元素也是需要保持與父元素寬度一樣的,但并不一定就是個(gè)圖片。 那么僅僅起名為
.fit
也許會(huì)更合適。
...你可以看出我們最終想要的效果。
如果您將足夠的重點(diǎn)放在可復(fù)用性上,那么就會(huì)很自然的做到用可復(fù)用的通用類(lèi)來(lái)創(chuàng)建組件。
加強(qiáng)一致性
使用較小的,可組合的通用類(lèi)的最大好處是,你團(tuán)隊(duì)的所有開(kāi)發(fā)者可以從一個(gè)固定的列表里面選擇類(lèi)名。
想想有多少次你寫(xiě)樣式的時(shí)候心里想著“這個(gè)文本需要更深一點(diǎn)的顏色”,然后用darken()
函數(shù)去微調(diào)變量
$text-color的值?
又或者,
"這個(gè)字號(hào)應(yīng)該再小一點(diǎn),"
然后給你正在開(kāi)發(fā)的組件添加了
font-size: .85em?
看上去你做的很“對(duì)”,因?yàn)槟闶褂昧讼鄬?duì)的顏色和相對(duì)的字號(hào),而不是固定的值。
但如果你想在組件中把字色調(diào)深10%,而其他人則想調(diào)深12%,那該怎么辦?當(dāng)你回過(guò)神來(lái)的時(shí)候,發(fā)現(xiàn)你的樣式表中 有380種獨(dú)特的文字顏色.
只要你正在寫(xiě)新的CSS,這種情況在每個(gè)代碼庫(kù)中都會(huì)發(fā)生:
GitLab: 380個(gè)文本顏色,202個(gè)背景顏色,58個(gè)字體大小
Buffer: 124個(gè)文本顏色,86個(gè)背景顏色,54個(gè)字體大小
HelpScout: 198個(gè)文字顏色,133個(gè)背景顏色,67個(gè)字體大小
Gumroad: 91個(gè)文字顏色,28個(gè)背景顏色,48個(gè)字體大小
Stripe: 189個(gè)文字顏色,90個(gè)背景顏色,35個(gè)字體大小
GitHub: 157個(gè)文字顏色,155個(gè)背景顏色,56個(gè)字體大小
ConvertKit: 124個(gè)文本顏色,123個(gè)背景顏色,69個(gè)字體大小
這是因?yàn)槟阋獙?xiě)的每個(gè)CSS代碼塊都可以視為一個(gè)空白的畫(huà)布,你可以不受限制的隨意使用任何值。
你可以通過(guò)聲明變量和使用mixins加強(qiáng)代碼的一致性。但
每一行新的CSS仍然有可能會(huì)使樣式變得更復(fù)雜,添加更多的CSS是永遠(yuǎn)不會(huì)使CSS更簡(jiǎn)單的。
相反,用已經(jīng)存在的CSS類(lèi)名來(lái)給HTML添加樣式,用這種解決方式的話,空白畫(huà)布的問(wèn)題就不存在了。
想要顏色更深的字色嗎?添加
.text-dark-soft
類(lèi)。
想要稍小一些的字號(hào)嗎?使用
.text-sm
類(lèi)。
當(dāng)項(xiàng)目組中的每個(gè)人都可以從一個(gè)有限的列表中,選擇他們的樣式時(shí)。CSS樣式表容量就不會(huì)跟隨項(xiàng)目變大而直線上升,你就會(huì)獲得了相對(duì)的自由。
你仍然應(yīng)該創(chuàng)建組件
我的觀點(diǎn)與那些頑固的實(shí)用派CSS擁護(hù)者的區(qū)別之一就是,我不認(rèn)為你應(yīng)該僅用通用類(lèi)來(lái)給標(biāo)簽添加樣式。
如果你看了一些時(shí)下流行的通用類(lèi)框架,例如
(一個(gè)非常棒的庫(kù)),你會(huì)發(fā)現(xiàn)他們?cè)诩兇獾耐ㄓ妙?lèi)之外,甚至創(chuàng)建了按鈕的樣式:
<button class="f6 br3 ph3 pv2 white bg-purple hover-bg-light-purple">
Button Text
</button>
哇哦,讓我來(lái)把這個(gè)分解一下:
f6: 使用字體大小中的第6個(gè)字號(hào) ( 在 Tachyons 是.875rem)br3: 使用圓角弧度中的第3號(hào)(.5rem)ph3: 使用上下padding中的第3號(hào)(1rem)pv2: 使用左右padding中的第2號(hào)(.5rem)white: 使用白色字色bg-purple: 使用字色背景色hover-bg-light-purple: 鼠標(biāo)懸浮時(shí)使用淺紫色背景作為高亮
如果你需要多個(gè)有相同類(lèi)的按鈕(譯者:這個(gè)例子里的標(biāo)簽由多個(gè)類(lèi)名組合修飾。),Tachyons 建議的方法是創(chuàng)建一個(gè)抽象的HTML模版,而不是CSS。
舉個(gè)例子,如果你正在使用
Vue.js,你可以創(chuàng)建一個(gè)組件,像下邊這樣使用類(lèi)名:
`<ui-button color="purple">Save</ui-button>`
...組件的內(nèi)容如下:
<template>
<button class="f6 br3 ph3 pv2" :class="colorClasses">
<slot></slot>
</button>
</template>
<script>
export default {
props: ['color'],
computed: {
colorClasses() {
return {
purple: 'white bg-purple hover-bg-light-purple',
lightGray: 'mid-gray bg-light-gray hover-bg-light-silver',
// ...
}[this.color]
}
}
}
</script>
對(duì)于大多數(shù)項(xiàng)目來(lái)說(shuō),這是一個(gè)很好的方法。但
我依然認(rèn)為創(chuàng)建一個(gè)CSS組件比
創(chuàng)建一個(gè)基于HMTL模版的組件更加實(shí)用。
在我工作的這些項(xiàng)目中,會(huì)將這7個(gè)通用類(lèi)組合起來(lái),創(chuàng)建一個(gè)新的
.btn-purple類(lèi)。這要比給標(biāo)簽一個(gè)一個(gè)添加顆粒度很小的類(lèi)名要簡(jiǎn)單的多。(譯者:一旦其中一個(gè)類(lèi)名不需要了,每個(gè)用到這個(gè)組合類(lèi)的標(biāo)簽都要被修改,而作者的方法只要改新添加的
.btn-purple類(lèi)就可以了,不需要改HTML)。
...但是先要用通用類(lèi)構(gòu)建樣式
我的方法需要先使用通用類(lèi)給標(biāo)簽添加樣式的原因是,我希望使用通用類(lèi)來(lái)構(gòu)建網(wǎng)站樣式(譯者:言外之意,不添加任何新樣式)。而且只有當(dāng)他們出現(xiàn)時(shí)才會(huì)精確的復(fù)用重復(fù)的部分。
如果你用
作為你的預(yù)處理器,你可以將現(xiàn)有的類(lèi)用minxins混合。那意味著你只需要利用編輯器的多光標(biāo)功能就可以創(chuàng)建
.btn-purple
組件了:
[圖片上傳失敗...(image-abe0f4-1513407635274)]
但如果你用的是Sass或者其他沒(méi)有這種可以將多個(gè)通用類(lèi)進(jìn)行混合而生成新類(lèi)的方法的話,就會(huì)稍微麻煩一些,你需要做一些別的工作,才能支持。
當(dāng)然并不是每個(gè)組件類(lèi)都可以通過(guò)多個(gè)通用類(lèi)組合而來(lái)。元素間復(fù)雜的交互是很難只是用通用類(lèi)來(lái)解決的。例如當(dāng)鼠標(biāo)懸浮在父元素時(shí)希望子元素改變屬性時(shí)。所以你要經(jīng)過(guò)思考,選擇你認(rèn)為最簡(jiǎn)單的方法。
不要提前抽象
使用“組件優(yōu)先”的方法寫(xiě)CSS,意味著你創(chuàng)建的組件有可能永遠(yuǎn)不會(huì)被復(fù)用。這種提前抽象就是樣式表如此臃腫和復(fù)雜的原因。
以一個(gè)導(dǎo)航條為例。在你的應(yīng)用中重復(fù)寫(xiě)了多少次主導(dǎo)航標(biāo)簽?
在我的項(xiàng)目里,我一般只會(huì)寫(xiě)一次,在我的主布局文件里。
如果你先創(chuàng)建通用類(lèi),然后將這些通用類(lèi)組合。只有當(dāng)你看到令人煩惱的重復(fù)時(shí)再提取出小型組件,你也許永遠(yuǎn)不需要提取一個(gè)導(dǎo)航條組件。
相反,你的導(dǎo)航條看起來(lái)也許會(huì)像下邊這樣:
<nav>
<div></div>
<div>
</div>
</nav>
這里沒(méi)有什么可值得提取的。
難道這不是內(nèi)聯(lián)樣式嗎?
這種方法很容易讓人認(rèn)為是內(nèi)聯(lián)樣式。這種樣式是在你需要的時(shí)候?qū)⒁恍邮綄傩苑旁贖TML的標(biāo)簽上。但以我的經(jīng)驗(yàn)來(lái)看,這兩者有很大不同。
如果是內(nèi)聯(lián)樣式的話,你在選擇值的時(shí)候是沒(méi)有任何約束的。
這個(gè)標(biāo)記可能是
font-size: 14px,另一個(gè)就可能是font-size: 13px, 還有一個(gè)就可能是
font-size: .9em, 也有可能是
font-size: .85rem.
當(dāng)為每個(gè)新組件編寫(xiě)新的CSS時(shí),它與您所面臨的空白畫(huà)布問(wèn)題相同。
通用類(lèi)則強(qiáng)迫你選擇:
是用
text-sm
還是
text-xs?
我們可以用
py-3
和
py-4
嗎?
我是想用
text-dark-soft
還是
text-dark-faint?
你不能隨意使用你想用的值,而是只能從一個(gè)策劃好的列表里選擇。
你最終只用了10個(gè)或者12個(gè)文字顏色,而不是380個(gè)。
我的經(jīng)驗(yàn)是先創(chuàng)建通用類(lèi),再創(chuàng)建組件類(lèi),可以使設(shè)計(jì)保持一致性,剛開(kāi)始時(shí)這聽(tīng)起來(lái)可能不太直觀。
如何開(kāi)始
如果你對(duì)這個(gè)方法感興趣,下邊是一些值得參考的框架,不妨一試:
我在開(kāi)發(fā)一個(gè)免費(fèi)的開(kāi)源LESS框架,叫做
這個(gè)框架是圍繞本文的主旨開(kāi)發(fā)的,即先創(chuàng)建通用類(lèi),然后從重復(fù)的部分中提取可復(fù)用的組件: