CSS Module

阮一峰 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>`

由于在 .normalcomposes.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 的默认样式:

  1. <Swiper> 加一个CSS Modules 控制的 className 样式
<div class={styles.gallery}>
    <Swiper className={styles.swiperbox}>

// gallery.module.scss
.gallery {
    .swiperbox {
        width: 80vw;
        height: 90vh;
    }
}

这样,.gallery.swiperbox 都会被编译混淆,从而达到覆盖样式的目的。

  1. <Swiper> 加一个静态的 className 样式,通过 :global 声明为全局样式
<div class={styles.gallery}>
    <Swiper className="swiperbox">

// gallery.module.scss
.gallery {
    :global(.swiperbox) {
        width: 80vw;
        height: 90vh;
    }
}

这样,.gallery 会被编译,并放入映射对象中,而 .swiperbox 不会被编译。

  1. 更进一步,直接使用 <Swiper> 组件内部的样式,通过 :global 声明为全局模式
<div class={styles.gallery}>
    <Swiper>

// gallery.module.scss
.gallery {
    :global(.swiper) {
        width: 80vw;
        height: 90vh;
    }
}


如何保护 keyframes 的动画名?

@keyframes :global(blockly-shake) {}

Webpack 配置 CSS modules

Webpack: css-loader 中文文档
Webpack: css-loader 官方文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342