精读《CSS重构:样式表性能调优》

1 怎么看待重构

仅当重构能够改善架构或使代码符合编码规范时,才应进行重构。

对软件项目的代码质量负责的聪明人很可能理解重构的意义,但是不能理解的人可能会持有以下意见:

  • 花时间重写代码,却又看不到功能上的变化,既浪费时间,又浪费钱;

  • 如果代码还能正常工作,没必要修复;

  • 你应该当初就把代码写正确。

如果别人持有以上理由,而你对重构有足够的信心,我建议你重构代码,只要你能够保证开发进度,并且小心谨慎,不破坏其他功能。

不成熟的优化往往跟技术债务同样糟糕。

重构示例

2 级联

级联是浏览器决定为元素应用哪种样式的一种方法。样式根据选择器的特指度以及规则集出现的次序起作用。

2.1 选择器特指度

计算特指度时需要分析这些选择器(除了通用选择器 *)。为(a, b, c, d) 中的各个变量赋予相应的数值,就能得到特指度。

(1) 如果用 style 属性应用样式,则 a=1,否则 a=0。

(2) b 为 ID 选择器的数量。

(3) c 为类选择器、属性选择器和伪类的数量。

(4) d 为类型选择器和伪元素的数量。

示例

1
2
3
#nav-global > ul > li > a.nav-link {
color: #000000;
}

我们可以确定该选择器的特指度为(0, 1, 1, 3)

(1) 样式不是用 style 属性添加的,因此 a=0

(2) 只有 1 个 ID 选择器(#nav-global),因此 b=1

(3) 只有 1 个类选择器(.nav-link),因此 c=1

(4) 有 3 个类型选择器(ul、li 和 a),因此 d=3

怎么比较呢?最左侧的选择器特指度最高。举例来说,特指度 (1, 0, 0, 0) 高于 (0, 1, 1, 3),同理,(0, 2, 1, 3) 高于 (0, 1 ,1, 3)。

2.2 规则集顺序

如果两个声明块中的选择器特指度相同,且它们为同一元素的某个属性应用样式,那么在样式表中处于相对靠后位置的声明块中的属性优先级较高。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<head>
<title>Inline Styles and Specificity</title>
<style type="text/css">
#nav-global > ul > li > a.nav-link {
color: #ffffff;
}
#nav-global > ul > li > a.nav-link {
color: #000000;
}
</style>
</head>
<body>
<nav id="nav-global">
<ul>
<li>
<a href="#" class="nav-link">Link</a>
</li>
</ul>
</nav>
</body>
</html>

结果 color: #000000;

2.3 行内 CSS 和特指度

不管<style>块或外部样式表中的选择器有多么精确,它们都比不上为元素添加的行内样式精确。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<head>
<title>Inline Styles and Specificity</title>
<style type="text/css">
#nav-global > ul > li > a.nav-link {
color: #000000;
}
</style>
</head>
<body>
<nav id="nav-global">
<ul>
<li>
<a href="/" class="nav-link" style="color: #1200FF;">Link</a>
</li>
</ul>
</nav>
</body>
</html>

结果 color: #1200FF;

2.4 用 !important 声明覆盖级联样式

<style> 块或外部样式表中的样式,如要比其他样式(包括用 style 属性添加的行内样式)更精确,唯一的方法是在声明块中添加!important。多个块样式中,靠后优先级较高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<head>
<title>Inline Styles and Specificity</title>
<style type="text/css">
#nav-global > ul > li > a.nav-link {
color: #ffffff !important;
}
#nav-global > ul > li > a.nav-link {
color: #000000;
}
</style>
</head>
<body>
<nav id="nav-global">
<ul>
<li>
<a href="/" class="nav-link" style="color: #1200FF;">Link</a>
</li>
</ul>
</nav>
</body>
</html>

结果 color: #FFFFFF

3 编写更优质的 CSS

3.1 注释合理化

对日后的代码阅读中,有助于辅助理解。注释的内容应包括:

  • 文件内容
  • 选择器的依赖、用法等
  • 使用特定声明的原因(因为浏览器的怪癖而使用时,予以说明帮助尤为大)
  • 正被重构的、不应该继续使用的废弃样式

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* 主导航链接的样式
*
* @see templates/_navigation.html
*/

.nav-link {
padding: 4px;
text-decoration: none;
}

.nav-link:hover {
border-bottom: 4px solid #000000;

/*
* 防止因增加了4px下边框而导致元素移动
*/
padding-bottom: 0;
}

/* @deprecated */
.navigation-link {
color: #1200ff;
}

3.2 结构一致的规则集

1
2
3
4
5
6
// Recommended
selector {
property1: value;
property2: value;
property3: value;
}

tips

现在的前端项目一般都已工程化,无需手动挡压缩一行行的写,没不要,而且不便于阅读及定位查找,注释也不方便写。

用浏览器引擎前缀组织属性

常见的前缀有以下几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* 使用 Blink 或 WebKit 渲染引擎的浏览器
*/
-webkit- /* Chrome、Safari */

/*
* Gecko 渲染引擎的浏览器
*/
-moz- /* firefox */

