CSS Modules

CSS命名规范,基本又是每个团队都要面临的问题,规范必定会出一则,但最后能严格执行的,始终是一件很难的事。

刚来到京东 UED 团队的时候,大家都遵守使用『基于姓氏命名』法则并为之推广

基于姓氏命名

如下面常见的商品信息模块结构:

商品信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="gb_goods_item">
<div class="gb_goods_item_box">
<div class="gb_goods_item_image">
...
</div>
<div class="gb_goods_item_info">
<div class="gb_goods_item_title">
...
</div>
<div class="gb_goods_item_sidebar">
<div class="gb_goods_item_price">
...
</div>
<div class="gb_goods_item_shopcart">
<div class="gb_shopcart">
...
</div>
</div>
</div>
</div>
</div>
</div>

这套法则注重 CSS 语义的表达和管理,初衷就是为了方便区分样式模块的归属,也方便日后复用。但缺点也很明显:难理解,命名需要花额个的时间考虑其独特性,当『后代』结构代码相当复杂的时候,命名会太过沉长,不够灵活,再有就是始终无法根治代码污染和被污染的问题。

刚开始使用的时候,相当不习惯,争议也多,每次有新员工来都需要花不少时候解释,亦很难让之 100% 地乐意使用,即便有明确的团队规范文档,真正上线的时候,还是会出现『不规范』的代码。

尽管如此,权衡了当时的利弊,还是坚持用了下来。当 Node 的出现和不断强大,前端界得到了的极速的发展,前端工作流更为细致。React 的横空出世,突显超大型高效协作收益的组件化更是深得人心,而组件化的独立性及高复用性注定要解除 CSS 对组件的限制:样式代码污染问题。

『基于姓氏命名』显然不能满足这个要求,毕竟,CSS 样式总是全局作用的,『姓名』再独特,总会有同名同姓的机会。此时,CSS Modules 提供了一个很好的解决方案。

A CSS Module is a CSS file in which all class names and animation names are scoped locally by default.

CSS 文件中的所有类名和动画名的作用域都默认为当前作用域。

CSS Modules 给了 CSS 域的概念,先看一波 Css Modues 在实际应用上是怎么的一回事。

React 组件中常见的轮播图:

轮播图

JSX 的 Render 内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import styles from './styles/FocusSlider.css';

// FocusSlider
render() {
return (
<div className={styles.fsSlider} data-role="fsSlider">
<div className={styles.fsSliderMain}>
<Slider
sliderData={this.state.sliderData}
/>

</div>
<div className={styles.fsSliderBg}></div>
{/* 数据重加载提示 */}
<DataReqReload
isShow={this.state.isReloadData}
doReload={this.doReload}
/>

</div>
);

}

对应该的 FocusSlider.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
:local{
.fsSlider{
position: relative;

padding-top: -webkit-calc(100vw * 120 / 1125);
padding-top: calc(100vw * 120 / 1125);
}

.fsSliderMain{
position: relative;
z-index: 1;
min-height: -webkit-calc( ((100vw - 60px) * 570 / 960) + ((100vw - 60px) * 70 / 1125) );
min-height: calc( ((100vw - 60px) * 570 / 960) + ((100vw - 60px) * 70 / 1125) );
}

.fsSliderBg{
position: absolute;
left: 0;
right: 0;
top: 0;
height: -webkit-calc(100vw * 588 / 1125);
height: calc(100vw * 588 / 1125);
background: #000 url(../../../images/top100/fs_bg.jpg) no-repeat;
background-size: 100% auto;
}

.gbSlider{
background: #000;
}
}

编译后的代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
<div class="_1zbnOjlYzV-MBq1JhiGcZF" data-role="fsSlider">
<div class="svXZ19tF2m_fbg8KuFb6T">
<div class="lvyQyG394l5agFQKH8_du" data-role="gbSlider" style="opacity: 1;">
...
</div>
</div>
<div class="_27Jm0ogj1EtOf2dNHKvKC1"></div>
<!-- 数据重加载提示 -->
<div class="f1qHBJK89yiiDn4Tcb2It _3grBcj2DcL0IJGuHXl4LK6" data-role="reloadBox">
...
</div>
</div>

可以看到,FocusSlider.scss 里面对应定义的局部域样式名都变成了一个哈希字符串,同时FocusSlider.css文件也会被编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
._1zbnOjlYzV-MBq1JhiGcZF {
position:relative;
padding-top:calc(100vw * 120 / 1125)
}

