CSS 模塊化方案探討(BEM、OOCSS、CSS Modules、CSS-in-JS ...)

全文共 4000 餘字,預計花費 30 分鐘。

衆所周知,CSS 根據選擇器名稱去全局匹配元素,它沒有做用域可言,好比你在頁面的兩個不一樣的地方使用了一個相同的類名,先定義的樣式就會被覆蓋掉。CSS 一直缺少模塊化的概念,命名衝突的問題會持續困擾着你。每次定義選擇器名稱時,總會顧及其餘文件中是否也使用了相同的命名,這種影響在組件開發中尤其明顯。💣💣💣css

理想的狀態下,咱們開發一個組件的過程當中,應該能夠隨意的爲其中元素進行命名,只須要保證其語義性便可,而沒必要擔憂它是否與組件以外的樣式發生衝突。html

與 JavaScript 社區中的 AMD、CMD、CommonJS、ES Modules 等相似,CSS 社區也誕生了相應的模塊化解決方案:BEM、OOCSS、SMACSS、ITCSS,以及 CSS Modules 和 CSS-in-JS 等。前端

根據這些 CSS 模塊化方案的特色,我簡單的將它們分爲了三大類:vue

  1. CSS 命名方法論:經過人工的方式來約定命名規則。
  2. CSS Modules:一個 CSS 文件就是一個獨立的模塊。
  3. CSS-in-JS:在 JS 中寫 CSS。

image.png

CSS 命名方法論

爲了不 CSS 選擇器命名衝突的問題,以及更好的實現 CSS 模塊化,CSS 社區在早期誕生了一些 CSS 命名方法論,如 BEM、OOCSS、SMACSS、ITCSS、SUITCSS、Atomic CSS 等。node

它們幾乎都有一個共同的特色——爲選擇器增長冗長的前綴或後綴,並試圖經過人工的方式來生成全局惟一的命名。這無疑會增長了類命名的複雜度和維護成本,也讓 HTML 標籤顯得臃腫。react

BEM

BEM(Block Element Modifier)是一種典型的 CSS 命名方法論,由 Yandex 團隊(至關於中國的百度)在 2009 年前提出,它的核心思想是 經過組件名的惟一性來保證選擇器的惟一性,從而保證樣式不會污染到組件外webpack

BEM 命名規約是 .block-name__element-name--modifier-name,即 .模塊名__元素名--修飾器名 三個部分,用雙下劃線 __ 來明確區分模塊名和元素名,用雙橫線 -- 來明確區分元素名和修飾器名。你也能夠在保留 BEM 核心思想的前提下,自定義命名風格,如駝峯法、使用單下劃線、使用單橫線等。git

在 BEM 中不建議使用子代選擇器,由於每個類名已經都是全局惟一的了,除非是 block 相互嵌套的場景。github

<!-- 示例模塊 -->
<div class="card">
  <div class="card__head">
    <ul class="card__menu">
      <li class="card__menu-item">menu item 1</li>
      <li class="card__menu-item">menu item 2</li>
      <li class="card__menu-item card__menu-item--active">menu item 3</li>
      <li class="card__menu-item card__menu-item--disable">menu item 4</li>
    </ul>
  </div>
  <div class="card__body"></div>
  <div class="card__foot"></div>
</div>
.card {}
.card__head {}
.card__menu {}
.card__menu-item {}
.card__menu-item--active {}
.card__menu-item--disable {}
.card__body {}
.card__foot {}

使用 Sass/Less/Stylus 的父元素選擇器 & 能夠更高效的編寫 BEM:web

.card {
  &__head {}
  &__menu {
    &-item {
      &--active {}
      &--disable {}
    }
  }
  &__body {}
  &__foot {}
}

OOCSS

OOCSS(Object-Oriented CSS)即面向對象的 CSS,它借鑑了 OOP(面向對象編程)的抽象思惟,主張將元素的樣式抽象成多個獨立的小型樣式類,來提升樣式的靈活性和可重用性。

