组件级样式方案 CSS Modules 的入门教程

开发 前端
CSS Modules 是一种 CSS 的模块化方案,主要目的是解决 CSS 样式的全局命名空间问题。它通过将 CSS 类名和选择器局部化,让每个模块都有自己独立的样式作用域,就像在 JavaScript 中的模块一样,不同模块中的同名变量不会相互干扰,也不会导致全局样式的污染。

平时开发中我们应该没少为给 DOM 元素取类名更头疼的了。CSS 样式是全局的, 为了避免某个部分的样式被别处的意外覆盖,我们就要在取类名上想办法,流行的比如 OOCSS(Object Oriented CSS,.button.button-primary)、 BEM(.block_element--modifier);甚者更加细粒度的 Atomic CSS 方案,这里的代表是Tailwind CSS(.flex.justify-center.items-center.h-scree)。

背景介绍

当然,随着前端模块块、组件化的发展,还有一种折中方案 CSS Modules。CSS Modules[1] 可以因为组件化的流行而闻名。

图片图片

CSS  Module 的核心概念就是确保样式是组件级别的,它将 CSS 样式文件导入成对象并通过属性名方式在 DOM 元素上在 class attribute 上引入。

/* style.css */
.container {
  color: green;
}
import styles from './style.css';
element.innerHTML = '<div class="' + styles.container + '">';

当你导入 CSS Modules 文件时,类名实际上会被转换为唯一的标识符。例如,styles.container 在最终生成的 HTML 和 CSS 中可能会变成类似于 styles_container__[hash] 的本地作用域的形式,其中 [hash] 是一个根据内容生成的哈希值,确保全局唯一性。这样,我们就能很轻松的设置组件级别的样式,而不会担心会影响页面其他部分的样式。

由于 CSS Modules 太过好用,基本上现在每个前端项目脚手架工具都有提供支持。不管是 webpack、Vite 还是 Next.js,你都在对应的官方模板中取得 CSS Modules 的支持,在这些脚手架工具搭建的项目中,你通常只要将 CSS 文件以文件后缀改 .module.css(或是由 CSS 预处理器驱动的 .module.less、.module.scss 这些文件后缀)结尾,就会启用 CSS Modules 模式。

而在组织方式上,.module.css通常与组件文件放一起(像下面这样):

图片图片

如何使用

接下来我们来讲解 CSS Modules 各个场景下的使用。

创建项目

我们以 Vite React 项目模板为例,首先创建项目。

npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install 
npm run dev

项目默认就支持 CSS  Modules 的使用。

简单使用

先从最简单的使用讲起。删除 src/App.jsx 文件,创建  src/App/index.jsx 以及 src/App/index.module.css 文件,填充以下内容。

// src/App/index.jsx
import styles from './index.module.css'

function App() {
  return (
    <div className={styles.container}>
      <h1>My First React App</h1>
      <p>This is a paragraph</p>
    </div>
  );
}

export default App;
/* src/App/index.module.css */
.container {
  color: red;
}

渲染效果如下:

图片图片

可以看到 styles.container 渲染成了 _container_2rp1n_1,如上所示,**虽然 CSS Modules 并没有强制使用类名的格式,但还是首选采用驼峰命名[2](类似:styles.className)。

_container_2rp1n_1 是 Vite 的默认配置生成的结果,我们可以在 vite.config.js 中进行修改,让样式层级更加直观。

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

export default defineConfig({
  plugins: [react()],
+  css: {
+    modules: {
+      generateScopedName: '[name]__[local]___[hash:base64:5]'
+    }
+  }
})

修改后,效果如下:

图片图片

styles.container 渲染成了 index-module__container___KpZIM。更加配置可以参考 css.module 的配置说明[3]。

父子同名类名

接下来,我们在同级目录下创建 components/Hello/ 组件。

// src/components/Hello/index.jsx
import styles from './index.module.css'

export function Hello() {
  return (
    <div className={styles.container}>
      <h1>Hello World</h1>
      <p>This is a React component</p>
    </div>
  )
}
/* src/components/Hello/index.module.css */
.container {
  color: blue;
}

src/App/index.jsx 中引入:

import styles from './index.module.css'
+ import { Hello } from '../components/Hello'

function App() {
  return (
    <div className={styles.container}>
      <h1>My First React App</h1>
      <p>This is a paragraph</p>
+     <Hello />
    </div>
  );
}

export default App;

效果如下:

图片图片

虽然 <App> 和 <Hello> 组件中都使用了一样的类名,但最终生成出来是不一样的,这就是作用域样式了。

类名组合

除了提供 CSS 作用域类名的核心能力,CSS Modules 还有支持类名组合,说白了就是样类名组合能力。

下面,我们修改 App 组件:

/* src/App/index.module.css */
.container {
  display: flex;
  gap: 16px;
}

.button {
  cursor: pointer;
  padding-block: 8px;
  padding-inline: 16px;
  font-size: inherit;
}

.buttonExtend {
  composes: button;
  color: blue;
}

注意看,在 .buttonExtend 中,我们使用了 composes: button。

