為什麼引入CSS Modules
或者可以這麼說,CSS Modules為我們解決了什麼痛點。針對以往我寫網頁樣式的經驗,具體來說可以歸納為以下幾點:
全域樣式衝突
過程是這樣的:你現在有兩個模組,分別為A、B,你可能會單獨針對這兩個模組編寫自己的樣式,例如a.css、b.css,看一下代碼
// A.jsimport './a.css'const html = '<h1 class="text">module A</h1>'// B.jsimport './b.css'const html = '<h1 class="text">module B</h1>'
下面是樣式:
/* a.css */.text { color: red;}/* b.css */.text { color: blue;}
匯入到入口APP中
// App.jsimport A from './A.js'import B from './B.js'element.innerTHML = 'xxx'
由於樣式是統一載入到入口中,因此實際上的樣式合在一起(這裡暫訂為mix.css)顯示順序為:
/* mix.css *//* a.css */.text { color: red;}/* b.css */.text { color: blue;}
根據CSS的Layout規則,因此後面的樣式會覆蓋掉前面的樣式聲明,最終有效就是text
的顏色為blue
的那條規則,這就是全域樣式覆蓋,同理,這在js
中也同樣存在,因此就引入了模組化,在js中可以用立即執行函數運算式來隔離出不同的模組
var moduleA = (function(document, undefined){ // your module code})(document)var moduleB = (function(document, undefined){ // your module code})(document)
而在css中要想引入模組化,那麼就只能通過namespace
來實現,而這個又會帶來新的問題,這個下面會講到
嵌套層次過深的選取器
為瞭解決全域樣式的衝突問題,就不得不引入一些特地命名namespace
來區分scope
,但是往往有些namespace
命名得不夠清晰,就會造成要想下一個樣式不會覆蓋,就要再加一個新的namespace
來進行區分,最終可能一個元素最終的顯示樣式類似如以下:
.widget .table .row .cell .content .header .title { padding: 10px 20px; font-weight: bold; font-size: 2rem;}
在上一個元素的顯示上使用了7個選取器,總結起來會有以下問題:
- 根據CSS選取器的解析規則可以知道,層級越深,比較的次數也就越多。當然在更多的情況下,可能嵌套的層次還會更深,另外,這裡單單用了類別選取器,而採用類別選取器的時候,可能對整個網頁的渲染影響更重。
- 增加了不必要的位元組開銷
- 語義混亂,當文檔中出現過多的
content
、title
以及item
這些通用的類名時,你可能要花上老半天才知道它們到底是用在哪個元素上
- 可擴充性不好,約束越多,擴充性越差
【注】CSS的渲染規則可以參看這篇文章探究 CSS 解析原理
會帶來代碼的冗餘
由於CSS不能使用類似於js的模組化的功能,可能你在一個css檔案中寫了一個公用的樣式類,而你在另外一個css也需要這樣一個樣式,這時候,你可能會多寫一次,類似於這樣的
/* a.css */.modal { position: absolute; top: 0; bottom: 0; left: 0; right: 0; z-index: 1; background-color: rgba(0, 0, 0, 0.7);}.text { color: red;}/* b.css */.modal { position: absolute; top: 0; bottom: 0; left: 0; right: 0; z-index: 1; background-color: rgba(0, 0, 0, 0.7);}.text { color: blue;}
那麼在合并成app.css的時候,就會被編寫兩遍,雖然樣式不會被影響,但是這樣實際上也是一種位元組浪費,當然,上述的這種情況完全是可以通過公用全域樣式來達到目的,但是,這種代碼重複通常是在不知情的情況下發生的。
一些解決方案
針對上述的一些問題,也有一些解決方案,具體如下:
CSS前置處理器(Sass/Less等)
Sass,Less的用法這裡不再贅述,如果不清楚,可以自己查閱相關資料去瞭解一下。
CSS前置處理器最大的好處就是可以支援模組引入,用js的方式來編寫CSS,解決了部分scope
混亂以及代碼冗餘的問題,但是也不能完全避免。同時,也沒有解決全域樣式的衝突問題
一個SASS
的的檔案是這樣的:
/* app.sass */@import './reset'@import './color'@import './font'
可以實際上編譯之後,終究還是一個檔案,因此不可避免的會出現衝突樣式
BEM(Block Element Modifier)
There are only two hard problems in Computer Science: cache invalidation and naming things — Phil Karlton
BEM
就是為瞭解決命名衝突以及更好的語義化而生的。
BEM名詞解釋
命名規則
Block
作為最小的可複用單元,任意嵌套不會影響功能和外觀,命名可以為header
、menu
等等
<style> .header { color: #042; }</style><div class="header">...</div>
Element
依附Block存在,沒有單獨的含義,命名上語義盡量接近於Block,比如title
、item
之類
<style> .header { color: #042; } .header__title { color: #042; }</style><div class="header"> <h1 class="header__title">Header</h1></div>
Modifier
是一個元素的狀態顯示,例如active
、current
、selected
<style> .header--color-black { color: #000; } .header__title--color-red { color: #f00; }</style><div class="header header--color-black"> <h1 class="header__title"> <span class="header__title--color-red">Header</span> </h1></div>
【說明】
- Block完全獨立,可以嵌套,一個header是一個Block,header下的搜尋方塊也可以是一個Block
- 不可能出現
Block__Element-father__Element-son_Modifer
這種類名的寫法,BEM只有三級
- Modifier可以加在Block和Element上面
- Modifier作為一個額外的類名載入Block和Element上面,只是為了改變狀態,需要保留原本的class
一個完整的樣本
<form class="form form--theme-xmas form--simple"> <input class="form__input" type="text" /> <input class="form__submit form__submit--disabled" type="submit" /></form>
.form { }.form--theme-xmas { }.form--simple { }.form__input { }.form__submit { }.form__submit--disabled { }
參考連結:
- get BEM
- BEM(Block-Element-Modifier)
- 如何看待 CSS 中 BEM 的命名方式?
BEM解決了模組複用、全域命名衝突等問題,配合預先處理CSS使用時,也能得到一定程度的擴充,但是它依然有它的問題:
- 對於嵌套過深的層次在命名上會給需要語義化體現的元素造成很大的困難
- 對於多人協作上,需要統一命名規範,這同樣也會造成額外的effort
CSS Modules
說了這麼多,終於要到本文了
什麼是CSS Modules
根據CSS Modules的repo上的話來說是這樣的:
CSS files in which all class names and animation names are scoped locally by default.
所以CSS Modules並不是一個正式的聲明或者是瀏覽器的一個實現,而是通過構建工具(webpack or Browserify)來使所有的class達到scope的一個過程。
CSS Modules 解決了什麼問題
- 全域命名衝突,因為CSS Modules只關心組件本身,只要保證組件本身命名不衝突,就不會有這樣的問題,一個組件被編譯之後的類名可能是這樣的:
/* App.css */.text { color: red;}/* 編譯之後可能是這樣的 */.App__text___3lRY_ { color: red;}
命名唯一,因此保證了全域不會衝突。
可以使用composes
來引入自身模組中的樣式以及另一個模組的樣式:
.serif-font { font-family: Georgia, serif;}.display { composes: serif-font; font-size: 30px; line-height: 35px;}
應用到元素上可以這樣使用:
import type from "./type.css";element.innerHTML = `<h1 class="${type.display}"> This is a heading </h1>`;
之後編譯出來的模板可能是這樣的:
<h1 class="Type__display__0980340 Type__serif__404840"> Heading title</h1>
從另一個模組中引入,可以這樣寫:
.element { composes: dark-red from "./colors.css"; font-size: 30px; line-height: 1.2;}
因為CSS Modules只關注與組件本身,組件本身基本都可以使用扁平的類名來寫,類似於這樣的:
.root { composes: box from "shared/styles/layout.css"; border-style: dotted; border-color: green;}.text { composes: heading from "shared/styles/typography.css"; font-weight: 200; color: green;}
CSS Modules 怎麼用
CSS Modules不局限於你使用哪個前端庫,無論是React、Vue還是Angular,只要你能使用構建工具進行編譯打包就可以使用。
下面我使用webpack
為例,一步一步引入CSS Modules.
構建最初始的應用
.├── build│ └── bundle.js├── index.html├── node_modules├── package-lock.json├── package.json├── src│ ├── index.js│ └── styles└── webpack.config.js
index.js作為程式入口,styles檔案夾存放樣式檔案,配合webpack.config.js作為webpack設定檔。
// index.jsvar html = `<div class="header"> <h2 class="title">CSS Modules</h2></div>`document.getElementById('container').innerHTML = html;
樣式檔案:
/* global.css */* { margin: 0; padding: 0;}.container { padding: 20px;}/* index.css */.header { font-size: 32px;}.title { border-bottom: 1px solid #ccc; padding-bottom: 20px;}
模板檔案:
<!-- index.html --><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>css modules</title></head><body> <div id="container" class="container"></div> <script src="./build/bundle.js"></script></body></html>
全域安裝依賴,配置執行指令碼:
npm install webpack webpack-cli --save-dev
package.json
"scripts": { "build": "npx webpack && open index.html"}
在控制台執行npm run build
, 得到的結果為:
> css-modules-demo@1.0.0 build /Users/yhhu/Documents/coding/css-modules-demo> npx webpack && open index.htmlHash: 5810d2ecd760c08cc078Version: webpack 4.17.1Time: 78msBuilt at: 2018-08-26 15:09:31 Asset Size Chunks Chunk Namesbundle.js 3.97 KiB main [emitted] mainEntrypoint main = bundle.js[./src/index.js] 196 bytes {main} [built]
加入樣式以及loaders
package.json中加入能夠處理css的loader
module: { rules: [ { test: /\.js/, loader: 'babel-loader', include: __dirname + '/src', exclude: __dirname + '/src/styles' }, { test: /\.css$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { } } ] } ] }
index.js中引入兩個CSS檔案
// index.jsimport './styles/global.css'import './styles/index.css'const html = `<div class="header"> <h2 class="title">CSS Modules</h2></div>`document.getElementById('container').innerHTML = html;
編譯之後的執行結果為:
在瀏覽器中顯示為:
提取公有樣式
可以看到打包之後的build
目錄下只有一個bundle.js
,我們現在要把樣式檔案提取出來
./build/└── bundle.js
npm install --save-dev mini-css-extract-plugin
var MiniCssExtractPlugin = require("mini-css-extract-plugin");modules: { rules: [ // { // test: /\.css$/, // use: [ // { loader: "style-loader" }, // { // loader: "css-loader", // options: { // } // } // ] // }, { test: /\.css$/, use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: './build/styles' } }, { loader: "css-loader", options: { } } ] } ]},plugins: [ new MiniCssExtractPlugin({ filename: "[name].css", chunkFilename: "[id].css" })],
<!-- index.html --><!DOCTYPE html><head> <link rel="stylesheet" href="./build/main.css"></head><body> <div id="container" class="container"></div> <script src="./build/bundle.js"></script></body>
可以看到有main.css
產生
開啟css modules功能
預設在css-loader
中是不開啟css modules
功能的,要開啟可以設定modules: true
即可,更多可以參看官方css-loader
使用方法修改webpack.config.js
,如下:
{ test: /\.css$/, use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: './build/styles' } }, { loader: "css-loader", options: { modules: true } } ]}
修改index.js
檔案中的引用方式:
import './styles/global.css'import Index from './styles/index.css'const html = `<div class=${Index.header}> <h2 class=${Index.title}>CSS Modules</h2></div>`document.getElementById('container').innerHTML = html;
可以看到,之前都是直接import
一個css
檔案,而現在改成了匯出一個對象的形式,我們可以把Index
對象列印出來,看看具體是些什麼東西:
直接對應我們引用的方式,然後我們再看看產生出來的main.css
中具體有哪些東西:
* { margin: 0; padding: 0;}._2BQ9qrIFipNbLIGEytIz5Q { padding: 20px;}._3Ukt9LHwDhphmidalfey-S { font-size: 32px;}._3XpLkKvmw0hNfJyl8yU3i4 { border-bottom: 1px solid #ccc; padding-bottom: 20px;}
合成一個檔案之後,所有的類名都經過了雜湊轉換,因此確保了類名的唯一性,我們再看看瀏覽器中inspector
中的樣式應用,如下:
事實上,container
樣式我們是不需要轉換的,因為我是把它固定寫死在了容器上,那我們應該怎麼做呢?
全域範圍
要想一個類名不需要被裝換,那麼可以使用:global(className)
來進行封裝,這樣的類不會被轉換,會被原樣輸出,下面我們修改global.css
/* global.css */* { margin: 0; padding: 0;}:global(.container) { padding: 20px;}
我們再來看看main.css
就可以發現.container
類沒有被轉換
定義雜湊類名
CSS Modules預設是以[hash:base64]來進行類名轉換的,可辨識度不高,因此我們需要自訂
開啟自訂,可以使用一個配置參數localIdentName
,具體配置如下:
{ loader: "css-loader", options: { modules: true, localIdentName: '[path][name]__[local]--[hash:base64:5]' }}
類名組合
如果我們實作類別似於Sass
的繼承功能,我們需要怎麼做呢?CSS Modules中提供了composes
關鍵字讓我們來繼承另外一個類,修改index.css
如下:
.red { color: red;}.header { font-size: 32px;}.title { composes: red; border-bottom: 1px solid #ccc; padding-bottom: 20px;}
我們增加了一個red
的類名,在title
中實現繼承,編譯之後的結果為:
發現多了一個src-styles-index__red--1ihPk
的類名,正是我們上面繼承的那個類
除了在自身模組中繼承,我們還可以繼承其他檔案中的CSS規則,具體如下:
我們再styles
檔案夾下建立一個color.css
/* color.css */.red { color: red;}.blue { color: blue;}
然後在index.css
檔案中匯入
/* index.css */.red { color: red;}.header { font-size: 32px;}.title { color: green; composes: blue from './color.css'; composes: red; border-bottom: 1px solid #ccc; padding-bottom: 20px;}
最終我們會發現文字的顏色為綠色,可見自身模組聲明優先順序最高,如果把自身申明的color
去掉,那麼自身引入和從其他檔案引入的相同申明又該如何顯示呢?
答案是自身引入的聲明的優先順序會比較高。
總結
至此,所有的CSS Modules用法就已經介紹完畢了,至於後續的還有如何應用於React
、Vue
以及Angular
中,相信掌握了上面的內容之後就可以知道怎麼寫了,如何與前置處理器一起使用相信問題也不大。
最後,本文代碼倉庫庫為:github.com/Rynxiao/css-modules-demo
參考連結
- CSS Modules — Solving the challenges of CSS at scale
- github repo
- What are CSS Modules and why do we need them?
- Getting Started with CSS Modules
- Get BEM
- CSS Modules 用法教程
- CSS Modules使用詳解
- 探究 CSS 解析原理