/*
* 使用 Trident 渲染引擎的浏览器
*/
-ms- /* Internet Explorer */

示例

transform-origin 就是一个这样的属性,别忘了写原配transform-origin,并写在最后。

1
2
3
4
-ms-transform-origin: @origin;
-moz-transform-origin: @origin;
-webkit-transform-origin: @origin;
transform-origin: @origin;

tips

带前缀的 CSS 属性增加了维护成本,因为它们使得样式表急剧膨胀。为了弥补这一弱点,很多浏览器厂商转而使用选择性加入(opt-in)的功能,以便让开发人员实验最新的 CSS 属性。如果用户仍使用老式浏览器访问你的网站,你需要支持它们,然而,你也许还想继续支持带前缀的 CSS 属性。

3.3 保持选择器的简单

选择器能够做到非常精确,并不意味着它们就应该很精确。不要高度依赖页面的 HTML 结构。

示例

用于选择某一特定元素的 CSS 选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- bad 高度依赖页面的 HTML 结构 -->
<!DOCTYPE html>
<html>
<head>
<title>Keep Selectors Simple</title>
<style type="text/css">
div > nav > ul > li > a {
color: #1200ff;
}
</style>
</head>
<body>
<div>
<nav>
<ul>
<li>
<a href="./policies.html">Policies</a>
</li>
</ul>
</nav>
</div>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- good -->
<!DOCTYPE html>
<html>
<head>
<title>Keep Selectors Simple</title>
<style type="text/css">
a.nav-link {
color: #1200ff;
}
</style>
</head>
<body>
<div>
<nav>
<ul>
<li>
<a href="./policies.html" class="nav-link">Policies</a>
</li>
</ul>
</nav>
</div>
</body>
</html>
1
2
3
4
/*better */
.nav-link {
color: #1200ff;
}

高性能选择器

简单的选择器复用程度高,易于理解,而并不是因为它们更高效,虽然这一点显而易见。

  1. 从右向左匹配选择器

    浏览器从右向左匹配选择器,因此它能够忽略前面不匹配的元素。

  2. 关键选择器

    示例

    1
    2
    3
    body * {
    font-size: 12px;
    }

    正如前面讲过的,浏览器从右向左匹配元素,因此它能够及时排除与选择器不匹配的元素。选择器最右边的部分叫作关键选择器。该示例是以通用选择器 * 作为关键选择器的。

    单独使用通用选择器为所有元素(* {} )应用样式,浏览器可以很快完成渲染工作,因为它只需要匹配页面的每个元素。然而,当通用选择器与另一个选择器和结合符(祖先选择器)配合使用时,浏览器匹配合适的元素所做的工作要更多。只使用通用选择器,不要将其与结合符和其他选择器配合使用可以解决该题。

3.4 分离 CSS 和 JavaScript

为了区分 CSS 和 JavaScript 的职能,JavaScript 中用来选择元素的类和 ID,不应该再用来为元素添加样式。类似地,用 JavaScript 修改元素样式时,应该通过增加和删除类来实现。

3.4.1 在 JavaScript 中使用带前缀的类和 ID

比较简单的修改方法是,在只用于 JavaScript 的类和 ID 前添加js-。例如,如果我们要在 JavaScript 中选择与政策相关的一组选项卡,那么可以用 js-tab-group-policies 作为 ID。只用添加了 js- 前缀的类和 ID 作为 JavaScript 选择器,就可以消除 JavaScript 和 CSS 之间的依赖关系。

3.4.2 用类修改元素样式

JavaScript 修改样式通过 style 属性这种情况,不仅要在 CSS 文件中查找现有样式,还要在 JavaScript 文件中查找,徒增压力。

因此,若要修改 HTML 元素样式,可通过 JavaScrpit 为该元素增加或删除类。这样不仅可以应用合适的样式,该元素的 CSS 样式集也能跟其余的网站 CSS 合理地组织在一起。

3.5 使用类

类可根据需要复用多次,特指度低,方便覆盖,而 ID 相反,无法轻易覆盖,并且同一个网页中每个 ID 最多只能用一次。因此,对于持续变化的网站,编写 CSS 时,用类为元素增加样式更佳。

3.6 类名要有意义

因为它能表达清楚意思,看到它很容易理解给什么元素增加样式。它还具有一定的概括性。

避免使用过于模块化的类

有意义的类名,描述的是应用样式的元素,而不是为元素应用的样式。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 过于模块化的类 -->
<h1 class="font-bold uppercase blue-text margin-bottom-large no-padding">Too Many CSS Classes</h1>

<!-- 并不比行类样式更好 -->
<h2
style="font-weight: bold; text-transform: uppercase; color: #1200FF;
margin-bottom: 20px; padding: 0"
>
Too Many CSS Classes
</h2>

<!-- 不过于模块化 -->
<style>
.section-title {
color: #1200ff;
font-weight: bold;
margin-bottom: 20px;
padding: 0;
text-transform: uppercase;
}
</style>
<h2 class="section-title">Too Many CSS Classes</h2>

