React 中 CSS Modules 详解
1. 什么是 React 中的 CSS Modules
CSS Modules 是一种用于解决 CSS 命名冲突问题的模块化方案,它允许在 React 组件中使用局部作用域的 CSS 类名。在 CSS Modules 中,所有的类名默认都是局部作用域的,这意味着它们只在导入该 CSS 文件的组件中有效,不会影响其他组件。
CSS Modules 不是 React 特有的功能,它是一种通用的 CSS 模块化解决方案,可以与任何支持 ES Modules 的构建工具(如 Webpack、Rollup 等)一起使用。在 React 生态系统中,CSS Modules 是一种非常流行的样式管理方式。
2. 为什么在 React 中使用 CSS Modules
在 React 中使用 CSS Modules 主要是为了解决传统 CSS 的一些问题:
2.1 解决类名冲突问题
传统的 CSS 是全局作用域的,这意味着当多个组件使用相同的类名时,会发生样式冲突。
传统 CSS 的问题示例:
css
/* Button.css */
.button {
background-color: blue;
color: white;
padding: 8px 16px;
}
/* PrimaryButton.css */
.button {
background-color: red;
color: white;
padding: 8px 16px;
}
jsx
// Button.js
import React from 'react';
import './Button.css';
const Button = () => {
return <button className="button">普通按钮</button>;
};
export default Button;
// PrimaryButton.js
import React from 'react';
import './PrimaryButton.css';
const PrimaryButton = () => {
return <button className="button">主要按钮</button>;
};
export default PrimaryButton;
// App.js
import React from 'react';
import Button from './Button';
import PrimaryButton from './PrimaryButton';
const App = () => {
return (
<div>
<Button /> {/* 这里的按钮样式会被 PrimaryButton.css 覆盖 */}
<PrimaryButton />
</div>
);
};
export default App;
在上面的示例中,由于两个组件都使用了 .button 类名,后导入的 PrimaryButton.css 会覆盖 Button.css 中的样式,导致两个按钮都显示为红色。
2.2 避免样式污染
使用 CSS Modules 可以确保组件的样式不会影响其他组件,避免样式污染。
2.3 提高代码可维护性
CSS Modules 使得样式与组件紧密耦合,提高了代码的可维护性。当需要修改某个组件的样式时,只需要修改对应的 CSS 文件即可,不需要担心影响其他组件。
3. React 中 CSS Modules 的基本使用方法
3.1 创建 CSS Modules 文件
CSS Modules 文件的命名方式通常是 .module.css,例如 Button.module.css。
3.2 导入和使用 CSS Modules
在 React 组件中,可以使用 ES Modules 的方式导入 CSS Modules 文件,然后通过对象的方式访问其中的类名。
基本使用示例:
css
/* Button.module.css */
.button {
background-color: blue;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.primary {
background-color: red;
}
jsx
// Button.js
import React from 'react';
import styles from './Button.module.css';
const Button = ({ primary, children }) => {
const buttonClass = primary ? `${styles.button} ${styles.primary}` : styles.button;
return <button className={buttonClass}>{children}</button>;
};
export default Button;
// App.js
import React from 'react';
import Button from './Button';
const App = () => {
return (
<div>
<Button>普通按钮</Button>
<Button primary>主要按钮</Button>
</div>
);
};
export default App;
在上面的示例中,CSS Modules 会自动将类名转换为唯一的标识符,例如 .button 可能会被转换为 .Button_button__1a2b3c,这样就避免了类名冲突。
4. CSS Modules 的原理
CSS Modules 的工作原理主要包括以下几个步骤:
- 解析 CSS 文件 :构建工具(如 Webpack)的 CSS Modules 插件会解析
.module.css文件。 - 生成唯一类名:对于每个类名,插件会生成一个唯一的哈希值,通常是基于文件名、类名和内容生成的。
- 创建映射表:插件会创建一个从原始类名到生成的唯一类名的映射表。
- 替换类名:将 CSS 文件中的原始类名替换为生成的唯一类名。
- 导出映射表:将映射表导出为一个 JavaScript 对象,供组件使用。
原理示例:
css
/* Button.module.css */
.button {
background-color: blue;
color: white;
}
.primary {
background-color: red;
}
经过 CSS Modules 处理后,会生成类似以下的 CSS:
css
.Button_button__1a2b3c {
background-color: blue;
color: white;
}
.Button_primary__4d5e6f {
background-color: red;
}
同时会生成一个 JavaScript 对象:
javascript
{
button: 'Button_button__1a2b3c',
primary: 'Button_primary__4d5e6f'
}
组件可以通过导入这个对象来使用生成的唯一类名。
5. VSCode 中 CSS Modules 插件的介绍与使用
在 VSCode 中,有几个常用的 CSS Modules 插件可以提高开发效率:
5.1 CSS Modules
- 插件名称:CSS Modules
- 功能:提供 CSS Modules 类名的自动补全、悬停提示和跳转到定义等功能。
- 安装方法:在 VSCode 扩展商店中搜索 "CSS Modules" 并安装。
- 使用方法 :
- 安装插件后,在 JavaScript/TypeScript 文件中导入 CSS Modules 文件时,插件会自动识别。
- 当输入
styles.时,插件会显示可用的类名列表。 - 悬停在类名上时,会显示对应的 CSS 样式。
- 按住 Ctrl/Cmd 键并点击类名,可以跳转到 CSS 文件中的定义。
5.2 CSS Modules Syntax Highlighter
- 插件名称:CSS Modules Syntax Highlighter
- 功能:为 CSS Modules 文件提供语法高亮支持。
- 安装方法:在 VSCode 扩展商店中搜索 "CSS Modules Syntax Highlighter" 并安装。
6. 如何自定义打包之后的 className
在 CSS Modules 中,可以通过 generateScopedName 选项来自定义打包之后的类名格式。
6.1 在 Webpack 中配置 generateScopedName
javascript
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
generateScopedName: '[name]__[local]__[hash:base64:5]'
}
}
}
]
}
]
}
// ...
};
6.2 generateScopedName 选项说明
[name]:文件名[local]:原始类名[hash:base64:5]:基于内容生成的 5 位 base64 哈希值
自定义类名示例:
javascript
// 自定义格式
generateScopedName: 'app-[local]'
// 生成的类名:app-button
// 更复杂的格式
generateScopedName: '[name]_[local]_[hash:hex:4]'
// 生成的类名:Button_button_a1b2
7. CSS Modules 中全局变量和局部变量的设置
CSS Modules 支持全局变量和局部变量的设置,可以通过多种方式实现。
7.1 使用 :global() 和 :local() 伪类
:local():定义局部作用域的类名(默认):global():定义全局作用域的类名
示例代码:
css
/* 局部作用域(默认) */
.button {
background-color: blue;
}
/* 显式定义局部作用域 */
:local(.primary) {
background-color: red;
}
/* 全局作用域 */
:global(.global-button) {
background-color: green;
}
/* 嵌套使用 */
:local(.container) {
padding: 16px;
/* 容器内的全局类 */
:global(.title) {
font-size: 24px;
font-weight: bold;
}
}
jsx
// 使用全局类
div className={styles.container}>
<h1 className="title">标题</h1> {/* 全局类直接使用 */}
<button className={styles.button}>局部按钮</button>
<button className="global-button">全局按钮</button>
</div>
7.2 exportsGlobals 配置
exportsGlobals 选项用于指定哪些类名应该被导出为全局变量。 这样全局css变量也可以这样使用 {styles.globalButton}(之前只能这样className="global-button")
javascript
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
exportsGlobals: true
}
}
}
]
}
]
}
// ...
};
7.3 scopeBehaviour 配置
scopeBehaviour 选项用于指定 CSS Modules 的作用域行为,有两个值:
'local':默认值,所有类名都是局部作用域的'global':所有类名都是全局作用域的,除非使用:local()显式定义
javascript
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
// (默认值为 local;就是默认情况下css写的样式都是局部作用域的)
// 改成 global 之后:默认情况css写的样式都是全局作用域的
scopeBehaviour: 'global'
}
}
}
]
}
]
}
// ...
};
7.4 通过正则表达式匹配哪些 CSS 文件是默认全局的
默认情况下,所有启用了 CSS Modules 的 .module.css 文件中的样式都采用局部作用域 。但在实际项目中,我们可能希望某些特定的 CSS Modules 文件默认采用全局作用域。
例如:假设项目中所有 .module.css 文件都默认是局部作用域,但我们希望所有文件名中包含 Button1 的 CSS Modules 文件默认采用全局作用域。
我们可以通过 css-loader 的 globalModulePaths 配置来实现这一需求(注意:在 Webpack 中,exportGlobals 是一个独立的选项,用于导出全局选择器,而不是用于控制文件级别的作用域):
javascript
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.module\.css$/,
use: [
// 开发环境使用 style-loader,生产环境建议使用 MiniCssExtractPlugin
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
// 指定哪些 CSS Modules 文件默认使用全局作用域
// 正则表达式匹配文件路径,这里匹配所有包含 "Button1" 的文件
globalModulePaths: [
/Button1/ // 匹配文件名或路径中包含 "Button1" 的所有文件
]
}
}
}
]
}
]
}
// ...
};
说明:
globalModulePaths: [/Button1/]:在 Webpack 的css-loader配置中,此选项用于指定哪些 CSS Modules 文件默认使用全局作用域- 使用正则表达式
/Button1/匹配所有文件名或路径中包含 "Button1" 的 CSS Modules 文件 - 被匹配到的文件(如
Button1.module.css、MyButton1Styles.module.css等)将默认采用全局作用域 - 未被匹配到的文件仍保持默认的局部作用域
- 注意:
exportGlobals是一个独立选项,用于导出全局选择器(如:global(.global-class)),而不是控制文件级别的作用域
这种配置方式可以让我们在保持大部分 CSS Modules 文件为局部作用域的同时,灵活地将特定文件设置为全局作用域,无需在每个文件中使用 :global() 语法。
8. 其他配置的介绍与使用
8.1 localsConvention 配置
localsConvention 选项用于指定如何转换导出的类名的键名。
可用选项:
'asIs':保持原始类名'camelCase':转换为驼峰式命名'camelCaseOnly':仅转换为驼峰式命名,不保留原始类名'dashes':转换为短横线分隔命名'dashesOnly':仅转换为短横线分隔命名,不保留原始类名
javascript
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localsConvention: 'camelCase'
}
}
}
]
}
]
}
// ...
};
示例:
假设我们有以下 CSS 文件:
css
/* Button.module.css */
.button-primary {
background-color: red;
}
/* 注意:下面的例子会用到这个驼峰式命名的类 */
.mainButton {
background-color: blue;
}
1. localsConvention: 'asIs'
javascript
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localsConvention: 'asIs'
}
}
}
]
}
]
}
// ...
};
jsx
// React 组件中的使用
import styles from './Button.module.css';
// 保持原始类名,使用短横线命名
<button className={styles['button-primary']}>主要按钮</button>
// 驼峰式命名的类保持不变
<button className={styles.mainButton}>主要按钮</button>
2. localsConvention: 'camelCase'
javascript
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localsConvention: 'camelCase'
}
}
}
]
}
]
}
// ...
};
jsx
// React 组件中的使用
import styles from './Button.module.css';
// 可以使用转换后的驼峰式命名
<button className={styles.buttonPrimary}>主要按钮</button>
// 也可以使用原始的短横线命名
<button className={styles['button-primary']}>主要按钮</button>
// 驼峰式命名的类保持不变
<button className={styles.mainButton}>主要按钮</button>
3. localsConvention: 'camelCaseOnly'
javascript
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localsConvention: 'camelCaseOnly'
}
}
}
]
}
]
}
// ...
};
jsx
// React 组件中的使用
import styles from './Button.module.css';
// 只能使用转换后的驼峰式命名
<button className={styles.buttonPrimary}>主要按钮</button>
// 原始的短横线命名不再可用
// <button className={styles['button-primary']}>主要按钮</button> // 会报错
// 驼峰式命名的类保持不变
<button className={styles.mainButton}>主要按钮</button>
4. localsConvention: 'dashes'
javascript
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localsConvention: 'dashes'
}
}
}
]
}
]
}
// ...
};
jsx
// React 组件中的使用
import styles from './Button.module.css';
// 短横线命名的类保持不变
<button className={styles['button-primary']}>主要按钮</button>
// 驼峰式命名的类会被转换为短横线命名,同时保留原始命名
<button className={styles['main-button']}>主要按钮</button>
// 原始的驼峰式命名仍然可用
<button className={styles.mainButton}>主要按钮</button>
5. localsConvention: 'dashesOnly'
javascript
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localsConvention: 'dashesOnly'
}
}
}
]
}
]
}
// ...
};
jsx
// React 组件中的使用
import styles from './Button.module.css';
// 短横线命名的类保持不变
<button className={styles['button-primary']}>主要按钮</button>
// 驼峰式命名的类会被转换为短横线命名,原始命名不再可用
<button className={styles['main-button']}>主要按钮</button>
// 原始的驼峰式命名不再可用
// <button className={styles.mainButton}>主要按钮</button> // 会报错
8.2 处理多层 className
在 CSS Modules 中,可以使用嵌套语法和 :global() 伪类来处理多层 className。
示例:
css
/* Button.module.css */
.button {
background-color: blue;
color: white;
padding: 8px 16px;
/* 嵌套的局部类 */
&:hover {
background-color: darkblue;
}
/* 嵌套的全局类 */
&:global(.large) {
padding: 12px 24px;
}
/* 嵌套的组合类 */
&.primary {
background-color: red;
}
}
jsx
// 使用多层 className
import styles from './Button.module.css';
// 组合使用
<button className={`${styles.button} large`}>大按钮</button>
<button className={`${styles.button} ${styles.primary}`}>主要按钮</button>
9. Vue 中关于 CSS 的处理方式
在 Vue 中,处理 CSS 的方式与 React 有所不同。Vue 提供了内置的 CSS 作用域机制,不需要额外的构建工具配置。
9.1 Vue 中的 scoped CSS
Vue 中的 scoped 属性可以将样式限制在当前组件的作用域内,类似于 CSS Modules。
Vue 中 scoped CSS 的示例:
vue
<!-- Button.vue -->
<template>
<button class="button">
<slot></slot>
</button>
</template>
<style scoped>
.button {
background-color: blue;
color: white;
padding: 8px 16px;
}
.button:hover {
background-color: darkblue;
}
</style>
<!-- PrimaryButton.vue -->
<template>
<button class="button">
<slot></slot>
</button>
</template>
<style scoped>
.button {
background-color: red;
color: white;
padding: 8px 16px;
}
.button:hover {
background-color: darkred;
}
</style>
<!-- App.vue -->
<template>
<div>
<Button>普通按钮</Button>
<PrimaryButton>主要按钮</PrimaryButton>
</div>
</template>
<script>
import Button from './Button.vue';
import PrimaryButton from './PrimaryButton.vue';
export default {
components: {
Button,
PrimaryButton
}
};
</script>
9.2 Vue 中的 CSS Modules
Vue 也支持 CSS Modules,可以通过 module 属性实现。
Vue 中 CSS Modules 的示例:
vue
<!-- Button.vue -->
<template>
<button :class="$style.button">
<slot></slot>
</button>
</template>
<style module>
.button {
background-color: blue;
color: white;
padding: 8px 16px;
}
.button:hover {
background-color: darkblue;
}
</style>
<!-- App.vue -->
<template>
<div>
<Button>普通按钮</Button>
</div>
</template>
<script>
import Button from './Button.vue';
export default {
components: {
Button
}
};
</script>
10. Vue 与 React CSS Modules 的对比
10.1 底层原理对比
CSS Modules(React)的原理:
- 构建工具解析
.module.css文件 - 为每个类名生成唯一的哈希值
- 创建原始类名到唯一类名的映射表
- 替换 CSS 文件中的类名
- 导出映射表供组件使用
Vue scoped CSS 的原理:
- Vue 编译器解析带有
scoped属性的样式 - 为组件的根元素添加一个唯一的
data-v-xxx属性 - 为样式中的每个选择器添加
[data-v-xxx]属性选择器 - 这样样式就只对当前组件的元素生效
Vue CSS Modules 的原理: 与 React 中的 CSS Modules 原理类似,但是集成在 Vue 的单文件组件中,使用 $style 对象访问生成的类名。
10.2 异同点
相同点:
- 都解决了 CSS 类名冲突问题
- 都支持局部作用域和全局作用域
- 都需要构建工具的支持
不同点:
- Vue 的 scoped CSS 是内置的,不需要额外配置;React 的 CSS Modules 需要配置构建工具
- Vue 使用
data-v-xxx属性实现作用域隔离;React 使用哈希类名 - Vue 单文件组件中可以直接使用
scoped属性;React 需要使用.module.css命名约定 - Vue 支持
module属性直接在模板中使用$style对象;React 需要导入样式对象
10.3 示例对比
React CSS Modules 示例:
jsx
// Button.js
import React from 'react';
import styles from './Button.module.css';
const Button = ({ children }) => {
return <button className={styles.button}>{children}</button>;
};
export default Button;
// Button.module.css
.button {
background-color: blue;
color: white;
padding: 8px 16px;
}
Vue scoped CSS 示例:
vue
<!-- Button.vue -->
<template>
<button class="button">
<slot></slot>
</button>
</template>
<style scoped>
.button {
background-color: blue;
color: white;
padding: 8px 16px;
}
</style>
Vue CSS Modules 示例:
vue
<!-- Button.vue -->
<template>
<button :class="$style.button">
<slot></slot>
</button>
</template>
<style module>
.button {
background-color: blue;
color: white;
padding: 8px 16px;
}
</style>
总结
CSS Modules 是一种非常实用的 CSS 模块化解决方案,在 React 生态系统中得到了广泛的应用。它解决了传统 CSS 的类名冲突、样式污染等问题,提高了代码的可维护性。
在 Vue 中,虽然有自己的 scoped CSS 和 CSS Modules 实现,但核心思想与 React 中的 CSS Modules 是一致的,都是为了实现样式的模块化和作用域隔离。
选择使用哪种方案取决于项目的需求和技术栈,但无论选择哪种方案,都应该遵循 CSS 模块化的思想,避免全局样式污染,提高代码的可维护性。