CSS 模塊
歡迎來到未來
- 2015-08-19
假如你想要弄清楚在最近 CSS 思維發(fā)展中的拐點(diǎn) ,很有可能你就會挑選 Christopher Chedeau 在去年十一月份 “CSS in JS” 的講話。這是一個(gè)分水嶺,如同經(jīng)歷過高度碰撞后的粒子般急速前進(jìn),從原本的方向中確立了不同的思想分支。例如,React Style, jsxstyle 和 Radium 是目前用來設(shè)計(jì) React 樣式及其與之相關(guān)的項(xiàng)目的方法中最新,最聰明和可行的方法。如果創(chuàng)新是用來探索臨近可能 adjacent possible 的其中一種情形,那么 Christopher 則會靠近更多的可能性。

這一頁幻燈片讓很多人有一種似曾相識的感覺。
這里全都是從一方面以上影響大部分 CSS 代碼庫的合法性問題, Christopher 指出只要你愿意把樣式放進(jìn) JavaScript,這些都能得到很好的解決,盡管如此但是它也擁有自己的復(fù)雜之處和特質(zhì)??纯次抑疤岬降捻?xiàng)目中處理 :hover 狀態(tài)的一系列辦法,而在 CSS 里面已經(jīng)早就解決了。
CSS Modules team 覺得我們可以跟問題死磕,保持我們所喜歡的 CSS 并在 styles-in-JS 社區(qū)的佳作上繼續(xù)創(chuàng)作。所以,我們在看好我們自身的方法并堅(jiān)定捍衛(wèi) CSS 優(yōu)點(diǎn)的同時(shí),也由衷的感謝在其他方向不斷有所突破的人。謝謝!
讓我來告訴你們?yōu)槭裁?CSS 模塊化是未來。

