CSS 模塊

jopen 9年前發布 | 25K 次閱讀 CSS 前端技術

原文 http://segmentfault.com/a/1190000003130731


如果你想知道 CSS 最近發展的轉折點,你應該選擇去觀看 Christopher Chedeau 于 2010 年提出來的一個概念),那么 Christopher 則是讓許許多多的可能變得更加接近(譯者注:上面三個工具中的兩個靈感都是來自他的分享)。

CSS 模塊

上圖列出的這些都是在許多大型 CSS 代碼庫中存在的問題。Christopher 指出,只要將你的樣式通過用 JS 去管理,這些問題都能很好的解決。不得不說這的確是有道理的,但是這種方法有它的復雜性并會帶來其他的相關問題。其實只要看看瀏覽器是如何處理:hover偽類狀態的,我們就會發現有些東西在 CSS 中其實***很早***就解決了。

CSS 模塊小組 覺得我們可以更加合理的解決問題:我們可以繼續保持 CSS 現在的樣子,并在 styles-in-JS 社區的基礎上建立更合理的改進。雖然我們已經找到了解決辦法同時又捍衛了 CSS 原始的美,但我們仍然欠那些把我們推向這個結果的那些人一聲感謝。謝謝你們,朋友們!

下面讓我來向大家解說一下什么是 CSS 模塊并且為什么它才是未來吧。

CSS 模塊

第一步:像局部一樣無需考慮全局沖突

在 CSS 模塊中,每一個文件被編譯成獨立的文件。這樣我們就只需要使用通用簡單的類選擇器名字就好了。我們不需要擔心它會污染全局的變量。下面我就我們編寫一個擁有四個狀態的按鈕來說明這個功能。

https://jsfiddle.net/pqnot81c/embedded/result/

CSS 模塊之前我們是怎么做的

我們也許會使用 Suit 命名規范去進行樣式命名,這樣我們的 HTML 和 CSS 代碼看起來就像如下所示:

/* 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>

這樣寫看起來還挺棒的。使用 BEM 命令方式使我們有了 4 個樣式變量這樣我們不必使用嵌套選擇器。使用Button這種首字母大寫的方法可以很好的避免與之前的代碼或者是其他的依賴代碼進行沖突。另外我們使用了--語法這樣能很清楚的顯示出我們的依賴 Class。

總的來說,這樣做可以讓我們的代碼更易于維護,但是它需要我們在命名規范的學習上付出很多努力。不過這已經是目前 CSS 能給出的最好的解決辦法了。

CSS 模塊出來之后我們是怎么做的

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 */

請注意我們這里并沒有在任何地方使用button這個詞。不過反過來想,為什么我們一定要使用它呢?這個文件已經被命名成了submit-button.css了呀!既然在其它的語言中你不需要為你的局部變量增加前綴,沒道理 CSS 需要加上這個蹩腳的功能。

通過使用require或者import從 JS 中導入文件使得 CSS 模塊被編譯成為可能。

/ components/submit-button.js /
import styles from './submit-button.css';

buttonElem.outerHTML = &lt;button class=${styles.normal}&gt;Submit&lt;/button&gt;</pre>

你不必擔心大眾名字會倒置命名沖突,編譯后實際上類名是會自動生成并保證是唯一的。CSS 模塊為你做好一切,最終編譯成一個 CSS 與 JS 交互的ICSS后綴文件( 閱讀這里了解更多 )。因此,你的程序最終看起來可能會是這個樣子的:

<button class="components_submit_button__normal__abc5436">
  Processing...
</button>

如果你的類名變的和上面的例子差不多的話,那么恭喜你你成功了!

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 */

請注意所有的類都是相互獨立的,這里并不存在一個“**基類**”然后其它的類集成并“**覆蓋**”它的屬性這種情況。在 CSS 模塊中**每一個類都必須包含這個元素需要的所有樣式**(稍后會有詳細說明)。這使得你在 JS 中使用樣式的時候有很大的不同:

/ 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}</pre>

當然,如果是的工資是按照字符串長度來計算的話,你愛怎么做就怎么做吧!

React 示例

CSS 模塊并不是 React 特有的功能,但是不得不說在 React 中使用 CSS 模塊會更爽。基于這個理由,我覺得我有必要展示下面這個如飄柔般絲滑的示例:

/ 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> } }</pre>

你完全不需要擔心你的類命名會和全局的樣式表命名沖突,這樣能讓你的注意力更集中在**組件**上,而不是樣式。我敢保證,使用過一次之后,你會再也不想回到原來的模式中去。

然而這僅僅是一切的開始。CSS 模塊化是你的基本,但也是時候來思考一下如何把你的樣式們都集中到一塊了。

第二步:組件就是一切

上文中我提到了每一個類必須包含按鈕不同狀態下的**所有**的樣式,與 BEM 命名方式上相比,代碼上可能區別如下:

/ BEM Style /
innerHTML = &lt;button class="Button Button--in-progress"&gt;

/ CSS Modules / innerHTML = &lt;button class="${styles.inProgress}"&gt;</pre>

那么問題來了,你怎么在所有的狀態樣式中**共享**你的樣式呢?這個答案就是 CSS 模塊的強力武器 - **組件**:

.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這個關鍵詞將會使.normal類將.common內的所有樣式包含進來,這個有點像 Sass 的@extends語法。但是 Sass 依賴重寫你的 CSS 文件達到效果,而 **CSS 模塊最后會通過 JS 編譯導出,不需要修改文件**(譯者注:下面會有例子詳細說明)。

