• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

每个组件里有 js 逻辑和 css 样式。

js 逻辑是通过 es module 做了模块化的,但是 css 并没有。

所以不同组件样式都在全局,很容易冲突。

那 css 如何也实现像 js 类似的模块机制呢?

最容易想到的是通过命名空间来区分。

比如 aaa 下面的 bbb 下的 button,就可以加一个 aaabbbtn 的 class。

而 ccc 下的 button,就可以加一个 ccc__btn 的 class。

常用的 BEM 命名规范就是解决这个问题的。

BEM 是 block、element、modifier 这三部分:

  • 块(Block):块是一个独立的实体,代表一个可重用的组件或模块。

块的类名应该使用单词或短语,并使用连字符(-)作为分隔符。例如:.header、.left-menu。

  • 元素(Element):元素是块的组成部分,不能独立存在。

元素的类名应该使用双下划线()作为分隔符,连接到块的类名后面。例如:.left-menuitem、.header__logo。

  • 修饰符(Modifier):修饰符用于描述块或元素的不同状态或变体,用来更改外观或行为。

修饰符的类名应该使用双连字符(--)作为分隔符,连接到块或元素的类名后面。例如:.left-menuitem--active、.headerlogo--small。

但是,BEM 规范毕竟要靠人为来约束,不能保证绝对不会冲突。

所以最好是通过工具来做模块化,比如 CSS Modules。

我们先用一下 css modules 再介绍。

npx create-vite

用 vite 创建个 react 项目。

进入项目,安装依赖,把开发服务跑起来:

npm install
npm run dev

添加两个组件 Button1、Button2

Button1.tsx

import "./Button1.css";

export default function () {
    return (
        <div className="btn-wrapper">
            <button className="btn">button1</button>
        </div>
    );
}

Button1.css

.btn-wrapper {
    padding: 20px;
}

.btn {
    background: blue;
}

Button2.tsx

import "./Button2.css";

export default function () {
    return (
        <div className="btn-wrapper">
            <button className="btn">button2</button>
        </div>
    );
}

Button2.css

.btn-wrapper {
    padding: 10px;
}

.btn {
    background: green;
}

在 App.tsx 引入下:

渲染出来是这样的:

很明显,是样式冲突了:

这时候可以改下名字,把 Button1.css 该为 Button1.module.css

并且改下写 className 的方式。

import styles from "./Button1.module.css";

export default function () {
    return (
        <div className={styles["btn-wrapper"]}>
            <button className={styles.btn}>button1</button>
        </div>
    );
}

在浏览器看下:

现在就不会样式冲突了。

为什么呢?

可以看到,button1 的 className 变成了带 hash 的形式,全局唯一的,自然就不会冲突了。

这就是 css modules。

那它是怎么实现的呢?

看下编译后的代码就明白了:

它通过编译给 className 加上了 hash,然后导出了这个唯一的 className。

所以在对象里用的,就是编译后的 className:

在 vscode 里安装 css modules 插件:

就可以提示出 css 模块下的 className 了:

其实 vue 里也有类似的机制,叫做 scoped css

比如:

<style scoped>
    .guang {
        color: red;
    }
</style>
<template>
    <div class="guang">hi</div>
</template>

会被编译成:

<style>
    .guang[data-v-f3f3eg9] {
        color: red;
    }
</style>
<template>
    <div class="guang" data-v-f3f3eg9>hi</div>
</template>

通过给 css 添加一个全局唯一的属性选择器来限制 css 只能在这个范围生效,也就是 scoped 的意思。

它和 css modules 还不大一样,css modules 是整个 clasName 都变了,所以要把 className 改成从 css modules 导入的方式:

而 scoped css 这种并不需要修改 css 代码,只是编译后会加一个选择器

两者的使用体验有一些差别。

当然,在 vue 里可以选择 scoped css 或者 css modules,而在 react 里就只能用 css modules 了。

css modules 是通过 postcss-modules 这个包实现的,vite 也对它做了集成。

我们可以在 vite.config.ts 里修改下 css modules 的配置:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [react()],
    css: {
        modules: {
            generateScopedName: "guang_[name]__[local]___[hash:base64:5]",
        },
    },
});

比如通过 generateScopedName 来修改生成的 className 的格式:

generateScopedName 也可以是个函数,自己处理:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [react()],
    css: {
        modules: {
            // generateScopedName: "guang_[name]__[local]___[hash:base64:5]"
            generateScopedName: function (name, filename, css) {
                console.log(name, filename, css);

                return "xxx";
            },
        },
    },
});

传入了 className、filename 还有 css 文件的内容:

你可以通过 getJSON 来拿到编译后的 className:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [react()],
    css: {
        modules: {
            getJSON: function (cssFileName, json, outputFileName) {
                console.log(cssFileName, json, outputFileName);
            },
        },
    },
});