我們是這么描述 CSS 模塊的
第一步 本地默認(rèn)
在 CSS 模塊中,每一個(gè)文件都是單獨(dú)編譯的,所以你可以用一些簡單的類名選擇器和通用名稱,而不必?fù)?dān)心污染全局變量、比方說,我們正在構(gòu)建一個(gè)簡單的提交按鈕具有下列 4 種狀態(tài):正常、不可用、錯(cuò)誤、處理中。
開始CSS模塊之前
我們也許會這樣寫代碼,使用 普通古老的 CSS 和 HTML 的 Suit/BEM 風(fēng)格的類名:
/* components/submit-button.css */
.Button { /* all styles for Normal */ }
.Button--disabled { /* overrides for Disabled */ }
.Button--error { /* overrides for Error */ }
.Button--in-progress { /* overrides for In Progress */
<button class="Button Button--in-progress">Processing...</button>
這看起來確實(shí)挺好的,我們有這四種變體,然而 BEM 風(fēng)格的命名意味著我們不用沒有了可以嵌套的選擇器。我們用大寫字母開頭的 Button 避免前置的樣式或我們放進(jìn)的依賴。并且我們采用 --modifier 的類型所以我們可以清楚這個(gè)變體是需要基礎(chǔ)類名來應(yīng)用的。
總之,這是合理明確并且可維護(hù)的代碼,但是這需要對圍繞命名規(guī)范有可怕的認(rèn)知理解。然而,這是我們在標(biāo)準(zhǔn)的 CSS 所能做的最好的了。
利用 CSS 模塊
CSS模塊以為這你從不需要擔(dān)心你的命名空間變得普遍,就用在任何覺得有意義的地方就可以了。
/* components/submit-button.css */
.normal { /* all styles for Normal */ }
.disabled { /* all styles for Disabled */ }
.error { /* all styles for Error */ }
.inProgress { /* all styles for In Progress */
注意到我們并不到處使用 "button" 這個(gè)詞。為什么呢?這個(gè)文件早就命名為 "submit-button.css",在其他的的語言里,你并不需要去對擁有命名空間的本地變量進(jìn)行預(yù)處理,CSS 當(dāng)然也是。
這就讓 CSS 模塊編譯的方式變得可能 - 通過使用 require 和 import 從 JavaScript 中加載這些文件:
/* components/submit-button.js */
import styles from './submit-button.css';
buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`
實(shí)際上命名空間是自動生成并且唯一的。 CSS 模塊讓已經(jīng)為你考慮好了,并且編譯文件為 ICSS閱讀我之前的博客 的格式,介紹了 CSS 和 JS 是如何溝通的。所以,當(dāng)你運(yùn)行應(yīng)用的時(shí)候,會看到下面的東西:
<button class="components_submit_button__normal__abc5436">
Processing...
</button>
如果你在 DOM 看到這些東西,說明已經(jīng)成功了!

你是大猩猩,CSS 模塊是鯊魚
命名約定
再回來思考我們按鈕的例子:
/* components/submit-button.css */
.normal { /* all styles for Normal */ }
.disabled { /* all styles for Disabled */ }
.error { /* all styles for Error */ }
.inProgress { /* all styles for In Progress */
注意到所有的類型都是獨(dú)立的,與其一部分變成“基本類”,剩下的部分變成“覆蓋類”。在 CSS 模塊里,任何一個(gè)類名都需要對應(yīng)改變量的所有樣式(不止是短期的用處)。在 JavaScript 中,使用這些樣式會大有不同:
/* Don't do this */
`class=${[styles.normal, styles['in-progress']].join(" ")}`
/* Using a single name makes a big difference */
`class=${styles['in-progress']}`
/* camelCase makes it even better */
`class=${styles.inProgress}`
一個(gè) React 的例子
React 陣營本身和 CSS 模塊并沒有什么聯(lián)系。但是 React 卻提供了一個(gè)使用 CSS 模塊絕佳的經(jīng)歷,所以展示一個(gè)比較復(fù)雜的例子是值得的:
/* components/submit-button.jsx */
import { Component } from 'react';
import styles from './submit-button.css';
export default class SubmitButton extends Component {
render() {
let className, text = "Submit"
if (this.props.store.submissionInProgress) {
className = styles.inProgress
text = "Processing..."
} else if (this.props.store.errorOccurred) {
className = styles.error
} else if (!this.props.form.valid) {
className = styles.disabled
} else {
className = styles.normal
}
return <button className={className}>{text}</button>
}
}
在使用你自己樣式的時(shí)候,你可以不必?fù)?dān)心產(chǎn)生一個(gè)全局安全的 CSS 變量名,這樣會讓你更專注于組件而非樣式。并且一旦擺脫了這種持續(xù)的上下文切換,你會對于曾經(jīng)你的忍受感到驚訝。
第二步 組成是一切
早些時(shí)候我提過,每一個(gè)類名應(yīng)該包含一個(gè)按鈕不同狀態(tài)的所有樣式,而在 BEM 樣式里面你必須假設(shè)它不止有一個(gè)類名:
/* BEM Style */
innerHTML = `<button class="Button Button--in-progress">`
/* CSS Modules */
innerHTML = `<button class="${styles.inProgress}">`
等等,但是你要怎樣代表所有狀態(tài)共有的樣式?答案是 CSS 模塊最為給力的功能,組成 (composition):
.common {
/* all the common styles you want */
}
.normal {
composes: common;
/* anything that only applies to Normal */
}
.disabled {
composes: common;
/* anything that only applies to Disabled */
}
.error {
composes: common;
/* anything that only applies to Error */
}
.inProgress {
composes: common;
/* anything that only applies to In Progress */
}
composes 關(guān)鍵詞說明 .normal 包括了所有來自 .common 樣式,很像 Sass 里面的 @extend 關(guān)鍵詞。但是 Sass 會重寫你的 CSS 選擇器達(dá)到這個(gè)目的, 而 CSS 模塊會改變導(dǎo)出到 JavaScript 的類名。
在 Sass
讓我們舉一個(gè) BEM 的例子,并且應(yīng)用一些 Sass 的 @extend :
.Button--common { /* font-sizes, padding, border-radius */ }
.Button--normal {
@extends .Button--common;
/* blue color, light blue background */
}
.Button--error {
@extends .Button--common;
/* red color, light red background */
}
編譯到 CSS:
.Button--common, .Button--normal, .Button--error {
/* font-sizes, padding, border-radius */
}
.Button--normal {
/* blue color, light blue background */
}
.Button--error {
/* red color, light red background */
}
你可以在你的標(biāo)簽 <button calss="Button--error"> 只用一個(gè)類名來獲得你想要的公有 & 特殊的樣式。這是一個(gè)非常強(qiáng)大的概念,但是實(shí)踐起來卻有一些你需要注意的邊緣的案例 & 陷阱。感謝 Hugo Giraudel 這里有一個(gè)很好的問題總結(jié)和鏈接。
包含 CSS 模塊
composes 關(guān)鍵詞在概念上類似于 @extends,但是執(zhí)行起來是不相同。為了證明,先看一個(gè)例子:
.common { /* font-sizes, padding, border-radius */ }
.normal { composes: common; /* blue color, light blue background */ }
.error { composes: common; /* red color, light red background */ }
到達(dá)瀏覽器之后看起來會像下面這個(gè)樣子:
.components_submit_button__common__abc5436 { /* font-sizes, padding, border-radius */ }
.components_submit_button__normal__def6547 { /* blue color, light blue background */ }
.components_submit_button__error__1638bcd { /* red color, light red background */ }
在你的 JS 代碼里面, import styles from "./submit-button.css 返回:
styles: {
common: "components_submit_button__common__abc5436",
normal: "components_submit_button__common__abc5436 components_submit_button__normal__def6547",
error: "components_submit_button__common__abc5436 components_submit_button__error__1638bcd"
}
所以我們?nèi)匀豢梢栽谖覀兊拇a里面使用styles.normal 或者 styles.error , 我們會在渲染完的 DOM 里面獲得不同的類
<button class="components_submit_button__common__abc5436
components_submit_button__normal__def6547">
Submit
</button>
這就是 composes 的力量,你可以組合不同的獨(dú)立樣式組,而無需改變你的標(biāo)簽并且重寫你的 CSS 選擇器??。
第三步 在不同的文件共享
使用 Sass 或者 Less,每一個(gè)你 @import 的文件會在同一個(gè)全局工作區(qū)被處理。這就決定了你在一個(gè)文件如何定義變量和混合塊并在你所有的組件文件使用。這很實(shí)用,然而一旦你的變量名與其他的變量名產(chǎn)生了沖突(由于它們共享了同樣的命名空間),你會不可避免的重構(gòu)一個(gè) variables.scss 或者 settings.scss,并且對于你來說,哪個(gè)模塊依賴哪個(gè)變量變得不可見。而且你的 settings 文件會變得笨重。
這里有一個(gè)更好的方法(實(shí)際上 Ben Smithett 的post about using Sass & Webpack together 在 CSS 模塊項(xiàng)目有一個(gè)直接的影響,我推薦你去讀一讀)但是你同樣會受限于 Sass 的全局命名。
CSS 模塊在單一的文件運(yùn)行一次,所以并沒有全局空間的污染。就像在 JavaScript 中我們可以 import 或者 require 我們的依賴, CSS 模塊也可以讓我們從其他的文件 compose :
/* colors.css */
.primary {
color: #720;
}
.secondary {
color: #777;
}
/* other helper classes... */
/* submit-button.css */
.common { /* font-sizes, padding, border-radius */ }
.normal {
composes: common;
composes: primary from "../shared/colors.css";
}
使用組成(composes),我們得以使用命名普通的文件 color.css 并且使用它本地的名字來引用其中的一個(gè)類。由于組成改變了 exported 的類名而非 CSS 本身, composes 的聲明本身在到達(dá)瀏覽器之前就會從 CSS 本身刪除了:
/* colors.css */
.shared_colors__primary__fca929 {
color: #720;
}
.shared_colors__secondary__acf292 {
color: #777;
}
/* submit-button.css */
.components_submit_button__common__abc5436 { /* font-sizes, padding, border-radius */ }
.components_submit_button__normal__def6547 {}
<button class="shared_colors__primary__fca929
components_submit_button__common__abc5436
components_submit_button__normal__def6547">
Submit
</button>
實(shí)際上,在它到達(dá)瀏覽器的那一刻,我們本地名 “normal” 并沒有它自己的樣式。這是好事!因?yàn)檫@意味著我們可以使用一個(gè)新的具有本地意義的對象(一個(gè)稱為 “normal” 的實(shí)體)而無需添加新一行 CSS。我們越是能做到這一點(diǎn),蔓延在我們網(wǎng)站的視覺不一致以及到達(dá)瀏覽器之后的臃腫就會越少。
<small data-reactid=".0.1.1.1x.0">除此之外:這些空的類名可以很容易的被檢測并且被類似 csso 之類的檢查器刪除。 </small>
第四步 單一合理的模塊
組成是十分給力的,因?yàn)樗屇闳ッ枋鲆粋€(gè)元素而非它組成的樣式。這是另一種不同的概念上的實(shí)體到樣式的實(shí)體的映射。讓我們來看一看一個(gè) 樸素老舊的 CSS 的簡單例子:
.some_element {
font-size: 1.5rem;
color: rgba(0,0,0,0);
padding: 0.5rem;
box-shadow: 0 0 4px -2px;
}
這個(gè)元素以及樣式,簡單,然而卻有一個(gè)問題:colour, font-size, box-shadow, the padding,這些所有的東西都是有詳盡的細(xì)節(jié)規(guī)范的,即使我們想要在其他地方重用這些樣式。讓我們在 Sass 重構(gòu)一次:
$large-font-size: 1.5rem;
$dark-text: rgba(0,0,0,0);
$padding-normal: 0.5rem;
@mixin subtle-shadow {
box-shadow: 0 0 4px -2px;
}
.some_element {
@include subtle-shadow;
font-size: $large-font-size;
color: $dark-text;
padding: $padding-normal;
}
這是一個(gè)進(jìn)步,但我們只提取了大多數(shù)行的一半。$large-font-size 是排版和 $padding-normal 是布局的事實(shí)不過是通過其名稱,而不是在任何地方執(zhí)行表示。當(dāng)有一個(gè)類似于 box-shadow 的聲明的值并不會讓它成為一個(gè)變量,我們不得不用一個(gè) @mixin 或 @extends 來表示。
使用 CSS 模塊
通過使用組成,我們可以在可復(fù)用的部分聲明自己的組件。
.element {
composes: large from "./typography.css";
composes: dark-text from "./colors.css";
composes: padding-all-medium from "./layout.css";
composes: subtle-shadow from "./effect.css";
}
這種格式自然而然的會產(chǎn)生大量含有單一目的的文件,通過文件系統(tǒng)劃定不同的風(fēng)格而不是命名空間。加入你想要把不同的類型放在一個(gè)文件里,可以試一下下面的簡寫:
/* this short hand: */
.element {
composes: padding-large margin-small from "./layout.css";
}
/* is equivalent to: */
.element {
composes: padding-large from "./layout.css";
composes: margin-small from "./layout.css";
}
這樣使得通過極端粒度的類名為你的網(wǎng)站上每一個(gè)視覺效果添加別名提供了可能性:
.article {
composes: flex vertical centered from "./layout.css";
}
.masthead {
composes: serif bold 48pt centered from "./typography.css";
composes: paragraph-margin-below from "./layout.css";
}
.body {
composes: max720 paragraph-margin-below from "layout.css";
composes: sans light paragraph-line-height from "./typography.css";
}
這是一項(xiàng)我十分感興趣去探索的技術(shù)。在我看來,它結(jié)合了 Tachyons 原子 CSS 技術(shù),Semantic UI 的可讀性和獨(dú)立性等等最好的方面。
但我們現(xiàn)在只是在 CSS 模塊故事的開始,我們十分歡迎你在目前或者接下來的項(xiàng)目嘗試并與我們共創(chuàng)未來。
開始吧!
通過 CSS 模塊,我們希望我們可以幫你和你的團(tuán)隊(duì)保留你們現(xiàn)有盡可能多的 CSS 知識與產(chǎn)品,變得更為舒服和專業(yè)。我們已經(jīng)把語法添加到最低限度,努力確保有例子是接近你已經(jīng)工作的方式。我們在 Webpack, JSPM 以及 Browserify 都有示范項(xiàng)目,如果您使用的其中之一,我們總是在了望 CSS 模塊可以生效的新環(huán)境:服務(wù)器端支持的 NodeJS 正在 happening 而 Rails 正在初始。
為了然事情更為簡單,我在這里建了個(gè)例子而你無需安裝任何東西:
只要你準(zhǔn)備好了,可以去 CSS Modules 倉庫看一看,如果你有問題,請?zhí)峤?issue 來討論。CSS Modules team 規(guī)模較小,我們并不能知道所有的問題,所以希望能夠聽到你的想法。
