阮一峰 CSS Module
阿里大佬 CSS Module
ICSS
CSS Module 能最大化地结合现有 CSS 生态和 JS 模块化能力,API 简洁到几乎零学习成本。发布时依旧编译出单独的 JS 和 CSS。
它并不依赖于 React,只要你使用 Webpack,可以在 Vue/Angular/jQuery 中使用。
CSS Modules 内部通过 ICSS 来解决样式导入和导出这两个问题,分别对应 :import
和 :export
两个新增的伪类
:import("path/to/dep.css") {
localAlias: keyFromDep;
/* ... */
}
:export {
exportedKey: exportedValue;
/* ... */
}
但直接使用这两个关键字编程太麻烦,实际项目中很少会直接使用它们,我们需要的是用 JS 来管理 CSS 的能力。
结合 Webpack 的 css-loader 后,就可以在 CSS 中定义样式,在 JS 中导入:
/* components/Button.css */
.normal { /* normal 相关的所有样式 */ }
.disabled { /* disabled 相关的所有样式 */ }
/* components/Button.js */
import styles from './Button.css';
buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`
/* 生成的 HTML */
<button class="button--normal-abc53">Submit</button>
CSS Modules 对 CSS 中的 class 名都做了处理,使用对象来保存原 class 和混淆后 class 的对应关系
Object {
normal: 'button--normal-abc53',
disabled: 'button--disabled-def884',
}
所以,想要使用混淆后的类名,必须通过 JS import 引入,访问原类名 从而得到混淆的类名;而静态的使用方式是不会生效的,如 class="normal"
通过这些简单的处理,CSS Modules 实现了以下几点:
- 所有样式都是 local 的,解决了命名冲突和全局污染问题
- class 名生成规则配置灵活,可以此来压缩 class 名
- 只需引用组件的 JS 就能搞定组件所有的 JS 和 CSS
- 依然是 CSS,几乎 0 学习成本
样式默认局部
使用了 CSS Modules 后,就相当于给每个 class 名外加了一个 :local
,以此来实现样式的局部化;
如果你想切换到全局模式,使用对应的 :global
.normal {
color: green;
}
/* 以上与下面等价 */
:local(.normal) {
color: green;
}
/* 定义全局样式 */
:global(.btn) {
color: red;
}
/* 定义多个全局样式 */
:global {
.link {
color: green;
}
.box {
color: yellow;
}
}
凡是全局模式中的样式类名都不会被混淆编译,仍保持原有的类名,因此不会被加入映射关系对象中,使用时也必须通过静态引入的方式 class="btn"
组合样式
对于样式复用,CSS Modules 只提供了唯一的方式来处理:composes
组合
/* components/Button.css */
.base { /* 所有通用的样式 */ }
.normal {
composes: base;
/* normal 其它样式 */
}
.disabled {
composes: base;
/* disabled 其它样式 */
}
import styles from './Button.css';
buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`
由于在 .normal
中 composes
了 .base
,编译后会 normal
会变成两个 class
<button class="button--base-daf62 button--normal-abc53">Submit</button>
composes
还可以组合外部文件中的样式
/* settings.css */
.primary-color {
color: #f40;
}
/* components/Button.css */
.base { /* 所有通用的样式 */ }
.primary {
composes: base;
composes: primary-color from './settings.css';
/* primary 其它样式 */
}
对于大多数项目,有了 composes
后已经不再需要 Sass/Less/PostCSS
但如果你想用的话,由于 composes
不是标准 CSS 语法,编译时会报错,也就只能使用预处理器的语法来做样式复用了。
class名的命名
CSS Modules 的命名规范是从 BEM 扩展而来。
BEM 把样式名分为 3 个级别,分别是:
- Block:对应模块名,如 Dialog
- Element:对应模块中的节点名 Confirm Button
- Modifier:对应节点相关的状态,如 disabled、highlight
综上,BEM 最终得到的 class 名为 dialog__confirm-button--highlight
使用双符号 __
和 --
是为了和区块内单词间的分隔符区分开来。
虽然看起来有点奇怪,但 BEM 被非常多的大型项目和团队采用。
CSS Modules 中 CSS 文件名恰好对应 Block 名,只需要再考虑 Element 和 Modifier。
BEM 对应到 CSS Modules 的做法是:
/* .dialog.css */
.ConfirmButton--disabled {
}
你也可以不遵循完整的命名规范,使用 camelCase 的写法把 Block 和 Modifier 放到一起:
/* .dialog.css */
.disabledConfirmButton {
}
CSS Modules 推荐使用 Camel case
来命名 class,如 css.parentNode
。原因是如果使用其他规则,比如 Kebab case
,就没办法使用简单地属性访问方法,而只能这样:css["parent-node"]
CSS变量与JS变量
注意:CSS Modules 中没有变量的概念,这里的 CSS 变量指的是 Sass 中的变量。
上面提到的 :export
关键字可以把 CSS 中的 变量输出到 JS 中
/* config.scss */
$primary-color: #f40;
:export {
primaryColor: $primary-color;
}
/* app.js */
import style from 'config.scss';
// 会输出 #F40
console.log(style.primaryColor);
注 意
CSS Modules 只会转换 class 名和 id 选择器名相关的样式,其他如标签选择器、属性选择器、伪类等都不会转化!
所以有时候我们可以通过给关键节点加上 data-role
属性,然后通过属性选择器来覆盖样式
// dialog.js (React)
return (
<div className={styles.root} data-role='dialog-root'>
// ...
</div>
)
// dialog.css
[data-role="dialog-root"] {
// override style
}
覆盖第三方组件样式
环境:webpack + css-loader + sass + react + typescript
对于webpack,目前对 CSS Modules 支持最好的是 css-loader
修改 Swiper 的默认样式:
- 给
<Swiper>
加一个CSS Modules 控制的 className 样式
<div class={styles.gallery}>
<Swiper className={styles.swiperbox}>
// gallery.module.scss
.gallery {
.swiperbox {
width: 80vw;
height: 90vh;
}
}
这样,.gallery
和 .swiperbox
都会被编译混淆,从而达到覆盖样式的目的。
- 给
<Swiper>
加一个静态的 className 样式,通过:global
声明为全局样式
<div class={styles.gallery}>
<Swiper className="swiperbox">
// gallery.module.scss
.gallery {
:global(.swiperbox) {
width: 80vw;
height: 90vh;
}
}
这样,.gallery
会被编译,并放入映射对象中,而 .swiperbox
不会被编译。
- 更进一步,直接使用
<Swiper>
组件内部的样式,通过:global
声明为全局模式
<div class={styles.gallery}>
<Swiper>
// gallery.module.scss
.gallery {
:global(.swiper) {
width: 80vw;
height: 90vh;
}
}
如何保护 keyframes 的动画名?
@keyframes :global(blockly-shake) {}