3.7 创建更好的盒子

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<head>
<title>Determining Dimensions with the Box Model</title>
<style type="text/css">
.example-element {
background-color: #ff0000;
border: 5px solid #000000;
display: block;
height: 150px;
margin: 5px;
padding: 10px;
width: 150px;
}
</style>
</head>
<body>
<div class="example-element"></div>
</body>
</html>

根据给元素的 box-sizing 属性赋的值,盒子尺寸的计算方式有两种。在下面两种情况中,外边距都将影响盒子周边的空间,但是计算盒子的尺寸时,不需要考虑外边距。

  1. box-sizing: content-box;

    据上面示例,盒子的尺寸为 180px 180px,因为

    1
    2
    150px 高度 + 10px padding-top + 10px padding-bottom + 5px border-top + 5px border-bottom = 180px 计算后得到的高度
    150px 宽度 + 10px padding-left + 10px padding-right + 5px border-left + 5px border-right = 180px 计算后得到的宽度
  2. box-sizing: border-box;

    据上面示例,盒子的尺寸为 150px 150px,因为

    1
    2
    3
    150px 计算后得到的高度 - 10px padding-top - 10px padding-bottom - 5px border-top - 5px border-bottom = 120px
    剔除内边距和边框,盒子的隐性高度 150px 计算后得到的宽度 - 10px padding-left - 10px padding-right - 5px border-left -
    5px border-right = 120px 剔除内边距和边框,盒子的隐性宽度

content-box 和 border-box 两者没有优劣之分,但是很多人发现 border-box 更直观,因为它描述的是包括边框在内的元素的高度和宽度,而不只是内容区域的尺寸。为了保持一致性,通常选用其中一种并坚持使用。

具体设置方法是,用通用选择器进行设置,指定盒子的类型:

1
2
3
4
5
*,
*:after,
*:before {
box-sizing: border-box;
}

4 给样式分类

代码复用是优秀架构的一项基本原则。按照样式的功能对其进行分类和使用,代码的复用方式就会变得更加明显。

4.1 通用样式

浏览器自带的默认样式表叫作浏览器默认样式,它可为 HTML 元素应用默认的样式。因为不同厂商开发的浏览器不同,所以其默认样式表的某些属性和属性值可能有所差异。

通用样式是指为各种元素的属性设置默认值的样式,否则不同的浏览器将为其应用不同的

样式。

示例:规范了<hr>元素在不同浏览器中的样式