OOCSS 有兩個基本原則:

  1. 獨立的結構和樣式。即不要將定位、尺寸等佈局樣式與字體、顏色等表現樣式寫在一個選擇器中。
  2. 獨立的容器和內容。即讓對象的行爲可預測,避免對位置的依賴,子元素即便離開了容器也應該能正確顯示。

好比:咱們有一個容器是頁面的 1/4 寬,有一個藍色的背景,1px 灰色的邊框,10px 的左右邊距,5px 的上邊距,10px 的下邊距。之前對於這樣一個樣式,咱們經常給這個容器建立一個類,並把這些樣式寫在一塊兒。像下面這樣。

<div class="box"></div>

<style>
  .box {
    width: 25%;
    margin: 5px 10px 10px;
    background: blue;
    border: 1px solid #ccc;
  }
</style>

然而使用 OOCSS 的話,咱們不能這樣作,OOCSS 要求爲這個容器建立更多的「原子類」,而且每一個樣式對應一個類,這樣是爲了後面能夠重複使用這些組件的樣式,避免重複寫相同的樣式,就拿這個實例來講,咱們給這個容器增長下面的類:

<div class="size1of4 bgBlue solidGray mt-5 ml-10 mr-10 mb-10"></div>

<style>
  .size1of4 { width: 25%; }
  .bgBlue { background: blue; }
  .solidGray { border: 1px solid #ccc; }
  .mt-5 { margin-top: 5px; }
  .mr-10 { margin-right: 10px }
  .mb-10 { margin-bottom: 10px; }
  .ml-10 { margin-left: 10px; }
</style>

OOCSS 最大的優勢是讓樣式可複用性最大化,也可以顯著減小總體的 CSS 代碼數量。缺點也很明顯,你須要爲每一個元素蒐集一大堆類名,這但是一個不小的體力活 😅。

在 OOCSS 中,類名既要能傳遞對象的用途,也要有通用性,例如 mod、complex、pop 等。若是將 CSS 類命名的太語義化,例如 navigation-bar,那麼就會將其限制在導航欄,沒法應用到網頁的其它位置。

SMACSS

SMACSS(Scalable and Modular Architecture for CSS)便可伸縮及模塊化的 CSS 結構,由 Jonathan Snook 在 2011 年雅虎時提出。

SAMCSS 按照部件的功能特性,將其劃分爲五大類:

  1. 基礎(Base)是爲HTML元素定義默認樣式,能夠包含屬性、僞類等選擇器。
  2. 佈局(Layout)會將頁面分爲幾部分,可做爲高級容器包含一個或多個模塊,例如左右分欄、柵格系統等。
  3. 模塊(Module)又名對象或塊,是可重用的模塊化部分,例如導航欄、產品列表等。
  4. 狀態(State)描述的是任一模塊或佈局在特定狀態下的外觀,例如隱藏、激活等。
  5. 主題(Theme)也就是換膚,描述了頁面的外觀,它可修改前面四個類別的樣式,例如連接顏色、佈局方式等。

SMACSS 推薦使用前綴來區分不一樣部件:

  1. 基礎規則是直接做用於元素的,所以不須要前綴。
  2. 佈局的前綴是 l-layout-,例如 .l-table.layout-grid 等。
  3. 模塊的前綴是 m- 或模塊自身的命名,例如 .m-nav.card.field 等。
  4. 狀態的前綴是 is-,例如 .is-active.is-current 等。
  5. 主題的前綴是 theme-,例如 .theme-light.theme-dark 等。
<form class="layout-grid">
  <div class="field">
    <input type="search" id="searchbox" />
    <span class="msg is-error">There is an error!</span>
  </div>
</form>

ITCSS

ITCSS(Inverted Triangle CSS,倒三角 CSS)是一套方便擴展和管理的 CSS 體系架構,它兼容 BEM、OOCSS、SMACSS 等 CSS 命名方法論。ITCSS 使用 分層 的思想來管理你的樣式文件,相似服務端開發中的 MVC 分層設計。

ITCSS 將 CSS 的樣式規則劃分紅如下的幾個層次:

  1. Settings:項目使用的全局變量,好比顏色,字體大小等等。
  2. Tools:項目使用的 mixins 和 functions。到 Tools 爲止,不會生成具體的 CSS 代碼。
  3. Generic:最基本的設定,好比 reset.css、normalize.css 等。
  4. Base:最基礎的元素(elements),好比 img、p、link、list 等。
  5. Objects:某種設計模式,好比水平居中,
  6. Components:UI 組件,好比 button、switch、slider 等。
  7. Trumps:用於輔助和微調的樣式,只有這一層纔可使用 !important

ITCSS 的分層邏輯越往下就越具體,越侷限在某個具體的場景。

image.png

根據 ITCSS 的思想,你能夠這樣組織你的 CSS 樣式文件:

stylesheets/
├── settings/
│   ├── colors.scss
│   ├── z-layers.scss
│   └── breakpoints.scss
├── tools/
│   ├── mixins.scss
│   └── functions.scss
├── generic/
│   ├── box-sizing.scss
│   └── normalize.scss
├── base/
│   ├── img.scss
│   └── list.scss
├── objects/
│   ├── grid.scss
│   └── media.scss
├── components/
│   ├── buttons.scss
│   └── slider.scss
├── trumps/
│   ├── widths.scss
│   └── gaps.scss
└── index.scss

下面是幾個基於 ITCSS 的模版項目,可供參考:

CSS Modules

📚 上面提到的這些 CSS 命名方法論,雖然已經不適用於當今的自動化工做流和大前端環境,可是他們有其誕生的時代背景,也確實推進了 CSS 模塊化的發展,其背後的設計思想一樣值得咱們學習,甚至有時候咱們仍然能在某些場合下看到他們的影子。

手寫命名前綴後綴的方式讓開發者苦不堪言,因而 CSS Modules 這種真正的模塊化工具就誕生了。

CSS Modules 容許咱們像 import 一個 JS Module 同樣去 import 一個 CSS Module。每個 CSS 文件都是一個獨立的模塊,每個類名都是該模塊所導出對象的一個屬性。經過這種方式,即可在使用時明確指定所引用的 CSS 樣式。而且,CSS Modules 在打包時會自動將 id 和 class 混淆成全局惟一的 hash 值,從而避免發生命名衝突問題。

這裏僅羅列一些 CSS Modules 的核心特性,更具體的用法能夠參考 官網阮老師的《CSS Modules 用法教程》

CSS Modules 特性:

  • 做用域:模塊中的名稱默認都屬於本地做用域,定義在 :local 中的名稱也屬於本地做用域,定義在 :global 中的名稱屬於全局做用域,全局名稱不會被編譯成哈希字符串。
  • 命名:對於本地類名稱,CSS Modules 建議使用 camelCase 方式來命名,這樣會使 JS 文件更乾淨,即 styles.className
    可是你仍然能夠執拗己見地使用 styles['class-name'],容許但不提倡。🤪
  • 組合:使用 composes 屬性來繼承另外一個選擇器的樣式,這與 Sass 的 @extend 規則相似。
  • 變量:使用 @value 來定義變量,不過須要安裝 PostCSS 和 postcss-modules-values 插件。
/* style.css */
:global(.card) {
  padding: 20px;
}
.article {
  background-color: #fff;
}
.title {
  font-size: 18px;
}
// App.js
import React from 'react'
import styles from './style.css'

export default function App() {
  return (
    <article className={styles.article}>
      <h2 className={styles.title}>Hello World</h2>
      <div className="card">Lorem ipsum dolor sit amet.</div>
    </article>
  )
}

編譯結果:

<style>
  .card {
    padding: 20px;
  }
  .style__article--ht21N {
    background-color: #fff;
  }
  .style__title--3JCJR {
    font-size: 18px;
  }
</style>

<article class="style__article--ht21N">
  <h2 class="style__title--3JCJR">Hello World</h2>
  <div class="card">Lorem ipsum dolor sit amet.</div>
</article>

CSS Modules 集成

在 webpack 中使用 CSS Modules(開啓 css-loader 的 modules 特性):

// webpack.config.js -> module.rules
{
  test: /\.(c|sa|sc)ss$/i,
  exclude: /node_modules/,
  use: [
    'style-loader',
    {
      loader: 'css-loader',
      options: {
        importLoaders: 2,
        // 開啓 CSS Modules
        modules: true,
        // 藉助 CSS Modules,能夠很方便地自動生成 BEM 風格的命名
        localIdentName: '[path][name]__[local]--[hash:base64:5]',
      },
    },
    'postcss-loader',
    'sass-loader',
  ],
},

在 PostCSS 中使用 CSS Modules(使用 postcss-modules 插件):

// postcss.config.js
module.exports = {
  plugins: {
    'postcss-modules': {
      generateScopedName: '[path][name]__[local]--[hash:base64:5]',
    },
  },
}

配合 CSS 預處理器使用

使用 CSS Modules 時,推薦配合 CSS 預處理器(Sass/Less/Stylus)一塊兒使用。

CSS 預處理器提供了許多有用的功能,如嵌套、變量、mixins、functions 等,同時也讓定義本地名稱或全局名稱變得容易。

:global(.title) {
  color: yellow;
}

:global {
  .global-class-name {
    color: green;
  }
}

VSCode 擴展支持

在 VSCode 中寫 CSS Modules 代碼,默認是沒有自動提示和跳轉至定義處的功能,不夠智能。

能夠安裝 CSS Modules 擴展。

l0EwY2Mk4IBgIholi.gif

CSS-in-JS

React 的出現,打破了之前「關注點分離」的網頁開發原則,因其採用組件結構,而組件又強制要求將 HTML、CSS 和 JS 代碼寫在一塊兒。表面上看是技術的倒退,實際上並非。

React 是在 JS 中實現了對 HTML 和 CSS 的封裝,賦予了 HTML 和 CSS 全新的「編程能力」。對於 HTML,衍生了 JSX 這種 JS 的語法擴展,你能夠將其理解爲 HTML-in-JS;對於 CSS,衍生出一系列的第三方庫,用來增強在 JS 中操做 CSS 的能力,它們被稱爲 CSS-in-JS。

隨着 React 的流行以及組件化開發模式的深刻人心,這種"關注點混合"的新寫法逐漸成爲主流。

Any application that can be written in JavaScript, will eventually be written in JavaScript. —— Jeff Atwood

CSS-in-JS 庫目前已有幾十種實現,你能夠在 CSS in JS Playground 上快速嘗試不一樣的實現。下面列舉一些流行的 CSS-in-JS 庫:

image.png

styled-components 💅

styled-components 是目前最流行的 CSS-in-JS 庫,在 React 中被普遍使用。

它使用 ES6 提供的模版字符串功能來構造「樣式組件」。

// styles.js
import styled, { css } from 'styled-components'

// 建立一個名爲 Wrapper 的樣式組件 (一個 section 標籤, 並帶有一些樣式)
export const Wrapper = styled.section`
  padding: 10px;
  background: deepskyblue;
`

// 建立一個名爲 Title 的樣式組件 (一個 h1 標籤, 並帶有一些樣式)
export const Title = styled.h1`
  font-size: 20px;
  text-align: center;
`

// 建立一個名爲 Button 的樣式組件 (一個 button 標籤, 並帶有一些樣式, 還接收一個 primary 參數)
export const Button = styled.button`
  padding: 10px 20px;
  color: #333;
  background: transparent;
  border-radius: 4px;

  ${(props) => props.primary && css`
    color: #fff;
    background: blue;
  `}
`
// App.js
import React from 'react'
import { Wrapper, Title, Button } from './styles'

// 而後,像使用其餘 React 組件同樣使用這些樣式組件
export default function App() {
  return (
    <Wrapper>
      <Title>Hello World, this is my first styled component!</Title>
      <Button>Normal Button</Button>
      <Button primary>Primary Button</Button>
    </Wrapper>
  )
}

更多使用技巧(更具體的內容請參考 官方文檔):

  • 能夠經過插值的方式給樣式組件傳遞參數(props),這在須要動態生成樣式規則時特別有用。
  • 能夠經過構造函數 styled() 來繼承另外一個組件的樣式。
  • 使用 createGlobalStyle 來建立全局 CSS 規則。
  • styled-components 會爲自動添加瀏覽器兼容性前綴。
  • styled-components 基於 stylis(一個輕量級的 CSS 預處理器),你能夠在樣式組件中直接使用嵌套語法,就像在 Sass/Less/Stylus 中的那樣。
  • 強烈推薦使用 styled-components 的 Babel 插件 babel-plugin-styled-components(固然這不是必須的)。它提供了更好的調試體驗的支持,好比更清晰的類名、SSR 支持、壓縮代碼等等。
  • 你也能夠在 Vue 中使用 styled-components,vue-styled-components,不過好像沒人會這麼作\~
  • 默認狀況下,模版字符串中的 CSS 代碼在 VSCode 中是沒有智能提示和語法高亮效果的,須要安裝 擴展

在 Vue 中編寫 CSS 的正確姿式

方式一:使用 Scoped CSS(推薦)

<style> 區塊添加 scoped 屬性便可開啓「組件樣式做用域(Scoped CSS)」。

在背後,Vue 會爲該組件內全部的元素都加上一個全局惟一的屬性選擇器,形如 [data-v-5298c6bf],這樣在組件內的 CSS 就只會做用於當前組件中的元素。

<template>
  <header class="header">header</header>
</template>

<style scoped>
.header {
  background-color: green;
}
</style>

編譯結果:

<header class="header" data-v-5298c6bf>header</header>

<style>
.header[data-v-5298c6bf] {
  background-color: green;
}
</style>

方式二:使用 CSS Modules

<style> 區塊添加 module 屬性便可開啓 CSS Modules。

在背後,Vue 會爲組件注入一個名爲 $style 的計算屬性,並混淆類名,而後你就能夠在模板中經過一個動態類綁定來使用它了。

<template>
  <header :class="$style.header">header</header>
</template>

<style module>
.header {
  background-color: green;
}
</style>

編譯結果:

<header class="App__header--382G7">header</header>

<style>
.App__header--382G7 {
  background-color: green;
}
</style>

在 React 中編寫 CSS 的正確姿式

React 並無給咱們提供與 Vue 的 scoped 相似的特性,咱們須要經過其餘方式來實現 CSS 模塊化。

  1. 使用 styled-components:styled-components 是最流行也是最好用的 CSS-in-JS 庫,它將 CSS、JS 以及 React 開發中最流行的一些語法整合起來,易上手,且功能強大。
  2. 使用 CSS Modules:在外部管理 CSS,而後將類名映射到組件內部,他會爲每一個 class 都分配一個全局惟一 hash。另外,這兩個插件會幫你更好地在 React 中使用 CSS Modules:react-css-modulesbabel-plugin-react-css-modules

CSS Modules 與 styled-components 是兩種大相徑庭的 CSS 模塊化方案,它們最本質的區別是:前者是在外部管理 CSS,後者是在組件中管理 CSS。二者沒有孰好孰壞,若是你能接受 CSS-in-JS 這種編程模式,更推薦使用 styled-components。若是一時沒法接受,以爲其過於激進了,那就用 CSS Modules。It doesn't matter,選擇了哪個,就用哪個的體系去管理項目就行了。

參考資料

  1. BEM: A New Front-End Methodology — Smashing Magazine
  2. Battling BEM CSS: 10 Common Problems And How To Avoid Them — Smashing Magazine
  3. An Introduction To Object Oriented CSS (OOCSS) — Smashing Magazine
  4. MicheleBertoli/css-in-js: React: CSS in JS techniques comparison
  5. CSS Modules 用法教程 - 阮一峯的網絡日誌
  6. CSS in JS 簡介 - 阮一峯的網絡日誌
相關文章
相關標籤/搜索