/* src/App/index.jsx */
import styles from './index.module.css'

function App() {
  console.log('styles', styles)

  return (
    <div className={styles.container}>
      <button className={styles.button}>button</button>
      <button className={styles.buttonExtend}>extend button</button>
    </div>
  );
}

export default App;

查看效果:

图片图片

会发现,styles.buttonExtend 的值为 index-module__buttonExtend___vz-2k index-module__button___gnncC,组合了 styles.button 的值,composes 的作用有点类似 Bootstrap 中 **.button.button-primary** 的用法,不过对 CSS Modules 来说,这种关系是在 module.css 文件中声明的,无需在 DOM 元素 class 上显式书写。

当然,你还可以在类名上直接应用伪类:

.button:hover {
  transform: scale(1.1);
  transition: transform 0.15s ease;
}

效果如下:

图片图片

如此,2 个按钮就有了 hover 时的放大效果了。

比较厉害的是,composes 的类名来源还可以是来自外部文件的,类似下面的用法

.otherClassName {
  composes: className from './style.module.css';
}

我们创建一个 src/App/other.module.css 文件出来,将 .button 的类名导入进来:

/* src/App/other.module.css */
.button {
  cursor: pointer;
  padding-block: 8px;
  padding-inline: 16px;
  font-size: inherit;
}

.button:hover {
  transform: scale(1.1);
  transition: transform 0.15s ease;
}

修改 src/App/index.module.css:

.buttonExtend {
  composes: button from './other.module.css';
  color: blue;
}

查看效果:

图片图片

发现由 .button 产生的类名变成 other-module 下面的了。

局部类名下的全局样式设置

现在,我们在 App.jsx 中将 Hello 组件重新引入,并且给 .container 类名一个 color 设置:

import styles from './index.module.css'
+ import { Hello } from '../components/Hello'

function App() {
  return (
    <div className={styles.container}>
      <button className={styles.button}>button</button>
      <button className={styles.buttonExtend}>extend button</button>
+      <Hello />
    </div>
  );
}

export default App;
.container {
  display: flex;
  gap: 16px;
+ color: coral;
}

查看效果:

图片图片

可以看到,虽然我们在 .container 上设置了 coral 的颜色,但不会覆盖 Hello 上 .container 的颜色设置。

图片图片

这个时候,如果我们想覆盖 Hello 下的 div 的颜色设置,就要用到 :global 了。:global 不是标准 CSS 的一部分,而是像 CSS Modules 这类 CSS-in-JS 库扩展出来的,用来逃逸模块作用域为其下的元素应用样式。

修改 src/App/index.module.css,增加以下样式:

.container :global div {
  color: yellowgreen;
}

:global div 也可以写成 :global(div),效果是一样的。

查看效果:

图片图片

发现样式生效了,当然最好的方式还是直接使用类名。

修改 src/components/Hello/index.jsx 以及 src/App/index.module.css:

// src/components/Hello/index.jsx
import styles from './index.module.css'

export function Hello() {
  return (
-    <div className={styles.container}>
+    <div className={`helloWrapper ${styles.container}`}>
      <h1>Hello World</h1>
      <p>This is a React component</p>
    </div>
  )
}
/* src/App/index.module.css */
- .container :global div
+ .container :global .helloWrapper {
  color: yellowgreen;
}

:global .helloWrapper 也可以写成 :global(.helloWrapper),效果是一样的。

查看效果:

图片图片

这样我们设置的样式就更加有针对性了!

总结

CSS Modules 是一种 CSS 的模块化方案,主要目的是解决 CSS 样式的全局命名空间问题。它通过将 CSS 类名和选择器局部化,让每个模块都有自己独立的样式作用域,就像在 JavaScript 中的模块一样,不同模块中的同名变量不会相互干扰,也不会导致全局样式的污染。

除此之外,CSS Modules 还提供了样式组合、局部类名下的全局选择器选择,丰富了它的能力。

责任编辑:武晓燕 来源: 写代码的宝哥
相关推荐

2010-08-17 13:28:31

DIVCSS

2024-07-30 09:08:32

2024-11-12 15:46:37

2010-08-16 09:56:05

DivCSS

2014-05-26 15:35:55

Web组件Web Compone

2009-07-08 15:12:48

Java Servle

2013-08-29 14:12:52

Storm分布式实时计算

2010-08-03 13:06:15

Flex Builde

2010-03-12 14:04:32

Python入门教程

2022-07-12 08:27:18

Zadig开源

2022-07-21 11:58:12

Docker

2010-08-16 09:32:01

DivCSS

2010-08-16 10:10:11

DIV+CSS

2010-07-20 16:19:54

Perl

2011-09-02 10:59:10

jQuery Mobi

2018-03-22 14:59:13

Docker入门容器

2013-06-24 13:38:34

HTML5 DataList

2010-06-18 16:56:50

UML建模语言

2011-07-21 10:29:18

iPhone 开发

2010-05-21 12:50:45

Subversion快
点赞
收藏

51CTO技术栈公众号