1
2
3
4
5
6
7
8
9
/**
* 1. 增加在Firefox浏览器中应用的盒子尺寸类型。
* 2. 在Edge和IE浏览器中,显示溢出部分。
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}

tips

开源通用样式表中的很多样式,它们的最大用途体现在解决传统浏览器的兼容问题方面。最通用的样式表是 Nicolas Gallagher 和 Jonathan Neal 开发的 normalize.css

4.2 基础样式

基础样式旨在为设置更加细致的样式提供基础。遵循的基本原则是:为元素应用基础样式之外的其他样式

时,不需要重写大量基础样式就能实现设计目标。

4.2.1 定义基础样式

基础样式应该只为最笼统的使用场景设置属性和属性值。通常我们为元素设置以下基础属性:

  • color
  • font-family
  • font-size
  • font-weight
  • letter-spacing
  • line-height
  • margin
  • padding

如果你搭建的是设计非常复杂的应用类网站,那么这些样式只能满足初级需求。对可复用的组件,也许需要更复杂的样式。

你在编写基础样式时,需考虑上面列出的这些属性,但并不是任何时候都要全部设置,因为它们都继承自祖先元素(除了 margin 和 padding)。如果 margin 和 padding 应该使用继承来的属性值,则用 inherit 作为属性值。对于某种特定类型的元素,应该纳入基础样式的其他属性或伪类。

tips

利用继承

对于 color、font-family、font-size、font-weight、letter-spacing 和 line-height 属性,子元素继承自父元素,因此子元素的属性值不总是需要设置。关于这些 CSS 属性值是否继承的完整列表,请见 https://www.w3.org/TR/CSS21/propidx.html。需要指定样式的完整 HTML 元素列表,请见 https://www.w3.org/TR/html-markup/elements.html

4.2.2 文档元数据元素

记录元数据的标签包括 <head>、<title>、<base>、<link> 和 <meta>。因为它们不可见,故不能为其添加样式。

4.2.3 区块元素

区块元素包括 <address>、<article>、<aside>、<body>、<footer>、<header>、<nav> 和 <section>。该类元素通常包含其他元素,它们组成了 HTML 文档的各种区域。

考虑一下给区块元素设置以下属性:

  • color
  • font-family
  • font-size
  • font-weight
  • letter-spacing
  • line-height
  • padding

<body> 元素也许还需要设置背景色 background 属性。

示例:为区块元素设置基础样式

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
body {
background: #ffffff;
color: #333333;
font-family: Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.3;
padding: 5% 20%;
}

article,
footer,
header,
nav {
padding: 0;
}

article,
nav {
margin-bottom: 12px;
margin-top: 12px;
}

footer {
margin-top: 12px;
}

header {
margin-bottom: 12px;
}

4.2.4 标题和文本元素

标题元素包括 <h1>—<h6> 六级标题,用于定义 HTML 文档每个区域的标题。文本元素包括 <fifigure>、<fifigcaption>、<p> 和 <pre>,用来展示块状文本。

为标题和文本元素定义基础样式时,需考虑以下属性:

  • font-family
  • font-size
  • font-weight
  • letter-spacing
  • line-height
  • margin-bottom
  • margin-top

示例:标题和文本元素的基础样式

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
30
31
32
33
34
35
36
37
38
39
40
41
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: Georgia, Times, serif;
font-weight: 100;
line-height: 1.1;
margin: 0.5em 0;
}

h1 {
font-size: 36px;
}

h2 {
font-size: 24px;
}

h3 {
font-size: 21px;
}

h4 {
font-size: 18px;
}

h5 {
font-size: 16px;
}

h6 {
font-size: 14px;
}

p,
pre {
margin-bottom: 12px;
margin-top: 12px;
}

4.2.5 锚点标签元素

锚点标签为其他 HTML 文档或同一 HTML 文档的其他区域提供链接。它们常用 :link、:visited、:focus、:hover 和 :active 伪类展示状态,因此为其定义基础样式时,不要忘记这些伪类,以及顺序影响的样式覆盖。

为锚点标签及其伪类定义基础样式时,应该考虑的常用属性包括:

  • background-color
  • border
  • color
  • font-weight
  • text-decoration

tips

若从超链接元素的 :foucs 伪类中删除 outline 属性,且不在页面上以可见的形式加以提示,对于只用键盘或使用其他交互方式有别于鼠标的用户而言,严重影响网站的可用性。

示例:超链接的基础样式

1
2
3
4
5
6
7
8
9
10
11
12
a,
a:visited,
a:focus,
a:hover,
a:active {
color: inherit;
text-decoration: underline;
}

a:hover {
background-color: #ffff00;
}

4.2.6 文本语义元素

文本语义元素是指为文本提供更多含义或结构的元素。这些元素通常为行内元素,其中包括 <abbr>、<b>、<cite>、<code>、<data>、<dfn>、<em>、<i>、<kbd>、<s>、<strong>、<sub>、<sup>、<time> 和 <u> 等标签。

该类元素用来修改文本的样式,为其定义基础样式时,考虑以下属性:

  • color
  • font-family
  • font-size
  • font-weight

示例:给<code>标签定义样式

1
2
3
4
5
6
code {
color: #00ff00;
font-family: monospace;
font-weight: 500;
line-height: 1.5;
}

4.2.7 列表

列表元素包括 <ol>(有序列表)、<ul>(无序列表)和 <dl>(定义列表)元素。列表的一些应用场景包括水平导航、商品列表、社交媒体用户的个人主页等。

为有序和无序列表元素定义基础样式时,应该考虑以下属性:

  • font-family
  • font-size
  • list-style-type 或 list-style-image
  • list-style-position
  • line-height
  • margin-bottom
  • margin-top
  • padding-left

为了防止子元素的缩进,<ol> 或 <ul> 元素的 padding-left 属性值应该设置为 0。子元素 <li> 从父元素<ol>或 <ul>继承 font-family、font-size 和 line-height 属性,但是不继承 margin 或 padding 属性。

4.2.8 组合元素

组合元素(grouping element)包括 <div>、<main> 和 <span>。通常没必要为其定义基础样式,它们的样式根据具体情况用类来定义。然而,如果<main>标签用作可见的容器,最好为其设置 margin 和 padding 属性。

4.2.9 表格

需要用到的元素包括 <table>、<caption>、<colgroup>、<col>(列)、<tbody>(表格主体)、<thead>(表头)、<tfoot>(表格的页脚)、<tr>(表格行)、<td>(表格单元格)和 <th>(表头单元格)元素

<table>元素定义基础样式时,需要考虑以下属性:

  • border-collapse
  • border-spacing
  • border(border-width、border-color 和 border-style)
  • empty-cells
  • font-family
  • font-size
  • letter-spacing
  • line-height

<thead>、<tbody> 和 <tfoot> 元素定义基础样式时,需要考虑以下属性:

  • background-color
  • color
  • text-align
  • vertical-align

<th> 和 <td> 元素定义基础样式时,需要考虑以下属性:

  • background-color
  • border(border-width、border-color 和 border-style)
  • color
  • font-family
  • font-size
  • letter-spacing
  • line-height
  • text-align
  • vertical-align

综上,也许应该为表格定义类似如下样式:

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
table {
border-collapse: collapse;
border-spacing: 0;
empty-cells: show;
border: 1px solid #000000;
}

tfoot,
thead {
text-align: left;
}

thead {
background-color: #acacac;
color: #000000;
}

th,
td {
border-left: 1px solid #000000;
padding: 0.5em 1em;
}

th:first-child,
td: first-child {
border-left: none;
}

4.2.10 表单

表单元素包括<form>、<label>、<input>、<button>、<select>、<datalist>、<optgroup>、<option>、<textarea>、<output>、<progress>、<meter>、<fifieldset> 和 <legend>。为该类元素定义基础样式时,应考虑以下属性:

  • font-family
  • font-size
  • line-height
  • margin
  • padding

子元素 <legend>、<label> 和 <input> 的 font-weight、font-size 和 font-family 属性通常不同于父元素<form>,因此应该在子元素上定义这三个属性。

有一些表单元素,我们很难为其添加样式,因为很多浏览器会忽略应用于它们的属性。例如,浏览器会忽略为复选框和单选按钮的 border-color、border-width、background-color 和许多其他属性定义的样式。解决办法是,自定义复选框和单选按钮组件,隐藏表单的控制控件,并使用其他 HTML 元素实现按钮效果,但这不是基础样式所要实现的。

为表单元素定义以下样式,也许比较合理:

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
fieldset {
border: 0;
margin: 0;
padding: 0;
}
input {
display: block;
font-size: inherit;
padding: 4px;
width: 100%;
}
label {
display: block;
font-weight: 900;
margin-bottom: 6px;
padding: 4px;
}
legend {
border: 0;
color: #000000;
display: block;
font-size: 1.2em;
margin-bottom: 12px;
padding: 0 12px;
width: 100%;
}

4.2.11 图像

图像可以用 <img> 或 <picture> 标签展示。为图像元素定义基础样式时,应该考虑如下属性:

  • border
  • max-width
  • vertical-align

<img> 元素置于设置了大小的块状元素之中时,将 max-width 属性设置为父容器的 100%,能够防止图像溢出容器。

示例

1
2
3
4
5
img {
border: none;
max-width: 100%;
vertical-align: middle;
}

4.3 组件样式

可复用组件是指添加了样式的元素或元素组合利用视觉隐喻,使得用户与网站的交互更加容易。

4.3.1 定义需要实现的行为

创建可复用组件之前,思考以下问题将对你很有帮助。

  • 只有一个组件,还是有一个以上的组件组合在一起?

  • 组件是行内元素、块状元素还是其他类型(例如:组件以绝对定位的方式独立于文档

    流吗)?

创建可复用组件的过程可以简化如下:

(1) 创建组件之前,定义需要实现的行为。

(2) 保持组件样式的粒度,设置合理的默认值。

(3) 若需要重写组件组的可见样式,用容器元素将它们包起来,为该容器定义一个具有区别

度的类。

(4) 将定义元素尺寸的任务交给结构化容器。

示例:构建一个简单的选项卡组件

1
2
3
4
5
6
7
<nav>
<ul class="tab-group">
<li class="tab active"><a href="#">Tab One</a></li>
<li class="tab"><a href="#">Tab Two</a></li>
<li class="tab"><a href="#">Tab Three</a></li>
</ul>
</nav>

4.3.2 保持组件样式的粒度

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* Tab Component Styles
*/