.svXZ19tF2m_fbg8KuFb6T {
position:relative;
z-index:1;
min-height:calc(((100vw - 60px) * 570 / 960) + ((100vw - 60px) * 70 / 1125))
}

._27Jm0ogj1EtOf2dNHKvKC1 {
position:absolute;
left:0;
right:0;
top:0;
height:calc(100vw * 588 / 1125);
background:#000 url(/static/media/fs_bg.70affa5b.jpg) no-repeat;
background-size:100% auto
}

._2EHIbU8ioFFyvcDBD64P4_ {
background:#000
}

Css Modules 使得样式的类名在局部作用域内独一无二而不会对全局的样式造成污染,这正是组件化所急需的,似乎也是为 React 而生。

这样一来,上面『基于姓氏命名』商品信息结构用 Css Moduels 就可以这样写了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import styles from '../styles/GoodsItem.css';

render() {
return (
<div class={styles.goodsItem}>
<div class={styles.itemBox}>
<div class={styles.image}>
...
</div>
<div class={styles.info}>
<div class={styles.title}>
...
</div>
<div class={styles.sideBar}>
<div class={styles.price}>
...
</div>
<div class={styles.shopCart}>
<ShopCart
sku={}
/>

</div>
</div>
</div>
</div>
</div>
);

}

这样我们就可以更专注组件的开发了。那么如何使用?其实用起来很简单

  • 如果你是用 React,并且使用伟大的 Create React App,默认是支持 Css Modules 的。
  • 如果你是用 Vue,恭喜你,并不用为这个而操心,Vue 自带样式域的处理;
  • 如果都不是,那么你可能需要 Webpack 的 css-loader,里面你还可以定制哈希类名。
  • 你还可以用 PostCSS-ModulesPostCSS 的一个插件,在任何地方使用 Css Mudous 😳

更具体的文档可以看一下 github 上的 css-modules

也可以看一下阮老师的《CSS Modules 用法教程》

Css Modules 用起来虽然很爽,但实际应用中,有一点美中不足,就是组件在局部作用域下的定制问题,最常见的就是『换肤』需求了,如下图

商品列表

上图两个商品列表,数据结构和数据来源都是一样的,只是布局和样式略有不同,如果用传统的 CSS 方案写样式,会先写一个基准样式,再写『皮肤』样式:

1
2
3
4
5
6
7
8
9
10
11
<div class="goods goods_a">
<div class="goods_item">
...
</div>
</div>

<div class="goods goods_b">
<div class="goods_item">
...
</div>
</div>

如果在组件系统中,上面的商品列表中,有可能是这样的结构:

在 Goods 的容器 GoodsContaniner 组件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import styles from '../styles/GoodsContainer.css';

render() {
return (
<div className={style.goodsContainer}>
<GoodsA>
<GoodsItem />
</GoodsA>

<GoodsB>
<GoodsItem />
</GoodsB>
</div>
);

}

如果 GoodsAGoodsBGoodsItem 都是独立的组件,Css Modules 对应的样式也是局部作用域独立的,GoodsAGoodsB 就不能对 GoodsItem 做样式定制了。

对这种情况,可以在组件结构中为样式写好扩展属性,如在 GoodsItem 组件中写成:

1
2
3
4
5
6
7
8
9
import styles from '../styles/GoodsItem.css';

render() {
return (
<div className={style.goodsItem} data-role="goodsItem">
...
</div>
);

}

这样在 GoodsAGoodsB 中就可以控制到 GoodsItem

1
2
3
4
5
6
7
:local {
.goodsA{
[data-role="goodsItem"] {
...
}
}
}

可能你会问,如果这样写那岂不是很麻烦?这种场景完全可以通过预规划来规避,如规划好 GoodsItem 组件的样式作用域范围(global 作用域)或提前设计好组件的结构,预留参数对 GoodsItem 组件进行样式控制。

但是实际项目迭代过程中,有时候我们是无法预知项目后续发展规模的,很大可能会在你第一版写好的组件中,不断迭代功能,如上面提到的『换肤』场景,这就很有必要写好 data-role 为组件的样式做扩展了。

如果有项目使用组件系统的,CSS 方案的挑选,Css Modules 是不二的选择。

引用某电影的一句台词:『如果你 x 过驴仔 o,就 x 唔翻转头了!』,Css Modules 正是那个驴仔。