CSS 模块化:给样式上把 “锁”,再也不怕类名冲突了

137 阅读4分钟

写 CSS 时,你是不是遇到过这种尴尬:自己写的.button样式明明没问题,一引入别人的组件,按钮突然变成了奇怪的样子?原来对方也用了.button类名,两个样式 “打架” 了。

这就是 CSS 的 “全局污染” 问题 —— 所有样式都在同一个作用域,类名重复就会冲突。而 “CSS 模块化” 就是来解决这个问题的,它能给每个样式类名加一把 “锁”,让样式只在自己的组件里生效。今天就用 React+Vite 的例子,聊聊 CSS 模块化到底是怎么回事,以及它为什么能解决类名冲突。

一、先看问题:类名重复会 “打架”

image.png 我们在App.jsx 中引入<Button><AnotherButton>这两个组件

  • Button组件:用.button类名,背景色是天蓝色;
  • AnotherButton组件:也用.button类名,背景色是粉色。

运行起来你会发现两个按钮的样式 “串了”

image.png 这是因为CSS 的 “全局作用域” 特性—— 不管通过什么方式引入(import<link><style>),只要 CSS 类名相同,后加载的样式就会覆盖先加载的样式。

就像两个同学都叫 “小明”,老师点名时两个人都站起来 —— 重名了,分不清谁是谁。

二、CSS 模块化:给类名加 “独一无二的标识”

CSS 模块化的解决思路很简单:让每个类名在打包后变成独一无二的,这样就不会和其他组件的类名重复了。

在 React+Vite 项目里,只需把 CSS 文件命名为[文件名].module.css(比如button.module.css),就能自动启用模块化。

看个具体例子:

1. Button 组件的样式(button.module.css

/* 源码里的类名是.button */
.button {
  background-color: skyblue;
  color: white;
  padding: 10px 20px;
}

2. Button 组件的代码(Button.jsx

// 导入模块化CSS,得到一个styles对象
import styles from './button.module.css';

const Button = () => {
  // 通过styles.button访问类名(模块化会处理这个类名)
  return <button className={styles.button}>Button</button>;
};

3. AnotherButton 组件的样式(another-button.module.css

/* 源码里也用了.button,但模块化会处理 */
.button {
  background-color: pink;
  color: white;
  padding: 10px 20px;
}

4. 页面同时引入两个组件

import Button from './components/Button';
import AnotherButton from './components/AnotherButton';

function App() {
  return (
    <>
      <Button />
      <AnotherButton /> 
    </>
  );
}

神奇的事情发生了:

image.png

两个组件都用了.button类名,但样式没有冲突 ——Button是天蓝色,AnotherButton是粉色。

三、原理:模块化是怎么给类名 “上锁” 的?

打开浏览器开发者工具,你会发现两个按钮的类名被 “偷偷修改” 了:

image.png 这就是模块化的核心魔法:在打包时,自动给类名添加唯一的哈希值,让原本相同的类名变得独一无二。

  • 哈希值由文件名和类名生成(比如button.module.css里的.button,哈希包含buttonbutton);
  • 不同文件里的同名类名,哈希值不同,因此不会冲突;
  • 开发者写代码时只用关心源码里的类名(如.button),打包后的处理完全自动化。

注意

  1. 类名不能用横线连接?不,能!
    如果 CSS 里写.btn-primary,模块化后可以通过styles['btn-primary']访问(因为横线在 JS 里不能直接作为对象属性名):

    /* style.module.css */
    .btn-primary {
      color: red;
    }
    
    // 组件里这样用
    <button className={styles['btn-primary']}>按钮</button>
    
  2. 想写全局样式怎么办?
    模块化 CSS 里,加:global()可以声明全局样式(不添加哈希):

    /* 全局样式:所有p标签都生效 */
    :global(p) {
      margin: 0;
    }
    
    /* 全局类名:其他组件也能直接用 */
    :global(.global-class) {
      font-size: 16px;
    }
    

四、模块化的优点:为什么要这么用?

  1. 彻底解决类名冲突
    每个组件的样式都是 “局部作用域”,不会影响其他组件,也不会被其他组件影响。就像给每个样式上了锁,只有自己能打开。
  2. 提高代码可维护性
    不用再为了避免冲突起冗长的类名(比如header-button__userfooter-button__admin),直接用简洁的.button.title即可。
  3. 样式和组件绑定
    当组件被删除时,对应的模块化 CSS 也可以安全删除,不用担心影响其他地方 —— 不像全局 CSS,删之前还要到处找 “有没有地方用到”。
  4. 开发体验友好
    导入styles对象后,IDE 会有自动补全(比如输入styles.会提示所有类名),减少拼写错误。

五、和 Vue 的 scoped 有啥区别?

用过 Vue 的同学可能会觉得眼熟 ——Vue 的<style scoped>也能实现样式隔离。它们的目标一样(避免冲突),但实现方式略有不同:

  • Vue 的scoped是给 DOM 元素加一个data-v-xxx属性,然后通过属性选择器(.button[data-v-xxx])限制样式作用域;
  • CSS 模块化是给类名加hash,通过唯一类名限制作用域。

本质上都是 “让样式只作用于当前组件”,只是技术手段不同。