.tab {
background-color: #f2f2f2;
border-bottom: 1px solid #eeeeee;
border-top: 1px solid #eeeeee;
bottom: -1px;
display: inline-block;
margin-left: 0;
margin-right: 0;
margin-top: 4px;
position: relative;
}

.tab:first-child {
border-left: 1px solid #eeeeee;
border-top-left-radius: 4px;
}

.tab:last-child {
border-right: 1px solid #eeeeee;
border-top-right-radius: 4px;
}

.tab.active {
background-color: #ffffff;
border-bottom: 1px solid #2196f3;
color: #000000;
}

.tab:hover {
background-color: #f9f9f9;
}

.tab > a {
color: inherit;
display: block;
height: 100%;
padding: 12px;
text-decoration: none;
width: 100%;
}

/**
* Tab Component Containers
*/

.tab-group {
border-bottom: 1px solid #eeeeee;
list-style: none;
margin: 0;
padding-left: 0;
}

4.3.3 根据需要,改写元素容器的样式

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* Horizontal Tab Groups
*/

.tab-group {
border-bottom: 1px solid #eeeeee;
}

.tab-group .tab {
border-bottom: 1px solid #eeeeee;
border-top: 1px solid #eeeeee;
bottom: -1px;
display: inline-block;
}

.tab-group .tab:first-child {
border-left: 1px solid #eeeeee;
border-top-left-radius: 4px;
}

.tab-group .tab:last-child {
border-right: 1px solid #eeeeee;
border-top-right-radius: 4px;
}

.tab-group .tab.active {
border-bottom: 1px solid #2196f3;
}

/**
* Vertical Tab Groups
*/

.tab-group-vertical {
border-left: 1px solid #eeeeee;
}

.tab-group-vertical .tab {
border-left: 1px solid #eeeeee;
border-right: 1px solid #eeeeee;
left: -1px;
display: block;
}

.tab-group-vertical .tab:first-child {
border-top: 1px solid #eeeeee;
border-top-right-radius: 4px;
}

.tab-group-vertical .tab:last-child {
border-bottom: 1px solid #eeeeee;
border-bottom-right-radius: 4px;
}

.tab-group-vertical .tab.active {
border-left: 1px solid #2196f3;
}

