返回介绍

CSS 编码技巧

发布于 2025-02-20 12:32:05 字数 15532 浏览 0 评论 0 收藏 0

缩简代码

在软件开发过程中,保持代码的简洁和可维护性是最大的挑战,对于 CSS 来说,同样如此。实际上,可维护代码的一个重要特性就是要 缩简需求变化时所需修改的代码量 。假设放大一个按钮需要对十处代码做出修改,那么你就有可能遗漏其中的一些细节,如果这些代码本来就不是你写的,那么就更有可能发生这种疏漏。即使需要修改的细节显而易见,即使你准确地找到了这些细节,那么你也在无形中浪费了大量的时间——这些时间本该有更大的创造力。

此外,这并不只是应对未来的需求变化。可扩展的 CSS 在完成首次编写后,只需要用少量代码创建适当的变量,那么进行重写和覆盖时所需的代码量就会很少。下面让我们来看一个例子。

请先看一下下面的 CSS,它被用来美化下图所示的按钮:

按钮

padding: 6px 16px;
border: 1px solid #446d88;
background: #58a linear-gradient(#77a0bb, #58a);
border-radius: 4px;
box-shadow: 0 1px 5px gray;
color: white;
text-shadow: 0 -1px 1px #335166;
font-size: 20px;
line-height: 30px;

这段代码在可维护性上有几点问题,让我们来修改一下。首先看到的就是字体的单位。如果我们想要更改字体大小(比如为了创建更大、更显眼的按钮),那么就需要同时修改行间距,因为它们在这里使用的都是绝对值。此外,这里的行间距并不能有效地与字体大小关联起来,以至于我们需要手动计算各种字体大小下的行间距。 当属性值相互关联时,应该在代码中体现它们的关联性。 对于上述的代码,行间距是行高的 150% ,所以,使用下面的代码将更具可维护性:

font-size: 20px;
line-height: 1.5;

既然我们都写成了这样,为什么还要给字体大小指定一个绝对数值?虽然绝对数值易于使用,但是每次需求变化时你就需要重新修改,比如现在我们想让父级字体更大一些,那么就需要在样式表中使用绝对数值修改每一条相关的样式规则,显然这是不可取的。更好的方式是使用百分比或者类似 em 的单位:

font-size: 125%; /* 假设父级字体为 16px */
line-height: 1.5;

现在,如果我修改了父级字体大小,那么按钮也会相应的变大。不过,按钮看起来和之前不大一样了,如下图所示:

按钮

这是因为其他的特效还是和之前一样袖珍,仍然不具有伸缩性。只需要使用类似 em 的单位,我们就可以将其他特效变成可伸缩的,最终所有的属性值都关联到了字体大小上。至此,我们只需修改字体大小就能控制整个按钮的大小了。

padding: .3em .8em;
border: 1px solid #446d88;
background: #58a linear-gradient(#77a0bb, #58a); 
border-radius: .2em;
box-shadow: 0 .05em .25em gray;
color: white;
text-shadow: 0 -.05em .05em #335166;
font-size: 125%;
line-height: 1.5;

现在,按钮看起来很像是原始版本的放大版:

按钮

在这里,我们想要字体大小与父级字体相关联,所以使用了 em 。在某些情况下,你想要让字体和根节点的字体相关联(比如 <html> 节点的字体),但是这时使用 em 就会让计算变得非常复杂。此时,最好使用 rem 单位。虽然“相关性”在 CSS 中很重要,但你关联前需要考虑下什么元素需要被关联在一起。

请注意,在某些属性上我们仍然使用了绝对数值。 对于按钮的哪些效果需要可伸缩,哪些不需要可伸缩这一问题,应该主观判断而不能客观要求。 比如此处的按钮,不论按钮如果缩放,我们始终要求它的边框粗细为 1px

