文章目录
《CSS in Depth》新版封面
9.2 把模块组合成更大的结构 Modules composed into larger structures
每个模块都应该各司其职。消息模块的职责是让消息提示醒目;媒体模块的职责是在一段文本中配置一张图片。我们可以简洁明了地概括出它们的目标:有的是为了版面布局,有的则是出于整体风格的考虑。当模块试图完成的核心任务不止一个时,就应该考虑将其拆分为更小的模块。
为此,本节将实现一个下拉菜单来演示说明(如图 9.5 所示)。该菜单与第六章(6.3.1 小节)里创建的版本类似。
【图 9.5 下拉菜单效果图】
创建模块之前应该先问问自己:“从更高的层面上看,这个模块的职责是什么?”对于本例,您的回答可能是这样的:“用按钮触发下拉菜单并让菜单项由上至下堆叠排列。”
就这个场景而言,这样的回答还算是个恰如其分的描述。但我有一个经验法则:“如果在描述模块职责时必须使用 和 这个词,那么当下很可能是在描述多项职责。”那它究竟是要触发菜单,还是要堆叠排列菜单项呢?
当我们需要用 和(and) 来描述模块职责时,不妨思考一下是否在描述两种(甚至更多的)职责;当然也有可能不是,毕竟这条经验并非放之四海而皆准的。但如果答案是肯定的,就需要为每个职责分别定义模块。这是模块封装的一个非常重要的原则,也叫做 单一职责原则(Single Responsibility Principle)。要尽可能把多种职责分配到不同的模块中,这样每个模块就可以确保小巧精炼、聚焦功能点并且易于理解。
9.2.1 模块中多个职责的拆分 Dividing multiple responsibilities among modules
本节演示通过两个不同的模块创建下拉菜单的方法。第一个模块可以叫作 下拉(dropdown) 模块,其中包含一个控制容器可见性的按钮元素。换言之,该模块负责显示与隐藏容器。我们也可以描述按钮外观以及指示操作行为的小三角形符号。阐明模块的细节虽然会用到 和(and) 的字眼,但它们都从属于首要职责,因此这么处理是没问题的。
第二个模块叫 菜单(menu) 模块,用于放置链接列表。将菜单模块的一个实例放入下拉模块的容器内,就可以构建出完整的页面了。
将代码清单 9.11 中的样式添加到页面中。这段代码主干是一个下拉模块,其中包含菜单模块。代码中还有一小段 JavaScript
脚本,用于在点击切换按钮时实现菜单的开关功能。
代码清单 9.11 用两个模块构造一个下拉菜单
<div class="dropdown">
<button class="dropdown__toggle">Main Menu</button><!-- 下拉菜单的切换按钮 -->
<div class="dropdown__drawer"><!-- drawer 抽屉子元素作菜单容器 -->
<ul class="menu"><!-- 位于 drawer 元素内部的菜单模块 -->
<li><a href="/">Home</a></li>
<li><a href="/coffees">Coffees</a></li>
<li><a href="/brewers">Brewers</a></li>
<li><a href="/specials">Specials</a></li>
<li><a href="/about">About us</a></li>
</ul>
</div>
</div>
<script type="module">
var toggle = document.querySelector('.dropdown__toggle');
toggle.addEventListener('click', function (event) { // 点击切换按钮时切换 is-open 样式类
event.preventDefault();
var dropdown = event.target.parentNode;
dropdown.classList.toggle('is-open');
}
);
</script>
这里使用了双下划线标记来表明 toggle
和 drawer
同为下拉模块的子元素。点击切换按钮将显示或隐藏 drawer
抽屉元素。JavaScript
代码为下拉模块的主元素添加或移除 is-open
样式类,以此来实现下拉效果:按下切换按钮时变为 <div class="dropdown is-open">
;再次点击则恢复为 <div class="dropdown">
。
下拉模块的样式如代码清单 9.12 所示,将其添加到样式表中。这些样式跟第六章中的演示的版本类似,只是为了符合双下划线标记的类名约定更新了相关类名。这样就实现了下拉效果,只是其中的菜单项还没有指定样式。
代码清单 9.12 定义下拉模块样式
@layer modules {
.dropdown {
display: inline-block;
position: relative; /* 为绝对定位的 drawer 抽屉元素建立一个包含块 */
}
.dropdown__toggle {
padding: 0.5em 2em 0.5em 1.5em;
border: 1px solid #ccc;
font-size: 1rem;
background-color: #eee;
}
.dropdown__toggle::after {
content: "";
position: absolute;
right: 1em;
top: 1em;
border: 0.3em solid;
border-color: black transparent transparent; /* 利用边框样式绘制三角形(详见第 6 章) */
}
.dropdown__drawer {
display: none; /* 初始隐藏 drawer 元素,出现 is-open 类时再恢复显示 */
position: absolute;
left: 0;
top: 2.1em;
min-width: 100%;
background-color: #eee;
}
.dropdown.is-open .dropdown__toggle::after {
top: 0.7em;
border-color: transparent transparent black; /* 下拉框打开时翻转三角形 */
}
.dropdown.is-open .dropdown__drawer {
display: block; /* 初始隐藏 drawer 元素,出现 is-open 类时再恢复显示 */
}
}
上述代码中,主元素设置了相对定位,由此为抽屉元素创建了一个包含块(containing block),接着在包含块内设置绝对定位。切换按钮也指定了一些样式,包括 ::after
伪元素中的三角形效果;添加 is-open
样式类后,抽屉元素又重新显示并同时翻转三角形标记。
这段代码共计 35 行,涉及不少内容,但还不至于在使用模块时毫无头绪。当后续需要修改模块时,就会发现模块越小越好,这样就能快速定位到问题所在。
9.2.1.1 CSS 模块中的定位技巧 Positioning in a module
该下拉模块是我们使用定位的首个 CSS 模块,其中创建了模块自己的包含块(即主元素上的 position: relative
声明)。绝对定位的元素(即 drawer
元素与 ::after
伪元素)就是基于同一模块内的位置来定位的。
我们应该尽量让需要定位的元素关联到同一模块内的其他元素,这样当把模块放到另一个设置了定位的容器内时,才不致于弄乱既定样式。
9.2.1.2 关于状态类 State classes
注意,样式类 is-open
在下拉模块中有特定的用途。我们在模块中使用 JavaScript
动态添加或移除这个类。它也是 状态类(state class) 的一个示例,因为它代表着模块在当前状态下的表现。
根据惯例,状态类一般是以 is-
或 has-
开头。这样状态类的目的就会比较明显,它们表示模块当前状态下的一些特征或者即将发生的变化。类似的例子还有 is-expanded
、is-loading
以及 has-error
等。这些状态类的具体表现取决于使用它们的模块。
9.2.1.3 菜单模块 The menu module
实现了下拉模块,下面来处理菜单模块。我们无需关心下拉菜单的打开和关闭,相关功能已经在下拉模块中实现了。菜单模块只需要实现各菜单项的样式即可。
对应的样式如代码清单 9.13 所示。将其添加到您的样式表中。
代码清单 9.13 菜单模块的样式
@layer modules {
.menu {
margin-block: 0;
padding-inline-start: 0;
list-style-type: none; /* 移除浏览器默认的列表项样式 */
border: 1px solid #999;
}
.menu > li + li {
border-block-start: 1px solid #999; /* 在各列表项链接间添加一条边框 */
}
.menu > li > a { /* 扩大链接的可点击区域 */
display: block;
padding: 0.5em 1.5em;
background-color: #eee;
color: #369;
text-decoration: none;
}
.menu > li > a:hover { /* 给鼠标悬停添加高亮效果 */
background-color: #fff;
}
}
这些样式与第六章中的下拉菜单相同,每个 <li>
都是模块的一个子元素,因此我觉得没必要为每个元素都添加一个双下划线形式的样式类,而是直接使用后代选择器 .menu > li
来实现目标。
菜单模块是完全独立的,并不依赖于下拉模块。这使得代码更加简单,因为大可不必为了理解一个模块而不得不先理解另一个模块,同时也有利于更加灵活地复用模块。
您也可以创建一个风格迥异的菜单(无论是以变体的形式还是完全独立的模块形式)并根据需求在拉下模块内部进行使用;甚至可以把菜单模块用到下拉模块以外的任意位置。我们无法预知页面后续的需求是什么,但有了可复用的样式模块,就可以确保在一定程度上提供前后一致的样式效果。
将代码组织成模块需要一定的实践经验。本例中将下拉模块与菜单模块分开实现也并没有什么不容置疑的理由——这两个模块确实是要协同工作的,并且在实际项目中估计也很少会遇到需要在下拉菜单中支持不同类型菜单的情况。
只是这样处理后,我发现它们在概念上很容易区分开。这样每个模块就变小了,而轻量小巧的模块往往更容易理解,将来也更容易修改。有时,模块也会自然而然地变大;如果拆分起来实在很困难,那就不要拆分,除非遇到特定需求再来考虑。
9.2.2 模块的命名 Naming modules
为模块命名是个很伤脑筋的问题。开发模块的时候我们可以用个临时的名称,但在您认为模块已经开发完毕之前,请务必注意规范命名。这也许算是模块化 CSS 开发中最难的部分了。
回想一下前面章节里介绍的媒体模块,我们曾用它来展示一张跑步者的图片以及跑步的小贴士,如图 9.6 所示。
【图 9.6 包含图片和跑步小贴士的 media 媒体模块效果图】
假设我们还没有为该模块命名,现在有个页面需要用到它,我们可能会叫它跑步提示模块(running-tip)。这样的命名很贴切,看上去也挺合适;但它也可能用于其他方面。如果使用同样的 UI 元素做别的事情,该如何处理?比如沿用跑步类网站的主题风格,可能需要用到一连串的模块来列出即将举办的赛事信息,这时候“跑步提示”的命名方式就显得格格不入了。
无论使用场景是什么,模块的命名都应该有意义。同时也要避免使用简单描述视觉效果的名称。
将该模块命名为“带图片的灰色盒子”(gray-box-with-image)看似通用,要是今后遇到要改成浅蓝色背景呢?或者需要重新设计网站呢?这样的命名方式显然就不好使了,到头来还得重新命名,然后再到 HTML 里所有用到它的地方进行更新替换。
我们应该换一种思路,思考模块代表的含义究竟是什么。但要真正做到这一点绝非易事。媒体模块的 media 这个命名就很恰当,它表示一种图文混排的版式,并给人以强烈的印象,同时也没有将模块局限于任何特定的场景或视觉效果。
模块要适用于各种不同的场景,因此命名应该简单好记为上。当网站有很多页面的时候,可能会多次用到某个模块。届时您可能会用这样的表述来和团队其他成员进行沟通:“这里就用 ‘媒体(media)’ 模块吧”,或者“这些 ‘板块(tiles)’ 都挤到一起了”。
至此,我们已经实现了消息模块、媒体模块、下拉模块以及菜单模块。一些比较值得推荐的模块命名包括面板(panel)、警告(alert)、可折叠部分(collapsible-section)、以及表单控件(form-control)等等。如果从一开始就对网站的整体设计有全面把控,无疑将更有利于模块的规范命名。例如,可能有两个 UI 元素都可以叫作 板块(tile),然而它们毫不相关,此时就应该分别给它们更明确的命名(比如分别叫作媒体板块(media-tile)和标题板块(headline-tile))。
有些人强制使用两个单词来命名每个模块,旨在避免模块指代不明;因为您也吃不准什么时候会需要另一个新的 tile 模块。如果现有的 tile 模块的命名较为明确,那么在命名新模块时就相对会容易些,不至于同前一个模块名混淆。
在给模块的变体类命名时,也应该遵循同样的原则。例如,如果已经有按钮模块(button module)了,就不宜使用 button--red
和 button--blue
来命名红色和蓝色的变体子类。网站设计在将来可能会改变,而您并不知道这些按钮的颜色是否会相应变化。推荐使用一些更有意义的名称,例如 button--danger
或 button--success
等。
使用 large
或 small
这样具有相对意味的词语来命名修饰符的做法并不可取,但也是可以接受的。没人说过网站重构时不能修改 button--large
的尺寸,只要它还是比标准按钮大一些就行。但一定要避免使用像 button--20px
这样的过于精确的修饰符类名。
第 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 版高分译文专栏,全网首发,精译精校,持续更新,计划今年内完成全书翻译,敬请期待!!!
目前已完结的章节(可进入本专栏查看详情,连载期间完全免费):