4.3.4 将尺寸的定义交由结构化容器

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
<!DOCTYPE html>
<html>
<head>
<title>Example: Vertical Tabs</title>
<style type="text/css">
*,
*:after,
*:before {
box-sizing: border-box;
}

body {
margin: 0;
padding: 0;
}

/**
* Tab Component Styles
*/

.tab {
background-color: #f2f2f2;
margin-left: 0;
margin-right: 0;
position: relative;
}

.tab:hover {
background-color: #f9f9f9;
}

.tab.active {
background-color: #ffffff;
color: #000000;
}

.tab > a {
color: inherit;
display: block;
height: 100%;
padding: 12px;
text-decoration: none;
width: 100%;
}

/**
* Tab Component Containers
*/

.tab-group,
.tab-group-vertical {
list-style: none;
margin: 0;
padding-left: 0;
}

.tab,
.tab-group,
.tab-group-vertical {
border-color: #eeeeee;
border-style: solid;
border-width: 0;
}

/**
* Horizontal Tab Groups
*/

.tab-group {
border-bottom-width: 1px;
}

.tab-group .tab {
border-bottom-width: 1px;
border-top-width: 1px;
bottom: -1px;
display: inline-block;
}

.tab-group .tab:first-child {
border-left-width: 1px;
border-top-left-radius: 4px;
}

.tab-group .tab:last-child {
border-right-width: 1px;
border-top-right-radius: 4px;
}

.tab-group .tab.active {
border-bottom-color: #2196f3;
border-bottom-width: 1px;
}

/**
* Vertical Tab Groups
*/

.tab-group-vertical {
border-left-width: 1px;
}

.tab-group-vertical .tab {
border-left-width: 1px;
border-right-width: 1px;
left: -1px;
display: block;
}

.tab-group-vertical .tab:first-child {
border-top-width: 1px;
border-top-right-radius: 4px;
}

.tab-group-vertical .tab:last-child {
border-bottom-width: 1px;
border-bottom-right-radius: 4px;
}

.tab-group-vertical .tab.active {
border-left-color: #2196f3;
border-left-width: 1px;
}

.tabbed-pane {
margin: 200px;
}
</style>
</head>
<body>
<nav class="tabbed-pane">
<ul class="tab-group-vertical">
<li class="tab active"><a href="#">Tab One</a></li>
<li class="tab"><a href="#">Tab Two</a></li>
<li class="tab"><a href="#">Tab Three</a></li>
</ul>
</nav>
</body>
</html>

4.4 结构化样式

结构化样式包括组件及其容器。既然需要为布局定义尺寸,我们可以用结构化样式设置尺寸,然后将其添加给组件和容器。

示例:由标题栏、侧边栏和内容区域组成的布局,当视口变小时,标题栏、侧边栏和内容区域垂直排列

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
<!DOCTYPE html>
<html>
<head>
<title>Example: Tabs</title>
<style type="text/css">
*,
*:after,
*:before {
box-sizing: border-box;
}

body {
margin: 0;
padding: 0;
}

/**
* Tab Component Styles
*/

.tab {
background-color: #f2f2f2;
margin-left: 0;
margin-right: 0;
position: relative;
}

.tab:hover {
background-color: #f9f9f9;
}

.tab.active {
background-color: #ffffff;
color: #000000;
}

.tab > a {
color: inherit;
display: block;
height: 100%;
padding: 12px;
text-decoration: none;
width: 100%;
}

/**
* Tab Component Containers
*/

.tab-group,
.tab-group-vertical {
list-style: none;
margin: 0;
padding-left: 0;
}

.tab,
.tab-group,
.tab-group-vertical {
border-color: #eeeeee;
border-style: solid;
border-width: 0;
}

/**
* Horizontal Tab Groups
*/

.tab-group {
border-bottom-width: 1px;
}

.tab-group .tab {
border-bottom-width: 1px;
border-top-width: 1px;
bottom: -1px;
display: inline-block;
}

.tab-group .tab:first-child {
border-left-width: 1px;
border-top-left-radius: 4px;
}

.tab-group .tab:last-child {
border-right-width: 1px;
border-top-right-radius: 4px;
}

.tab-group .tab.active {
border-bottom-color: #2196f3;
border-bottom-width: 1px;
}

/**
* Vertical Tab Groups
*/

.tab-group-vertical {
border-left-width: 1px;
}

.tab-group-vertical .tab {
border-left-width: 1px;
border-right-width: 1px;
left: -1px;
display: block;
}

.tab-group-vertical .tab:first-child {
border-top-width: 1px;
border-top-right-radius: 4px;
}

.tab-group-vertical .tab:last-child {
border-bottom-width: 1px;
border-bottom-right-radius: 4px;
}

.tab-group-vertical .tab.active {
border-left-color: #2196f3;
border-left-width: 1px;
}

/**
* Tab Component Containers
*/

.tabbed-pane {
display: block;
width: 100%;
}

.tabbed-pane .tab-group {
float: left;
width: 45%;
}

.tabbed-pane .tab-group:first-child {
margin-right: 5%;
}

.tabbed-pane .tab-group:last-child {
margin-left: 5%;
}

