《深入解析CSS》 |模块化CSS
引入
掌握浏览器如何渲染CSS很重要,了解如何在项目中编写和组织CSS也很重要。组织CSS代码使其更易于理解和维护。
模块化CSS(Modular CSS)是指把页面分割成不同的组成部分,这些组成部分可以在多种上下文中重复使用,并且互相之间没有依赖关系。最终目的是:当我们修改其中一部分CSS时,不会对其他部分产生意料之外的影响。
我们把样式表的每个组成部分称为模块(module),每个模块独立负责自己的样式,不会影响其他模块内的样式。也就是说,在CSS里引入了软件封装的原则。CSS中没有数据和传统函数的概念,但是有选择器及其命中的页面元素。为了达到封装的目的,这些会成为模块的组成部分,并且每个模块都只负责少量的DOM元素的样式。
封装(encapsulation)——相关的函数和数据集合在一起组成对象,通常用来隐藏结构化对象内部的状态或值,从而使外部因素不能操作对象内部。
有了封装的思想,我们就可以为页面上那些彼此分立的组件定义模块了。像导航菜单、对话框、进度条、缩略图,等等。可以通过为DOM元素设置一个独一无二的的类名来识别每个模块。同时,每个模块包含一系列子元素,构建成页面上的组件。模块内部可以嵌套其他模块,最终构成完整的页面
基础样式
开始写模块化样式之前,需要先配置好环境。每个样式表的开头都要写一些给整个页面使用的通用规则,这些规则通常被称为基础样式。其他的样式是构建在基础样式之上的。基础样式本身并不是模块化的,但它会为后面编写模块化样式打好基础:
/*覆盖盒模型*/
:root {
box-sizing: border-box;
}
*,*::before,*::after {
box-sizing: inherit;
}
/*默认字号与字体*/
body {
font-family: Helvetica, Arial, sans-serif;
margin: 0;
}
其他常用的基础样式还包括链接的颜色、标题的样式、外边距等。<body>
标签默认的外边距很小,你可能会考虑将它的外边距去掉。根据项目的实际情况,你也可能想为表单字段、表格和列表等添加一些样式。
- 基础样式应该是通用的,只添加那些影响页面上大部分或者全部内容的样式
- 选择器应只用标签类型或者偶尔用用伪类选择器
- 基础样式提供一些默认的渲染效果,之后可以很方便地根据需要覆盖基础样式
- 基础样式配置完成以后很少会再修改。会在基础样式之上构建模块化CSS
- 基础样式后面的内容将主要由各种模块组成
创建模块
下面来创建一个短消息通知的模块。每个模块都需要一个独一无二的名称,我们把这个模块叫作“message”:
使用一个类名为message的div标记该模块
<div class="message">
Save successful
</div>
.message{
padding:0.8em 1.2em;
border-radius:0.2em;
border:1px solid #265559;
color:#265559;
background-color:#e0f0f2
}
模块的选择器由单个类名构成
这很重要。对比一下,如果使用一个类似于#sidebar .message的选择器,就意味着这个模块只能用在#sidebar元素内部。现在没有这些约束,模块就可以在任意上下文中重复使用
创建模块不但可以精简代码(减少重复),还可以保证视觉一致性。这样看上去更专业,不会给人仓促堆砌的感觉。用户在潜意识里也会更容易相信我们的应用程序
模块的变体
保持一致性确实不错,但有时候需要特意避免一致。对于这个短消息模块,比如需要显示一条报错的消息,这时候应该使用红色而不是之前的蓝绿色。这可以通过定义修饰符来实现
通过定义一个以模块名称开头的新类名来创建一个修饰符。例如,消息模块的error修饰符应该叫作message-error。通过包含模块名称,可以清楚地表明这个类属于消息模块。下面我们为模块创建三个修饰符:成功、警告和错误:
/*基础消息模块*/
.message{
padding:0.8em 1.2em;
border-radius:0.2em;
border:1px solid #265559;
color:#265559;
background-color:#e0f0f2
}
/*1.成功*/
.message-success{
color:#2f5926;
border-color:#2f5926;
background-color:#cfe8c9
}
/*2.警告*/
.message-warning{
color:#594826;
border-color:#594826;
background-color:#e8dec9
}
/*3.错误*/
.message-error{
color:#59262f;
border-color:#59262f;
background-color:#e8c9cf
}
修饰符的样式不需要重新定义整个模块,它覆盖要改变的部分:
<div class="message message-error">
密码错误
</div>
按钮模块及其变体
下面将实现一个按钮模块,其中包含大小和颜色选项的变体。可以用不同的颜色为按钮添加视觉意义。绿色代表积极的行为,比如保存和提交表单;红色意味着警告,有利于防止用户不小心点击取消按钮:
/*基础按钮模块*/
.button {
padding: 0.5em 0.8em;
border: 1px solid #265559;
border-radius: 0.2em;
background-color: transparent;
font-size: 1rem;
}
/*1.成功的颜色变体*/
.button--success {
border-color: #cfe8c9;
color: #fff;
background-color: #2f5926;
}
/*2.危险的颜色变体*/
.button--danger {
border-color: #e8c9c9;
color: #fff;
background-color: #a92323;
}
/*1.小号字体变体*/
.button--small {
font-size: 0.8rem;
}
/*2.大号字体变体*/
.button--large {
font-size: 1.2rem;
}
<button class="button button--large">Read more</button>
<button class="button button--success">Save</button>
<button class="button button--danger button--small">Cancel</button>
双连字符
双连字符的写法可能看起来有点儿多余,但当我们开始创建名称很长的模块的时候,比如导航菜单或者文章摘要,好处就显现出来了。
为这些模块添加修饰符后,类名将如nav-menu--horizontal或者pull-quote--dark。双连字符的写法很容易区分哪部分是模块名称,哪部分是修饰符。nav-menu--horizontal和nav--menu-horizontal分别代表了不同的含义。这样一来,即使项目里有很多名称相似的模块,也很容易分辨它们。
不要使用依赖语境的选择器
假设我们正在维护一个网站,里面有浅色调的下拉菜单。有一天老板说,网页头部的下拉菜单需要改成带白色文本的深色调。
如果没有模块化CSS,我们可能会使用类似于.page-header.dropdown
的选择器来覆盖dropdown类
提供的默认颜色。
现在要写模块化CSS,这样的选择器是严格禁用的。虽然上面写法可以解决需求,但接下来可能会带来很多问题。下面我们来分析一下:
第一,我们必须考虑把这段代码放在哪里,是和网页头部的样式放在一起,还是跟下拉菜单的样式放在一起?如果我们添加太多类似的单一目的的规则,样式之间毫无关联,到最后样式表会变得杂乱无章。并且,如果后面需要修改样式,你还能想起来它们放在哪里吗?
第二,这种做法提升了选择器优先级。当下次需要修改代码的时候,我们需要满足或者继续提升优先级。
第三,后面可能需要在其他场景用到深色的下拉列表。刚才创建的这个下拉列表是限定在网页头部使用的。如果侧边栏也需要同样的下拉列表,我们就得为该规则集添加新的选择器来匹配两个场景,或者完整地复制一遍样式
第四,重复使用这种写法会产生越来越长的选择器,将CSS跟特定的HTML结构绑定在一起。例如,如果有个#products-page.sidebar .social-media div:first-child h3这样的选择器,样式集就会和指定页面的指定位置紧紧耦合。
这些问题是开发人员处理CSS的时候遭受挫折的根源。使用和维护的样式表越长,情况越糟。新样式需要覆盖旧样式时,选择器优先级会持续提升。到后面不知不觉地就会发现,我们写了一个选择器,其中包含两个ID和五个类名,只是为了匹配一个复选框。
在样式表中,元素被各种彼此不相关的选择器匹配,这样很难找到它使用的样式。理解整个样式表的组织方式变得越来越困难,你搞不明白它是怎样把页面渲染成这样的。搞不懂代码就意味着bug变得常见,可能很小的改动就会弄乱大片的样式。删除旧代码也不安全,因为你不了解这段代码是干什么的,是否还在用。样式表越长,问题就愈发严重。
模块化CSS就是要尝试解决这些问题。当模块需要有不同的外观或者表现的时候,就创建一个可以直接应用到指定元素的修饰符类。比如,写.dropdown--dark
,而不是写成.page-header .dropdown
。通过这种方式,模块本身,并且只能是它本身,可以决定自己的样式表现。其他模块不能进入别的模块内部去修改它。这样一来,深色下拉列表并没有绑定到深层嵌套的HTML结构上,也就可以在页面上需要的地方随意使用。
千万不要使用基于页面位置的后代选择器来修改模块。坚决遵守这个原则,就可以有效防止样式表变成一堆难以维护的代码。
多元素模块
有时模块需要由多个元素组成。比如我们不可能只靠一个元素实现下拉菜单或者模态框。下面来创建一个更复杂的模块。这是一个媒体对象,这个模块由四个元素组成:div容器、容器包含的一张图片、标题、正文:
/*主容器*/
.media {
padding: 1.5em;
background-color: #eee;
border-radius: 0.5em;
}
/*清除浮动*/
.media::after {
content: "";
display: block;
clear: both;
}
/*图片子元素*/
.media__image {
float: left;
margin-right: 1.5em;
}
/*正文子元素*/
.media__body {
overflow: auto;
margin-top: 0;
}
/*正文内的标题*/
.media__body > h4 {
margin-top: 0;
}
<div class="media">
<img class="media__image" src="01.jpeg">
<div class="media__body">
<h4>Strength</h4>
<p>
Strength training is an important part of
injury prevention. Focus on your core—
especially your abs and glutes.
</p>
</div>
</div>
效果如图:
-
创建模块的变体。比如把图片从左浮动改成右浮动
.media--right>.media__image{ float: right; }
<div class="media media--right"> ... </div>
-
避免在模块选择器中使用通用标签名
我们在媒体模块中使用了选择器
.media__body > h4
来匹配标题元素。这么做是允许的,因为<h4>
标签就是用来标识一个次要标题的。同样的方式也可以用在带列表的模块上,相比为列表里的每个项目都添加menu__item类名,使用menu > li
匹配菜单项简单多了我们应该避免使用基于通用标签类型的匹配,类似于
.page-header >span
的选择器太宽泛了。最初建立模块的时候,可能只是用span标签做一件事,但谁也说不准以后会不会出于其他目的再添加第二个span。后面再为span追加类名就比较麻烦了,因为我们需要在HTML标记中找到所有用到模块的地方,全部改一遍。
把模块组合成更大的结构
每个模块应该只做一件事情,用名称简洁明了地概括出它们的目标。
有的模块是为了版面布局,有的是为了编写体例。当模块想要完成不只一件事的时候,我们应该考虑把它拆分成更小的模块。做一个下拉菜单来演示一下:
当我们需要使用并来描述模块职责的时候,思考一下是不是在描述两种甚至更多的职责。如果是,则需要为每个职责分别定义模块。这是模块封装的一个非常重要的原则,我们把它叫作单一职责原则。尽可能把多种功能分散到不同的模块中,这样每个模块就可以保持精炼、聚焦,并且容易理解。
拆分不同模块的职责
-
第一个模块叫作下拉,其中包含一个控制容器可见性的按钮。换句话说,这个模块负责展示和隐藏容器。
我们也可以描述按钮的外观和代表行为的小三角来阐述模块的细节,这些细节都是从属于首要职责的,因此可以描述
-
第二个模块叫作菜单,是放置链接的列表。把菜单模块的一个实例放入下拉模块的容器内,就可以构成完整的界面。
用两个模块构造一个下拉菜单
<!-- 下拉模块 -->
<div class="dropdown">
<!-- 下拉的触发按钮 -->
<button class="dropdown__toggle">Main Menu</button>
<!-- 抽屉子元素 -->
<div class="dropdown__drawer">
<!-- 菜单模块 -->
<ul class="menu">
<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="text/javascript">
(function () {
var toggle = document.querySelector('.dropdown__toggle');
// 点击按钮触发类
toggle.addEventListener('click', function (event) {
event.preventDefault();
var dropdown = event.target.parentNode;
dropdown.classList.toggle('is-open');
});
}());
</script>
这里使用了双下划线标记,表示触发器和抽屉是下拉模块的子元素。点击触发器可以显示或者隐藏抽屉元素。JavaScript代码为下拉模块的主元素添加或者移除is-open类,以此来实现这个功能。
/*下拉模块*/
.dropdown {
display: inline-block;
position: relative;
}
.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;
}
/* 初始时隐藏抽屉 */
.dropdown__drawer {
display: none;
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;
}
/* 触发isopen类显示 */
.dropdown.is-open .dropdown__drawer {
display: block;
}
/* 菜单模块 */
.menu {
padding-left: 0;
margin: 0;
list-style-type: none;
border: 1px solid #999;
}
.menu > li + li {
border-top: 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;
}
在模块里使用定位
这是我们第一个使用定位的模块,其中创建了模块自己的包含块(主元素的position:relative)。绝对定位的元素(抽屉元素和::after伪元素)就是基于同一个模块内的位置来定位的。应该尽量让需要定位的元素关联到同一个模块内的其他元素。只有这样,我们把模块放在另一个有定位的容器里的时候,才不会弄乱样式。
状态类
is-open类在下拉模块中有特定的用途。我们在模块里使用JavaScript动态地添加或移除它。它也是状态类(state class)的一个示例,因为它代表着模块在当前状态下的表现。按照惯例,状态类一般以is-或者has-开头。这样状态类的目的就会比较明显,它们表示模块当前状态下的一些特征或者即将发生的变化。再举一些状态类的示例,比如is-expanded、is-loading或者has-error等。这些状态类具体会表现成什么样子取决于使用它们的模块。
菜单模块
每个<li>
都是菜单模块的子元素,所以没必要为每个元素添加双下划线类,直接使用后代选择器.menu > li
已经足够明确了。菜单模块是完全独立的,不依赖于下拉模块。这使得代码更简单,因为我们不需要理解在这个模块之前先搞懂另一个,也有助于更加灵活地复用模块。
预处理器和模块化CSS
所有的预处理器(比如Sass或者LESS)都提供了把分散的CSS文件合并成一个文件的功能。
我们可以用多个文件和多个目录来组织样式,最后提供一个文件给浏览器。这样可以减少浏览器发起的网络请求数,开发者也可以把代码文件拆分成易于维护的大小。
如果你正好在使用某种预处理器,强烈建议把CSS里的每个模块都放在各自对应命名的文件里,并按实际需要将这些文件组织到不同目录中。然后创建一个主样式表,引入所有的模块。这样一来,你想修改某个模块时就不必到一个冗长的样式表里面搜索了,因为很清楚去哪儿找它。
你可以创建一个main.scss文件,里面只包含@import语句,如下所示:
预处理器会从base.scss中引入基础样式,并从每个模块文件引入相应的模块样式,然后输出一个包含所有样式的样式表文件。这样每个模块都单独拥有一个便于维护的文件。
模块命名
模块的命名应该有意义,无论使用场景是什么。同时也要避免使用简单地描述视觉效果的名称。把这个模块叫作“带图片的灰盒子”看上去比较通用一些,但是如果之后要改成浅蓝色背景呢?或者重新设计网站呢?这样的名称就不能用了
我们应该换一种思路,思考模块代表什么含义。“媒体模块”这个名称就很恰当,它代表了一种图文混排的版式。
模块要适用于各种不同场景,而其名称应该简单易记。当网站有很多页面的时候,我们可能会多次用到某个模块。
目前,已经实现了消息模块、媒体模块、下拉模块和菜单模块。一些比较好的模块名称包括面板(panel)、警告(alert)、可折叠的部分(collapsible-section)、表单控制项(form-control)等。如果从一开始就对网站的整体设计有全面的了解,会有助于命名。例如,你可能觉得有两个UI元素都可以叫作板块(tile),然而它们毫不相关,这时候就应该更明确地命名它们(比如媒体板块和标题板块)。
有些人强制使用两个词来命名每个模块,这样就可以避免模块指代不明确。为模块的变体类命名的时候,应该遵守同样的原则。例如,如果已经有按钮模块了,就不应该使用button--red和button--blue命名红色和蓝色变体子类。网站设计在将来有可能会改变,你不知道这些按钮的颜色会不会也跟着变化。应该使用一些更有意义的名称,比如button--danger和button--success。
工具类
有时候需要用一个类来对元素做一件简单明确的事,比如让文字居中、让元素左浮动,或者清除浮动。这样的类被称为工具类(utility class)。从某种意义上讲,工具类有点像小号的模块。工具类应该专注于某种功能,一般只声明一次。工具类是唯一应该使用important注释的地方。事实上,工具类应该优先使用它。这样的话,不管在哪里用到工具类,都可以生效。
/*在容器内实现文字居中*/
.text-center {
text-align: center !important;
}
/*左浮动*/
.float-left {
float: left;
}
/*清除浮动*/
.clearfix::before,
.clearfix::after {
content: " ";
display: table;
}
.clearfix::after {
clear: both;
}
/*隐藏某个元素*/
.hidden {
display: none !important;
}
CSS方法论
目前,在模块化CSS的基础上发展建立了一些新的方法论。这些方法论并不是以任何库或者技术的形式出现的,但确实为开发者组织CSS代码提供了一些引导。
这些实践对于CSS领域具有里程碑意义。值得花时间研究一下其中比较重大的几个。有的比较简单,只提供了一些编码指导;有的比较严格,硬性规定了样式代码的组织形式。每种方法论都有自己的术语和命名规范,但最终都是为了实现CSS模块化。
- OOCSS——面向对象的CSS,由Nicole Sullivan创建。
- SMACSS——可扩展的、模块化CSS架构,由JonathanSnook创建。
- BEM——块(Block)、元素(Element)和修饰符(Modifier),由Yandex公司提出。
- ITCSS——倒三角形CSS,由Harry Roberts创建。
OOCSS仅是基于一些引导原则,ITCSS对类的命名和样式归类有明确的规则,SMACSS和BEM则介于两者之间。
SMACSS增加了布局样式的部分,用来处理页面主要区域的布局(侧边栏、页脚、网格系统等)。ITCSS则进一步将类别分为七个层。