CSS是一門幾十分鐘就能入門,但是卻需要很長的時間才能掌握好的語言。它有著它自身的一些複雜性與侷限性。其中非常重要的一點就是,本身不具備真正的模組化能力。
系列文章連結 ↓ ↓
1. 面臨的問題
你可能會說,CSS有@import
功能。然而,我們都知道,這裡的@import
僅僅是表示引入相應的CSS檔案,但其模組化核心問題並未解決——CSS檔案中的任何一個選擇器都會作用在整個文件範圍裡。
因此,其實我們面臨的最大問題就是——所有的選擇器都是在一個全域性作用域內的。一旦引入一個新的CSS檔案,就有著與預期不符的樣式表現的風險(因為一些不可預測的選擇器)。
而如今的前端專案規模越來越大,已經不是過去隨便幾個css、js檔案就可以搞定的時代。與此同時的,對於一個大型的應用,前端開發團隊往往也不再是一兩個人。隨著專案與團隊規模的擴大,甚至是專案過程中人員的變動,如何更好進行程式碼開發的管理已經成為了一個重要問題。
回想一下,有多少次:
- 我們討論著如何對class進行有效的命名,以避免協作開發時的衝突;
- 我們面對一段別人寫的css、html程式碼,想要去修改,然後瘋狂查詢、猜測每個類都是什麼作用,哪些是可以去掉的,哪些是可以修改的——到最後我們選擇重新新增一個新的class;
- 我們準備重構程式碼時,重構也就成了重寫
- ……
用CSS實現一些樣式往往並不是最困難的所在,難的是使用一套合理的CSS架構來支援團隊的合作與後續的維護。
What we want is to be able to write code that is as transparent and self-documenting as possible.
本系列文章會介紹一些業界在探索CSS模組化程式中提出的方案。再上一篇文章裡我們介紹了如何使用BEM和名稱空間來規範與架構我們的CSS。這一篇文章主要介紹了,如何在webpack中使用一種類似“CSS模組化”的解決方案———Local Scope,來規避一些開發中的問題。
2. 什麼是Local Scope
通常來說,CSS中的所有選擇器可以算是“全域性作用域”。而“Local Scope”顧名思義,使CSS具有類似於區域性作用域的能力,同時搭配類似JavaScript中模組化的寫法,到達CSS模組化的效果。
這麼說可能有些抽象,我們可以來看一個例子。
在webpack中引入css往往是這樣的:
// index.css
.title {
font-size: 30px;
color: #333;
}
// index.js
import `./index.css`;
funciont createTitle(str) {
var title = document.createElement(`h1`);
title.appendChild(document.createTextNode(str));
title.setAttribute(`class`, `title`);
document.body.appendChild(title);
}
createTitle(`Hi!`);
複製程式碼
由於,webpack中將js、css、png等這些資源都視為模組,所以可以通過import匯入。但是,實際上,對於匯入的所有css,其“地位”都是平等的,都是在全域性有效的。例如:
// index.css
.title {
font-size: 30px;
color: #333;
}
// other.css
.title {
font-size: 15px;
color: #999;
}
// index.js
import `./index.css`;
import `./other.css`;
funciont createTitle(str) {
var title = document.createElement(`h1`);
title.appendChild(document.createTextNode(str));
title.setAttribute(`class`, `title`);
document.body.appendChild(title);
}
createTitle(`Hi!`);
複製程式碼
當我們引入了新的CSS檔案other.css後,其中的.title
和index.css中的.title
有著同樣的“作用域”——全域性。
回想一下在JavaScript中:
// a.js
var a = 1;
// other.js
var a = 2;
// index.html
<script src="./a.js"></script>
<script src="./other.js"></script>
<script>
console.log(a); // 2
</script>
複製程式碼
如果某個html頁面通過script
標籤引入這兩js檔案,那麼a的值必定會有衝突,其中一個會被覆蓋。如果使用模組化的方式,可以變成:
// a.js
export var a = 1;
// other.js
export var a = 2;
// app.js
import {a} from `./a`;
import {a as other} from `./other`;
console.log(a); // 1
console.log(a); // 2
複製程式碼
而所謂的Local Scope就是webpack中在CSS上針對這種問題的一個解決方案。類似JavaScript的模組化,通過對CSS檔案進行模組引用與匯出的方式,能夠在開發時,更有效得控制各個class的作用範圍。
3. 使用方法
首先,需要在webpack中對css-loader
進行一定的配置。
// loader: `css-loader`,
// options: {
// modules: true,
// localIdentName: `[local]__[name]--[hash:base64:5]`
// }
const config = {
entry: `./src/index.js`,
output: {
path: path.resolve(__dirname, `dist`),
filename: `bundle.js`
},
module: {
rules: [{
test: /.css$/,
use: [{
loader: `style-loader`
}, {
loader: `css-loader`,
options: {
modules: true,
localIdentName: `[local]__[name]--[hash:base64:5]`
}
}]
}]
}
};
複製程式碼
然後,我們還是使用前一節例子中的那個場景:
// index.css
:local .title {
font-size: 30px;
color: #333;
}
// other.css
:local .title {
font-size: 15px;
color: #999;
}
// index.js
import styles from `./index.css`;
import others from `./other.css`;
funciont createTitle(str) {
var title = document.createElement(`h1`);
title.appendChild(document.createTextNode(str));
// styles.title font-size: 30px;color: #333;
title.setAttribute(`class`, styles.title);
document.body.appendChild(title);
}
createTitle(`Hi!`);
複製程式碼
其中需要注意的有三個地方:
- 第一個是在CSS檔案中的,類選擇器前多了
:local
這個語法。通過新增:local
就可以指示webpack,這不是一個“全域性”的選擇器(當然,實際上也是全域性的,後面會簡單解釋)。 - 第二個地方是在js檔案中,將
import `index.css`
變為了import styles from `./index.css`
。是不是看著很熟悉,沒錯,和JavaScript中的模組化方案用法一樣。 - 第三個地方,在使用到該class的地方,由原來的
title.setAttribute(`class`, ‘title’)
變為了title.setAttribute(`class`, styles.title)
。這樣我們可以選擇在一部分dom元素上使用styles.title
,即index.css
的樣式;在另一部分dom元素上使用other.css
的樣式。
這樣就解決了我們之前提到的問題。
當然,有些時候,我們希望類選擇器中的某一部分仍然是“全域性”的,那麼我們可以這麼寫:
:local .title :global(.sub-title) { color: #666; }
複製程式碼
4. 關於Local Scope
雖然我們上面說了這麼多次的“模組化”、“作用域”、“全域性”,然而,實際上,對於CSS這門語言來說,它在自己本身的邏輯上是不具備這些特點的。而webpack中Local Scope的相關方案,其實也並不是(CSS本身邏輯支援的)真正意義上所謂的模組化。所以很多地方我都打上了引號。
如果對著打包後的頁面,開啟chrome控制檯,會發現,我們的html是這個樣子的
<body>
<h1 class="title__index--330EV">Hi!</h1>
</body>
複製程式碼
h1
標籤的class
並不是我們在CSS中所寫的title
,而是一串奇怪的字串title__index--330EV
。
當使用webpack進行打包時,由於檢查到:local
這個語法,因此會為.title
這個class生成一個新的class名稱,而我們在js檔案中所使用的styles.title
對應的就是這個新的classname。
所以可以理解,其實當前的CSS語法邏輯中中並沒有實際意義上所謂的local scope,但是,通過webpack打包時的操作,我們會為每個:local
的class生成一個唯一的名稱,而我們使用樣式實際是指向了這個classname。這就實現了兩個CSS檔案中,相同名稱的class在使用時就不會有衝突了,相當於避開了“全域性作用域”。
如果開啟打包後的bundle.js
,我們可以發現一段很有趣的程式碼
// ……其餘省略
// exports
exports.locals = {
"title": "title__index--330EV"
};
// ……其餘省略
// exports
exports.locals = {
"title": "title__other--3vRzX"
};
複製程式碼
這是在兩個不同的模組內的部分。上面一個就是匯出的index.css
中對應的classname,下面一個就是other.css
的。通過styles.title
就可以引用到title__index--330EV
這個實際值。
最後,再來說一下title__index--330EV
這個值得由來。在上一節的一開始,我們對webpack進行了配置,其中有一行
localIdentName: `[local]__[name]--[hash:base64:5]`
複製程式碼
其實就是指示了唯一標識的命名方式:local
是class的名稱,name
是檔案的名稱,而最後加上hash
值。當然,你完全可以使用其他你喜歡的方式。預設是使用[hash:base64]
。
5. 寫在最後
其實webpack中的CSS模組化方案Local Scope,粗淺的來說也是通過生成唯一的classname來避免衝突,控制作用範圍。只是和BEM不同,BEM是一個建議標準,更多的還是人為的操控,而webpack中的Local Scope則提供了一個完整的模組化與打包方案。在一定程度上提高了開發的效率,降低了錯誤率。