/**
* Structural Styles
*/

.global-nav {
float: left;
padding: 5% 0;
width: 10%;
}

.content {
float: left;
padding: 5%;
width: 80%;
}
</style>
</head>
<body>
<nav class="global-nav">
<ul class="tab-group-vertical">
<li class="tab"><a href="#">Home</a></li>
<li class="tab active"><a href="#">Policies &amp; Fees</a></li>
<li class="tab"><a href="#">Documents</a></li>
<li class="tab"><a href="#">Billing</a></li>
</ul>
</nav>

<main class="content">
<nav class="tabbed-pane">
<ul class="tab-group">
<li class="tab active"><a href="#">Policy One</a></li>
<li class="tab"><a href="#">Policy Two</a></li>
<li class="tab"><a href="#">Policy Three</a></li>
</ul>

<ul class="tab-group">
<li class="tab active"><a href="#">Fee One</a></li>
<li class="tab"><a href="#">Fee Two</a></li>
<li class="tab"><a href="#">Fee Three</a></li>
</ul>
</nav>
</main>
</body>
</html>

4.5 功能样式

!important 声明通过告诉浏览器,某一声明应该用于与其所在的规则集选择器相匹配的元素,而不管声明块的特指度高低,从而改写了级联样式。

功能样式是指谨慎的开发人员在定义 HTML 元素的类时为其指定的样式,或满足特定条件时,用 JavaScript 添加的样式。

示例:按下按钮隐藏某个元素

1
2
3
.hidden {
display: none !important;
}

4.6 浏览器特定样式

老的浏览器有一些怪癖,我们可以使用浏览器特定 CSS 技术

示例:IE7 的 inline-block 属性值有时无法实现预期效果解决技巧

1
2
3
4
5
.selector {
display: inline-block;
*display: inline;
zoom: 1;
}

上述代码有两个问题。其一,*display 属性不合法,因为它存在句法错误(属性名不能以 * 开始)。其二,我们为了兼容过时的浏览器,利用“黑技术”编写了存在句法错误的 CSS。如果你确实无法放弃支持需要采用“黑技术”的浏览器,那么应该将这些语句单独写到一块,并且添加注释来解释这些代码的用途。例如,如果你需要用 CSS 技巧支持老的 Internet Explorer 浏览器,那么应该将其放到单独的样式表中,并用条件注释(conditional comments)添加对该样式表的引用,只为特定版本的浏览器加载这些样式。

1
2
3
<!–-[if IE 7]>
<link rel="stylesheet" href="ie7.css" type="text/css" />
<![endif]–->

5 测试

测试 CSS 有难度,因为不同的平台、屏幕尺寸和设备都需要测试。

测试时需要考虑很多因素,其中包括以下几点:

  • 正在用什么浏览器测试网页?
  • 如何在不同的操作系统上测试各种各样的浏览器?
  • 正在多大的窗口浏览网页?
  • 如何快速测试大量网页?
  • 如何验证你所看到的效果是正确的?
  • 如果你无法获得某些设备,如何测试网站在这些设备上的效果?

5.1 需要测试的重点浏览器

理想情况下,你只需要支持多数用户访问网站所用的浏览器即可(具体的阈值因公司而异)。网站用户所使用的浏览器、设备及其版本号,可用网站分析工具获取,非常简单。

5.2 浏览器市场份额

自行查阅相关资料

5.3 Google Analytics 的浏览器统计数据和屏幕分辨率

Google Analytics 是 Google 提供的免费增值服务,它是最常用的网站分析工具之一。请参考分析工具的文档,了解更多信息。

5.4 测试多个浏览器

普遍的是人工测试。你可能想下载以下浏览器:

  • Google Chrome
  • Firefox
  • Safari
  • Microsoft Edge
  • IE

为了测试 CSS 在移动端的效果,你需要从合适的应用市场下载适合于设备的各种浏览器。

5.4.1 IOS 系统的 Safari 浏览器

要用 iOS 系统的 Safari 浏览器测试,可以使用 iOS 设备的原生应用或 Xcode 的 iOS 模拟器。你可以从 Apple App Store 免费下载 Xcode(图 5-5),但不幸的是,Xcode 只能在 Mac OS 系统上运行,无法安装到 Windows 系统。

5.4.2 安卓

安卓设备可以用 Android Studio 的模拟器测试

5.5 用开发者工具测试

主流浏览器自带开发者工具,可以帮助开发者打造更好的网站。

(1) 模拟设备尺寸

(2)) 文档对象模型(DOM)和 CSS 样式

5.6 视觉回归测试

视觉回归测试是一种测试方法,它通过比较作为基准的用户界面图像和开发过程同一用户界面的图像,来检测不符合预期的改动。

视觉回归测试技巧

  • 测试重要的点
  • 保持测试的粒度
  • 使用多种浏览器

用 Gemini 执行视觉回归测试

Gemini项目是 Yandex 团队开发的视觉回归测试工具。使用该工具,你可以编写脚本,自动截取网站在主流浏览器中的截图,然后将其与基准图像比较,不同之处将以高亮形式标记出来。

