当前内容所在位置(可进入专栏查看其他译好的章节内容)
- 【第三部分 现代 CSS 代码组织】
- 【第九章 CSS 的模块化与作用域】
- 9.1 模块的定义
- 9.1.1 模块和全局样式
- 9.1.2 一个简单的 CSS 模块
- 9.1.3 模块的变体 ✔️
- 9.1.4 多元素模块 ✔️
- 9.2 将模块组合为更大的结构
文章目录
- 9.1.3 模块的变体 Variations of a module
- 9.1.3.1 按钮模块的变体形式 Button module variants
- 9.1.3.2 不要使用有语境依赖的选择器 Avoiding context-dependent selectors
- 9.1.4 包含多个元素的模块 Modules with multiple elements
- 9.1.4.1 同时使用变体和子元素 Using variants and subelements together
- 9.1.4.2 避免在模块选择器中使用通用标签名 Avoiding generic tag names in module selectors
《CSS in Depth》新版封面
译者按
本篇为 9.1 小节 CSS 模块定义的下篇,通过两个具体的示例对模块化 CSS 的应用即推广进行了具体讲解。由于新版引入了最新的层叠图层即 Flexbox 布局等相关语法,使得 CSS 按模块化组织更加得心应手,结构层次也更加清晰。学习本篇重点在于理解作者的思路和决策依据,而非照本宣科,机械照搬。
9.1.3 模块的变体 Variations of a module
保持一致性确实不错,但有时候需要特意避免完全一致。方才的消息模块固然好用,但在某些情况下也需要作些调整。比如,如果需要显示一则报错信息,这时就应该使用红色而不是之前的蓝绿色(teal);再比如,您可能需要区分单纯的信息和示意操作成功的通知(如保存成功),这时就可以通过定义 修饰符(modifiers) 来实现。
修饰符可以通过一个以模块名称开头的新类名来定义。例如,消息模块的 error
修饰符可以写作 message-error
。通过包含模块名称,您就明确指定了该样式类属于消息模块。
关于 BEM 类名约定
带双连字符以及双下划线的类名是一种常见的类名约定,它们都属于 BEM 方法论的范畴。BEM 是 Block-Element-Modifier 的首字母缩写。
在 BEM 类名约定中,一个 block 块表示的是模块的主元素(main element),通常为一个具有描述性的、唯一的类名,如
message
。BEM 中的 element 元素则表示模块中的子元素(有时也可以为子孙元素),并通常用双下划线来连接 block 块名称与 element 元素名称,例如用media__image
来引用media
媒体模块中的图片元素。而 BEM 中的 modifier 修饰符则是在创建模块的变体形式时添加到 block 块中的某个类名称,它们通常用 block 块名称后的一组双连字符来加以区分,例如message--error
。值得一提的是,BEM 只是众多类名约定中的一种,但由于其名气最大,本章也采纳了里面的部分做法;其他广受欢迎的类名约定还包括
OOCSS
、SMACSS
、ITCSS
以及SUITCSS
。虽然在具体的约定内容和命名规则上略有不同,但目的都是一样的:以模块化的方式来组织页面样式。
下面为示例模块创建三个修饰符:成功(success
)、警告(warning
)和错误(error
)。将代码清单 9.4 添加到样式表中,通过 BEM 中的双连字符约定来指明该修饰符属于 message
消息模块。
代码清单 9.4 带修饰符类名的消息模块
@layer modules {.message { /* 基础 message 模块样式 */padding: 0.8em 1.2em;border-radius: 0.2em;border: 1px solid #265559;color: #265559;background-color: #e0f0f2;}.message--success { /* success 修饰器将 message 模块改为绿色 */color: #2f5926;border-color: #2f5926;background-color: #cfe8c9;}.message--warning { /* warning 修饰符将 message 模块改为黄色 */color: #594826;border-color: #594826;background-color: #e8dec9;}.message--error { /* error 修饰符将 message 模块改为红色 */color: #59262f;border-color: #59262f;background-color: #e8c9cf;}
}
修饰符的样式不用重新定义整个模块,只需覆盖需要变更的部分即可。在本例中,则表示修改文字、边框及背景的颜色。
如代码清单 9.5 所示,要让修饰符样式生效,只需将主模块类名和对应的修饰符类名同时添加到目标元素上即可。这样既应用了模块的默认样式,又可以根据需求利用修饰符重写个别样式。
代码清单 9.5 使用了错误修饰符 error 的消息模块示例
<div class="message message--error"><!-- 将两个样式类同时放入元素中 -->Invalid password
</div>
同理,需要展示成功或警告消息时,添加对应的修饰符即可。它们会改变模块的颜色,而其他修饰符还能改变模块的大小甚至布局。
9.1.3.1 按钮模块的变体形式 Button module variants
再来看看另一个模块的变体形式。我们将实现一个按钮模块,其中包含尺寸大小和颜色样式的变体形式(如图 9.2 所示)。这样就能用不同的颜色为按钮添加视觉意义。绿色代表积极的行为,比如保存和提交表单;红色则表示警告,以防用户不小心点到或意外取消按钮。
【图 9.2 使用了不同尺寸和颜色修饰符的按钮效果】
代码清单 9.6 给出了上述按钮的具体样式,其中包括基础按钮模块和四个修饰符样式类:两个尺寸修饰符和两个颜色修饰符。将这些代码添加到样式表中。
代码清单 9.6 按钮模块及其修饰符样式代码
@layer modules {.button { /* 按钮基础样式 */padding: 0.5em 0.8em;border: 1px solid #265559;border-radius: 0.2em;background-color: transparent;font-size: 1rem;}.button--success { /* 绿色的 success 颜色变体形式 */border-color: #cfe8c9;color: #fff;background-color: #2f5926;}.button--danger { /* 红色的 danger 颜色变体形式 */border-color: #e8c9c9;color: #fff;background-color: #a92323;}.button--small { /* 小字号的变体形式 */font-size: 0.8rem;}.button--large { /* 大字号的变体形式 */font-size: 1.2rem;}
}
字号修饰符旨在设置字体的大小。在第二章中我们用过类似的技巧:通过变更字号来调整元素相对单位 em
的具体取值,进而改变内边距和圆角半径的大小,无需分别重写对应的声明样式。
提示
一个模块中的所有代码务必要集中放到同一个地方,以便让模块一个接一个的组成最终想要的样式表。
有了这些修饰符,写 HTML 的时候就有了多种选择:既可以根据按钮的重要程度来添加修饰符类并控制其大小,也可以搭配不同的颜色来给用户提供相应的语境。
代码清单 9.7 中的 HTML 通过不同的修饰符组合来创建各种按钮效果。将其添加到页面的任意位置,看看效果如何:
代码清单 9.7 使用修饰符创建多种类型的按钮样式
<button class="button button--large">Read more</button><!-- 带大号修饰符的按钮模块 -->
<button class="button button--success">Save</button><!-- 带成功修饰符的按钮模块 -->
<button class="button button--danger button--small">Cancel</button><!-- 带危险和小号修饰符的按钮模块 -->
这里的第一个按钮是大号字体;第二个是表示操作成功的绿色按钮;第三个则带有两个修饰符:一个变更颜色(danger
危险修饰符),另一个变更大小(small
小号修饰符),最终效果如图 9.2 所示。
双连字符的写法看上去稍显多余,但在创建名称很长的模块时,比如 nav-menu
导航菜单或 pull-quote
文章摘要时,该写法的好处就凸显出来了。给这些模块添加修饰符后,类名就可以分别写作 nav-menu--horizontal
、pull-quote--dark
的形式。
双连字符的写法很容易区分哪部分是模块名称,哪部分是修饰符;nav-menu--horizontal
和 nav--menu-horizontal
分别代表了不同的含义。如此一来,即便项目中存在很多名称相仿的模块,也可以很容易进行分辨。
9.1.3.2 不要使用有语境依赖的选择器 Avoiding context-dependent selectors
假设我们正在维护一个网站,里面有浅色调的下拉菜单。现在需要将页面标题的下拉菜单样式反转,使其变为深底白字效果。
如果没有模块化 CSS,这时可能会用类似于 .page-header .dropdown
的选择器,先选中要修改的下拉菜单,再通过重写 dropdown
类的默认样式实现目标;但在模块化 CSS 中,上述选择器的写法是严格禁用的。虽然后代选择器可以满足当下的需求,但后续也很可能带来诸多问题,下面来具体梳理一下这些问题。
首先,必须考虑将这段代码放到哪里:是和页面标题样式放到一起合适呢,还是和下拉菜单放到一起合适?如果设置太多类似的单一用途的样式规则,彼此间毫无关联,到最后样式表肯定会变得杂乱无章;要是后续需要修改样式,您还能快速回忆起它们在哪儿吗?如果换作其他同事来修改,他们能快速定位吗?
其次,这种处理方式提升了选择器优先级。当下次需要修改样式时,则不得不达到或者继续提升该优先级(specificity)。
再者,后续可能需要在其他场景下应用深色的下拉列表,而之前创建的版本需要满足位于页面标题区的大前提。如若侧边栏也需要同样的下拉列表效果,就只能为该规则集添加新的选择器来同时匹配两个业务场景,或者完全复刻一遍样式。
最后,反复采用这样的写法会导致选择器越写越长,最终令 CSS 与特定的 HTML 结构深度绑定。例如,对于选择器 #products-page .sidebar .social-media div::first-child h3
而言,该样式集就与指定页面的指定位置紧密耦合到了一起。
这些问题是开发人员处理 CSS 时屡屡受挫的根源。要使用和维护的样式表越长,情况就会越糟糕。当新样式需要覆盖旧样式时,选择器的优先级也会持续攀升,以致于到最后才猛然发现某个选择器为了选择一个复选框,竟然已经包含了两个 ID 和五个样式类。
在样式表中,元素可能会被各种彼此不相关的选择器选中,以致于很难找出其真正生效的样式。此时理解整个样式表的组织方式变得愈发困难,根本理不清它们是如何将页面渲染成现在这样的。代码搞不明白则意味着 Bug 屡见不鲜,可能很小的样式变更都会波及一大片区域。删除旧代码风险就更高了,因为没人知道这段代码是做什么用的、是否还在生效。样式表越长,类似的问题就愈发严重。模块化 CSS 就是要尝试解决这些问题。
当模块需要不同的外观或者呈现效果时,可以创建一个直接作用于指定元素的修饰符类。例如将选择器 .page-header .dropdown
直接写作 .dropdown--dark
。这样一来,最终效果由且仅由该模块本身决定,其他模块将无法对其进行修改。鉴于深色系下拉菜单效果不与 HTML 中的任何深度嵌套结构绑定,它也就可以在页面任意位置随意设置。
切忌通过基于页面位置的后代选择器来修改模块样式。只要严格遵守这一原则,就可以有效防止样式表变成一堆难以维护的代码。
9.1.4 包含多个元素的模块 Modules with multiple elements
我们已经创建了 message
消息和 button
按钮两个模块,它们既简单又实用,并且都由单个元素组成;但是有很多模块需要多个元素,我们不可能单凭一个元素来实现下拉菜单或者模态框效果。
下面来创建一个更复杂的样式模块。它是一个由前端开发与 CSS 领域专家 妮可·沙利文(Nicole Sullivan) 1 率先为其命名的媒体模块(media module),如图 9.3 所示:
【图 9.3 由四个元素组成的 media 媒体模块】
这个模块由四个元素组成:一个 div
容器、以及容器内的一张图片、和正文区,最后是正文中的标题。与其他模块一样,我们将给主容器添加 media
样式类作为模块名称。
对于图片和正文,则可以使用样式类 media__image
和 media__body
。它们以模块名开头,并通过双下划线连接模块与子元素名称。这也是 BEM
命名约定的另一种形式。与双连字符表示的修饰符一样,这样的类名可以清楚的告知该元素所扮演的角色及所属的模块。
媒体模块的样式代码如代码清单 9.8 所示。将它们添加到您的本地样式表中:
代码清单 9.8 包含子元素的媒体模块样式代码
@layer modules {.media { /* 主容器 */display: flex;gap: 1.5em;padding: 1.5rem;background-color: #eee;border-radius: 5px;}.media__image { /* 图片元素样式 */align-self: start;}.media__body { /* 正文元素样式 */overflow: auto;margin-block-start: 0;}.media__body > h4 { /* 正文中的标题样式 */margin-block-start: 0;color: #333;}
}
注意,此时无需使用太多后代选择器。虽然图片是媒体模块的一个子元素,也可以写作 .media > .media__image
,但大可不必;因为样式类 media__image
已经包含了模块名称,足以确保该名称是唯一的。
而正文区中的标题直接用到了后代选择器。其实也可以使用 media__title
这样的样式类(或者写作 media__body__title
,以完整表示出其在整个模块层级中的位置),但是大部分时候没必要这么写。本例中的 <h4>
标签已经足够语义化来表明它是媒体模块的标题了。只是这样一来,标题就不能用其他级别的 HTML 标签(如 <h3>
或 <h5>
)了。如果不希望限制得这么死板,可以改为样式类来选中元素。相关 HTML 标记如代码清单 9.9 所示:
代码清单 9.9 媒体模块的 HTML 标记
<div class="media"><img class="media__image" src="runner.png"alt="Silhouette of a person running"><!-- 图片子元素 --><div class="media__body"><!-- 正文子元素 --><h4>Strength</h4><!-- 标题子元素 --><p>Strength training is an important part ofinjury prevention. Focus on your core—especially your abs and glutes.</p></div>
</div>
这是一个多功能模块,可以在各种尺寸的容器内生效,并随着容器宽度的变化进行自适应调整。正文也可以包含多个段落,或者使用不同尺寸大小的图片(可以考虑给图片设置一个 max-width
属性,以避免挤占正文区域)。
9.1.4.1 同时使用变体和子元素 Using variants and subelements together
我们也可以创建模块的变体形式。例如,可以很轻松地将图片从左浮动改为右浮动(如图 9.4 所示)。
【图 9.4 把媒体模块的图片改到右侧渲染】
变体样式类 media--right
就可以实现上述效果,将该类名添加到媒体模块的 div
主容器上(即 <div class="media media--right">
)即可;然后通过类名选中图片并令其浮动方式为向右浮动。
将该修饰符样式类添加到 HTML 对应元素上后,再根据代码清单 9.10 更新 modules
模块图层中的样式,即可看到最终效果。
代码清单 9.10 为媒体模块定义一个向右浮动的变体形式
@layer modules {.media--right {flex-direction: row-reverse;}
}
上述代码将颠倒各弹性元素的排列顺序,将图片置于容器右侧。
在某些情况下,变体形式的模块样式可能涉及子元素样式的调整,此时可以通过后代选择器(descendant selectors)实现,例如 .media--right .media__image
。
9.1.4.2 避免在模块选择器中使用通用标签名 Avoiding generic tag names in module selectors
我们在媒体模块中使用了选择器 .media__body > h4
来选中标题元素,这样写是说得通的,因为 <h4>
标签本就是用来标识一个次级标题的。对于包含列表项的模块这种写法同样适用,相比为列表中的各列表项分别添加一个 menu__item
的样式类,通过 .menu > li
来进行匹配则要简单很多,尽管该写法尚存争议。
我们应该避免使用过于通用的 HTML 标签来选中目标元素,尤其是 div
元素和 span
元素。类似于 .page-header > span
的选择器太宽泛了。一开始创建模块的时候,可能只是用 span
标签实现了某一效果,但谁能保证后续不会出于其他考虑再加一个 span
呢?到最后才给 span
元素追加类名就比较棘手了,因为需要在 HTML 中找出所有用到过该模块的位置,并全部修改一遍。
P.S.:相信通过本节的学习,您也可以跟我一样,对目前市面上常见的 CSS 框架中的类名的写法有更为清晰的认识,比如 Bootstrap 中的很多前端组件样式,就是用的 BEM 命名约定:定义一个 success 按钮,其样式类就是一个 btn 模块,写作
btn btn-success
;再比如,从阿里图标库定制某些图标效果时也会使用类似的声明方式。将这些具体场景和本篇知识点结合,就能更好地理解 CSS 模块化的用法与注意事项。
关于《CSS in Depth》(中译本书名《深入解析 CSS》)
第 1 版 | 第 2 版 | |
---|---|---|
读者评分 | 原版:4.7(亚马逊);中文版:9.3(豆瓣) | 原版:5.0(亚马逊);中文版:暂无,待出版 |
出版时间 | 原版:2018 年 3 月;中文版:2020 年 4 月 | 原版:2024 年 7 月;中文版:暂无,待出版 |
原价 | 原版:$44.99;中文版:¥139.00 | 原版:$59.99;中文版:暂无,待出版 |
现价 | 原版:$36.49;中文版:¥52.54 起步 | 原版:$52.09;中文版:暂无,待出版 |
原版国内预订 | 起步价 ¥461.00 | 起步价 ¥750.00 |
本专栏为该书第 2 版高分译文专栏,全网首发,精译精校,持续更新,计划今年内完成全书翻译,敬请期待!!!
目前已完结的章节(可进入本专栏查看详情,连载期间完全免费):
- 第一章 层叠、优先级与继承(已完结)
- 1.1 层叠
- 1.2 继承
- 1.3 特殊值
- 1.4 简写属性
- 1.5 CSS 渐进式增强技术
- 1.6 本章小结
- 第二章 相对单位(已完结)
- 2.1 相对单位的威力
- 2.2 em 与 rem
- 2.3 告别像素思维
- 2.4 视口的相对单位
- 2.5 无单位的数值与行高
- 2.6 自定义属性
- 2.7 本章小结
- 第三章 文档流与盒模型(已完结)
- 3.1 常规文档流
- 3.2 盒模型
- 3.3 元素的高度
- 3.4 负的外边距
- 3.5 外边距折叠
- 3.6 容器内的元素间距问题
- 3.7 本章小结
- 第四章 Flexbox 布局(已完结)
- 4.1 Flexbox 布局原理
- 4.2 弹性子元素的大小
- 4.3 弹性布局的方向
- 4.4 对齐、间距等细节处
- 4.5 本章小结
- 第五章 网格布局(已完结)
- 5.1 构建基础网格
- 5.2 网格结构剖析 (上)
- 5.2.1 网格线的编号(下)
- 5.2.2 网格与 Flexbox 配合(下)
- 5.3 两种替代语法
- 5.3.1 命名网格线
- 5.3.2 命名网格区域
- 5.4 显式网格与隐式网格(上)
- 5.4.1 添加变化 (中)
- 5.4.2 让网格元素填满网格轨道(下)
- 5.5 子网格(全新增补内容)
- 5.6 对齐相关的属性
- 5.7 本章小结
- 第六章 定位与堆叠上下文(已完结)
- 6.1 固定定位
- 6.1.1 创建一个固定定位的模态对话框
- 6.1.2 在模态对话框打开时防止屏幕滚动
- 6.1.3 控制定位元素的大小
- 6.2 绝对定位
- 6.2.1 关闭按钮的绝对定位
- 6.2.2 伪元素的定位问题
- 6.3 相对定位
- 6.3.1 创建下拉菜单(上)
- 6.3.2 创建 CSS 三角形(下)
- 6.4 堆叠上下文与 z-index
- 6.4.1 理解渲染过程与堆叠顺序(上)
- 6.4.2 用 z-index 控制堆叠顺序(上)
- 6.4.3 深入理解堆叠上下文(下)
- 6.5 粘性定位
- 6.6 本章小结
- 第七章 响应式设计(已完结)
- 7.1 移动端优先设计原则(上篇)
- 7.1.1 创建移动端菜单(下篇)
- 7.1.2 给视口添加 meta 标签(下篇)
- 7.2 媒体查询(上篇)
- 7.2.1 深入理解媒体查询的类型(上篇)
- 7.2.2 页面断点的添加(中篇)
- 7.2.3 响应式列的添加(下篇)
- 7.3 流式布局
- 7.4 响应式图片
- 7.5 本章小结
- 第八章 层叠图层及其嵌套
- 8.1 用 layer 图层来操控层叠规则(上篇)
- 8.1.1 图层的定义(上篇)
- 8.1.2 图层的顺序与优先级(下篇)
- 8.1.3 revert-layer 关键字(下篇)
- 8.2 层叠图层的推荐组织方案
- 8.3 伪类 :is() 和 :where() 的用法
- 8.4 CSS 嵌套的使用
- 8.4.1 嵌套选择器的使用
- 8.4.2 深入理解嵌套选择器
- 8.4.3 媒体查询及其他 @规则 的嵌套
- 8.5 本章小结
Nicole Sullivan 既是 媒体模块 概念的提出者之一,同时也是 OOCSS(Object-Oriented CSS)和 SMACSS(Scalable and Modular Architecture for CSS)两种 CSS 方法论的重要人物之一,媒体模块这个概念最早是 OOCSS 设计模式的一部分,旨在通过面向对象的方式组织 CSS 代码,强调结构和表现分而治之,以便于样式的复用与后期维护。 ↩︎