新人别再被CSS污染搞疯了!CSS Modules实战指南

前端新人别再被CSS污染搞疯了!CSS Modules实战指南

你是不是也经历过改个按钮样式,结果首页全炸了?那种手指还在键盘上悬浮着、心脏却直接骤停的感觉,我懂。你明明只是想把登录按钮从蓝色改成红色,点下保存的那一刻,整个页面的按钮都红了,包括那个本该灰掉的"取消"按钮,还有顶部导航里那个不起眼的用户头像下拉菜单——全红了。这时候你才意识到,上周同事写的.btn { background: blue; }和你今天写的.btn-red { background: red; }在全局命名空间里撞了个满怀。

全局样式像病毒一样传染,改一处崩三处——别硬扛了,CSS Modules就是来救你的。这不是什么新鲜玩意儿,也不是Facebook那种巨头才会用的高大上技术,它就是你隔壁工位那个每天准时下班的老哥偷偷在用的保命神器。

CSS为啥在组件化时代成了“毒瘤”

咱们得先聊聊这个让人血压飙升的历史遗留问题。CSS诞生于1996年,那时候网页还是个朴素的文档,JavaScript还在玩泥巴,整个互联网都在用表格布局。CSS的设计者们当时做梦也想不到,二十多年后前端会进化出组件化这种怪物——每个按钮、每个卡片、每个弹窗都想要自己的独立王国,但CSS天生就是全球同此凉热的联合国模式。

class名字一重复就串样,尤其多人协作时,.btn-red 变成 .btn-red-actually-blue。你见过那种项目吗?style.css里躺着三千行代码,.active这个类名出现了十八次,每次都有微妙的差别:有时候是color: red,有时候是font-weight: bold,有时候是display: block。更可怕的是命名空间战争,为了避免冲突,你开始写.my-component-btn,我写.header-wrapper-btn,他写.content-area-btn-sidebar-left。最后的结果就是HTML膨胀得像泡发的海参,DevTools里Inspect元素的时候,那个class属性长得能绕地球三圈。

不是你写得烂,是传统CSS压根没考虑模块隔离这回事。BEM命名规范?有用,但靠自觉。像不像你们公司的代码规范?写得时候都信誓旦旦,三个月后全是"临时方案,后续优化"。而且BEM写起来真的很累啊,block__element--modifier这种写法,每次打三个下划线两个横杠,我都怀疑自己不是在写代码而是在打摩斯电码。

