背景

什么是 CSS Modules?

CSS Modules 这个词在不同语境下可能有不同意思,这里指的是 这个库

如果你用过 Vue,那么CSS Modules 就非常好理解了。Vue 中可以声明样式为 scoped,也就是说这些样式只作用于当前组件,不会对当前组件外的任何元素生效。这样就可以避免在全局范围内刚好取了相同名字的 CSS 类的冲突问题。可以说,你在 Vue 中写的绝大部分样式都应该是 scoped

但 React 本身不具备相当于 Vue 中 scoped 这样的模块化样式功能,所以若想实现同样的效果,CSS Modules 就派上用场了。

CSS Modules 实现原理

CSS Modules 的实现原理与 Vue 的 scoped 不完全相同,但也十分类似。

在 Vue 中,如果你使用了 scoped 样式,那么当前组件会被分配一个独一无二的 ID,类似于 data-v-f3f3eg9 这样,组件下的所有元素都会被加上这个属性。然后 Vue 会修改你的 CSS 规则,在每个选择器上都加上 [data-v-f3f3eg9] 的限制。这样一来,当前组件中的规则对组件之外的元素就不会生效了,因为组件之外的元素不会有同样的属性。

CSS Modules 的实现方法更为直接,就是把你写的每个类的名字修改成一个独一无二的名字。例如,你写了以下 CSS:

.button {
	background-color: red;
}

那么编译之后,这个类可能就变成 button_1Aw-M 这样的独一无二的名字了。一般是会加后缀,但也可以配置成不同的规则(我当前所在的项目就特别损,完全生成了一堆乱码,根本看不出来之前对应的类名是什么,特别不方便调试)。

问题和解决方法

假设你要使用一个别人写的组件,该组件已经有默认的样式了,但你希望覆盖其中的部分样式。

该组件代码大致如下,首先是 CSS:

 /* base-component.modules.css */
.container {
	background-color: red;
	.target {
		color: white;
	}
}

然后 import 到 React 组件中:

// BaseComponent.jsx
...
import styles from './base-component.module.css'

const BaseComponent = () => {
  return (
    <div className={styles.container}>
      <div className={styles.target}></div>
	</div>
  )
}

这样就实现了对两个嵌套 div 分别的样式指定。

嵌套样式覆盖不生效

如果像下面这样写,是行不通的。首先修改 BaseComponent

// BaseComponent.jsx
import styles from './base-component.module.css'
import cx from 'classnames';

const BaseComponent = ({ containerClass }) => {
  return (
    {/* 注意这里 */}
    <div className={cx(styles.container, containerClass)}>
      <div className={styles.target}></div>
	</div>
  )
}

然后新建一个 CSS 文件:

 /* my-component.modules.css */
.container {
	background-color: green;
	.target {
		color: black;
	}
}

然后导入到自己的 React 组件中:

// MyComponent.jsx
import styles from './my-component.module.css'
import BaseComponent from './BaseComponent.jsx'

const MyComponent = () => {
  return (
    <BaseComponent containerClass={styles.container} />
  )
}

为啥不行呢?新加的 container类是没问题的,但 container 中嵌套的 target 类却不会生效。因为base-component.module.csss 和你新加的 my-component.module.csss 是两个不同的 CSS Module,虽然都有 target 这个类,但编译之后会有各自不同的类名。如果希望指定 target 类的样式,就必须把 target 的类名也传递过去。

换句话说,只传递最外层的类名是不够的,需要把所有类名都传过去,包括嵌套的类。如果给每个类名都加一个 prop 来传就太混乱了,所以最好直接把 styles 也就是 CSS Module 对象)整个传过去。

例如,修改 BaseComponent.jsx

// BaseComponent.jsx
import styles from './base-component.module.css'
import cx from 'classnames';

const BaseComponent = ({ externalStyles }) => {
  return (
    {/* 注意这里 */}
    <div className={cx(styles.container, externalStyles.container)}>
      {/* 以及这里 */}
      <div className={cx(styles.target, externalStyles.target)}></div>
	</div>
  )
}

然后在自己的 React 组件中,直接把 styles 传过去:

// MyComponent.jsx
import styles from './my-component.module.css'
import BaseComponent from './BaseComponent.jsx'

const MyComponent = () => {
  return (
    <BaseComponent externalStyles={styles} />
  )
}

如何确保覆盖?

解决了嵌套的问题,还是不能保证覆盖。因为在上面的例子中,新加的样式和原有的样式具有相同的特殊性(Specificity),即,都是通过同样数量的类名选择的。

按理来说,在特殊性相同的情况下,后声明的规则会覆盖先声明的规则;但由于使用了 CSS Modules,你不太好控制哪个规则先声明,哪个后声明。

当然,一个简单的方法是,如果有新样式,直接丢弃旧样式,全部使用新的。但这样的问题是,无法实现“部分覆盖”,需要把本来不需要覆盖的相同部分也重写一遍。这样肯定不好。

其实最终的解决办法很简单,但我在同事的提醒下才恍然大悟。为了提高新样式的特殊性,可以额外加一层 div

// MyComponent.jsx
import styles from './my-component.module.css'
import BaseComponent from './BaseComponent.jsx'

const MyComponent = () => {
  return (
  	<div className=styles.wrapper>
      <BaseComponent externalStyles={styles} />
    </div>
  )
}

相应的 CSS:

 /* my-component.modules.css */
.wrapper {
	.container {
		background-color: green;
		.target {
			color: black;
		}
	}
}

这样就可以确保新样式可以覆盖旧样式了!

Logo

前往低代码交流专区

更多推荐