不过,通常我们所需要修改的不只是按钮的尺寸。对配色的修改也是一个很重要的方面。比如,如果我们想要创建一个红色的“取消”按钮,或者是绿色的 “OK” 按钮又该如何做呢?一般来说,我们至少需要重写四条样式( border-colorbackgroundbox-shadowtext-shadow )。不用多说你也会理解,重新计算主体颜色( #58a )的亮度、理清所需颜色的亮度,将是一份非常麻烦的事情。此外,如果我们将按钮添加到了非白色背景的页面中,又该如何修改呢?实际上,上述按钮的灰色阴影只在白色背景下才会效果明显。

一个简单的修改方式是,对主体颜色的亮度叠加半透明的黑白色:

padding: .3em .8em;
border: 1px solid rgba(0,0,0,.1);
background: #58a linear-gradient(hsla(0,0%,100%,.2),transparent);
border-radius: .2em;
box-shadow: 0 .05em .25em rgba(0,0,0,.5);
color: white;
text-shadow: 0 -.05em .05em rgba(0,0,0,.5);
font-size: 125%;
line-height: 1.5;

提示:使用 HSLA 而不是 RGBA 表示半透明白色的好处在于,由于无需重复,它的字符更少,编写的更快。

接下来,我们可以使用不同的颜色重写背景颜色了,如下图所示:

按钮

button.cancel {
  background-color: #c00;
}

button.ok {
  background-color: #6b0;
}

现在,整个按钮的样式更加灵活了。不过,这个示例并没有阐述怎样让代码保持简洁,更多技巧请阅读接下来的内容。

可维护性 VS 简洁

有些时候, 代码的可维护性和简洁是相互敌对的。 就像是前面的按钮示例,最终的代码量比最初的代码量还增加了一些。请思考一下下面的这段代码,它的作用是为某元素设置边框粗细,除左边框外都设置为 10px

border-width: 10px 10px 10px 0;

虽然只是一条样式,但是当我们修改边框的粗细时,至少要修改三处地方。如果我们将该样式写成两句,那么修改起来就会更加方便,而且可以预见的是也更加冗余阅读:

border-width: 10px;
border-left-width: 0;

有些人可能会争论说 em 单位才是 CSS 的第一个变量,因为它引用了 font-size 的值。大多数的百分比也是同样的角色,虽然这并不令人感到兴奋。

currentColor

CSS Color Level 3 为开发者提供了许多新的颜色关键字,比如 lightgoldenrodyellow ——虽然目前来看这并没多大用处。不过,其中也有一些非常有用的颜色关键字,比如从 SVG 借鉴来的 currentColor 。实际上,该属性并不是一个固定不变的颜色值,它会根据 color 的属性值生成相应的属性值——这让它成为了 CSS 中的第一个变量。虽然它的作用有限,但意义重大。

有关于 currentColor 相关的介绍,可以阅读《 使用 CSS 的 currentColor 变量扩展颜色级联 》一文。

让我们来举个例子,假设要让所有的水平线( <hr> )和文本保持统一的颜色,那么就可以使用 currentColor

hr {
  height: .5em;
  background: currentColor;
}

你可能已经注意到了,现有的很多属性就有相同的特性。比如,如果没有给边框添加颜色,那么边框的颜色会被渲染为文本的颜色。这是因为 currentColor 同样是许多 CSS 颜色属性的初始值: border-colorbox-shadowoutline-color ……

未来,当我们可以 CSS 的原生属性控制颜色时,那时我们能够使用更多的变量,而 currentColor 也将会更有用处。

继承

虽然很多开发者意识到了 inherit 关键字的重要性,但往往会忘记使用它。 inherit 可以被应用到任何的 CSS 属性上,并且会根据父级元素的属性计算出恰当的值(如果是伪元素,那么就会根据当前元素属性来计算)。比如,让表单元素和页面其他元素具有相同的字体,无需一一指定,直接使用 inherit 即可:

input, select, button { font: inherit; }

同样的道理,让超链接和页面文本具有相同的颜色,也可以使用 inherit

a { color: inherit; }

inherit 关键字也同样适用于 background 之类的属性。比如,创建一个拼写气泡(speech bubbles),让它自动继承输入框的背景和边框,如下图所示:

气泡

一个拼写检查气泡,它获得了父级元素的背景色和边框。

.callout { 
  position: relative; 
}

.callout::before {
  content: "";
  position: absolute;
  top: -.4em;
  left: 1em;
  padding: .35em;
  background: inherit;
  border: inherit;
  border-right: 0;
  border-bottom: 0;
  transform: rotate(45deg);
}

相信你的眼睛,而不是数字

人类的双眼并不是完美的输入设备(input device)。有时候,精确的尺寸却在视觉上呈现了一种不精确的感觉。比如在视觉设计中,众所周知的是当元素垂直居中时,有些细节是无法察觉的。相反,元素需要略高于几何中心,才能被视为垂直居中。如下图所示:

气泡

在第一个矩形中,棕色的正方形经数学计算是垂直居中的,但视觉上并不是;在第二个矩形中,棕色的正方形被放置在了略高于中心的位置,但视觉上更像是垂直居中。

同样在经典的设计中,众所周知的是类似 “O” 的圆形字符需要略大于矩形字符,只是因为我们的视觉会认为圆形略小与矩形。如下图所表现的效果。

气泡

圆形虽然看起来更小,但它的边界实际上和正方形是一致的。

类似的视觉错觉在视觉设计中非常常见 ,往往需要进行适当的修正。一个非常常见的例子就是包含文字的容器内边距。这一问题的原因是容器忽略了文本的数量,可能是一个单词,也可能是好几段文字。如果我们为容器的四个边指定了相同的内边距,那么就会像下图所示:

气泡

为容器的四个内边距添加相同的数值( .5em ),但是容器内的文字让上下两边的内边距显得比左右两边更大一些。

容器看起来并不协调。究其原因就是 在水平方向上字体形状更加连贯,导致我们的眼睛认为垂直方向上的多余空间都是内边距 。因此,我们在垂直方向上减少内边距,才能让四边的内边距看起来一致。如下图所示:

气泡

让左右两边的内边距更大一些( .3em .7em ),整体看起来更协调了。

响应式设计

RWD (Responsive Web Design,响应式设计) 在过去几年很流行。然而,大家的焦点往往放在了响应式网站的重要性上,而忽略了什么才是响应式网站的优势。

开发响应式网站,常见的方式就是在多种分辨率上测试一个网站,然后通过不断地叠加媒体查询修正出现的问题。然而,每一次使用媒体查询都增加了未来维护 CSS 的负担,它们实际上并不值得被添加。未来维护这段 CSS 代码的开发者需要反复检查是否所有的媒体查询都生效了,而且还可能会去修改这些媒体查询,而这么繁多的细节往往又容易被遗忘,继而导致了某些断层。你添加的媒体查询语句越多,那么 CSS 的碎片化现象就会越严重。

这并不是说使用媒体查询是一个糟糕的方式。 使用方法得当,才会事半功倍。 不过,媒体查询应该是我们的终极解决方案,只有当其他方法都无法完成响应式设计,或者需要在或大或小的视区上完全更改页面布局时(比如将侧边栏转换为水平方向),才应该使用媒体查询。之所以这么说,是因为媒体查询本质上不是用来修复连续性错误的。媒体查询主用于指定视区的阈值(或者称之为断点),除非其他的代码也是灵活可扩展的,否则媒体查询只能在特定的分辨率起作用,本质上并没有解决问题。

不必多说相信你也会理解, 媒体查询的断点不应该由具体的设备来决定,而应该由设计本身来决定。 一方面是因为不同尺寸的设备太多(特别是我们需要为未来的设备而考虑),网站需要尽可能地适应各种分辨率;另一方面是因为在电脑桌面上可能以任意尺寸的窗口打开网页。如果你自信地认为自己的设计可以应用到各种窗口大小,那又何必担心某些特定设备的分辨率呢?

根据第九页“缩简代码”一节的原则来编写媒体查询,将会让你免于不断地重写断点内的样式,减少断点内的问题。

这里有一些技巧,可以让你避免添加无用的媒体查询:

  • 使用百分比而不是固定宽度。如果不能使用百分比,那么就是用与视窗(Viewport) 相关的单位( vw , vh , vmin , vmax ),它们能够根据视区的宽度或高度生成相应的数值。
  • 如果你想为分辨率更高的页面使用固定宽度,那么请使用 max-width ,而不要使用 width ,这样做的好处是即使没有使用媒体查询,仍然能够确保页面适配较低的分辨率。
  • 不要忘记将可替换元素设为 max-width: 100%; ,常见的可替换元素包括: imgobjectvideoiframe
  • 当需要将背景图填充整个容器时,使用 background-size: cover; 可以让图片在容器改变大小时仍然保持填充,增加代码的可维护性。不过,一定要牢记带宽不是无限的,在移动端页面加载大尺寸的图片,再使用 CSS 进行缩放是非常不明智的做法。
  • 在栅格布局中添加图片或者其他元素时,最好浏览器根据视区的宽度来决定列数。使用弹性盒布局( Flexible Box,又被成为 Flexbox )、声明为 display: inline-block 或者让文本环绕图片,都可以达到这一目的。
  • 当使用多栏布局时,应该使用 column-width 而不是 column-count ,这样的好处即使分辨率很低,内容也不会拥挤在一起,而会正常排布在一列之中。

通常来说,我们需要坚守的原则就是: 在媒体查询的断点中,使用流式布局和相对大小 。当设计足够灵活时,创建响应式布局并不会使用过多的媒体查询语句。在 2010 年末,Basecamp 的设计师关于这一话题写道:

“可以证明的是,只需要在最终的产品上增加少许的 CSS 媒体查询语句,就可以让布局适应各种设备。如此简单的关键在于,布局本身就是流式的。所以对于视区较窄的小尺寸屏幕,我们所要做的就是压缩外边距,增加可利用空间,以及更改侧边栏布局。” —— Experimenting with responsive design in Iterations

如果你发现自己需要大量的媒体查询语句适应不同尺寸的屏幕,那么你需要重新审视一下自己的代码,因为十有八九,问题的根据不在于响应式的设计。

建议在媒体查询中使用 em 而不是 px 。使用 em 可以在布局发生改变时自动进行缩放。

明智地使用简写形式

对于下面的两行代码,你可能会知道它们之间的差异:

background: rebeccapurple;

background-color: rebeccapurple;

前者是一个简写写法,它会创建一个颜色为 rebeccapurple 的背景,而后者( background-color )可能会得到一个粉色的渐变、一只猫的背景图……这是因为在后者之外,可能还存在一个 backgound-image 属性在发挥作用。这就是使用普通写法(longhands,比如这里使用的 background-size )时常会发生的问题:无法重设其他的属性,继而影响了最终的效果。

当然,你可以重设其他所有的属性(这里就不演示了),但你很有可能会有所遗漏。或者,CSS WG 在未来会引入更多的普通写法属性,那么你的代码又会失效。除非刻意地为某些元素使用普通写法属性,比如我们在第九页“缩简代码”一节中对按钮颜色所做的处理,否则不要畏惧缩写形式,它们是优秀的防御性代码,可以有效在未来保持可用性。

组合使用普通写法和简写写法也很有用。当某些属性的属性值是由逗号分隔的参数列表,那么使用组合方式就可以让减少代码中的重复,比如 background 属性。下面的示例就很好地解释了这一做法:

background: url(tr.png) no-repeat top right / 2em 2em,
    url(br.png) no-repeat bottom right / 2em 2em,
    url(bl.png) no-repeat bottom left / 2em 2em;

请注意,其中 background-sizebackground-repeat 的属性值对于每张图片都是相同的,而且还重复了三次。我们可以将重复的属性值移动到全写属性中,然后该属性根据 CSS 参数列表的扩展规则,扩展到所有相关列表的项目中:

background: url(tr.png) top right,
      url(br.png) bottom right,
      url(bl.png) bottom left;
background-size: 2em 2em;
background-repeat: no-repeat;

现在,我们只需要修改一次,就可以改变 background-sizebackground-repeat 属性了。

我应该使用预处理器吗?

你也许已经听过类似 LESSSassStylus 等预处理器的大名了。它们为编写 CSS 提供了许多方便的特性,比如变量、混合宏、函数、嵌套规则、颜色操纵方法等等。

合理使用预处理器,可以在大型项目中保持代码的简洁和灵活性 ,而原生的 CSS 由于功能的缺乏往往限制了我们的开发。当我们坚持代码的健壮性、灵活性以及简洁时,我们就会时常受制于语言的能力。此外,预处理器本身也有一些缺陷:

  • 无法追踪 CSS 文件的大小和复杂性。简洁短小的代码经过预处理器可能就会编译出冗杂的 CSS 代码来。

冷知识:怪异的简写语法 你可能已经注意到了,在简写写法和普通写法的示例中,为 background 简写写法指定 bacground-size 时,需要额外添加 background-position 属性(虽然属性值就是默认值),并且需要使用反斜线分隔它们。为什么此类简写写法的语法如此怪异呢? 这么做主要是为了消除歧义。虽然在这个示例中 top right 很明显是 background-position 的属性、 2em 2embackground-size 的属性,而且这些属性也是和顺序无关的。但是,对于 50% 50% ,你觉得它是 background-size 的属性,还是 background-position 的属性呢?当你使用普通写法时,CSS 解析器能够了解你的意思,但是使用普通写法时,解析器就无法从 50% 50% 这一属性推断出它所指向的属性了。这就是在这里使用反斜线的目的了。

对于大多数的简写写法,很少有这种歧义,而且属性的顺序也没有限制。不过,让属性值的顺序对应合理的语法是一种非常好的做法,可以有效避免混淆和错误。如果你熟悉正则表达式和语法,那么也可以根据相关规范查看语法属性——这可能是判断属性值是否有特定顺序的最快方法。

  • 因为在开发者工具中看到的 CSS 并不是你写的 CSS(由预处理器编译生成的),调试错误变得更加困难。鉴于可以通过 SourceMaps 获得调试支持,这已经不是什么难题了。SourceMap 是一个很棒的新技术,它可以告知浏览器生成的 CSS 在预处理器中的 CSS 中的行号,以此来缓解这个问题。
  • 在开发流程中,使用预处理器具有一定的延迟。虽然它们的编译速度很快,但仍然会占用一定的时间编译 CSS,而在编译结束前你只能等待。
  • 参与到我们代码库中的人,需要付出更多的努力以理解预处理器中的概念。对于我们的合作者,要么他们是熟悉预处理器的人,要么我们就必须教他们使用。所以我们对合作伙伴的选择受到了限制,不然我们就得花费额外的时间训练他们,二者两者都不是最优的方案。
  • 不要忘记抽象泄露法则的教诲:“所有有意义的抽象,在一定程度上都是有漏洞的。”预处理器是人类开发的,就像人类开发的其他非凡语言,它们都是有漏洞的——这些漏洞往往难以察觉,而我们往往不会怀疑预处理器出错了,而会怀疑是 CSS 写错了。

除了上述列出的问题,预处理器也让开发者深深地依赖上了它们,即使在某些无需使用预处理器的小项目中,开发者也会惯性使然地使用它们,而开发者所不知道的,预处理器的大多特性都会在未来添加到原生的 CSS 中。惊喜吗?是的, 许多预处理器引以为傲的特性都会被融入原生的 CSS 中:

  • 已有一个类似变量的自定义属性草案被提出( CSS Custom Properties for Cascading Variables
  • 源自 CSS Values & Units Level 3 的函数 calc() 不仅功能强大,而且也广受支持。
  • CSS Color Level 4 中的 color() 方法就会提供操纵颜色的功能。
  • 在 CSS WG 中已经就规则嵌套开展了多场严肃的讨论,并形成了一份草案。

请注意,上述提到的原生特性通常比预处理器提供的特性更加强大,因为它们是动态的。比如,预处理器是无法计算类似 100% - 50px 的表达式,因为在页面渲染完成前,预处理器是无法知道这里的百分比对应的具体数值是多少。与之相比,原生 CSS 的 calc() 方法则可以轻松胜任这项工作。相同的是,预处理器的变量也无法像下面一样使用:

ul { --accent-color: purple; }
ol { --accent-color: rebeccapurple; }
li { background: var(--accent-color); }

你能理解这里做了什么吗?在有序列表中,列表项的背景色将会使用 rebeccapurple ,而在无序列表中,列表项的背景色将会使用 purple 。试试看预处理能不能做到!在这个示例中,虽然我们只使用了后代选择器,但重点在于展示原生 CSS 变量的动态性。

Myth 是一个处于实验阶段的预处理器,它专注于模拟原生 CSS 的特性,而不是引入新的语法,非常类似于一个 CSS 的腻子脚本。

因为上述的原生 CSS 特性尚未获得良好的支持,所以在大多数情况下使用预处理器是不可避免的。我的建议是在项目中以纯 CSS 起步,当不能保持代码的简洁时,切换为预处理器。为了避免完全依赖预处理器或在不适合的项目中使用预处理器,所以你要慎重的使用它们,而不是盲目的在新项目之初就使用它们。

如果你们想知道的话,那么我要告诉你接下章节中示例的样式就是使用 SCSS 编写的,虽然最初使用的是纯 CSS,但当代码变得复杂之后,为了可维护性切换为了 SCSS。谁说 CSS 和预处理器只能用于 Web?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文