/* 传统的灾难现场 */
/* Header.css */
.nav { background: #333; }
.active { color: #fff; font-weight: bold; }

/* Sidebar.css */
.menu { padding: 20px; }
.active { background: #f0f0f0; border-left: 3px solid blue; }

/* 结果:导航栏的active同时拥有了白色文字和灰色背景,丑得惊天地泣鬼神 */

这时候你可能想说,那我用CSS-in-JS啊,styled-components不香吗?香,但那是另一个坑。你写个简单的按钮,要引入一个库,增加运行时开销,调试的时候看到一堆sc-bdVaJa这种类名,你根本不知道对应的是哪个文件。而且热更新有时候抽风,样式改了页面不刷新,你得手动F5。CSS Modules不一样,它就是CSS,纯粹的血统,只是戴了个面具。

CSS Modules到底干了啥魔法

好,重点来了。CSS Modules到底干了啥?它的核心原理简单到让你想拍大腿:它给每个class名自动加哈希后缀,比如.title 变成 .title_3kLm9,彻底杜绝冲突。这个哈希是根据文件路径和内容算出来的,只要你不改文件位置和内容,这个哈希就是固定的。

重点:它不是新语言,就是webpack或Vite帮你偷偷重命名,你写的还是原汁原味CSS。这意味着什么?意味着你不需要学习新的语法!不需要记新的API!你还是会写display: flex,还是会写@media查询,所有CSS3的特性都原生支持。它只是在构建阶段做了一次"偷天换日",把你在文件A里写的.container改成.container__a1b2c,把文件B里的.container改成.container__d3e4f,这样它们就不会打架了。

/* Button.module.css - 你就正常写 */
.primary {
  background: #007bff;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s;
}

.primary:hover {
  background: #0056b3;
  transform: translateY(-2px);
}

.danger {
  composes: primary; /* 等下会讲这个神器 */
  background: #dc3545;
}

.danger:hover {
  background: #c82333;
}
// Button.jsx
import styles from './Button.module.css'; // 关键:文件名必须是.module.css

function Button({ variant = 'primary', children }) {
  // styles现在是一个对象:{ primary: 'primary_a3f4g', danger: 'danger_b5h6j' }
  return (
    <button className={styles[variant]}>
      {children}
    </button>
  );
}

看到没?你在JS里访问styles.primary,拿到的其实是编译后的类名primary_a3f4g(具体哈希值取决于你的配置)。webpack在构建时会把CSS里的.primary替换成.primary_a3f4g,同时JS里的引用也对应替换,天衣无缝。而且这是编译时的事,运行时没有任何额外开销,比起CSS-in-JS那种运行时计算,性能上就是F1赛车和共享单车的区别。

怎么在React/Vue里丝滑接入

我知道你们最烦配置,配个webpack能配到怀疑人生,各种问题"Module not found"、“Loader chain broke”,报错信息跟谜语人一样。好消息是,现在主流工具都帮你配好了,开箱即用。

React这边,如果你用的是create-react-app(虽然Dan Abramov现在更推荐Vite了,但CRA还活着),默认就支持,直接 import styles from './xxx.module.css'。看清楚后缀,必须是.module.css,少个module就是全局样式,这是最容易踩的坑之一。用Vite更简单,官方模板直接支持,连配置都不用看。

// React + Vite 实战示例
// components/UserCard/UserCard.jsx
import React from 'react';
import styles from './UserCard.module.css'; // 导入变成一个对象
import avatarImg from './avatar.png';

export function UserCard({ user }) {
  // 这里可以console.log(styles)看看里面长啥样
  console.log('样式对象:', styles); 
  // 输出大概是:{ card: 'card_ab3f8d', avatar: 'avatar_9c2e1a', name: 'name_7f4d2b' }
  
  return (
    <div className={styles.card}>
      <img 
        src={avatarImg} 
        alt={user.name} 
        className={styles.avatar} // 自动映射到唯一类名
      />
      <div className={styles.info}>
        <h3 className={styles.name}>{user.name}</h3>
        <p className={styles.bio}>{user.bio || '这个人很懒,什么都没写'}</p>
        <div className={styles.actions}>
          <button className={`${styles.btn} ${styles.btnPrimary}`}>
            关注
          </button>
          <button className={`${styles.btn} ${styles.btnGhost}`}>
            私信
          </button>
        </div>
      </div>
    </div>
  );
}
/* UserCard.module.css */
.card {
  display: flex;
  align-items: center;
  padding: 16px;
  background: #ffffff;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  transition: transform 0.2s, box-shadow 0.2s;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}

.avatar {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  object-fit: cover;
  border: 3px solid #f0f0f0;
  /* 看,伪类照样用 */
  transition: transform 0.3s;
}

.avatar:hover {
  transform: scale(1.1);
}

.info {
  margin-left: 16px;
  flex: 1;
}

.name {
  margin: 0 0 8px 0;
  font-size: 18px;
  font-weight: 600;
  color: #1a1a1a;
}

.bio {
  margin: 0;
  font-size: 14px;
  color: #666;
  line-height: 1.5;
  /* 多行省略号这种高级特性也完全OK */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.actions {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}

.btn {
  padding: 6px 16px;
  border: 1px solid transparent;
  border-radius: 6px;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.2s;
}

.btnPrimary {
  composes: btn; /* 继承btn的基础样式 */
  background: #007bff;
  color: white;
}

.btnPrimary:hover {
  background: #0056b3;
}

.btnGhost {
  composes: btn;
  background: transparent;
  border-color: #ddd;
  color: #666;
}

.btnGhost:hover {
  border-color: #007bff;
  color: #007bff;
  background: rgba(0,123,255,0.05);
}

Vue单文件组件里加个 <style module> 就行,不用配babel插件那些鬼东西。Vue的集成更丝滑,因为Vue的SFC(单文件组件)本来就是自闭环的。

<!-- Vue 3 + CSS Modules 示例 -->
<template>
  <div :class="$style.userCard">
    <img 
      :src="avatar" 
      :class="$style.avatar"
      @click="handleAvatarClick"
    />
    <div :class="$style.content">
      <h3 :class="[$style.title, isVip && $style.vipTitle]">
        {{ user.name }}
        <span v-if="isVip" :class="$style.vipBadge">VIP</span>
      </h3>
      <p :class="$style.description">{{ user.desc }}</p>
      
      <!-- 动态类名绑定 -->
      <div :class="getStatusClass(user.status)">
        状态: {{ user.status }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
  user: Object
});

const isVip = computed(() => props.user.level > 5);

const handleAvatarClick = () => {
  console.log('点击了头像,当前样式对象:', $style);
};

// 在setup里访问样式对象
const getStatusClass = (status) => {
  // 注意这里访问方式稍有不同,Vue会把module样式注入到$style对象
  const baseClass = $style.status;
  const statusMap = {
    online: $style.online,
    offline: $style.offline,
    busy: $style.busy
  };
  return [baseClass, statusMap[status] || $style.unknown].join(' ');
};
</script>

<style module>
/* 这里的所有class都会被自动转换 */
.userCard {
  display: flex;
  padding: 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 16px;
  color: white;
}

.avatar {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  border: 4px solid rgba(255,255,255,0.3);
  cursor: pointer;
  transition: all 0.3s ease;
}

.avatar:hover {
  transform: rotate(5deg) scale(1.1);
  border-color: white;
}

.content {
  margin-left: 20px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.title {
  margin: 0 0 8px 0;
  font-size: 24px;
  font-weight: bold;
}

.vipTitle {
  composes: title;
  color: #ffd700; /* 金色VIP */
  text-shadow: 0 2px 4px rgba(0,0,0,0.2);
}

.vipBadge {
  display: inline-block;
  margin-left: 8px;
  padding: 2px 8px;
  background: #ffd700;
  color: #333;
  font-size: 12px;
  border-radius: 4px;
  font-weight: bold;
}

.description {
  margin: 0;
  opacity: 0.9;
  line-height: 1.6;
}

.status {
  margin-top: 12px;
  padding: 4px 12px;
  border-radius: 20px;
  font-size: 12px;
  display: inline-block;
  width: fit-content;
}

.online {
  composes: status;
  background: rgba(0,255,0,0.2);
  color: #90ee90;
}

.offline {
  composes: status;
  background: rgba(128,128,128,0.3);
  color: #d3d3d3;
}

.busy {
  composes: status;
  background: rgba(255,0,0,0.2);
  color: #ff6b6b;
}

.unknown {
  composes: status;
  background: rgba(255,255,255,0.1);
}
</style>

Vue里用CSS Modules有个小技巧:如果你不想用$style.xxx这种略显啰嗦的写法,可以配置css.modules选项,让Vue直接把样式对象注入到style变量里,或者用computed封装一下。但说实话,$style挺好的,一眼就能看出这是局部样式,维护的时候特别清晰。

类名组合怎么玩才不翻车

好,现在你知道怎么基础使用了。但真实业务没那么简单,你总有一些公共样式想复用。比如所有按钮都有基础的内边距、圆角、过渡动画,然后不同的按钮只是改改颜色和背景。这时候composes就派上用场了。

想继承公共样式?用 composes: button from './common.module.css'。这个语法看起来有点像ES6的import,但它是CSS Modules特有的。它会把那个类的规则"合并"进来,而不是覆盖。

/* common.module.css - 公用样式库 */
.btnBase {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 8px 16px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s ease;
  /* 防止按钮被文字撑变形 */
  white-space: nowrap;
}

.iconWrapper {
  display: inline-flex;
  margin-right: 6px;
  width: 16px;
  height: 16px;
}

/* 一些工具类 */
.textEllipsis {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.flexCenter {
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Button.module.css */
.primary {
  /* 从common继承btnBase的所有样式 */
  composes: btnBase from './common.module.css';
  background-color: #1890ff;
  color: white;
  box-shadow: 0 2px 0 rgba(0,0,0,0.045);
}

.primary:hover {
  background-color: #40a9ff;
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(24,144,255,0.3);
}

.secondary {
  composes: btnBase from './common.module.css';
  background-color: white;
  color: #666;
  border: 1px solid #d9d9d9;
}

.secondary:hover {
  color: #1890ff;
  border-color: #1890ff;
  background-color: #e6f7ff;
}

/* 甚至可以在同一个文件内composes */
.withIcon {
  composes: primary; /* 继承上面的primary */
  padding-left: 12px; /* 微调一下,给图标留空间 */
}

.withIcon svg {
  margin-right: 6px;
  transition: transform 0.2s;
}

.withIcon:hover svg {
  transform: rotate(15deg);
}
// Button.jsx
import styles from './Button.module.css';

function Button({ children, variant = 'primary', icon }) {
  return (
    <button className={styles[variant]}>
      {icon && <span className={styles.iconWrapper}>{icon}</span>}
      <span className={styles.textEllipsis}>{children}</span>
    </button>
  );
}

// 使用示例
<Button variant="primary">主要按钮</Button>
<Button variant="secondary">次要按钮</Button>
<Button variant="withIcon" icon={<IconRocket />}>带图标的</Button>

但别滥用!compose太多层,调试时你会在DevTools里迷失自我。我曾经见过一个项目,.finalButton composes了.mediumButton,后者又composes了.baseButton.baseButton又从外部引入.uiKitButton,最终生成的class字符串长得像火车一样:uiKitBase_f3a2 medium_d9c1 final_a7e8。在浏览器里审查元素的时候,你完全不知道哪个类名对应哪条规则,排查样式问题的时候恨不得把电脑扔出窗外。

我建议composes最多嵌套三层,而且尽量在同一个文件内解决。如果真的需要很多层继承,那可能是你的组件拆分粒度有问题,考虑用JS来组合样式而不是CSS的composes。

动态类名还能不能用了

当然能!这是我问得最多的问题。很多人以为CSS Modules就是把类名锁死了,不能动态切换,其实完全不是。你完全可以像之前一样动态操作className,只是方式稍微有点不同。

styles['active'] 或者用模板字符串 styles[${status}Btn]。但要注意,因为最终生成的类名是哈希后的,所以你拼字符串的时候得基于原始的名字拼,不是基于哈希后的名字。

// 动态类名实战 - Tab切换组件
import React, { useState } from 'react';
import styles from './Tabs.module.css';

function Tabs({ tabs }) {
  const [activeKey, setActiveKey] = useState(tabs[0]?.key);
  
  // 方法1:直接对象访问
  const getTabClass = (key) => {
    // 如果key是'home',访问styles.home
    // 如果是active的,还要加上styles.active
    const baseClass = styles[key]; // 注意:key必须符合CSS类名命名规范
    const activeClass = activeKey === key ? styles.active : '';
    
    return `${baseClass} ${activeClass}`.trim();
  };
  
  // 方法2:用数组join,更clean
  const getTabClassV2 = (key) => {
    return [
      styles.tabItem,          // 基础样式
      styles[key],             // 特定tab的样式(如果有)
      activeKey === key && styles.active  // 条件样式
    ].filter(Boolean).join(' ');
  };

  return (
    <div className={styles.container}>
      <div className={styles.tabList}>
        {tabs.map(tab => (
          <button
            key={tab.key}
            className={getTabClassV2(tab.key)}
            onClick={() => setActiveKey(tab.key)}
            data-active={activeKey === tab.key}
          >
            {tab.icon && <span className={styles.icon}>{tab.icon}</span>}
            <span>{tab.label}</span>
            {/* 小红点提示 */}
            {tab.badge && <span className={styles.badge}>{tab.badge}</span>}
          </button>
        ))}
      </div>
      
      <div className={styles.content}>
        {tabs.find(t => t.key === activeKey)?.content}
      </div>
    </div>
  );
}
/* Tabs.module.css */
.container {
  background: white;
  border-radius: 8px;
  overflow: hidden;
}

.tabList {
  display: flex;
  border-bottom: 1px solid #e8e8e8;
  background: #fafafa;
}

.tabItem {
  position: relative;
  padding: 12px 24px;
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 14px;
  color: #666;
  transition: all 0.3s;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px; /* 盖住border */
}

.tabItem:hover {
  color: #1890ff;
  background: rgba(0,0,0,0.02);
}

/* 激活状态 */
.active {
  color: #1890ff;
  font-weight: 500;
  border-bottom-color: #1890ff;
  background: white;
}

/* 特定tab的特殊样式 */
.home {
  /* home tab特有的样式 */
  color: #ff6b6b;
}

.settings {
  /* settings tab的样式 */
}

.badge {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 6px;
  height: 6px;
  background: #ff4d4f;
  border-radius: 50%;
  animation: pulse 2s infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

.content {
  padding: 20px;
  min-height: 300px;
}

但别拼错字,拼错了不报错,只是样式消失——静默失败最坑人。如果你写styles.actvie(拼写错误),JS不会报错,因为访问对象不存在的属性只是返回undefined,最后你的className就是undefined,元素就没类名了,样式不生效,但你可能半天找不到原因。所以我强烈建议用TypeScript配合CSS Modules,有类型检查,拼错字直接红波浪线报错。

// 用TypeScript的话,可以声明模块类型
// typings/css-modules.d.ts
declare module '*.module.css' {
  const classes: { [key: string]: string };
  export default classes;
}

// 或者更严格的,用typed-css-modules工具生成类型定义
// Button.module.css.d.ts
export const primary: string;
export const secondary: string;
export const large: string;
// ...

和Tailwind、UnoCSS这些原子化方案打架吗

其实不冲突!这是另一个高频问题。现在满大街都在吹Tailwind,“不用离开HTML写样式”,“Bundle Size极小”,吹得神乎其神。然后你们团队已经上了CSS Modules,就纠结了:要不要迁移?能不能混用?会不会重复?

CSS Modules管结构隔离,Tailwind管快速布局。Tailwind的优势是原子化,写布局很快,flex items-center justify-between p-4一行搞定,不用写CSS文件。但Tailwind的问题也是这个——当你的组件有复杂的交互状态(hover、focus、disabled的各种组合),或者需要特定的动画关键帧,那一长串的className依然很难维护,而且全堆在HTML里,看着头疼。

我现在的项目就是俩一起用,一个负责“不污染”,一个负责“快如闪电”。具体怎么混?我的策略是:

  1. 布局用Tailwind:grid、flex、padding、margin这些,改起来快,视觉还原的时候调数值方便
  2. 组件具体样式用CSS Modules:特定的颜色主题、复杂的伪类交互、动画关键帧,这些放在.module.css里
// 混用实战 - Card组件
import styles from './Card.module.css';

function Card({ title, content, footer, variant = 'default' }) {
  return (
    <div className={`
      ${styles.card} 
      ${styles[variant]}
      rounded-xl shadow-lg overflow-hidden
      transform transition-all duration-300 hover:scale-[1.02]
    `}>
      {/* 头部:Tailwind管布局,CSS Modules管颜色主题 */}
      <div className="p-6 border-b border-gray-100">
        <h3 className={`
          text-xl font-bold mb-2 
          ${styles.title}
        `}>
          {title}
        </h3>
        {variant === 'premium' && (
          <span className="inline-block px-2 py-1 text-xs font-semibold bg-yellow-100 text-yellow-800 rounded">
            高级版
          </span>
        )}
      </div>
      
      {/* 内容区 */}
      <div className={`
        p-6 
        ${styles.content}
        prose prose-slate max-w-none
      `}>
        {content}
      </div>
      
      {/* 底部 */}
      {footer && (
        <div className={`
          px-6 py-4 bg-gray-50 
          flex items-center justify-between
          ${styles.footer}
        `}>
          {footer}
        </div>
      )}
    </div>
  );
}
/* Card.module.css - 只管自家事 */
.card {
  /* 结构性的、高度定制的样式 */
  position: relative;
}

.card::before {
  /* 装饰性元素 */
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 4px;
  background: linear-gradient(90deg, #1890ff, #36cfc9);
  opacity: 0;
  transition: opacity 0.3s;
}

.card:hover::before {
  opacity: 1;
}

/* 不同变体的主题色 */
.default {
  composes: card;
  --card-accent: #1890ff;
}

.premium {
  composes: card;
  --card-accent: #faad14;
  border: 1px solid #faad14;
}

.danger {
  composes: card;
  --card-accent: #ff4d4f;
}

.title {
  color: var(--card-accent, #333);
}

.content {
  line-height: 1.8;
}

/* 复杂动画,Tailwind写起来太累 */
.footer {
  animation: slideUp 0.4s ease-out;
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

这种混用模式,既享受了Tailwind的快速布局能力,又保留了CSS Modules对复杂样式的控制力,而且完全没有样式污染的问题,因为即使是Tailwind的utility class,也是写在组件里的,不会影响到其他组件。

开发时怎么快速看生成的真实类名

打开浏览器DevTools,你会发现元素class长这样:header_title__2xKlM。别慌,这是正常现象,而且Vite/webpack dev模式下类名还带原始名,方便调试。

其实CSS Modules的命名规则是可配置的。默认是[name]_[local]__[hash:base64:5],也就是文件名_原类名__哈希值。所以在开发环境,你一眼就能看出这是Header组件里的title类。生产环境可以配置成更短的哈希,节省字节。

// webpack配置示例(如果你需要自定义)
{
  loader: 'css-loader',
  options: {
    modules: {
      // 开发环境:可读性好
      localIdentName: '[path][name]__[local]--[hash:base64:5]',
      
      // 生产环境:极致压缩
      // localIdentName: '[hash:base64:6]',
      
      // 或者用函数动态决定
      getLocalIdent: (context, localIdentName, localName, options) => {
        // 可以在这里根据文件名做特殊处理
        return `myapp_${localName}_${hash}`;
      }
    }
  }
}
// vite.config.js配置
export default {
  css: {
    modules: {
      localsConvention: 'camelCaseOnly', // 把kebab-case自动转camelCase
      generateScopedName: (name, filename, css) => {
        // 自定义生成规则
        const file = filename.split('/').pop().split('.')[0];
        return `${file}_${name}_${hash}`;
      }
    }
  }
}

有个调试小技巧:如果你在控制台console.log(styles),看到的是编译后的映射对象,但如果你把source map打开,在Chrome DevTools的Sources面板里,你能直接看到原始的CSS文件,点击类名还能直接跳转到源代码位置,和写普通CSS体验完全一样。

性能有影响吗?打包体积会爆炸?

放心,只是多了点哈希字符,gzip一压几乎没差别。比起你手动加前缀.my-component-btn,它更省事还更安全。

咱们算笔账:假设你一个项目有100个组件,每个组件平均10个类名。手动BEM命名可能是.header-nav__button--primary,大概35个字符。CSS Modules生成的是.header_nav__btn__a3f4g,大概25个字符。实际上CSS Modules生成的更短!而且重复率更低,压缩效果更好。

真正该担心的是CSS-in-JS,那些库通常需要运行时插入style标签,还有解析样式的开销。CSS Modules是零运行时,所有工作在构建时完成,产物就是纯CSS文件,浏览器原生解析,速度飞快。

# 看看你的打包产物
npm run build

# 检查CSS文件大小
ls -lh dist/assets/*.css

# 用gzip看看实际传输大小
gzip -9 -c dist/assets/index.css | wc -c

比起性能,更应该操心的是source map有没有配置好,开发体验是否流畅。CSS Modules在这方面无可挑剔。

遇到样式没生效?先查这三件事

我整理了一份血泪史,这三个坑我每个都踩过三遍以上。

第一件事:文件后缀是不是.module.css?漏了module就变全局了。这是最离谱但又最常见的错误。有时候你从网上拷贝代码,sublime text或者vscode没显示文件后缀,你以为它是.module.css,其实它是.css。然后样式就全局污染了,改一个按钮,全站按钮都变,排查半天发现是文件后缀问题。所以建议在编辑器里开显示文件后缀,或者配置eslint规则检查。

第二件事:import有没有解构对?styles.title别写成style.title。有的新手(包括我多年前)会写:

import style from './Button.module.css'; // 单数

function Button() {
  return <button className={styles.primary}> // 这里用了复数styles!
}

JavaScript不会报错,因为styles是undefined,styles.primary就报错了。但有时你配置了全局styles变量,或者用了某个库的命名空间也叫styles,结果就静默失败了。建议是启用ESLint的no-undef规则,或者用TypeScript。

第三件事:动态类名是不是undefined?console.log(styles)看一眼保平安。特别是动态类名:

// 错误示范
function Button({ type }) {
  // 如果type传了'submit',但styles里没有.submit,结果就是undefined
  return <button className={styles[type]}>提交</button>;
}

// 安全写法
function Button({ type = 'default' }) {
  const className = styles[type] || styles.default; // 给个fallback
  console.log('当前样式:', type, styles); // 调试时看一眼
  return <button className={className}>提交</button>;
}

还有个小众但致命的坑:composes的时候路径写错了。composes: btn from './common.css'(漏了.module),结果common.css里的全局样式被引入了,然后瞬间污染全局。记住,composes也只能用.module.css文件。

最后唠句实在的:别等项目烂到没法维护才用

早点上CSS Modules,团队协作时少吵八百架,晚上睡觉都踏实。我见过太多项目,前期图快,所有人往一个global.css里扔代码,三个月后这个文件上万行,谁都不敢删里面的样式,因为不知道哪里在用。然后就开始!important战争,你的优先级1024,我的就是1025,最后演变成行内样式大战。

CSS Modules就是给代码加了把锁,你的样式就是你的,谁也抢不走,谁也污染不了。新人入职不用背那么多命名规范,大胆写.title.container就行,反正会自动哈希。Code Review的时候不用纠结"这个类名会不会和别人的重复",只看逻辑对不对。

毕竟,谁不想写CSS的时候像在自家后院种花,而不是在雷区蹦迪呢。你可以悠闲地喝着咖啡,慢慢调整那个按钮的圆角,不用担心改完之后其他页面的按钮变成了异形。那种安全感,那种从容,是全局CSS给不了你的。

赶紧在你的下一个组件里试试CSS Modules吧,就现在,把那个.css改成.module.css,然后享受那种"终于Cleanup了"的快感。等你用习惯了,回头看那些还在用BEM写.header__nav-list-item--active的代码,你会露出慈(xian)祥(qi)的微笑。

这就是CSS Modules,一个在组件化时代让你重新爱上写CSS的小工具。不花哨,但足够靠谱。

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