【CSS模組化之路2】webpack中的Local Scope

AlienZHOU發表於2019-03-01

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則提供了一個完整的模組化與打包方案。在一定程度上提高了開發的效率,降低了錯誤率。

相關文章