第二个参数就是 css 模块导出的对象:

那如果在 Button1.module.css 里想把 .btn-wrapper 作为全局样式呢?

这样写:

可以看到,现在编译后的 css 里就没有对 .btn-wrapper 做处理了:

只不过,因为 global 的 className 默认不导出,而我们用 styles.xxx 引入的:

所以 className 为空:

这时候,或者把 className 改为这样:

或者在配置里加一个 exportsGlobals:true

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [react()],
    css: {
        modules: {
            getJSON: function (cssFileName, json, outputFileName) {
                console.log(cssFileName, json, outputFileName);
            },
            exportGlobals: true,
        },
    },
});

可以看到,现在 global 样式也导出了:

相对的,模块化的 className 就用 :local() 来声明:

默认是 local。

如果你想默认 global,那也可以配置:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [react()],
    css: {
        modules: {
            getJSON: function (cssFileName, json, outputFileName) {
                console.log(cssFileName, json, outputFileName);
            },
            exportGlobals: true,
            scopeBehaviour: "global",
        },
    },
});

可以看到,现在就正好反过来了:

默认是 global,如果是 local 的要单独用 :local() 声明。

你还可以通过正则表达式来匹配哪些 css 文件是默认全局:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [react()],
    css: {
        modules: {
            getJSON: function (cssFileName, json, outputFileName) {
                console.log(cssFileName, json, outputFileName);
            },
            exportGlobals: true,
            globalModulePaths: [/Button1/],
        },
    },
});

还有一个配置比较常用,就是 localsConvention:

当 localsConvention 改为 camelCase 的时候,导出对象的 key 会变成驼峰的:

那在组件里就可以这样写:

这些就是 css modules 相关的配置了。

此外,还有一个地方需要注意,就是多层的 className 的时候:

.btn-wrapper {
    padding: 20px;
}

.btn .xxx{
    background: blue;
}

每一层的 className 都会编译:

有时候只要最外层 className 变了就好了,内层不用变,就可以用 :global() 声明下:

.btn-wrapper {
    padding: 20px;
}

.btn :global(.xxx) {
    background: blue;
}

用 scss 之类的预处理时也是一样。

用 :global 包裹一层,内层的 className 不会被编译:

.btn {
    :global {
        .xxx {
            background: blue;
            .yyy {
                color: #000;
            }
        }
    }
}

在 vite 里用 css modules 是这么用,在 cra 里也是一样。

创建个 cra 的项目:

npx create-react-app --template=typescript css-modules-cra

把服务跑起来:

npm run start

把 App.css 改为 App.module.css

在 App.tsx 引入下:

这样就开启了 css modules:

用法是一样的。

实现 css modules 也是用的 postcss-modules 这个 postcss 插件。

只不过是用 webpack 的 css-loader 封装了一层。

我们把本地代码保存:

git init
git add .
git commit -m 'init'

然后把 webpack 配置放出来:

npm run eject

项目下会多一个 config 目录这下面就是 webpack 配置:

改一下配置:

modules: {
  mode: 'local',
  // getLocalIdent: getCSSModuleLocalIdent,
  localIdentName: "guang__[path][name]__[local]--[hash:base64:5]"
},

重新跑开发服务:

npm run start

现在的 className 就变了:

更多配置可以看 css-loader 的文档

和 vite 的 css modules 配置都差不多,虽然配置项名字不一样。

总结

不同组件的 className 可能会一样,导致样式冲突。

为此,我们希望 css 能实现像 js 的 es module 一样的模块化功能。

可以用 BEM 的命名规范来避免冲突,但是这需要人为保证,不够可靠。

一般都是用编译的方式,比如 CSS Modules 或者 vue 的 Scoped CSS。

它是通过 postcss-modules 实现的,可以把 css 的 className 编译成带 hash 的形式。

然后在组件里用 styles.xxx 的方式引入。

在 vite、cra 里都对 css modules 做了支持,只要用 xx.module.css、xxx.module.scss 等结尾,就默认开启了 css modules。

还可以通过各种配置来做更多定制:

  • scopeBehaviour: 默认 local 或者 global
  • getJSON:可以拿到 css 模块导出的对象
  • exportGlobals: 全局的 className 也导出到对象
  • globalModulePaths:哪些文件路径默认是全局 className
  • generateScopedName:定制 local className 的格式
  • localsConvention: 导出的对象的 key 的格式

在 webpack 的 css-loader 里也有类似的配置。

现在的组件开发基本都有模块化的要求,所以 CSS Modules 在日常开发中用的特别多。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
26.快速掌握Tailwind:最流行的原子化CSS框架
Next
28.CSSInJS:快速掌握styled-components