Sass

按照 BEM 的命名規范,我用 Sass 的@extends寫的話可能會像如下的代碼:

.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 */
}

你可以只需要**一**個類來標記你的元素<button class="Button--error">,但是你能得到包括共有的和特例的所有樣式。這看起來真的非常厲害,但是你需要注意這種語法在一些特別的情況下存在問題和陷阱。Hugo Giraudel 在他的 文章 中做了很好的總結,感謝他!

CSS 模塊

composes語法看起來很像@extends但是他們的工作方式是不同的。為了巖石一下,讓我們來看一個例子:

.common { /* font-sizes, padding, border-radius */ }
.normal { composes: common; /* blue color, light blue background */ }
.error { composes: common; /* red color, light red background */ }

編譯后的文件可能是像如下一樣:

.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"
}

所以我們依然可以使用style.normal或者style.error在我們的代碼中,**仍舊會有多個類樣式渲染在我們的 DOM 上**。

<button class="components_submit_button__common__abc5436 
               components_submit_button__normal__def6547">
  Submit
</button>

這就是composes語法的厲害之處,你可以在不重寫你的 CSS 的情況下對你的元素混合使用不同類的樣式。

第三步:文件間共享

Sass 或者 LESS 中,你可以在每個文件中使用@import在全局工作區間內共享樣式。這樣你就可以在一個文件中定義變量或者函數(mixins)并在你的其它組件文件中共享使用。這樣做是有好處的,但是很快你的變量命名就會與其它的變量名稱相沖突(雖然它在另外一個全部空間下),你不可避免的會重構你的variables.scss或者settings.scss,最后你就會發現你已經看不懂到底哪個組件依賴哪個變量了。最后的最后你會發現你的配置文件變量名稱冗余到變得非常 不實用

針對上述問題仍然是有更好的解決辦法的(試試上 Ben Smithett 的文章 Sass 和 Wepack 的混合使用 給了 CSS 模塊話很大的啟發,我推薦大家去讀一讀這篇文章),但是不管怎么做你還是局限在了 Sass 的全局環境下。

CSS 模塊一次只運行一個文件,這樣可以避免全局上下文的污染。而且像 JS 使用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";
}

使用組件,我們能夠深入到每一個像colors.css一樣的基礎樣式表中,并隨意重命名它。又因為組件只是改變了最后**導出**的類名稱,而不是 CSS 文件本身,composes語句在瀏覽器解析之前就會被刪除。

/* 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>

事實上,當它被瀏覽器解析之后,我們的局部是不存在一個"**normal**"樣式的。這是一件好事!這意味著我們可以增加一個局部名字有意義的對象(可能就叫"**normal**")而不用在 CSS 文件中新增代碼。我們使用的越多,對我們的網站會造成更少的視覺誤差以及在用戶瀏覽器上更少的不一致。

題外話:空的類樣式可以使用 csso 這樣的工具來檢查去除掉。

第四步:功能單一模塊

組件是非常強大的,因為它確實的讓你描述了一個元素是什么,而不是它由那些樣式組成。這是一種不同的方式去描述概念示例(**元素**)到樣式實體(**樣式規則**)之間的映射關系。讓我們看一個簡單的 CSS 例子:

.some_element {
  font-size: 1.5rem;
  color: rgba(0,0,0,0);
  padding: 0.5rem;
  box-shadow: 0 0 4px -2px;
}

這些元素,樣式都是特別簡單的。然而也存在著問題:顏色,字體大小,盒子陰影,內邊距-這里的一切都是量身定制的,這讓**我們想要在其它地方復用這些樣式**的時候變得有些困難。下面讓我們用 Sass 重構這些這些代碼:

$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; }</pre>

這是一個進化版,但是我們僅僅只達到了**一部分**目標。事實上$large-font-size和$padding-normal只是在名字上表示了它的用途,并不能在任何地方都執行。像box-shadow這種定義沒辦法賦值給變量,我們不得不實用mixin或者@extends語法來配合。

CSS 模塊

通過使用組件,我們可以使用我們可復用的部分定義我們的組件:

.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";
}

這種寫法勢必會有很多單一功能文件產生,然而通過使用文件系統來管理不同用途的樣式比起用命名空間來說要好的多。如果你想要從一個文件中導入多個類樣式的話,有一種簡單的寫法:

/ 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"; }</pre>

這開辟了一種可能,使用**極細粒**的類樣式定義一些樣式別名去給每一個網站使用:

.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"; }</pre>

我對這種技術非常感興趣,我覺得,它混合了像 Tachyons 那樣變量分離可讀的好處(譯者注:就是說 CSS 模塊的命名簡單易懂,組件復用方便)。

但是 CSS 模塊化之路僅僅是剛剛開始,未來我們希望大家能嘗試更多為它寫出譜寫出新的篇章。

翻滾吧!CSS 模塊!

我們希望 CSS 模塊化能有助于你和你的團隊在你們現有的 CSS 和產品的基礎上維護代碼,讓它變得更舒適更高效。我們已經接近可能的把額外的語法減少到最少,并盡量確保語法和現有的變化不大。我們有 Webpack,Rails 的支持已經提上議程準備進行了。

但是為了讓它變得更簡單,我在 Plunker 上制作了一個 預覽示例 ,你不用安裝任何東西就可以運行它:

CSS 模塊

這里還很小,我們還沒有看到一些有用的例子,歡迎你們給我們投稿。

最后的最后,祝大家開心寫樣式,幸福每一天!

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!