CSS Modules
CSS Modules 是一种用于解决 CSS 作用域问题的技术方案,其核心原理是通过 局部作用域隔离 避免样式冲突,同时保留 CSS 的原生特性。
基本原理
- 局部作用域:css-modules 会对类名进行哈希处理,使每个类名具有唯一性。转换后的类名只在当前模块(组件)内生效,实现了样式的局部隔离。
- 全局作用域:可以通过
:global
关键字来定义全局样式,这些样式不会被哈希处理,仍然可以在全局范围内使用。
特点
- 样式组合:可以通过
composes
关键字实现样式的继承和组合,方便复用已有样式。
css
/* styles.module.css */
.base {
color: red;
}
.primary {
composes: base;
color: blue;
}
- 全局样式:使用
:global
定义全局样式。
css
/* styles.module.css */
/* 不会被哈希处理 */
:global(.global-class) {
color: green;
}
- 本地化变量:可以通过
:local
明确指定某些样式为局部作用域。
css
/* styles.module.css */
:local(.local-class) {
color: red;
}
- 支持CSS预处理器:css-modules 可以与 Sass、Less 等预处理器结合使用,如变量、嵌套等。
scss
// Button.module.scss
$primary-color: #4285f4;
.base {
padding: 10px;
&:hover {
opacity: 0.9;
}
}
.primary {
composes: base;
background: $primary-color;
}
- 导出变量,可以支持在JS和CSS中共享变量(也支持export显示导出)
css
/* variables.module.css */
@value primary: #4285f4;
@value secondary: #34a853;
@value breakpoints: "./breakpoints.module.css"; /* 导入其他文件变量 */
使用
js
import { primary, secondary } from './variables.module.css';
console.log(primary); // 输出 "#4285f4"
CSS-Moduels 实现原理
要自己实现 CSS Modules 的核心功能,需要完成类名局部化处理、样式解析和映射关系生成这三个核心步骤。以下是一个简化的实现思路和关键技术点:
核心目标
- 将 CSS 中的类名(.className)转换为唯一哈希值(避免全局冲突)
- 生成类名映射表(原始类名 → 哈希类名)供 JS 引用
- 支持 composes 样式组合和 :global() 全局声明语法
工具
- CSS 解析器:使用 postcss 或 css 库解析 CSS 语法,生成 AST(抽象语法树)
- 哈希算法:用 md5 或 sha1 生成类名哈希(可加盐确保唯一性)
- 文件处理:读取 CSS 文件,处理后输出转换后的 CSS 和映射 JSON
实现步骤
1. 第一步:解析CSS生成AST
通过 PostCSS 解析 CSS 代码,获取包含选择器、声明等信息的 AST
点击展开代码
js
const postcss = require('postcss');
const fs = require('fs');
// 读取 CSS 文件内容
const cssContent = fs.readFileSync('./style.module.css', 'utf8');
// 解析为 AST
const ast = postcss.parse(cssContent);
2. 第二步:处理类名局部化
遍历 AST 中的选择器,对类名进行哈希转换(核心逻辑):
点击展开代码
js
const crypto = require('crypto');
// 生成哈希类名(加盐确保唯一性,如文件名+类名)
function generateScopedName(localName, fileName) {
const hash = crypto.createHash('md5')
.update(`${fileName}${localName}`) // 用文件名+类名做哈希
.digest('hex')
.slice(0, 6); // 取前6位缩短长度
return `_${localName}__${hash}`; // 格式:_原始类名__哈希
}
// 存储类名映射(原始 → 哈希)
const classMap = {};
// 遍历 AST 中的规则
ast.walkRules(rule => {
// 处理选择器(如 .class1, .class2 → 转换为哈希类名)
rule.selectors = rule.selectors.map(selector => {
// 跳过 :global() 包裹的全局选择器
if (selector.startsWith(':global(')) {
return selector.replace(/:global\((.*?)\)/, '$1');
}
// 处理局部类名(仅匹配 .class 形式)
return selector.replace(/\.([a-zA-Z0-9_-]+)/g, (match, localName) => {
// 生成哈希类名并记录映射
if (!classMap[localName]) {
classMap[localName] = generateScopedName(localName, 'style.module.css');
}
return `.${classMap[localName]}`;
});
});
});
3. 第三步:处理 composes 语法
composes 用于复用其他类名,需要解析并展开样式:
点击展开代码
js
ast.walkRules(rule => {
const composesDeclarations = [];
// 收集并移除 composes 声明
rule.walkDecls('composes', decl => {
composesDeclarations.push(decl.value);
decl.remove(); // 从原规则中删除 composes
});
// 处理复用逻辑(简化版:直接复制被复用类的样式)
composesDeclarations.forEach(composesValue => {
// 支持 composes: base; 或 composes: base from './other.css';
const [localName, fromPath] = composesValue.split(' from ').map(s => s.trim());
if (fromPath) {
// 跨文件复用:需读取其他文件的类名映射(复杂,此处简化)
console.log(`从 ${fromPath} 复用 ${localName}`);
} else {
// 同文件复用:复制被复用类的样式
const targetClass = `.${localName}`;
ast.walkRules(targetRule => {
if (targetRule.selectors.includes(targetClass)) {
// 复制声明到当前规则
targetRule.each(decl => rule.append(decl.clone()));
}
});
}
});
});
4. 第四步:处理 :global() 语法
:global() 用于定义全局样式,需要特殊处理,避免哈希处理。 遍历 AST 中的选择器,对 :global() 包裹的类名进行特殊标记,后续处理时排除这些类名。
5. 第五步:生成映射关系
- 将处理后的 AST 转换回 CSS 字符串
- 将类名映射表输出为 JSON(供 JS 导入)
点击展开代码
js
// 生成处理后的 CSS
const processedCss = ast.toString();
fs.writeFileSync('./style.module.css.processed.css', processedCss);
// 生成类名映射 JSON
fs.writeFileSync('./style.module.css.json', JSON.stringify(classMap, null, 2));
6. 第六步:在 JS 中使用映射表
最终 JS 可以导入映射表,使用哈希类名。
点击展开代码
js
// 导入生成的映射表
const styles = require('./style.module.css.json');
// 使用转换后的类名
const element = document.createElement('div');
element.className = styles.button; // 如 "_button__a3b7d1"
局限性:
上述代码是核心逻辑的简化版,真实场景还需处理:
- 嵌套选择器(如 .parent .child)的哈希转换
- 动画 @keyframes 的局部化处理
- 与 CSS 预处理器(Sass/LESS)的兼容
- 性能优化(如缓存哈希结果)