- CSS揭秘
- (希)Lea Verou
- 7212字
- 2020-08-29 01:34:15
CSS编码技巧
尽量减少代码重复
在软件开发中,保持代码的DRY和可维护性是最大的挑战之一,而这句话对CSS也是适用的。在实践中,代码可维护性的最大要素是尽量减少改动时要编辑的地方。举例来说,如果在放大一个按钮时需要在一堆规则中进行10处修改,那就很可能会漏改其中某处,当你在给别人善后时更是如此。即使这些要修改的地方很明显,或者最终可以找齐它们,但你还是浪费了时间,原本可以利用这些时间来做点更有意义的事情。
而且,这还不仅仅是后期修改的问题。灵活的CSS通常更容易扩展:在写出基础样式之后,只用极少的代码就可以扩展出不同的变体,因为只需覆盖一些变量就可以了。让我们来看一个例子。
先来看看下面这段CSS,它给按钮添加了一些效果(参见图1-4):
图1-4
在我们的示例中会一直用到这个按钮
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;
这段代码在可维护性方面存在一些问题,我们来一一修复。最软的杮子应该是跟字体尺寸相关的部分了。如果我们决定改变字号(可能是为了生成一个更大、更重要的按钮),就得同时调整行高,因为这两个属性都写成了绝对值。更麻烦的是,行高并没有反映出它跟字号的关系,因此我们还得做些算术,算出字号改变之后的行高该是多少。当某些值相互依赖时,应该把它们的相互关系用代码表达出来。在这个例子中,行高是字号的1.5倍。因此,把代码改成下面这样会更易维护:
font-size:20px; line-height:1.5;
既然跨出了这一步,我们为什么还把字号定为绝对长度值呢?没错,绝对值很容易掌控,但每当你想要修改它们的时候,它们都会回头反咬你一口。比如说,如果我们决定把父级的字号加大,就不得不修改每一处使用绝对值作为字体尺寸的样式。如果改用百分比或em单位就好多了:
图1-5
只放大字体会破坏按钮的其他效果(最突兀的就是圆角),因为它们都被指定了一些绝对的长度值
font-size:125%; /* 假设父级的字号是 16px */ line-height:1.5;
现在,如果我们改变父级的字号,按钮的尺寸就会随之变化。但是,它看起来很不协调(参见图1-5),因为所有其他效果都是为一个小按钮设计的,并没有跟着缩放。如果我们把这些长度值都改成em单位,那这些效果的值就都变成可缩放的了,而且是依赖字号进行缩放。按照这种方法,我们就可以在一处控制按钮的所有尺寸样式了:
padding:.3em .8em; border:1px solid #446d88; background:#58a linear-gradient(#77a0bb,#58a); border-radius:.2em; box-shadow:0 .05em .25emgray; color:white; text-shadow:0-.05em .05em#335166; font-size:125%; line-height:1.5;
现在我们的大号按钮看起来更像是一个原按钮的等比例放大版本了(参见图1-6)。请注意还有一些长度值是绝对值。此时就需要重新审视到底哪些效果应该跟着按钮一起放大,而哪些效果是保持不变的。比如在这个例子中,我们希望按钮的边框粗细保持在1px,不受按钮尺寸的影响。
图1-6
现在我们可以把按钮放大,而且它的所有效果也都跟着放大了
小提示
推荐使用HSLA而不是RGBA来产生半透明的白色,因为它的字符长度更短,敲起来也更快。这归功于它的重复度更低。
不过,让按钮变大或变小并不是我们唯一想要改动的地方。颜色是另一个重要的变数。比如,假设我们要创建一个红色的取消按钮,或者一个绿色的确定按钮,该怎么做呢?眼下,我们可能需要覆盖四条声明(border-color、background、box-shadow和text-shadow),而且还有另一大难题:要根据按钮的亮面和暗面相对于主色调#58a变亮和变暗的程度来分别推导出其他颜色各自的亮色和暗色版本。此外,若我们想把按钮放在一个非白色的背景之上呢?显然使用灰色(gray)作投影只适用于纯白背景的情况。
其实只要把半透明的黑色或白色叠加在主色调上,即可产生主色调的亮色和暗色变体,这样就能简单地化解这个难题了:
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;
现在我们只要覆盖background-color属性,就可以得到不同颜色版本的按钮了(参见图1-7):
图1-7
只要改变背景色,就可以得到其他颜色版本的按钮了
button.cancel{ background-color:#c00; } button.ok{ background-color:#6b0; }
我们的按钮现在已经非常灵活了。不过,这个例子并没有涵盖所有能让代码变得更DRY的方法。你会在下面几节中发现更多的技巧。
1.代码易维护vs.代码量少
有时候,代码易维护和代码量少不可兼得。比如在上面的例子中,我们最终采用的代码甚至比一开始的版本略长。来看看下面的代码片断,我们要为一个元素添加一道10px宽的边框,但左侧不加边框。
border-width:10px 10px 10px 0;
只要这一条声明就可以搞定了,但如果日后要改动边框的宽度,你需要同时改三个地方。如果把它拆成两条声明的话,改起来就容易多了,而且可读性或许更好一些:
border-width:10px; border-left-width:0;
2. currentColor
在CSS颜色(第三版)(http://w3.org/TR/css3-color)规范中,增加了很多新的颜色关键字,比如lightgoldenrodyellow等,其实并不是很常用。但是,我们还得到了一个特殊的颜色关键字currentColor,它是从SVG那里借鉴来的。这个关键字并没有绑定到一个固定的颜色值,而是一直被解析为color。实际上,这个特性让它成为了CSS中有史以来的第一个变量。虽然功能很有限,但它真的是个变量。
举个例子,假设我们想让所有的水平分割线(所有<hr>元素)自动与文本的颜色保持一致。有了currentColor之后,我们只需要这样写:
图1-8
提示框的小箭头从父元素那里获取了背景色和边框样式
hr{ height:.5em; background:currentColor; }
你可能已经注意到了,很多已有的属性也具有类似的行为。举例来说,如果你没有给边框指定颜色,它就会自动地从文本颜色那里得到颜色。这是因为currentColor本身就是很多CSS颜色属性的初始值,比如border-color和outline-color,以及text-shadow和box-shadow的颜色值,等等。
未来,我们在原生CSS中拥有处理颜色的函数后,currentColor就会变得更加有用,因为我们可以用这些函数来产生其各种深浅明暗的变体。
3.继承
尽管绝大多数开发者都知道有inherit这个关键字,但还是很容易遗忘它。inherit可以用在任何CSS属性中,而且它总是绑定到父元素的计算值(对伪元素来说,则会取生成该伪元素的宿主元素)。举例来说,要把表单元素的字体设定为与页面的其他部分相同,你并不需要重复指定字体属性,只需利用inherit的特性即可:
input,select,button{font:inherit; }
与此类似,要把超链接的颜色设定为与页面中其他文本相同,还是要用inherit:
a{color:inherit; }
这个inherit关键字对于背景色同样非常有用。举个例子,在创建提示框的时候,你可能希望它的小箭头能够自动继承背景和边框的样式(参见图1-8):
.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); }
相信你的眼睛,而不是数字
人的眼睛并不是一台完美的输入设备。有时候精准的尺度看起来并不精准,而我们的设计需要顺应这种偏差。举一个在视觉设计领域广为人知的例子吧,我们的眼睛在看到一个完美垂直居中的物体时,会感觉它并不居中。实际上,我们应该把这个物体从几何学的中心点再稍微向上挪一点,才能取得理想的视觉效果。来亲身体验一下这件怪事吧(参见图1-9)。
与此类似,在字体设计领域广为人知的是,圆形的字形(比如0)与矩形字形相比,需要稍微放大一些,因为我们倾向于把圆形感知得比其实际尺寸更小一些。你也可以在图1-10中体验一下。
这些视觉上的错觉在任何形式的视觉设计中都普遍存在,需要我们有针对性地进行调整。一个非常常见的例子是给一个文本容器设置内边距。不论内容文本有多长,是一个单词还是几个段落,这个问题都会出现。假如我们给容器的四边指定相同的内边距,则实际效果看起来并不相等,就像图1-11显示的那样。原因在于,字母的形状在两端都比较整齐,而顶部和底部则往往参差不齐,从而导致你的眼睛把这些参差不齐的空缺部分感知为多出来的内边距。因此,如果我们希望四边的内边距看起来是基本一致的,就需要减少顶部和底部的内边距。你可以在图1-12中看出这种差异。
关于响应式网页设计
响应式网页设计(Responsive Web Design, RWD)在最近几年风靡一时。但是,人们大多只是在不停念叨网页的“响应式”是多么重要,而极少有人去深入探讨怎样才能做好响应式设计。
比较常见的实践是用多种分辨率来测试一个网站,然后添加越来越多的媒体查询(Media Query)规则来修补网站在这些分辨率下出现的问题。然而对于今后的CSS改动来说,每个媒体查询都会增加成本,而这种成本是不应轻易上升的。未来每次对CSS代码的修改都要求我们逐一核对这些媒体查询是否需要配合修改,甚至可能要求我们反过来修改这些媒体查询的设置。这一点常常被我们忽略,后患无穷。你添加的媒体查询越多,你的CSS代码就会变得越来越经不起折腾。
这并不是说媒体查询是一种不良实践。只要用对了,它就是利器。但是,你只应该把它作为最后的手段。比如你想把网站做得弹性灵活,但其他尝试全都失败了;或者我们希望在较大或较小的视口下完全改变网站的设计形态(譬如,把侧栏改成水平布局)。我这么说的原因在于,媒体查询不能以一种连续的方式来修复问题。它们的工作原理基于某几个特定的阶梯(亦称“断点”),如果大部分样式代码并不是以弹性的方式来编写的,那么媒体查询能做的只是修补某个特定分辨率下的特定问题——这本质上只是把灰尘扫到地毯下面而已。
图1-9
在第一个矩形中,棕色方块在数学层面上是完美垂直居中的,但看起来并不是这样;在第二个矩形中,方块从几何中心向上轻微移动了一点儿,但它在人类的眼睛看来却是恰好居中的
图1-10
圆形看起来要小一些,但实际上它占据的宽高和方形是完全一样的
图1-11
为容器的四边指定了相同的内边距(这里用了.5em),但实际看起来上下空得多,左右空得少
图1-12
如果把左右内边距增大一些(这里把padding属性写成.3em .7em),看起来就明显更加统一了
小提示
不妨考虑在你的媒体查询中使用em单位取代像素单位。这能让文本缩放在必要时触发布局的变化。
当然,有一点上面并没有提到,媒体查询的断点不应该由具体的设备来决定,而应该根据设计自身来决定。这不仅是因为我们的网站需要面向的设备太多了(尤其是考虑到未来的设备时),还因为一个网站在桌面端可能会以任意尺寸的窗口来显示。如果你有信心自己的设计在任何可能出现的视口尺寸下都能良好工作,谁关心这些设备的分辨率具体是多少呢?
遵从“尽量减少代码重复”所描述的原则对此也是有帮助的,因为你不需要去覆盖媒体查询里同样数量的声明。这在本质上减轻了它们所产生的维护成本。
下面还有一些建议,可能会帮你避免不必要的媒体查询。
■使用百分比长度来取代固定长度。如果实在做不到这一点,也应该尝试使用与视口相关的单位(vw、vh、vmin和vmax),它们的值解析为视口宽度或高度的百分比。
■当你需要在较大分辨率下得到固定宽度时,使用max-width而不是width,因为它可以适应较小的分辨率,而无需使用媒体查询。
■不要忘记为替换元素(比如img、object、video、iframe等)设置一个max-width,值为100%。
■假如背景图片需要完整地铺满一个容器,不管容器的尺寸如何变化,background-size: cover这个属性都可以做到。但是,我们也要时刻牢记——带宽并不是无限的,因此在移动网页中通过CSS把一张大图缩小显示往往是不太明智的。
■当图片(或其他元素)以行列式进行布局时,让视口的宽度来决定列的数量。弹性盒布局(即Flexbox)或者display: inline-block加上常规的文本折行行为,都可以实现这一点。
■在使用多列文本时,指定column-width(列宽)而不是指定column-count(列数),这样它就可以在较小的屏幕上自动显示为单列布局。
总的来说,我们的思路是尽最大努力实现弹性可伸缩的布局,并在媒体查询的各个断点区间内指定相应的尺寸。当网页本身的设计足够灵活时,让它变成响应式应该只需要用到一些简短的媒体查询代码。Basecamp的设计师在2010年写到过这种非常规情况。
“结果我们发现,想让网页在一堆不同的设备上合理展示,只需要在最终产品上添加一点CSS媒体查询就可以了。这件事情之所以这么简单,关键在于我们的布局原本就是弹性可伸缩的。因此,优化网页在小屏幕上的表现,其实只意味着把一些外边距收拢到最小程度,然后把因为屏幕太窄而无法显示成双列的侧栏调整为单列布局而已。”
——在Iterations中实践响应式设计(http://signalvnoise.com/posts/2661-experimenting-with-responsive-design-in-iterations)
如果你发现自己需要一大堆媒体查询才能让设计适应大大小小的屏幕,那么不妨后退一步,重新审视你的代码结构。因为在所有的情况下,响应式都不是唯一需要考虑的问题。
合理使用简写
你可能知道,以下两行CSS代码并不是等价的:
background:rebeccapurple; background-color:rebeccapurple;
前者是简写,它可以确保你得到rebeccapurple纯色背景;但如果你用的是展开式的单个属性(background-color),那这个元素的背景最终有可能会显示为一个粉色的渐变图案、一张猫的图片或其他任何东西,因为同时可能会有一条background-image声明在起作用。在使用展开式属性的写法时,通常会遇到这样的问题:展开式写法并不会帮助你清空所有相关的其他属性,从而可能会干扰你想要达到的效果。
当然,你可以把所有的展开式属性全都设置一遍,然后收工,但你可能会漏掉几个;又或者,CSS工作组可能会在未来引入更多的展开式属性,那时你的代码就无法完全覆盖它们了。不要害怕使用简写属性。合理使用简写是一种良好的防卫性编码方式,可以抵御未来的风险。当然,如果我们要明确地去覆盖某个具体的展开式属性并保留其他相关样式,那就需要用展开式属性,就像我们在“尽量减少代码重复”一节中为了得到按钮的其他颜色版本所做的那样。
展开式属性与简写属性的配合使用也是非常有用的,可以让代码更加DRY。对于那些接受一个用逗号分隔的列表的属性(比如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-size和background-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-size和background-repeat了。你会发现这个技巧在本书中的使用非常普遍。
我应该使用预处理器吗
你很可能听说过像Stylus(http://stylus-lang.com/)、Sass(http://sass-lang.com/)或LESS(http://lesscss.org/)这样的CSS预处理器。它们为CSS的编写提供提供了一些便利,比如变量、mixin、函数、规则嵌套、颜色处理等。
如果使用得当,它们在大型项目中可以让代码更加灵活,而CSS自身在这方面确实有很大局限。只要我们在代码健壮性、灵活性和DRY方面有追求,就会感受到CSS在这方面的局限。不过,预处理器也不是完美无缺的。
■CSS的文件体积和复杂度可能会失控。即使是简洁明了的源代码,在经过编译之后也可能会变成一头从天而降的巨兽。
■调试难度会增加,因为你在开发工具中看到的CSS代码并不是你写的源代码。不过这个问题已经大大好转了,因为已经有越来越多的调试工具开始支持SourceMap。SourceMap是一种非常酷的新技术,正是为了解决这个痛点而生的,它会告诉浏览器哪些编译生成的CSS代码对应哪些预处理器CSS代码,精确到行号。
■预处理器在开发过程中引入了一定程度的延时。尽管它们通常很快,但仍然需要差不多一秒钟的时间来把你的源代码编译成CSS,而你不得不等待这段时间才能预览到代码的效果。
小花絮怪异的简写语法
你可能已经注意到前面那个背景属性简写的例子了:在background简写属性中指定background-size时,需要同时提供一个background-position值(哪怕它的值就是其初始值也需要写出来),而且还要使用一个斜杠(/)作为分隔。为什么有些简写的语法如此怪异?
这通常都是为了消除歧义。在这个例子中,top right显然是background-position,而2em 2em是background-size,不管它们的顺序如何。但是,请设想一下50% 50%这样的值,它到底是background-size还是background-position呢?当你在使用展开式属性时,CSS解析器明白你的意图;而当你使用简写属性时,解析器需要在没有属性名提示的情况下弄清楚50% 50%到底指什么。这就是需要引入斜杠的原因。
对绝大多数的简写属性来说,并没有这样的歧义问题,因而简写属性的多个值往往可以随意排列。不过,我还是建议你养成随手查阅语法的好习惯,以免犯错。如果你对正则表达式以及规范的语法描述方式(grammar)很熟悉的话,不妨直接在相关规范中查询语法描述。如果要确定某个属性的值是否有明确的顺序要求,这可能是最快的方式。
■每次抽象都必然会带来更高的学习成本,每当有新人加入到我们的代码库中,这个问题都会重演。他要么已经对我们选择的这门预处理器“方言”很熟悉,要么得从头学。这意味着我们要么强制协作者接受我们的选择,要么花费额外的时间来培训,而这两者都不是我们想要的。
■另外,别忘了还有抽象泄漏法则:“所有重大的抽象机制在某种程度上都存在泄漏的情况。”预处理器是由人类写出来的,就像所有由人类写出来的大型程序一样,它们有它们自己的bug。这些bug可能会潜伏很久,因为我们很少会怀疑预处理器的某个bug才是我们CSS出错的幕后元凶。
除了上面列出的这些问题,预处理器还可能导致这种风险:网站开发者可能会不自觉地“依赖”和“滥用”。因为在某些时候,预处理器并不必要。比如在小型项目中;或者在未来,说不定预处理器最受欢迎的那些特性都被加入了原生CSS中。很惊讶吗?没错,很多受预处理器启发的特性都已经以各种方式融入到原生CSS中了。
■有一份关于(跟变量类似的)自定义属性的草案,叫作CSS自定义属性暨层叠式变量(http://w3.org/TR/css-variables-1)。
■CSS值与单位(第三版)中的calc()函数,不仅在处理运算时非常强大,而且已经得到了广泛的支持,当下可用。
■CSS颜色(第四版)(http://dev.w3.org/csswg/css-color)引入的color()函数会提供颜色运算方法。
■关于嵌套,CSS工作组内部正在进行一些正式的讨论,甚至以前还有过一份相关的草案(ED)。
请注意,这些原生特性通常比预处理器提供的版本要强大得多,因为它们是动态的。举个例子,预处理器完全不知道如何完成100% -50px这样的计算,因为在页面真正被渲染之前,百分比值是无法解析的。但是,原生CSS的calc()在计算这样的表达式时没有任何压力。与此类似,下面这样的变量玩法在预处理器中是不可能做到的:
ul{——accent-color:purple; } ol{——accent-color:rebeccapurple; } li{background:var(——accent-color); }
你看清楚这段代码的意图了吗?在有序列表中,列表项的背景色将是rebeccapurple;但在无序列表中,列表项的背景色将是purple。试试用预处理器能否做到!当然,在这个例子中,我们可以直接使用后代选择符,只不过这个例子的重点在于向你展示CSS的原生变量所具备的动态性。
图1-13
Myth(http://myth.io)是一款实验性质的预处理器,它只模拟上述原生的CSS新特性,而不是引入私有语法。它本质上扮演了CSSpolyfill的角色
上面提到的原生CSS特性绝大多数在目前还没有得到很好的支持,因此在很多情况下,如果可维护性很重要(它确实很重要),使用预处理器是不可避免的。我的建议是,在每个项目开始时使用纯CSS,只有当代码开始变得无法保持DRY时,才切换到预处理器的方案。为了避免可能发生的“依赖”或“滥用”,在引入预处理器的问题上需要冷静决策,不应该在每个项目一开始时就不动脑筋顺着惯性来。
可能你还不知道(或许直接跳过了前言,啧啧),这里再说一次,这本书的样式是用SCSS写的。这些样式代码以纯CSS起步,而且只在代码增长得太过复杂以致无法维护时才切到SCSS。谁说CSS和预处理器只能用在网页上?