官方檔案的介紹如下:
A CSS Modules is a CSS file in which all class names and animation names are scoped locally by default.
所有的類名和動畫名稱預設都有各自的作用域的 CSS
檔案。CSS Modules 並不是 CSS 官方的標準,也不是瀏覽器的特性,而是使用一些構建工具,比如 webpack
,對 CSS
類名和選擇器限定作用域的一種方式(類似名稱空間)
本文來介紹一下 CSS Modules
的簡單使用,以及 CSS Modules
的實現原理(CSS-loader 中的實現)
新建一個專案,本文的 Demo
npx create-react-app learn-css-modules-react
cd learn-css-modules-react
# 顯示 webpack 的設定
yarn eject
看到 config/webpack.config.js
,預設情況下,React 腳手架搭建出來的專案,只有 .module.css
支援模組化,如果是自己搭建的話,可以支援 .css 檔案的字尾等
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
}
其中 getStyleLoaders 函數,可以看到 css-loader 的設定
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
// ...
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
// ...
];
// ...
return loaders;
};
我們就基於這個環境當做範例進行演示
之前的樣式
首先,我們將 App.css
修改成 App.module.css
,然後匯入 css,並設定(這裡有個小知識點,實際上 CSS Modules 推薦的命名是駝峰式,主要是這樣的話,使用物件 style.className
就可以存取。如果是如下,就需要 styles['***-***'])
import styles from './App.module.css';
// ...
<header className={styles['App-header']}></header>
就會根據特定的規則生成相應的類名
這個命名規則是可以通過 CSS-loader 進行設定,類似如下的設定:
module: {
loaders: [
// ...
{
test: /\.css$/,
loader: "style-loader!css-loader?modules&localIdentName=[path][name]---[local]---[hash:base64:5]"
},
]
}
預設情況下,我們發現,在 css modules 中定義的類名必須得通過類似設定變數的方式給 HTML
設定(如上範例所示)
那麼我能像其他的 CSS
檔案一樣直接使用類名(也就是普通的設定方法),而不是編譯後的雜湊字串麼?
使用 :global
的語法,就可以宣告一個全域性的規則
:global(.App-link) {
color: #61dafb;
}
這樣就可以在 HTML 中直接跟使用普通的 CSS 一樣了
但這裡感覺就是 CSS Modules 給開發者留的一個後門,我們這樣的 CSS,還是直接放在普通 .css 檔案中比較好,我理解這就是 React 為什麼對待 .css 和 .module.css 不同字尾進行不同的處理的原因
在 CSS Modules 中,一個選擇器可以繼承另一個選擇器的規則,這稱為 "組合"("composition")
比如,我們定義一個 font-red,然後在 .App-header 中使用 composes: font-red;
繼承
.font-red {
color: red;
}
.App-header {
composes: font-red;
/* ... */
}
不僅僅可以同一個檔案中的,還可以繼承其他檔案中的 CSS 規則
定義一個 another.module.css
.font-blue {
color: blue;
}
在 App.module.css 中
.App-header {
/* ... */
composes: font-blue from './another.module.css';
/* ... */
}
我們還可以使用變數,定義一個 colors.module.css
@value blue: #0c77f8;
在 App.module.css 中
@value colors: "./colors.module.css";
@value blue from colors;
.App-header {
/* ... */
color: blue;
}
總體而言,CSS Modules 的使用偏簡單,上手非常的快,接下來我們看看 Webpack 中 CSS-loader
是怎麼實現 CSS Modules
的
看 lib/processCss.js
中
var pipeline = postcss([
...
modulesValues,
modulesScope({
// 根據規則生成特定的名字
generateScopedName: function(exportName) {
return getLocalIdent(options.loaderContext, localIdentName, exportName, {
regExp: localIdentRegExp,
hashPrefix: query.hashPrefix || "",
context: context
});
}
}),
parserPlugin(parserOptions)
]);
主要看 modulesValues
和 modulesScope
方法,實際上這兩個方法又是來自其他兩個包
var modulesScope = require("postcss-modules-scope");
var modulesValues = require("postcss-modules-values");
這個包主要是實現了 CSS Modules 的樣式隔離(Scope Local)以及繼承(Extend)
它的程式碼比較簡單,基本一個檔案完成,原始碼可以看這裡,這裡會用到 postcss 處理 AST 相關,我們大致瞭解它的思想即可
實際上,假如你沒有設定任何的規則時候會根據如下進行命名
// 生成 Scoped name 的方法(沒有傳入的時候的預設規則)
processor.generateScopedName = function (exportedName, path) {
var sanitisedPath = path.replace(/\.[^\.\/\\]+$/, '').replace(/[\W_]+/g, '_').replace(/^_|_$/g, '');
return '_' + sanitisedPath + '__' + exportedName;
};
這種寫法在很多的原始碼中我們都可以看到,以後寫程式碼的時候也可以採用
var processor = _postcss2['default'].plugin('postcss-modules-scope', function (options) {
// ...
return function (css) {
// 如果有傳入,則採用傳入的命名規則
// 否則,採用預設定義的 processor.generateScopedName
var generateScopedName = options && options.generateScopedName || processor.generateScopedName;
}
// ...
})
css ast
主要有 3 種父類別型
@value
還有幾個個比較重要的子型別:
不同的型別進行不同的遍歷
rule
、 decl
的子型別// Find any :local classes
// 找到所有的含有 :local 的 classes
css.walkRules(function (rule) {
var selector = _cssSelectorTokenizer2['default'].parse(rule.selector);
// 獲取 selector
var newSelector = traverseNode(selector);
rule.selector = _cssSelectorTokenizer2['default'].stringify(newSelector);
// 遍歷每一條規則,假如匹配到則將類名等轉換成作用域名稱
rule.walkDecls(function (decl) {
var tokens = decl.value.split(/(,|'[^']*'|"[^"]*")/);
tokens = tokens.map(function (token, idx) {
if (idx === 0 || tokens[idx - 1] === ',') {
var localMatch = /^(\s*):local\s*\((.+?)\)/.exec(token);
if (localMatch) {
// 獲取作用域名稱
return localMatch[1] + exportScopedName(localMatch[2]) + token.substr(localMatch[0].length);
} else {
return token;
}
} else {
return token;
}
});
decl.value = tokens.join('');
});
});
css.walkRules 遍歷所有節點資訊,無論是 atRule、rule、comment 的父類別型,還是 rule
、 decl
的子型別,獲取 selector
// 遞迴遍歷節點,找到目標節點
function traverseNode(node) {
switch (node.type) {
case 'nested-pseudo-class':
if (node.name === 'local') {
if (node.nodes.length !== 1) {
throw new Error('Unexpected comma (",") in :local block');
}
return localizeNode(node.nodes[0]);
}
/* falls through */
case 'selectors':
case 'selector':
var newNode = Object.create(node);
newNode.nodes = node.nodes.map(traverseNode);
return newNode;
}
return node;
}
walkDecls
遍歷每一條規則,生成相應的 Scoped Name
// 生成一個 Scoped Name
function exportScopedName(name) {
var scopedName = generateScopedName(name, css.source.input.from, css.source.input.css);
exports[name] = exports[name] || [];
if (exports[name].indexOf(scopedName) < 0) {
exports[name].push(scopedName);
}
return scopedName;
}
關於實現 composes 的組合語法,有點類似,不再贅述
這個庫的主要作用是在模組檔案之間傳遞任意值,主要是為了實現在 CSS Modules 中能夠使用變數
它的實現也是隻有一個檔案,具體檢視這裡
檢視所有的 @value
語句,並將它們視為區域性變數或匯入的,最後儲存到 definitions
物件中
/* Look at all the @value statements and treat them as locals or as imports */
// 檢視所有的 @value 語句,並將它們視為區域性變數還是匯入的
css.walkAtRules('value', atRule => {
// 類似如下的寫法
// @value primary, secondary from colors
if (matchImports.exec(atRule.params)) {
addImport(atRule)
} else {
// 處理定義在檔案中的 類似如下
// @value primary: #BF4040;
// @value secondary: #1F4F7F;
if (atRule.params.indexOf('@value') !== -1) {
result.warn('Invalid value definition: ' + atRule.params)
}
addDefinition(atRule)
}
})
假如是匯入的,呼叫的 addImport 方法
const addImport = atRule => {
// 如果有 import 的語法
let matches = matchImports.exec(atRule.params)
if (matches) {
let [/*match*/, aliases, path] = matches
// We can use constants for path names
if (definitions[path]) path = definitions[path]
let imports = aliases.replace(/^\(\s*([\s\S]+)\s*\)$/, '$1').split(/\s*,\s*/).map(alias => {
let tokens = matchImport.exec(alias)
if (tokens) {
let [/*match*/, theirName, myName = theirName] = tokens
let importedName = createImportedName(myName)
definitions[myName] = importedName
return { theirName, importedName }
} else {
throw new Error(`@import statement "${alias}" is invalid!`)
}
})
// 最後會根據這個生成 import 的語法
importAliases.push({ path, imports })
atRule.remove()
}
}
否則則直接 addDefinition,兩個的思想大致我理解都是找到響應的變數,然後替換
// 新增定義
const addDefinition = atRule => {
let matches
while (matches = matchValueDefinition.exec(atRule.params)) {
let [/*match*/, key, value] = matches
// Add to the definitions, knowing that values can refer to each other
definitions[key] = replaceAll(definitions, value)
atRule.remove()
}
}
CSS Modules 並不是 CSS 官方的標準,也不是瀏覽器的特性,而是使用一些構建工具,比如 webpack,對 CSS 類名和選擇器限定作用域的一種方式(類似名稱空間)。通過 CSS Modules,我們可以實現 CSS 的區域性作用域,Class 的組合等功能。最後我們知道 CSS Loader 實際上是通過兩個庫進行實現的。其中, postcss-modules-scope
—— 實現CSS Modules 的樣式隔離(Scope Local)以及繼承(Extend)和 postcss-modules-values
——在模組檔案之間傳遞任意值