Skip to content

CSS Modules

CSS Modules 是一种用于解决 CSS 作用域问题的技术方案,其核心原理是通过 局部作用域隔离 避免样式冲突,同时保留 CSS 的原生特性。

基本原理

  1. 局部作用域:css-modules 会对类名进行哈希处理,使每个类名具有唯一性。转换后的类名只在当前模块(组件)内生效,实现了样式的局部隔离。
  2. 全局作用域:可以通过 :global 关键字来定义全局样式,这些样式不会被哈希处理,仍然可以在全局范围内使用。

特点

  1. 样式组合:可以通过 composes 关键字实现样式的继承和组合,方便复用已有样式。
css
/* styles.module.css */
.base {
  color: red;
}

.primary {
  composes: base;
  color: blue;
}
  1. 全局样式:使用 :global 定义全局样式。
css
/* styles.module.css */ 
/* 不会被哈希处理 */
:global(.global-class) {
  color: green;
}
  1. 本地化变量:可以通过 :local 明确指定某些样式为局部作用域。
css
/* styles.module.css */
:local(.local-class) {
  color: red;
}
  1. 支持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;
}
  1. 导出变量,可以支持在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 的核心功能,需要完成类名局部化处理、样式解析和映射关系生成这三个核心步骤。以下是一个简化的实现思路和关键技术点:

核心目标

  1. 将 CSS 中的类名(.className)转换为唯一哈希值(避免全局冲突)
  2. 生成类名映射表(原始类名 → 哈希类名)供 JS 引用
  3. 支持 composes 样式组合和 :global() 全局声明语法

工具

  1. CSS 解析器:使用 postcss 或 css 库解析 CSS 语法,生成 AST(抽象语法树)
  2. 哈希算法:用 md5 或 sha1 生成类名哈希(可加盐确保唯一性)
  3. 文件处理:读取 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. 第五步:生成映射关系

  1. 将处理后的 AST 转换回 CSS 字符串
  2. 将类名映射表输出为 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"

局限性:

上述代码是核心逻辑的简化版,真实场景还需处理:

  1. 嵌套选择器(如 .parent .child)的哈希转换
  2. 动画 @keyframes 的局部化处理
  3. 与 CSS 预处理器(Sass/LESS)的兼容
  4. 性能优化(如缓存哈希结果)