tips

除了 Gemini,还有多种视觉回归测试 工 具,其中最常用的两个是 Wraith 和 PhantomCSS。

5.7 维护你的代码

5.7.1 编码规范

5.7.2 模式库

以前的模式库(也称样式指南)是网站使用的一组用户界面模式,它展示了每种模式相关的重要信息,包括以下几点:

  • 何时(不)使用模式的指导
  • 解释模式使用方式的示例代码
  • 使用某一模式而不用另一模式的原因

tips

这里的样式指南并非指 CSS 样式,而是包含设计价值观、设计指南、组件库,可参见ant.design

  1. 优点

    • 可复用
    • 开发迅速
    • 组件汇集到一起,保证用户界面一致性
  2. 建设模式库

    tips

    模式库的更多资源

6 代码的组织和重构策略

6.1 按照样式从最不精确到最精确组织 CSS

  1. 通用样式
  2. 基础样式
  3. 组件及其容器的样式
  4. 结构化样式
  5. 功能性样式
  6. 浏览器特定样式(如果一定要)

6.2 多个文件还是一个大文件

首先我们一起看看将 CSS 传给浏览器时会发生什么,然后再一起讨论是用一个还是多个 CSS 文件。

6.2.1 提供 CSS

用户访问包含 CSS 文件(与之相对的是使用行内 CSS)的网站时,浏览器首先要请求 CSS 文件,然后将其下载下来,再解析它们并应用恰当的样式。因此,我们需要尽可能地使需要下载的 CSS 文件缩小,以便提高加载速度。

拼接指的是将多个文件合并为一个文件的过程。

压缩是指从 CSS 文件中删除所有不必要的空格、注释和换行,而不改变代码行为的过程。

6.2.2 单一的 CSS 文件

适用小型项目

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
30
31
32
33
34
35
36
37
/**
* 通用样式
* ---------------------------------------------
*/
/**
* 基础样式
* ---------------------------------------------
*/
/* 基础样式:表单 */
/* 基础样式:标题 */
/* 基础样式:图像 */
/* 基础样式:列表 */
/* 基础样式:表格 */
/* 等等 */
/**
* 组件样式
* ---------------------------------------------
*/
/* 组件样式:消息框 */
/* 组件样式:按钮 */
/* 组件样式:轮播框 */
/* 组件样式:下拉框 */
/* 组件样式:模态框 */
/* 等等 */
/**
* 结构化样式
* ---------------------------------------------
*/
/* 结构化样式:结算区域的布局 */1
/* 结构化样式:侧边栏的布局 */
/* 结构化样式:主区域的布局 */
/* 结构化样式:个人设置区域的布局 */
/* 等等 */
/**
* 功能样式
* ---------------------------------------------
*/

6.2.3 多个 CSS 文件

随着项目成长,只用一个 CSS 文件,代码维护难度可能逐渐增大,当大到难以维护时,显然需要将其拆分为多个文件。

你的项目结构看起来像下面这样:

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
30
31
32
|-css/
| |-normalizing-styles
| | |- normalize.css
| |
| |-base-styles
| | |- forms.css
| | |- headings.css
| | |- images.css
| | |- lists.css
| | |- tables.css
| | |- etc.
| |
| |-component-styles
| | |- alerts.css
| | |- buttons.css
| | |- carousel.css
| | |- dropdowns.css
| | |- modals.css
| | |- etc.
| |
| |- structural-styles
| | |- layout-checkout.css
| | |- layout-sidebar.css
| | |- layout-primary.css
| | |- layout-settings.css
| | |- etc.
| |
| |- utility-styles
| | |- utility.css
| |
| |- browser-specific-styles
| | |-ie8.css

6.3 重构前审查 CSS

可以从以下角度审查:

  • 所用到的属性列表
  • 使用某一特定属性的声明块列表
  • 使用的颜色数量
  • 使用的最高和最低特指度
  • 拥有最高和最低特指度的选择器
  • 选择器的长度

6.4 重构策略

条件允许的话,应该只对你能够维护的小块代码进行重构,并做到经常评审和发布。如果一次重构和发布大量代码,引入错误的风险更大,因为改动的内容更多。

  1. 保持规则集结构的一致性
  2. 删除僵尸代码
  3. 分离 CSS 和 JavaScript
  4. 分离基础样式
  5. 删除冗余的 ID
  6. 将 ID 转化为类
  7. 区分功能性样式
  8. 定义可复用组件样式
  9. 删除行内 CSS 和过于模块化的类
  10. 隔离面向定义浏览器的 CSS 样式

6.5 评估重构是否成功

  • 你的网站崩溃了吗
  • UI bug 数
  • 减少开发和测试时间

相关链接

[1] CSS 重构:样式性能调优

[2] cssrefectoring-example

[3] normalize.css

[4] CSS 属性继承的完整列表

[5] HTML 元素的完整列表

[6] 浏览器市场份额分析(2020.10 已停止数据更新)

[7] 百度统计-流量研究院

[8] 谷歌分析平台

[9] 谷歌编码规范

[10] WordPress 编码规范

[11] 18F 前端指南