CSS 精通指南(五)
原文:CSS Mastery
十一、前沿视觉效果
单独使用 CSS 对创造性的设计进行编码总是很困难。直到最近,这种语言本身在你可以使用的视觉效果方面还相当有限。从像 Photoshop 这样的图形编辑包中重新创建视觉效果是困难的,如果不是不可能的话,而且经常需要令人讨厌的黑客。
我们总是能够通过牺牲简单性(纯粹用于表示目的的额外元素)或性能(页面上图像太多,JavaScript 做不出视觉效果)来解决这些限制。
在这一章中,我们将看看如何使用各种 CSS 特性来实现这些效果。其中一些是非常新的,目前只支持有限的浏览器,而另一些已经存在了几年。许多已经在 SVG 中存在了很长时间,但只是现在才渗透到 CSS 中——我们将在本章的后面看到一些这种协调的例子。
所有这些技术都可以让你的设计更上一层楼:它们是将原材质提升到新高度的调味品。因此,应该谨慎使用它们,并在应用时考虑到逐步增强。您还应该意识到,这些技术中的许多都有相关的缺陷。因此,即使在支持它们的浏览器中,它们也经常是一项正在进行的工作。
在本章中,我们将探讨以下主题:
-
CSS 形状
-
通过 CSS 和 SVG 使用剪辑路径和蒙版
-
CSS 混合模式
-
使用 CSS 和通过 SVG 过滤
在图 11-1 中,我们拼凑了一个充满视觉效果的页面(描述一些天体)。就在几年前,这些效果在 CSS 中是不可能重现的。页面布局,如果曾经尝试过的话,将会用大量的图片和额外的元素重新创建。
图 11-1。使用一系列视觉效果的页面
今天,这些类型的效果实际上可以在许多浏览器中用 CSS 来实现,如果它们失败了,可以让它们优雅地失败。在页面上同时使用大量的视觉效果仍然需要小心,因为它们会带来性能损失;有些比其他人多。尽管如此,将这些效果作为 CSS 的一部分还是有很大的好处。它们变得不那么依赖于粗糙的标记,并且更容易维护。此外,一旦功能在竞争浏览器中标准化,性能往往会随着时间的推移而变得更好。
在这一章的剩余部分,我们将会浏览在“观星”页面例子中使用的所有技术,以及更多。
打破常规:CSS 形状
正如我们之前说过的,网页布局都是关于矩形的,所以有一个内在的盒子。在前面的章节中,我们已经看到了如何通过使用图像和渐变来引入更有机的感觉,以及通过使用圆角来创建更柔和的形状甚至圆形来解决这个问题的例子。
CSS 形状是一个新的标准,允许在网页设计中使用更广泛的形状。形状元素影响页面中内容的实际流动,而不仅仅是表面外观。
内部和外部形状
CSS Shapes 由两组新属性组成:一组设置影响框内内容的形状,另一组设置影响形状元素周围内容流的外部形状。在图 11-2 中,一个元素被设置为圆形。左边的示例显示了外部形状如何影响围绕圆形流动的内容,而右边的示例显示了内部形状如何影响圆形内的内容。
图 11-2。外部形状与内部形状
这两种形状方法是在 CSS 形状规范的不同层次中定义的。shape-outside 属性(在 CSS Shapes Level 1 中定义)是唯一一个已经达到合理成熟度的属性,并且已经开始进入浏览器。我们将 shape-inside 排除在这一部分之外,因为它还没有在任何浏览器中实现,但它很可能很快就会出现。
shape-outside 属性只对浮动元素有效。它的工作原理是雕刻出一个影响元素外部内容流动的形状,但它不会改变元素本身的外观。
在我们示例的“月亮”部分,文本通过 shape-outside 围绕月亮图像的形状流动(见图 11-3 ),如下所示:
图 11-3。文字在月亮图像外呈圆形流动
.fig-moon {
float: right;
max-width: 40%;
shape-outside: circle();
}
在我们深入了解形状如何工作之前,值得注意的是形状外部如何影响布局。图像文件本身具有黑色背景。如果我们改变这部分页面的背景颜色,形状的效果会显示得更清楚(见图 11-4 )。图像本身仍然是一个正方形,但是文本在图像上流动,围绕着图像内部的一个圆形。在不支持 CSS 形状的浏览器中,文本将正常围绕矩形流动。
图 11-4。文本流入已成形元素的元素边界
注意
如图 11-4 所示,文字仅跟随浮动左侧的形状。您只能让形状影响一侧的行框,因此即使形状在自身右侧留出空间,文本也不会流到那里。
形状函数
月亮图像的 shape-outside 属性的值为 circle()。有许多这样的形状函数:圆()、椭圆()、多边形()和插入()。除了 insert()之外,大多数都是不言自明的,insert()表示从盒子边缘插入的矩形,可以选择圆角。它基本上是旧 CSS 2.1 clip 属性的增强版本,但语法略有不同。
圆和椭圆的语法类似于我们在第五章中看到的调整和定位径向渐变的语法:
.shape-circle {
/* circles take 1 radius and a position value: */
**shape-outside: circle(150px at 50%);**
}
.shape-ellipse {
/* ellipses take 2 radii and a position value: */
**shape-outside: ellipse(150px 40px at 50% 25%);**
}
就像梯度函数一样,圆和椭圆也有一些合理的默认值。月亮图像的 circle()值没有提供参数,这导致将圆形定位在元素的中心,并将半径扩展到最近的一侧。
inset()形状通过提供一个长度列表来工作,该列表表示从上、右、下和左边缘的距离,很像边距或填充简写。当提供一到三个值时,同样的边距或填充的缩短规则也适用。还可以通过添加 round 关键字,后跟与 border-radius 属性相同的半径值来提供圆角值:
.shape-inset {
/* shape the outside of the box 20px from
* the top and bottom edges and 30px from
* the left and right edges, with 10px
* radius rounded corners.
*/
**shape-outside: inset(20px 30px round 10px);**
}
一个更复杂的例子是使用 polygon() shape 函数。这使您可以为长方体表面上相对于左上角的点提供一个坐标对列表,并在它们之间绘制一条线,从而形成一个形状。列出的最后一个点连接到第一个点以闭合造型。在“行星”部分,我们已经根据土星的图像创建了一个多边形。
快速创建多边形的最简单方法是使用 CSS Shapes Editor 插件,该插件可用于 Google Chrome 和 Opera 开发工具(github.com/oslego/chrome-css-shapes-editor)。Chrome 和 Opera 都支持形状,并在检查元素时提供形状预览。该插件增加了额外的工具,这样你既可以可视化一个形状如何影响页面,也可以通过创建和拖动控制点来创建新的控制点(见图 11-5 )。
图 11-5。用谷歌浏览器的形状插件在土星图像上绘制的多边形
现在,我们可以将生成的多边形形状复制并粘贴到代码中:
.fig-planet {
float: right;
max-width: 65%;
shape-outside: polygon(41.85% 100%, 22.75% 92.85%, 5.6% 73.3%, 0.95% 52.6%, 5.6% 35.05%, 21.45% 17.15%, 37.65% 12.35%, 40% 0, 100% 0%, 100% 100%);
}
多边形上每个点的坐标在这里表示为百分比,以获得最大的灵活性,但您也可以使用其他长度,如像素、ems,甚至 calc()表达式。
形状图像
基于复杂图像创建多边形可能会很繁琐。幸运的是,我们还可以基于图像透明度直接从图像源创建形状。我们可以用所需的轮廓形状创建一个单独的图像文件,但是 Saturn 图像已经是一个带有透明度的 PNG,所以我们可以用它来生成形状。我们需要做的就是将 shape-outside 值从 polygon()函数更改为指向图像的 url()函数:
.fig-planet {
float: right;
max-width: 65%;
shape-outside: url(img/saturn.png);
}
如果我们现在检查 Chrome 开发工具(DevTools)中的图像,如图 11-6 所示,我们可以看到图像中的透明度数据被拾取,生成了形状。
图 11-6。图像透明部分的轮廓用于创建形状
小费
如果您尝试在浏览器中直接打开 HTML 文件,即使浏览器支持 CSS 形状,也不会成功。您需要通过 web 服务器获取页面,以便引用的图像具有适当的 HTTP 头,将它描述为来自与 CSS 相同的源服务器。这是一些较新的浏览器中存在的安全权衡,以防止引用的文件对您的计算机做不安全的事情。
默认情况下,形状轮廓将从图像完全透明的轮廓生成,但是我们可以使用 shape-image-threshold 属性更改该值。默认值为 0.0(完全透明),而较高的值(接近 1.0)意味着在创建形状边缘之前可以接受较高的不透明度值。例如,如果我们将土星图像更改为使用 0.9 的图像阈值,半透明环将不会包含在形状轮廓中,文本将会与它们重叠(参见图 11-7 ):
图 11-7。使用形状-图像-阈值,图像的半透明部分现在在生成形状时会被忽略
.fig-planet {
float: right;
max-width: 65%;
shape-outside: url(img/saturn.png);
shape-image-threshold: 0.9;
}
异形框和页边距
除了使用形状函数或图像,我们还可以使用元素的参考框来生成形状。这一开始听起来可能很奇怪,因为这是我们想要摆脱的四方形,但形状也将遵循圆角。
例如,如果我们回到月亮的例子,我们可能希望改变该部分的背景颜色,但同时去掉图像周围的黑色方框,如图 11-8 所示。我们可以在图像上使用边界半径来创建圆形:
图 11-8。将边界半径应用于月球图像
.fig-moon {
float: right;
max-width: 40%;
**border-radius: 50%;**
}
边界半径本身不会生成形状,但是我们可以告诉 shape-outside 属性使用现在的圆形边界框作为形状的参考:
.fig-moon {
float: right;
max-width: 40%;
border-radius: 50%;
**shape-outside: border-box;**
}
外部形状现在回到圆形,跟随元素的边框。形状的其他可能的引用框值是内容框、填充框和边距框。我们以前见过引用框(具有类似框大小和背景剪辑的属性),除了边距框。因为形状在浮动区域上操作,所以它们也可以包含边距,所以这个关键字对形状来说是特殊的——没有框大小:例如,边距框。
形状项目上的边界框也将遵循边界半径。这意味着我们可以对月亮图像使用普通的边距声明来在其周围创建一些空间:
.fig-moon {
float: right;
max-width: 40%;
border-radius: 50%;
shape-outside: margin-box;
**margin: 2em;**
}
文本现在将围绕弯曲的边距形状流动。如果我们在 Chrome DevTools 中检查该项目,我们将看到该形状的行为,以及原始的边距(见图 11-9 )。
图 11-9。使用边界框作为形状参考,边界距离跟随圆角
如果我们想给土星图像更复杂的形状添加一个边距,有一个新的属性叫做 shape-margin 来设置整个形状周围的边距距离,而不管创建它的方法如何(见图 11-10 ):
图 11-10。在土星图像形状上添加形状边距属性
.fig-planet {
max-width: 65%;
shape-outside: url(img/saturn.png);
shape-margin: 1em;
}
CSS 形状的浏览器支持
在撰写本文时,CSS 形状只能在较新的基于 WebKit 或 Blink 的浏览器中工作:Google Chrome、Opera 和 Safari 7.1+(或 iOS 8+上的 Mobile Safari)。
剪裁和遮罩
CSS Shapes 允许您改变元素形状周围的内容流,但不允许您改变元素本身的外观。我们看到,添加边框半径是视觉上塑造元素的一种方式。还有其他方法可以影响元素的形状,例如使元素的一部分透明。
裁剪使用路径形状来定义元素可见性完全打开和关闭的锐边。蒙版略有不同,用于设置元素区域的透明度。剪裁也会影响对象的撞击表面,而遮罩则不会。例如,只有当鼠标指针位于被裁剪元素的可见部分时,才会触发悬停效果。当您将鼠标悬停在被屏蔽的元素上时,无论鼠标指针下方的部分是否可见,any :hover 规则都将变为活动状态。
剪报
CSS 2.1 中首次引入了 clip,带有 clip 属性。然而,它只能用于绝对定位的元素,使用 rect()函数将它们裁剪成矩形。无聊!
幸运的是,新的 clip-path 属性允许我们以更令人兴奋的方式裁剪元素。它可以使用与 CSS 形状相同的基本形状函数来定义如何裁剪元素。我们还可以使用一个 SVG 文档来裁剪一个元素,通过一个 URL 引用其中的一个元素。
我们将从使用形状函数来查看版本开始。在撰写本文时,这个版本只能在基于 Blink 和 WebKit 的浏览器中工作,除了 prefix 属性之外,还需要一个-webkit-前缀。在接下来的例子中,为了简洁起见,我们将坚持使用标准的无前缀属性。
观星示例页面中的所有部分都被剪辑,以使它们稍微倾斜(见图 11-11 )。
图 11-11。这一页的所有部分都被修剪过,以使它们稍微倾斜
每个部分都被赋予了类名 stacked,对于这个类名,我们添加了一个规则,将裁剪路径定义为多边形形状:
.stacked {
clip-path: polygon(0 3vw, 100% 0, 100% calc(100% - 3vw), 0% 100%);
}
这个多边形形状不像前面的土星形状那样复杂,这给了我们一个深入研究语法的机会。多边形中的每个点都表示为一对空格分隔的值,这些点用逗号分隔。
从左上角开始,我们从 x 轴上的 0 和 y 轴上的 3vw 开始剪辑。我们在这里使用相对于视口的单位来保持相对于视口大小的角度。下一个坐标对在元素的右上角,所以坐标对是 100% 0。下一个点是右下角的 3vw,不能用百分比表示,因为我们从顶部开始。这意味着我们需要将其计算为 100% - 3vw。最后,我们将最后一个点放在元素的左下角,0 100%。
由于剪辑路径只影响元素的渲染外观,而不影响页面的流动,因此被剪辑的元素之间现在会有透明的间隙(参见图 11-12 )。为了解决这个问题,我们可以为每个堆叠的元素应用一个负的边距,比我们裁剪掉的 3vw 距离稍大一些,这样部分就会重叠。我们只想在支持 clip-path 的浏览器中使用这个负边距,这给了我们一个使用@supports-rule 的绝佳机会。由于这些新的视觉效果只在最近的浏览器中实现,我们可以安全地用这种方式来定义它们。
图 11-12。仅用剪贴,各部分之间会有间隙
@supports ((clip-path: polygon(0 0)) or
(-webkit-clip-path: polygon(0 0))) {
.stacked {
margin-bottom: -3.4vw;
}
}
在@supports-block 中,我们测试了对由一个点组成的最小多边形形状的支持。
通过此修复,这些部分可以很好地堆叠起来,不支持 clip-path via 形状的浏览器可以获得没有重叠的正常直线部分。
使用 SVG 剪辑源进行剪辑
您可以使用 polygon()、circle()、ellipse()和 inset()函数创建剪辑路径,就像在 CSS 形状中一样。对于更复杂的形状,使用图像编辑器创建它们,然后使用图形作为剪辑形状的源可能更容易。这就是我们对图 11-13 所示的页面导航中的形状所做的。
图 11-13。导航部分中的复杂形状是从 SVG 源中截取的
为了实现这一点,我们需要使用 SVG 来创建我们的剪辑路径,然后使用这个剪辑源的 URL 引用来代替 shape 函数。首先,我们需要在 Illustrator、Sketch 或 Inkscape 等图形编辑程序中创建形状。这个过程没有想象中那么简单,但是可行的。
导航本身是一个包含页面内链接的无序列表:
<nav class="stacked section nav-section inverted">
<ul class="wrapper">
<li><a href="#moon">The Moon</a></li>
<li><a href="#sun">The Sun</a></li>
<li><a href="#planets">Planets</a></li>
<li><a href="#milky-way">Galaxy</a></li>
<li><a href="#universe">Universe</a></li>
</ul>
</nav>
我们将把导航样式的细节排除在这个例子之外;简单地说,我们使用 flexbox 来水平放置项目,并以默认字体大小将它们调整为 100×100 像素的正方形。
接下来,我们在支持 SVG 的图形编辑器中创建一个图像,在本例中是 Adobe Illustrator。图像尺寸也被设置为 100×100 像素(参见图 11-14 )。我们通过创建两个黑色形状来绘制行星:一个圆形和一个旋转的省略号。接下来,我们将图形保存为一个名为 clip.svg 的 SVG 文件,这个过程在不同的图形编辑软件之间有所不同;我们将省略这方面的细节,并将重点放在一般的工作流程上。
图 11-14。在 Illustrator 中创建行星形状
如果我们现在在代码编辑器中打开 SVG 文件,它看起来会像这样:
<svg xmlns=http://www.w3.org/2000/svg width="100px" height="100px" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45"/>
<ellipse transform="matrix(-0.7553 0.6554 -0.6554 -0.7553 -12.053 54.99)" cx="50" cy="50" rx="63.9" ry="12.8"/>
</svg>
为了将这个图像转换成一个剪辑路径,我们需要将内容包装在一个元素中,并给这个元素一个 ID:
<svg
width="100px" height="100px" viewBox="0 0 100 100">
**<clipPath id="saturnclip">**
<circle cx="50" cy="50" r="40.1"/>
<ellipse transform="matrix(0.7084 -0.7058 0.7058 0.7084 -20.7106 49.8733)" cx="50" cy="50" rx="62.9" ry="12.8"/>
**</clipPath>**
</svg>
我们终于准备好引用 CSS 中 clip.svg 文件内部的剪辑路径了:
.nav-section [href="#planets"] {
clip-path: url(img/clip.svg#saturnclip);
}
使用这种技术,您可以在一个 SVG 文件中保存许多剪辑源,并通过它们在 URL 片段中的 ID 来引用它们。
可悲的是,为了使 SVG 剪辑源可靠地工作,浏览器的当前状态留下了两个需要克服的主要障碍:
-
到目前为止,只有 Firefox 允许将外部剪辑源应用于 CSS 中的 HTML 内容——其他浏览器最终可能会效仿。
-
SVG 中的坐标被解释为像素,因此剪辑形状是不灵活的,不会随着它所应用的 HTML 内容而调整大小。测量中的百分比在技术上是有效的,但缺乏支持。
这些障碍都有解决方案,但是它们需要对我们的代码进行一些轻微的重组。
内嵌 SVG 片段源
不支持外部剪辑源引用的浏览器允许使用 SVG 剪辑路径,只要 CSS、HTML 和 SVG 都在同一个文件中。
如果将 CSS 放在与内容内联的
<!-- Here's the element we want to clip -->
<li><a href="#planets">Planets</a></li>
<style>
/* in the same HTML file, we put the CSS for the clip-properties */
.nav-section [href="#planets"] {
clip-path: url(img/clip.svg#saturnclip);
}
</style>
<!-- Still in the same HTML file, the clipping path inline as SVG -->
<svg xmlns=http://www.w3.org/2000/svg height="0" viewBox="0 0 100 100">
<clipPath id="saturnclip">
<circle cx="50" cy="50" r="40.1"/>
<ellipse transform="matrix(0.7084 -0.7058 0.7058 0.7084 -20.7106 49.8733)" cx="50" cy="50" rx="62.9" ry="12.8"/>
</clipPath>
</svg>
前面的技术为我们提供了稍微好一点的跨浏览器支持,但代价是牺牲了在一个外部 SVG 文件中包含所有剪辑路径的可重用性,以及不必在 HTML 中乱搞。
注意
基于 WebKit 的浏览器有一个缺陷,裁剪路径的位置坐标从页面的左上角开始,而不是相对于元素。为了使它们正确定位,最后一个示例还在被剪辑的项目上使用了 transform: translate(0,0 ),这在视觉上没有任何作用,但修复了问题。
使用对象边界框来调整剪辑路径的大小
下一个问题是,剪辑路径不会随着导航项目的大小而调整大小;它具有 100×100 像素的硬编码大小。
我们可以使用两个坐标系来确定裁剪路径的大小。缺省值称为“使用中的用户空间”,意思是应用剪辑路径的内容的坐标系。在我们的例子中,这意味着剪辑路径中的一个单元被解释为被剪辑的 HTML 内容中的一个 CSS 像素。
另一个坐标系称为“对象边界框”,它使用一个单位相对于被剪切内容大小的比例。在此比例中,x 轴上的值 0 表示剪辑内容边框的左边缘,1 表示右边缘。同样的,0 是 y 轴上盒子的顶部,1 是底部。
对于较简单的图形,您可以手动更改这些值——在我们 100×100 像素的图像中,值 50 会变成 0.5,依此类推——但是对于较复杂的图形,这太容易出错了。更简单的解决方案是在导出 SVG 之前,在图像编辑软件中将图形大小调整为 1×1 像素。
在最后一个示例中,我们将 objectBoundingBox 值用于内嵌 SVG 剪辑路径。对于 Saturn 剪辑路径,最终代码如下所示:
<clipPath id="saturnclip" **clipPathUnits="objectBoundingBox"**>
<circle cx="**0.5**" cy="**0.5**" r="**0.45**"/>
<ellipse transform="matrix(-0.7553 0.6554 -0.6554 -0.7553 **1.2053 0.5499**)" cx="**0.5**" cy="**0.5**" rx="**0.639**" ry="**0.125**"/>
</clipPath>
浏览器对剪辑路径的支持
使用内嵌 SVG 方法裁剪路径,您可以针对大多数现代浏览器:Chrome、Opera、Safari 和 Firefox 都支持这个版本。基于 WebKit 和 Blink 的浏览器也支持裁剪路径的基本形状功能。可悲的是,IE 根本不支持裁剪路径。在撰写本文时,Edge 也缺少支持,但它已经在路线图上,很可能很快就会添加进来。在撰写本文时,SVG 剪辑源的外部引用只能在 Firefox 中使用,但预计在不久的将来可以在其他地方使用。
掩饰
“观星”页面页眉中的标题似乎在地球图形“大气层”的后面(见图 11-15 )。这种逐渐透明是通过掩蔽实现的。
图 11-15。“观星”标题被渐变蒙版图像所掩盖
Safari 早在 2008 年就实现了屏蔽,使用了一个名为- webkit-mask-image 的非标准属性。此属性允许您拍摄图像并将其用作被遮罩元素透明度级别的来源。这是基于蒙版中每个像素的 alpha 等级:透明程度。当遮罩图像完全透明时,被遮罩的元素也将完全透明。相反,遮罩的完全不透明部分将使被遮罩的元素完全可见。遮罩的颜色值是不相关的,因此最常见的方法是使用灰度图像来进行遮罩。
除了创建图像文件,我们还可以使用 CSS 渐变来创建遮罩。这正是我们在标题中所做的:
.header-title {
mask-image: radial-gradient(ellipse 90% 30% at 50% 50%,
rgba(0,0,0,0) 45%,
#000 70%);
mask-size: 100% 200%;
}
您将认识到语法:遮罩图像的声明与背景属性的声明非常相似。例如,mask-image 属性在语法上就像背景图像一样工作;您甚至可以在彼此之上声明多个遮罩图像。
除了选择遮罩图像,您还可以指定大小和位置。在这个例子中,我们选择使用两倍高度的蒙版图像,以将其放置在文本的底部,而不是放置在那里。如果我们简单地向下移动渐变图像,蒙版图像表面的顶部将是透明的,这将掩盖掉文本的顶部。图 11-16 展示了渐变遮罩在文本上的大小和位置。
图 11-16。遮罩图像,就像它是文本顶部的图像一样
自从最初的 WebKit 实现以来,遮罩属性正在被标准化和扩展,并与相应的 SVG 效果相协调。是的,没错:就像 clip-path 一样,屏蔽存在于 SVG 中,并且也适用于 HTML 内容。
在撰写本文时,基于 WebKit 和 Blink 的浏览器提供了对 alpha 透明遮罩图像的前缀 webkit-mask-properties 的支持。除了 Firefox,它们还支持 SVG 源。除了 Firefox,所有的都需要我们看到的裁剪路径的内联方法。
.header-title {
/* inline CSS, pointing to an inline SVG <mask> element */
mask: url(#ellipseMask);
}
我们创建的 CSS 渐变的 SVG 等价物如下所示:
<mask id="ellipseMask" maskUnits="objectBoundingBox" maskContentUnits="objectBoundingBox">
<radialGradient id="radialfill" r="0.9" cy="1.1">
<stop offset="45%" stop-color="#000"/>
<stop offset="70%" stop-color="#fff"/>
</radialGradient>
</mask>
就像裁剪路径一样,我们需要使用从 0-1 的 objectBoundingBox 坐标系来根据元素的边界调整蒙版表面的大小。SVG 遮罩还具有附加的 maskContentUnits 属性,该属性在这里为遮罩形状设置相同的坐标系。
SVG 蒙版源使用蒙版的亮度值,而不是 alpha。这意味着被遮罩的元素在遮罩较暗的地方是透明的,在遮罩较亮的地方是不透明的。在前面的 SVG 遮罩示例中,我们使用了从黑到白的渐变。
浏览器会自动假设您使用 alpha 遮罩来遮罩图像源,如果指向 SVG 源,则使用亮度遮罩。对于建议的标准版本,您可以使用 mask-type 属性在它们之间切换。
带-webkit 前缀的版本和提议的屏蔽标准之间还有一些进一步的区别。有关 WebKit 实现的属性和语法的完整列表,请参考 MDN 文档(developer . Mozilla . org/en-US/docs/Web/CSS/-WebKit-mask)。
带 SVG 遮罩的透明 JPEGs
页眉在两个地方使用了遮罩,其中一个比另一个更难被发现。标题本身使用了蒙版文字,但地球的背景图像(取自阿波罗探险队)实际上有它自己的蒙版。
这张图片是一张分辨率相当高的照片,标题有一个漂亮、平滑的渐变背景。在图 11-17 中,我们已经移除了文本,并稍微减轻了渐变,这样结果更加清晰可见。
图 11-17。有地球照片的页眉
通常使用 PNG 图像来获得具有烘焙透明度的照片图像。PNG 的缺点是文件太大——作为 PNG,地球图像大约有 190 KB。在这项技术中,我们将使用 SVG 的强大功能,通过遮罩将 alpha 透明度应用于 JPEG 文件。结果文件大约为 24 KB。
SVG 中的图像
我们需要做的第一件事是创建一个普通的 JPEG 图像,背景仍然在那里,如图 11-18 所示。
图 11-18。JPG 照片
接下来,我们创建一个名为 earth.svg 的 SVG“包装器”文件来加载位图图像。SVG 主要是一种矢量格式,但是您可以通过元素加载和使用 SVG 文件中的位图图像。我们最终将使用这个 SVG 文件作为 CSS 中的标题背景图片。
我们将使用 viewBox、width 和 height 属性将 SVG 图形调整到与位图图像相同的尺寸。viewBox 属性负责设置图像内部的坐标系,而 width 和 height 属性用于设置图像外部的尺寸。大多数浏览器不需要后者,但 IE 有一个缺陷,如果这两个缺失,就会扭曲 SVG 背景图像。
代码看起来像这样:
<svg width="1200" height="141" viewBox="0 0 1200 141" xmlns:xlink="http://www.w3.org/1999/xlink">
<image width="100%" height="100%" xlink:href="earth.jpg" />
</svg>
SVG 遮罩
接下来,我们需要创建遮罩。对于这个形状,我们可以用一个径向梯度,我们将确定它的大小和位置,以覆盖图像中地球的地平线。径向渐变在边缘有轻微的透明度。挑选出正确的坐标有点困难,但是我们可以通过使用图形编辑器使这变得更容易。在图 11-19 中,我们在 Adobe Illustrator 中的位图图像上画了一个巨大的半透明圆圈,以便快速获得一些测量值。我们也可以绘制一条路径来创建遮罩形状,但是径向渐变给了我们一个更平滑边缘的可能性。这只是一份一次性文件,用来提供正确的数字。
图 11-19。我们在照片上画了一个巨大的圆圈,以快速找出蒙版坐标
原来,渐变需要具有大约 1224 像素的半径,并且定位在 y 轴上的 1239 像素和 x 轴上的 607 像素。然后,我们在 earth.svg 文件中创建一个 SVG 元素,它由一个覆盖整个 SVG 视口的矩形组成,用径向渐变填充。
<mask id="earthmask">
<radialGradient gradientUnits="userSpaceOnUse" id="earthfill" r="1224" cx="607" cy="1239">
<stop offset="99.5%" stop-color="#fff"/>
<stop offset="100%" stop-color="#000"/>
</radialGradient>
<rect width="1200" height="141" fill="url(#earthfill)" />
</mask>
渐变色标从白色到黑色,边缘有轻微羽化。请注意,渐变维度的工作方式与剪辑路径相反,因为它们的大小默认情况下是根据 objectBoundingBox 维度来调整的。因此,我们还需要添加 gradientUnits="userSpaceOnUse "。
我们现在可以使用我们创建的遮罩来指向图像:
<mask id="earthmask"><!-- mask content here --></mask>
<image width="100%" height="100%" xlink:href="earth.jpg" **mask="url(#earthmask)"** />
内嵌图像
此时,如果我们要将它作为独立的 SVG 图形使用,我们的文件就可以完成了。问题是 SVG 背景图片无法加载其他资源。因此,最后一步是将位图图像(earth.jpg)转换为 Base64 编码的数据 URI。有很多工具和服务可以让你做到这一点,比如 http://duri . me——只需到那里拖放图像文件就可以得到文本字符串。
最后,我们将 SVG 中的图像文件引用换成编码字符串:
<image width="100%" height="100%" mask="url(#earthmask)" xlink:href="data:image/jpg;base64,/9j/4AAQSkZ..." />
注意,这个字符串可能很长。与二进制图像文件相比,Base64 编码增加了大约 30%的文件大小,但是由于原始 JPEG 文件大约为 18KB,所以我们总共得到 24 KB。
现在,我们终于准备好应用这个 SVG 图像作为标题背景,以及渐变:
.page-header {
background-image: url(img/earth.svg),
linear-gradient(to bottom, #000, #102133);
background-repeat: no-repeat;
background-size: 100% auto, cover;
background-position: 50% bottom;
}
这种技术可以在几乎所有支持 SVG 的浏览器中工作,但 IE9 和一些完全不支持 SVG 屏蔽的旧版本 Android 除外。
自动化技术
创建这个背景图片需要大量的工作,但是与透明的 PNG 相比,我们确实把文件大小缩小到了原来的十分之一。对于这个形状,径向渐变很好地完成了遮罩的功能,尽管需要一些手工操作。
对于更复杂的形状,有一个非常方便的 web 服务可以帮你做到这一点,叫做 ZorroSVG (mask,明白吗?).在quasimondo.com/ZorroSVG/,你可以上传一个透明的 PNG,它会吐回一个蒙版的 SVG,里面有一个 JPEG。缺点是,它将透明数据转换为位图蒙版,与将其绘制为 SVG 形状相比,会占用一些额外的空间。即便如此,你仍然有可能用这种技术获得大量的节省。
混合模式和合成
在 Photoshop、Sketch 或 Gimp 等图形编辑应用程序中,设计人员很早就可以在将设计元素叠加在一起时选择颜色的混合方式(见图 11-20 )。在 CSS 中,我们最近才被赋予对 alpha 混合的适当控制:透明 PNG 文件形式的常规透明度、rgba 背景颜色、不透明属性、遮罩等。不用说,设计师们希望看到与他们在图形编辑应用程序中使用的 CSS 相同的混合模式。在合成和混合标准中,这一天终于到来了。
图 11-20。在 Adobe Photoshop 中混合图层
合成是将图像图层合并在一起的技术术语。混合模式可能是最常见的合成方式。如果你以前没有使用过混合模式,或者没有考虑过它们是什么,它们都代表了将一个图像(称为源)的颜色值组合到另一个图像(称为目的地)之上的不同数学方法。
最简单的例子可能是“乘法”混合模式,其中源像素的每个颜色通道值与其后面像素的值相乘,从而产生更暗的图像。这有助于从灰度的角度来考虑这个例子,颜色从 0(黑色)到 1(白色)。如果源为 0.8,目标为 0.5,则结果像素的颜色值将为 0.8 × 0.5 = 0.4。
给背景图像着色
另一个例子是“光度”混合模式。它从光源获取亮度级别,并将它们应用到目标的色调和饱和度。我们的示例页面中的“银河系”部分有一个背景图像,带有一些相当鲜明的蓝色调。我们通过应用略带紫色的背景颜色,然后应用背景混合模式:亮度,对其进行了微调(见图 11-21 )。这将为图像着色,并为其提供更均匀的颜色范围。
图 11-21。用亮度混合模式给背景图像着色
.section-milkyway {
background-image: url(img/milkyway.jpg);
**background-color: #202D53;**
**background-blend-mode: luminosity;**
}
注意
如果你正在用黑白打印或单色电子阅读器阅读这篇文章:对不起!在这种情况下,很难展示色彩效果。您可以在书中的示例文件中找到工作代码。
如果不了解颜色数学,很难解释全部 16 种混合模式的作用。它们中的大多数只在某些情况下有用,比如光度允许你通过混合纯色层给图像着色。在图 11-22 中,我们将相对深蓝色的背景图像与浅粉色的背景颜色混合,向您展示当两个图层存在夸大的差异时每个模式的效果。
图 11-22。16 种混合模式
如果你想更深入地了解每种模式是如何工作的,我们推荐派尔·吉尔萨(www.slrlounge.com/school/photoshop-blend-modes/)的文章和视频《理解混合模式的终极视觉指南》。
正如我们在第五章中所讨论的,每个元素可以有多个背景图像,并且背景按照它们被声明的相反顺序堆叠在一起。背景色位于这一叠背景层的底部。如果您有多个图像层,您可以声明一个逗号分隔的背景混合模式列表,依次应用于每个层及其下面的层。
请注意,无论背景透明度如何,背景图层都不会与元素本身后面的内容混合在一起。你不能为一个单独的背景颜色层设置背景混合模式——混合元素是通过一个单独的属性来实现的,我们将在下一步解决这个问题。
混合元素
就像你可以混合背景层一样,你也可以混合元素和它们的背景。这意味着一个静态定位的子元素与其父元素混合,或者类似于一个绝对定位的元素覆盖了页面的另一部分。需要注意的是,不同堆栈环境中的元素不会相互融合;我们稍后将进一步研究这种影响。
语法与其背景对应物完全相同,但该属性被称为 mix-blend-mode。Saturn 图像使用屏幕混合模式来更好地适应页面部分的背景颜色(参见图 11-23 )。
图 11-23。土星图像使用屏幕混合模式来更好地适应背景
.fig-planet {
mix-blend-mode: screen;
}
屏幕混合模式是另一种更直接有用的模式。它得名于在同一个屏幕上投影两个图像到另一个之上,从而产生一个整体更亮的图像。在一个图像“发光”较少(即较暗)的情况下,来自第二个图像的任何光都会透过,反之亦然,从而生成整体较亮的图像。
这意味着白色源部分将完全不透明,但黑色源部分变得透明,这使它成为一种有用的遮罩技术。我们可以用它来制作一些有趣的“挖空文本”效果。
带有挖空文本的排版锁定
这种透视效果正是我们在示例页面的“可观察宇宙”部分标题中所做的,如图 11-24 所示。
图 11-24。使用屏幕混合模式的“印刷锁定”
文本位于图像顶部的白色背景上。这也是有时所谓的“排版锁定”的一部分,通过调整文本的大小或间距,使文本完全适合容器。CSS 使得这种效果有点棘手。它将与视口相关的单位一起工作,但即使是那些也有其缺点;例如,由于相对于视口的单元不相对于元素本身,我们需要在最大断点处锁定它们。
相反,该示例使用 SVG 文本来实现相对于元素大小的流畅文本大小调整。标题的标记包含一段 SVG:
<h2 class="universe-title">
<svg viewBox="0 0 400 120" role="presentation">
<text>
<tspan class="universe-span-1" x="6" dy="0.8em">The Observable</tspan>
<tspan class="universe-span-2" x="3" dy="0.75em">Universe</tspan>
</text>
</svg>
</h2>
SVG 文本本身是一个复杂的主题,但是要快速注意代码在做什么:
-
SVG 文本更像图形对象,而不像 HTML 内容那样流动。换行符不是自动的,所以每一行都需要用一个元素包装起来,并用手定位。
-
每个都是水平放置的,x 属性相对于 SVG 视窗的左边缘。
-
文本从行框的底部垂直放置。如果我们想保持大小的灵活性,我们需要垂直放置每一行,相对于它的大小有一个偏移量,作为一种行高。这就是 dy 属性的用途。
-
理论上,内联 SVG 中的文本对于屏幕阅读器来说应该是完全可访问的。实际上,一些辅助技术存在问题,但是添加 role="presentation "应该可以最大化可访问性。
由于内嵌在我们的 HTML 中,我们可以在普通的 CSS 中设计它的样式。请注意,SVG 中的文本颜色是由 fill 属性而不是颜色控制的。
.universe-span-1 {
font-size: 53.2px;
}
.universe-span-2 {
font-size: 96.2px;
}
.universe-title text {
fill: #602135;
text-transform: uppercase;
}
每个元素的大小由像素决定,以精确填充空间。需要注意的是,像素大小是相对于 SVG 片段的坐标系的,而不是 HTML。这意味着随着页面调整 SVG 的大小,文本的字体大小也随之调整,保持锁定不变。
为了保持元素本身在不同浏览器中的大小一致,我们使用了我们在第 5 章中看到的相同的纵横比。有关这方面的完整细节,请参见示例代码。然后,整个标题绝对位于图像的顶部。
最后,我们将混合模式添加到标题中:
@supports (mix-blend-mode: screen) {
.universe-title {
mix-blend-mode: screen;
}
.universe-title text {
fill: #000;
}
}
SVG 中的文本最初是用暗红色设计的,这种颜色与它后面的图像的整体颜色很协调。对于不支持 mix-blend-mode 的浏览器来说,这是一种备用方案(见图 11-25 )。在@supports 规则中,我们设置了混合模式,但也将文本的填充颜色更改为黑色,使其完全透明。
图 11-25。IE9 中出现的标题,不支持 mix-blend-mode。尽管如此,SVG 大小调整工作得很好,即使是在这个较旧的浏览器中
一般来说,考虑到渐进增强,混合模式并不难应用。影响通常是微妙的,当我们做更剧烈的改变时,@supports 规则会有所帮助。元素的混合模式适用于最新版本的 Chrome、Opera 和 Firefox,以及 Safari(Mac 上的 7.1 版本和 iOS8 的移动 Safari 版本)。然而,Safari 缺少对亮度、色调和颜色混合模式的支持。在撰写本文时,IE、Edge、Opera Mini 和 Android WebKit 浏览器都缺乏支持。
隔离
除了混合模式,我们可以用 CSS 控制的另一个合成方面是隔离。实际上,这意味着创建在组内融合但不在组外融合的元素组。我们之前提到过,不同堆叠环境中的元素(见第三章)不会融合在一起。
在图 11-26 中,我们有两个应用了多重混合模式的一组项目的例子。每组坐在有图案的背景上。在左边的例子中,混合模式不是孤立的,所以单个元素也与背景混合。在右边的示例中,图形的不透明度设置为 0.999,这将强制一个新的堆叠上下文并隔离混合。
图 11-26。左边的组一直融合到背景中,但是右边的组是孤立的
.item {
mix-blend-mode: multiply;
}
.group-b {
opacity: 0.999;
}
“B 组”中的项目相互融合,但不与背景融合。
我们可以使用新的隔离属性创建新的堆叠上下文(从而隔离组),而不会破坏不透明度。通过以下改变可以获得与上述相同的结果:
.group-b {
**isolation: isolate;**
}
CSS 中的图像处理:滤镜
现代 CSS 的下一个工具也是直接来自图像编辑软件:对元素应用图形过滤器。过滤器应用于整个元素及其子元素。这有点像对页面的一部分进行截图,然后像在 Photoshop 中一样调整图像的各个方面。(事实上,这个类比与浏览器实际上是如何实现这些东西的相差不远——我们将在第十二章回到这个话题。)过滤器在基于 WebKit 和 Blink 的浏览器中可用,如 Safari、Chrome 和 Opera,以及 Firefox 和 Edge,因此支持相当广泛。有十种不同的过滤器可用,加上在 SVG 中定义自己的过滤器的能力。我们将从浏览 CSS 中可用的预定义过滤器开始。
可调颜色处理滤镜
滤镜允许您将一种或多种效果按顺序应用到元素。其中一些是更通用的颜色处理滤镜,允许你调整亮度、对比度、饱和度等。下面的代码片段应该是非常自我描述的(结果见图 11-27 ):
图 11-27。应用了一系列过滤器的“Universe”标题
.universe-header {
filter: grayscale(70%) brightness(0.7) contrast(2);
}
我们已经将元素的饱和度降低了 70%,使其完全变成灰度,然后将亮度从 1(正常亮度)降低到 0.7,最后将对比度调高到正常值的两倍。
大多数过滤器可以采用百分比或数字值。对于可以上下变化的值,如对比度()、亮度()和饱和度(),默认值为 100%或 1。对于灰度()、反转()和棕褐色(),默认值为 0,接近 100%或 1。任何高于该值的值都被限制为最大值。
还有一个不透明度()滤镜,默认值为 1(或 100%),取值低至 0。此滤镜和 opacity 属性之间的区别在于,根据滤镜在滤镜链中的添加位置,滤镜可能会有不同的结果。相比之下,不透明度属性总是在应用所有滤镜后应用。我们将在本章的后面重新讨论应用的顺序。
最后,有几个过滤器的工作方式稍有不同,我们将使用“观星”页面中的示例分别检查它们。
色调旋转
太阳及其黑子的图像实际上是一张黑色背景的灰度照片。这不是最令人愉快的照片,在大多数情况下,你甚至会在把它放在页面上之前打开图像编辑器——你应该这样做,因为这可能是最好的执行路线。为了说明过滤器是如何工作的,让我们假设我们根本不能访问图像,只能访问 CSS。就像我们之前给背景图像着色一样,我们要给太阳图像着色,让它稍微亮一点。图 11-28 显示了没有应用任何滤镜的图像。
图 11-28。太阳的原始天文照片。不太闪亮
hue-rotate()滤镜允许我们根据标准色轮将图像的所有色调旋转一定的角度。明亮的黄色位于大约 40 度(从顶部开始)在这个轮子上,所以色调旋转(40 度)应该做到这一点。问题是,图像是灰度的,所以没有色调,色调旋转不会有任何影响!
为了解决这个问题,我们可以使用一个包含另一个过滤器的技巧。sepia()滤镜已经用位于色轮上大约 30 度的褐色色调给图像着色。然后,我们可以将它与大约 10 度的色调旋转链接在一起,得到正确的黄色细微差别。最后,我们需要降低对比度,提高亮度一点,让太阳发光。这需要在色调操作之前完成;否则黄色会变得太淡。记住,过滤器是按顺序应用的。
.fig-sun {
filter: contrast(0.34) brightness(1.6) sepia(1) hue-rotate(10deg);
}
接下来,我们用之前提到的 SVG 蒙版技术将黑色背景蒙版掉,得到图 11-29 中的结果:
图 11-29。应用了遮罩的过滤日光图像
.fig-sun {
filter: contrast(0.34) brightness(1.6) sepia(1) hue-rotate(10deg);
mask: url(#circlemask); /* points to a circular SVG mask we created */
}
裁剪形状上的阴影
我们要看的下一个滤镜是投影()。这个滤镜非常类似于框阴影和文本阴影属性,但是它有一些限制和额外的技巧。
当框阴影应用于元素的矩形边框形状时,drop-shadow()滤镜应用于元素的透明轮廓。这包括将阴影放在具有 alpha 透明度的图像上,并使它们跟随图像的轮廓,或者将阴影添加到使用 clip-path 成形的元素。
在观星页面的导航菜单中,项目被裁剪成不同的形状,然后用投影()过滤(结果见图 11-30 )。语法看起来与 text-shadow 属性完全一样:它包括 x 和 y 偏移、模糊半径和颜色。这意味着我们在 box-shadow 中找到的 spread 参数在这里丢失了。
图 11-30 。导航菜单项在其剪辑形状周围有投影效果。向右,一个悬停或聚焦的元素得到一个较亮的阴影,产生一个光晕效果
.nav-section li {
filter: **drop-shadow(0 0 .5em rgba(0,0,0,0.3))**;
}
CSS 滤镜效果在可用时使用专用图形芯片。这使得 drop-shadow()过滤器具有惊人的性能。例如,在制作阴影动画时,使用滤镜版本可能比使用方框阴影版本更好。在第十二章中,我们将深入开发工具,看看如何测量 CSS 属性对渲染的影响。我们要讨论的下一个效应对性能的影响就不那么好了。
模糊滤镜
blur()滤镜对元素应用高斯模糊。您为它提供一个长度设置模糊半径扩散的距离。在图 11-31 中,我们在示例页面的土星图像上设置了 10px 的模糊半径:
图 11-31。模糊了土星的图像
.fig-planet {
filter: blur(10px);
}
至少在当前的实现中,blur()过滤器往往是一个性能猪,所以要明智地使用它。这有点遗憾,因为模糊可以产生一些有趣的动画效果。当在界面中引导注意力,或者淡化背景中的某些东西时,模糊和聚焦是一种有效的工具。
背景滤镜
说到背景,还有一些地方过滤器已经潜入了 CSS。在示例页面的“银河”部分,我们使用了 2 级滤镜效果规范中的一个实验属性:背景滤镜。
它的工作方式与 filter 属性完全一样,但是它将滤镜应用于元素背景与其后面的页面的合成中。这使我们能够做一些漂亮的“毛玻璃”效果,例如(见图 11-32 )。
图 11-32。对半透明元素后面的背景应用背景模糊
.section-milkyway .section-text {
backdrop-filter: blur(5px);
background-color: rgba(0,0,0,0.5);
}
到目前为止,这个属性只在最近的基于 WebKit 的浏览器中实现,如 Safari 9(as-WebKit-background-filter)和 Google Chrome 中的一个标志后面。
使用图像过滤功能过滤背景图像
滤镜效果规范还规定了在 CSS 中加载图像时可以使用滤镜。要过滤背景图像,您可以通过 filter() 函数运行它。该函数使用与滤镜属性相同类型的滤镜链,但在加载图像时使用。图像是第一个参数,滤镜链是第二个参数。
例如,我们可以改变背景图像的不透明度并将其变为灰度(见图 11-33 ):
图 11-33。一个组件,我们已经改变了背景的不透明度,并把它完全灰度化
.figure-filtered {
background-image: filter(url(img/saturn.png), grayscale(1) opacity(0.4));
}
坏消息是浏览器支持。在撰写本文时,只有 WebKit browser 的最新实验版本支持这一功能。出于某种原因,所有其他实现了过滤器的浏览器都忽略了图像过滤器功能。
注意
Safari 9 确实有一个带前缀(且未记录)的 filter()函数版本,但它被严重破坏了,因为背景无法正确调整大小。因此,不要在这个属性上使用-webkit-前缀。
高级过滤器和 SVG
在像 Instagram 这样的照片应用程序中,你可以对图像应用预先合成的滤镜,通常是通过结合颜色叠加和我们迄今为止在速记滤镜功能中看到的一些操作。开发人员和设计师乌娜·克拉韦茨将 Instagram 滤镜合并成一个小型 CSS 库(una.im/CSSgram/),在这里巧妙地使用伪元素、CSS 渐变和混合模式来创建颜色叠加(见图 11-34 )。
图 11-34。CSSgram 库中一些过滤器的屏幕截图
CSS 滤镜最强大的一个方面是,我们可以使用 SVG 来创建这样的自定义滤镜,实际上对滤镜效果的复杂性没有限制,并且只需要较少的 CSS 工作。
CSS 版本的过滤器最初是作为 SVG 中的过滤器出现的。如同本章中的大多数其他视觉效果一样,它们开始渗透到 HTML 中。第一个浏览器是 Firefox,它允许我们对 HTML 内容应用简单的 SVG 过滤器,使用我们见过的裁剪和遮罩技术。然后是 2011 年由 Adobe、Apple 和 Opera 编写的 CSS 滤镜规范,该规范将 SVG 滤镜捆绑成我们目前看到的易用的“速记”滤镜功能。
事实上,所有 CSS 过滤器函数都是根据它们的 SVG 对应物来定义的。比如一个滤镜:灰度(100%);声明对应于这个 SVG 过滤器:
<filter id="grayscale">
<feColorMatrix type="matrix"
values=".213 .715 .072 0 0
.213 .715 .072 0 0
.213 .715 .072 0 0
0 0 0 1 0" />
</filter>
前面的滤镜声明只包含一个滤镜原语,由一个“颜色矩阵”滤镜效果元素(< feColorMatrix >)表示。颜色矩阵滤镜是一个非常通用的工具,允许您以各种方式将输入颜色映射到输出。确切地知道每个值做什么并不重要,但关键是灰度本身并不是一个低级的东西,而是一个通用颜色处理的结果——至少就 SVG 而言是这样。
还有其他几种滤镜原语,大多数效果都是组合几种的结果。例如,投影()滤镜由滤镜基元高斯模糊、偏移、泛光、合成和合并组成。
现在,有趣的部分是,我们可以创作自己的 SVG 过滤器并将其应用于 HTML 内容。这意味着我们可以自由地创建尽可能复杂的过滤器,只要我们在 SVG 中定义它们并将其作为过滤器的来源。这是通过过滤器声明中的 url()函数符号来完成的,就像屏蔽和剪辑一样。为了显示稍微复杂一点的东西,在图 11-35 中,我们从 SVG 的 CSSgrams 中重新创建了“1977”过滤器。
图 11-35。左边是原始图像,右边是过滤后的图像
在 CSSgram 版本的滤镜的原始代码中,有三个滤镜操作:对比度(1.1)亮度(1.1)饱和(1.3)。还有一个颜色覆盖伪元素,粉红色调设置为 0.3 的不透明度和屏幕的混合混合模式。由于过滤器是在 SVG 方面的规范中定义的,我们可以查找如何编写它们并计算值。结果我们需要两个 feComponentTransfer(用于对比度和亮度)和一个 feColorMatrix(用于饱和度)滤镜。我们可以用 feFlood 滤镜创建颜色叠加,它创建一个具有纯色填充的滤镜层。所有这些然后合并在一起使用一个混合过滤器,我们设置混合模式为屏幕。
<filter id="filter-1977" color-interpolation-filters="sRGB">
<feComponentTransfer result="contrastout">
<feFuncR type="linear" slope="1.1" intercept="-0.05"/>
<feFuncG type="linear" slope="1.1" intercept="-0.05"/>
<feFuncB type="linear" slope="1.1" intercept="-0.05"/>
</feComponentTransfer>
<feComponentTransfer in="contrastout" result="brightnessout">
<feFuncR type="linear" slope="1.1"/>
<feFuncG type="linear" slope="1.1"/>
<feFuncB type="linear" slope="1.1"/>
</feComponentTransfer>
<feColorMatrix in="brightnessout" type="saturate" values="1.3" result="img" />
<feFlood flood-color="#F36ABC" flood-opacity="0.3" result="overlay" />
<feBlend in="overlay" in2="img" mode="screen" />
</filter>
SVG 过滤器允许您通过分别用 in 和 result 属性命名输入和输出,将各种过滤器和过滤器原语的结果“管道化”到彼此之中。第一个 filter 原语没有定义,默认使用源图形作为输入。
我们现在可以在 CSS 中引用这个 SVG 片段:
.filter-1977 {
filter: url(#filter-1977);
}
SVG 过滤器是可链接和可组合的。你可以制造噪音,添加灯光效果,并根据自己的需要控制颜色通道。唯一的限制是你的想象力——以及对有点晦涩的语法的兴趣。但是要注意性能:SVG 效果还没有在浏览器中进行硬件加速,所以要谨慎使用自定义过滤器。
同样的警告也适用:一些浏览器对使用外部 SVG 片段标识符有限制,所以您可能需要暂时使用“全在一个 HTML 文件中”的技术。
应用于 HTML 的 SVG 过滤器在任何支持“速记”CSS 过滤器的地方都是受支持的,除了编写本文时的 Edge。注意 IE 版本 10 和 11 确实支持 SVG 中的过滤器*,但是不适用于 HTML 内容。*
视觉效果的应用顺序
由于我们可能会根据裁剪、遮罩、混合和过滤的顺序得到不同的(也许是不理想的)结果,因此这些属性有一个标准化的应用顺序。
所有的剪裁、蒙版、混合和过滤都是在设置了其他属性(除了不透明度,我们马上会谈到它)之后进行的:颜色、宽度、高度、边框、背景属性等等。设置元素的基本外观。然后是具有高级效果的“后处理”步骤,其中元素及其内容被有效地视为单个图像。
首先,过滤器按照它们被声明的顺序被应用。然后元素被剪裁,然后被屏蔽。请注意,由于裁剪和遮罩是在应用滤镜后发生的,因此我们不能直接在裁剪后的形状上使用 drop-shadow()滤镜。阴影(例如,模糊元素的边缘)将被剪掉。在观星示例的导航中,我们通过向项目内部的链接添加剪辑路径,但是向项目元素添加投影,解决了这个问题。
最后,是合成步骤,应用混合模式。他们用不透明属性共享这个步骤,这本身就是一种有效的混合。
摘要
在这一章中,我们已经从过去枯燥乏味、四四方方的书页中迈出了一大步。我们已经探索了如何用 CSS 形状来塑造页面的流动,以及如何用裁剪路径来消除视觉边界。使用遮罩,我们可以进一步控制设计元素的可见性。
我们还看了如何通过 CSS 混合模式最终实现不同图层的混合,这是许多设计师在图形编辑软件中习惯的。
CSS 过滤器开始将更多我们从图形世界中期待的效果添加到我们实际可以在浏览器中影响的设计中。
在这些效果中,我们看到了 CSS 是如何与 SVG 强大的图形编辑相协调的,让我们推动了网页设计的发展。
在这一章之后,我们将把 CSS 看做软件:如何编写模块化的、可读的和可维护的代码。
十二、代码质量和工作流程
在本书中,我们讨论了各种技术和(许多!)CSS 的不同规范和属性。在这个过程中,我们接触了一些负责任地思考这些解决方案的有用方法。在这最后一章中,我们将重新审视这些方法中的一些,以更深入地探究其中一些方法优于其他方法的原因。
掌握 CSS 是关于编写不仅能工作(而且工作得很好)而且具有可读性、可移植性和可维护性等品质的标记和样式。我们的目标是在这最后一章中给你所有你需要的知识,来解决写好 CSS 的更复杂的方面。
在大多数情况下,我们不会引入太多新标准,而是在理论和一些实际例子之间切换。在本章的最后,我们将探索一些有效处理代码的工具,并让你对语言的未来有所了解。
在这一章中,我们将涵盖以下主题,以帮助您编写更好的 CSS:
-
浏览器如何从样式表到呈现的网页
-
如何使用开发人员工具来帮助优化渲染性能
-
通过限制选择器类型和选择器深度来管理级联
-
HTML 与 CSS 中的命名方案和复杂性平衡
-
像 linters、预处理程序和构建系统这样的工具来处理复杂的 CSS
-
未来的标准,如定制属性、HTTP/2 和 Web 组件
调试 CSS:外部代码质量
在这一节中,我们将解释浏览器如何处理 HTML 和 CSS,以及我们如何使用这些知识来解决渲染性能等问题。
代码的这些方面有时被称为外部代码质量——对于使用最终结果的人来说是显而易见的。几个重要的例子包括:
-
正确性:代码是否按预期工作?我们在 CSS 中输入了正确的属性名吗,浏览器能理解吗?
-
可用性:代码的结果不仅看起来正确,而且可以使用吗?例如,可访问性就属于这一类。
-
健壮性:如果出了问题会怎么样?例如,我们可以声明两组属性,其中一组是旧浏览器的备用属性。
-
性能:设计加载速度快吗,动画和滚动流畅吗?
其中一些品质是在编写任何代码之前拥有正确心态的问题,我们在本书中一直试图展示可用性和健壮性的良好原则。当对真实世界的项目进行编码时,您需要深入思考正确性和良好的性能对于每个独特的组件意味着什么。这是使用大多数浏览器内置的开发工具的好地方。
在前面的章节中,我们已经看到了如何使用这些工具来查看哪些属性被应用到一个元素或者调试动画。开发者工具在不断改进,我们可以用它们做的远不止这些。例如,图 12-1 显示了我们如何使用 Firefox 中的开发工具来找出使用了哪个字体文件,而不仅仅是字体堆栈声明是什么。
图 12-1。使用 Firefox 开发工具来找出哪个字体文件被用在了www.microsoft.com的特定元素上
进一步深入开发工具,您会发现面板和按钮,让您检查其他质量。这使您不仅可以看到应用了什么,还可以看到如何应用以及何时应用。为了理解这些工具,了解一点浏览器如何解析 CSS 是有帮助的。
浏览器如何解释 CSS
接下来是从 CSS 文件到“屏幕上的像素”的旋风之旅,以便更好地理解我们编写的 CSS 的影响。以下部分描述的步骤代表了每次加载新页面时发生的情况的简化模型,但是当页面被交互时,一些(或所有)步骤也可能发生。
解析文件和构造对象模型
当你加载一个站点时,浏览器首先会收到一个 HTML 响应。这种响应被解释为彼此有关系的对象(节点)。例如,body 节点是 html 节点的后代,p 和 h1 节点可能存在于 body 节点中。这就是 DOM:文档对象模型(见图 12-2 )。
图 12-2。文档对象模型是浏览器内部理解 HTML 的方式
当在 HTML 文档中遇到指向 CSS 文件的链接元素时,浏览器将获取并解析该文件。类似于如何将 HTML 转换成 DOM 树,CSS 文件被解析成称为 CSSOM 的东西:CSS 对象模型。不仅是外部文件,样式元素或内联样式属性中的任何 CSS 都将被解析并添加到 CSSOM 中。就像 DOM 一样,它是一个树状结构,包含页面样式的组合层次结构(见图 12-3 )。
图 12-3。CSSOM 树表示样式表中样式的层次结构
每个 DOM 节点都与相关的 CSS 选择器相匹配,并进行最终的样式计算(基于级联、继承和特定性之类的东西)。
DOM 和 CSSOM 都是标准化的,并且应该跨浏览器工作。在这一步之后,如何从现在拥有的数据到屏幕上显示的内容取决于浏览器,但所有浏览器都遵循类似的步骤来实现这一点。
渲染树
呈现页面的下一步是构建另一个树结构,通常称为呈现树。在这里,每个对象都代表要在屏幕上呈现的内容。这个结构看起来有点像 DOM 的树,但是它们并不相同。例如,可视化隐藏的 DOM 节点将不会出现在渲染树中,而像::before 这样的伪元素可能会有一个不在 DOM 中的渲染对象。浏览器还需要表现页面视觉表现的其他方面,比如滚动块和视窗(见图 12-4 )。
图 12-4。一个简化的假想渲染树。head、title 和 meta 等元素没有自己的渲染对象。对于带有显示的元素也是如此:例如,none
构建渲染树时,渲染树中的每个对象都知道它应该是什么颜色,任何文本是什么字体,它是否有明确的宽度,等等。
布局
在下一阶段,计算每个渲染对象的几何属性。这被称为布局或回流阶段。浏览器将遍历渲染树,并试图找出每个项目在页面上的位置。
由于很多网页布局都是为了保持页面的流动,其中元素“推”上其他元素,这可能会变得相当复杂。图 12-5 来自程序员 Satoshi Ueyama 的一个有趣的视频(【www.youtube.com/watch?v=dnd… Gecko 引擎,展示了 Firefox 在布局网站时的实际回流操作。
图 12-5。一段视频截图,显示了火狐浏览器中www.wikipedia.org的缓慢回流
有时,在这个阶段需要构建具有自己的呈现属性的附加呈现对象。例如,一段具有特定字体大小的文本可能会生成一个换行符,将它拆分成两个匿名的行框。这反过来会影响父元素的最终高度,以及它后面的其他元素。
最终,每个渲染对象的位置都会被计算出来,是时候把它们放到屏幕上了。
绘画、合成和绘图
在一个非常简化的模型中,浏览器现在从渲染树中获取它可以学习到的一切,并将可视化表示放在屏幕上。实际上,事情要复杂一些。
当每个渲染对象的位置和属性被确定后,浏览器可以计算出屏幕上要显示的实际像素,这个过程被称为绘制。但除此之外,浏览器可能还需要做一些进一步的工作。
当浏览器知道最终图形表示的某个部分不能影响页面其余部分的显示时,它可能会决定将绘制工作分成不同的任务,每个任务负责页面的一个特定部分,称为层。
有些东西,如 3D 变换,甚至可以通过使用专用的图形芯片进行硬件加速。其他图层可能应用了滤镜或混合模式,这将决定它们如何与其他图层混合。这种将渲染分割成层,然后将它们重新组合成最终结果的任务称为合成。如果页面是用描图纸做的,这就相当于在不同的纸上画画,然后把它们粘在一起。
最后,页面准备好在屏幕上显示(或绘制)。唷!
优化渲染性能
如果页面中有任何变化,浏览器将需要再次执行前面的一些步骤。为了保持页面在屏幕上平滑显示,最好在 16 毫秒内完成,这是每次屏幕更新之间的时间,假设它有标准的 60hz 刷新率。
从性能角度来看,有些东西通常非常便宜,比如滚动:整个最终渲染只是在不同的位置重新绘制。当某个因素导致页面样式改变时,性能会有所下降。
如果我们在 JavaScript 中更改元素的宽度或高度属性,浏览器将需要进行布局、合成和绘制。仅改变文本的颜色不会影响布局,因此会触发绘画和合成。最后,我们能做的最便宜的操作是完全通过合成来完成的。
网站csstriggers.com是哪个属性映射到哪个渲染操作的便利参考(见图 12-6 )。该网站(由 Paul Lewis 创建)目前跟踪谷歌 Chrome 的渲染操作,但它们很可能在大多数浏览器中类似地工作。
图 12-6。CSS 属性和它们在浏览器中触发的工作量,来自csstriggers.com
我们可以使用开发人员工具来查看这些不同的步骤何时执行,以及最终的性能如何。通过进入 Chrome DevTools 中的时间轴面板,我们可以记录我们与页面的交互,并跟踪交互是否触发了特定的呈现步骤。其他浏览器也有时间线记录功能,但 Chrome DevTools 一直是功能最丰富的。在图 12-7 中,我们记录了一个 1.5 秒的时间线,我们滚动了一个示例页面,在滚动的同时,一个固定的标题被动画显示在视图中。
图 12-7。Chrome DevTools 中的时间线记录。单击右侧面板中的圆形图标可以开始和停止录制
我们可以放大到单个帧的级别,并确定浏览器内部正在进行何种操作。每个条代表一个渲染帧,每个条的彩色位代表一个渲染操作。在这种情况下,绿色代表绘画操作。在时间线下方,列出了每项操作,我们可以单击每一行以获得更多详细信息。
正如时间轴所示,某些东西导致了每一帧中的绘画操作。这并不可怕,但它可能会阻止在较慢的机器上平滑滚动,并且不应该在滚动时发生。为了弄清楚发生了什么,我们可以在渲染选项卡中打开一个叫做“绘画闪烁”的东西。然后,当我们与页面交互时,浏览器会在任何重新绘制的区域周围绘制一个颜色高亮(见图 12-8 )。
图 12-8。打开绘画闪烁显示,当我们滚动时,固定标题被重新绘制
当我们滚动时,固定的标题会不断地被重画,因为它会影响下面的滚动内容。幸运的是,浏览器优化了绘制区域,所以至少它不是整个页面。但是我们可以做得更好,通过强制浏览器在一个单独的层中渲染固定的部分,并且只进行合成。页眉的当前样式如下所示:
.page-head {
position: fixed;
top: 0;
left: 0;
width: 100%;
transition: top .25s ease-in-out;
}
.page-head-hide {
top: -3.125em;
}
那个。page-head-hide 规则是通过 JavaScript 切换的,当我们向下滚动时,它将页眉移出视图,当我们向上滚动时,它将页眉移回视图。
避免画图的诀窍是强制浏览器创建一个单独的硬件加速层来呈现标题,然后将它与页面的其余部分合成在一起。我们将使用 will-change 属性来做到这一点。此属性向浏览器提供了一个提示,表明此元素将来会更新 transform 属性。变换属性不会自己创建新层,但动画变换会创建新层。当浏览器得到预览提示,标题将在未来的动画,它将创建一个新的层的权利,从一开始。
这意味着我们可以过渡 transform 属性而不是 top 属性,一举两得:滚动性能和动画性能都会受益。
新的样式看起来像这样:
.page-head {
/* some styles left out for brevity */
transition: transform .25s ease-in-out;
transform: translateY(0);
will-change: transform;
}
.page-head-hide {
transform: translateY(-100%);
}
重新运行时间线记录现在显示没有绘画在进行。我们还可以通过打开渲染选项卡中的“显示层边界”选项来验证是否创建了一个单独的层。现在标题周围应该有一个彩色边框(见图 12-9 )。
图 12-9。当我们滚动时,时间线现在显示没有绘制操作正在进行。打开“显示图层边框”会在标题图层周围绘制一个彩色轮廓
注意
在撰写本文时,最新版本的 Firefox、Safari、Chrome 和 Opera 都支持 will-change 属性。对于更向后兼容的技术,您可以使用 3D 变换来移动标题,这也会强制一个单独的层。
像这样使用开发人员工具来窥探浏览器内部的能力还没有出现很长时间,而发现幕后到底发生了什么的工具正在突飞猛进地发展。您不必为项目中的每一个规则都考虑这么多细节,但是要理解 CSS 是如何工作的(以及为什么有些东西比其他东西更贵),掌握浏览器渲染和调试是非常重要的。
人类的 CSS:内部代码质量
我们应该始终考虑用户的需求,而不是开发人员的便利,所以花大力气保护代码的外部质量是有意义的。
这可能看起来有些矛盾,有些人可能会认为内部代码质量更加重要。举几个内部质量标记:
- ****(“不要重复自己”)代码有多枯燥:每个独特的问题是在一个地方解决的,还是如果你改变一个解决方案,你必须更新许多不同的地方?
*** 可读性:有人能在阅读时轻松理解代码做了什么吗?
* 可移植性:你的一段代码只有在与你的代码库的其他部分结合时才能工作,还是独立存在?
* 模块化:你能以一种不言而喻的方式将你的部分代码组合并重用到新的事物中吗?**
**这些品质如此重要的原因是它们会影响到编写或修改代码的人。如果外部质量有问题(一个 bug),没有人能理解导致 bug 的源代码,你就不知道怎么修复。高外部质量通常是高内部质量的结果,但反之则很少。
内部代码质量也更加主观,基于个人偏好以及每个单独项目的属性。所以,戴上你的批判护目镜,让我们一起探索吧!
理解 CSS 的形状
CSS 是根据几个设计原则构建的。最重要的原则之一是简单:CSS 应该易于学习。你不应该需要一个计算机科学学位来使用它。作为一名设计师,你应该能够掌握如何选择页面的一部分,并对其应用样式。这不需要广泛的软件构造知识。
将 CSS 视为软件
同时 CSS 是也是软件。作为软件,它的品质不仅仅是工作。对于一个快速原型来说,代码的质量在很大程度上是无关紧要的,只要它能完成工作。但是一旦某样东西成为活产品的一部分,代码的质量可能会产生广泛的反响。它将影响诸如随着时间的推移维护的成本有多高,出现新错误的可能性有多大,以及对新开发人员来说使用起来有多容易。
即使你正在创建的东西是一个人的项目,假设团队中至少有两个人也是健康的:你,和未来的你。当你在几个月或几年内修复某个 bug 时,你可能已经忘记了最初编写代码时你在想什么。
带上你自己的结构
CSS 通常被描述为一种声明性语言。简而言之,这意味着你用它来告诉计算机做什么,仅限于该语言知道如何做的一系列事情。相比之下,许多通用编程语言更像是命令式的,这意味着你可以用它们来告诉计算机如何*(以及以什么样的顺序)做事情的一步一步的指令。*
许多命令式编程语言都配备了少量的构建块,允许特定于您的代码的新型控制结构和逻辑。在 CSS 中却不是这样:它有可以调用的函数,比如 url()函数,但是它缺少允许你定义自己的函数的构件。
添加到文档中的所有 CSS 也共享一个全局范围。如果您有一个带有选择器 p 的规则,那么它将成为所有段落元素的样式计算中的一个因素,不管它来自哪个样式表或者它是如何加载的。选择器决定每个规则的范围,但是样式表和文档之间的连接总是全局的。例如,您不能创建一个包含 p 选择器的 CSS 文件,并以一种仅将它应用于页面一部分的段落的方式加载它。(这种模式在 Web 组件领域存在吗,这是一项仍处于起步阶段的技术。我们将在本章末尾回到 Web 组件。)
范围属性
有一种方法可以用自己的独立样式来样式化页面的一部分:样式元素上的 scoped 属性。这是一个相当笨拙的机制,浏览器制造商一直不愿意实现它(迄今为止只有 Firefox 在船上)。
使用 scoped 时,style 元素仅限于应用于父元素或其中的子元素。在下面的标记中,只有内部的
是红色的:
<p>I will not be red</p>
<div>
<p>I will be red.</p>
<style scoped>
p { color: red; }
</style>
</div>
虽然这是一个方便的概念,但它在向后兼容性方面效果不佳——不支持的浏览器无论如何都会全局应用这些样式。
许多编程语言都有名称空间的概念:代码无法影响外部世界或受外部世界影响的隔离上下文,除非显式导入或导出。这使得管理代码库变得更加容易,而不会在其他地方产生意想不到的后果。
CSS 语言的简单模型意味着我们想要强加的任何结构必须来自我们编写规则的方式。在这一章的下一部分,我们将看一个简单的例子,并试图推导出一些编写高质量 CSS 的指导方针。
代码质量:一个例子
图 12-10 中的警告消息框看起来完全一样,但实现方式不同。当我们查看源代码时,我们希望关于内部代码质量的一些理论性的谈论会变得更加清晰。
图 12-10。一个警告消息框,以三种略有不同的方式实现
第一个实现使用以下标记和 CSS:
<div id="pink-box">
<p>This is alert message implementation one</p>
</div>
div#pink-box {
border-radius: .5em;
padding: 1em;
border: .25em solid #D9C7CC;
background-color: #FFEDED;
color: #373334;
}
首先要注意的是选择器中 id 的使用。这可以防止在页面上的任何地方重用这个选择器,这是不必要的限制。使用 id 属性本身没有任何问题:它们对于页面内链接或 JavaScript 挂钩来说非常有用。也没有什么可以阻止你使用它们作为 CSS 选择器,但是高度的专一性(正如我们在第二章中讨论的)使得忽略任何规则的变化都很麻烦。像消息组件这样的东西很可能在页面上被覆盖和重复,所以在这种情况下,ID 肯定是个问题。
此外,我们在选择器中添加了一个完全不必要的 div 限定符,在这种情况下,它除了增加特异性之外什么也没做。以这种方式将元素选择器与 id 或类一起使用是很常见的——通常这是试图在其他地方覆盖某些过于具体的规则的结果。通常,解决方案不是升级特异性“军备竞赛”,而是重新思考您的命名策略。
另一个需要注意的是 id 属性名:#pink-box 描述了警告消息框的一个特定属性。我们可以决定将警告消息改为内部带有红色图标的白盒,这样类名就不再有意义了。
就样式声明而言,它们本身没有任何问题:边框、填充和边框半径属性有相对于字体的大小,文本、边框和背景有一些颜色。但是我们可以做得更好,看一下第二个实现,我们将突出一些明显的区别:
<div class="warning-message">
<p>This is alert message implementation two</p>
</div>
.warning-message {
border-radius: .5em;
padding: 1em;
border: .25em solid rgba(0, 0, 0, 0.15);
background-color: #FFEDED;
color: rgba(0, 0, 0, 0.8);
}
这里,类名的目的要清楚得多:它是一个警告消息组件,名称中省略了实现细节。颜色的定义不同:文本和边框的不同阴影都是使用半透明的黑色与粉色背景混合生成的。这意味着我们可以只改变背景颜色,免费获得另外两种颜色——少了一个更新代码的地方。
但是这个名字仍然是特定于一种样式的消息框的。如果我们有一个想要覆盖颜色的成功消息框规则,那么在标记中有一个以 warning 开头的类名是没有意义的。第三个示例解决了这个问题:
<div class="message message-warning">
<p>This is alert message implementation three</p>
</div>
.message {
border-radius: .5em;
padding: 1em;
border: .25em solid rgba(0, 0, 0, 0.15);
background-color: #ffffed;
color: rgba(0, 0, 0, 0.8);
}
.message-warning {
background-color: #FFEDED;
}
乍一看,这个例子使用了更多的代码来做同样的事情。诀窍在于。消息规则实际上是一种带有淡黄色的中性消息样式。那个。消息警告规则通过改变唯一不同的东西——背景颜色,将普通消息变成警告消息。
我们可以通过决定其他名称来轻松创建其他类型的消息规则,例如 green。消息成功规则(见图 12-11 ):
图 12-11。智能结构允许我们快速创建其他样式的消息框
.message-success {
background-color: #edffed;
}
通过以这种方式构建代码,我们获得了许多好处:
-
半透明的文本和边框让我们可以通过一个声明来创造新的变化。
-
名称消息与该组件的功能有关,而不是最终结果(某种配色方案的盒子)。希望对于不熟悉该代码的人来说,这个名称的目的也很清楚。
-
通过用基本名称(.消息)并将它们一起保存在 CSS 文件中,这样就更容易直观地扫描文件并识别这些规则的用途。
消息框最初的三种变体完全有效,并且在浏览器中的外观上完全相同。通过构建代码,给它一组不同的名称,并仔细选择如何应用属性,最终的实现在质量上有很大的不同。在这一章的剩余部分,我们将更深入地探讨一些用于编写高质量 CSS 的常见模式、方法和工具。
管理级联
我们可以从前面的消息示例中提取一些原则来帮助提高代码的质量:
-
使用类名作为主要的样式挂钩
-
使类名可读且清晰
-
打破单一目的的规则,避免不必要的重复
-
避免将元素类型绑定到样式规则
所有这些都有一个共同点:它们主要通过控制特异性来限制级联效应。
为什么要限制这种语言最强大的特性之一的使用呢?在某种程度上,这个问题本身就有答案,因为任何电动工具都需要附有安全说明——“使用时远离身体。”但是级联也是为了一个特定的目的而发明的:允许混合样式规则源(用户代理默认值、作者规则和用户规则)来决定文档的最终表示。
我们在这里使用“文档”这个词是有原因的——在 CSS 发明的时候,Web 主要被视为一种共享文本文档的技术。CSS 允许一种优雅的方式通过层叠和继承来提高一致性。它还带来了像用户样式表这样的概念:如果用户更喜欢用高对比度的样式表阅读网页,他们可以覆盖作者的样式。
虽然 Web 仍然具有文档模型的底层架构,但它现在被用来创建更高级的视觉设计和用户界面。实际上,这意味着作者样式的重要性转移。随着这些变得越来越复杂,有一种趋势是将它们划分开来,使它们更加可移植、独立和可预测。前面列出的原则是实现这一目的的起点。在下一节中,我们将从一个稍微不同的角度来看 CSS,找出我们如何进一步发展这些原则。
结构化命名方案和 CSS 方法
在前面的消息示例中,我们用。留言。这种“前缀”的思想不仅使代码更具可读性,而且以类似于名称空间思想的方式组织代码。
有几个人和组织已经承担了提出方法的任务,这些方法概括了到目前为止概述的质量原则,通常与这种结构化命名方案相结合,作为指导 CSS 作者的一种方式。您可能偶然发现了像 OOCSS、SMACSS 或 BEM 这样的名字,因为它们已经流行了好几年了。
OOCSS
OOCSS 代表面向对象的 CSS,它是由妮可·沙利文在 2009 年创造的一种编写 CSS 的方法。从许多方面来说,这是从可维护软件的角度探索如何编写 CSS 的浪潮的起点。对于 OOCSS,Nicole 使用了来自面向对象编程的隐喻,在面向对象编程中,与 CSS 中定义良好的规则集相关联的可重用类名充当了创建对象层次结构的一种方式。
在 OOCSS 中,类名(在语义正确的 HTML 的基础上)被用作解释组件在 UI 中的用途的主要机制。妮可称之为“视觉语义学”
也许 OOCSS 思维中最著名的例子是我们在第三章中第一次遇到的“媒体对象”——一种图像、视频或其他媒体的常见模式,位于文本块旁边(见图 12-12 )。通过将这种模式提取到单个对象中,并在需要的地方加入类名,Nicole 展示了可以从 CSS 中删除大量重复内容。
图 12-12。截图来自妮可·沙利文关于“媒体对象”的文章的评论部分,其中每个评论实际上都说明了该模式的原理
OOCSS 包括将“皮肤与结构”和“容器与内容”分开的建议将皮肤从结构中分离出来意味着你应该尽量避免编写像排版和颜色(皮肤)以及定位、浮动等这样的规则。(结构)。在这种情况下,您最好为每个方面创建一个单独的规则和类名。例如,“媒体对象”负责浮动图像和相关文本的布局,而颜色和版式则附加到组件本身。以下博客文章摘要的标记说明了标记中的类的组合:
<article class="media-block post-teaser">
<div class="media-body post-teaser-body">
<h2 class="post-title">Media object</h2>
<p>Article text goes here…</p>
</div>
<img class="media-fig" src="" alt="">
</article>
后置类可以代表这个组件的“皮肤”,媒体对象模式有自己的类名。
“容器与内容”的分离可以在我们在第七章看到的网格策略中看到。通过将组件如何适应页面布局的样式应用于技术上冗余的外部元素(。col),我们消除了与组件本身的样式冲突的风险:
<div class="row row-trio">
<div class="col">
<article class="media-block post-teaser">
<div class="media-body post-teaser-body">
<h2 class="post-title">Post teaser heading</h2>
<p>Article text goes here...</p>
</div>
<img class="media-fig post-fig" src="" alt="">
</article>
</div>
<!-- ..and so on, more post-teasers here.
</div>
SMACSS
CSS 的可扩展和模块化架构,或简称 SMACSS,是 Jonathan Snook 在雅虎工作时创造的一种方法。它与 OOCSS 有很多相似之处,比如提倡将类名和以组件为中心的规则集作为创建 UI 元素层次结构的主要机制,以及避免特殊性冲突。Jonathan 通过引入一种规则分类,使 SMACSS 的思路有所不同:
-
基本样式,为 HTML 元素提供默认样式以及基于元素属性的变体。
-
布局样式,处理网格系统和其他布局助手,类似于我们在第六章看到的抽象(“行”、“列”等)。).
-
模块样式,由所有规则组成,这些规则构成了特定于您正在构建的站点的组件:产品和产品列表、站点标题等。这是你的大部分样式可能结束的地方。
-
状态,即改变现有模块外观的覆盖。例如,菜单项可以是活动的,也可以是非活动的。
在坚持这种规则分类的过程中,SMACSS 鼓励你思考如何给事物命名,以及它们适合在哪里。这些规则通常应该按照描述的顺序包含在您的样式表中,以便它们从最一般的到最具体的。这是避免特异性斗争的另一部分,并以一种明智的方式使用级联。
除了样式的分类之外,SMACSS 方法还提倡在一些类名前使用前缀,以便更清楚地表明预期的目的。在第六章中,我们讨论了布局助手,并使用了像。SMACSS 建议您在这样的类前面加上能够传达其本质的前缀,比如。l-对于布局:
.l-row { /* row container */ }
.l-row-trio { /* row with three equally weighted "columns" */ }
.l-col { /* column container */ }
/* ...etc */
类似地,您可以用 is-来阐明状态前缀,因此一个名为。处于禁用状态的 productlist 可能以类似。is-productlist-disabled 或。productlist 被禁用。组件本身不使用特定的前缀,但是组件本身的名称可以作为任何子组件的前缀:
.productlist { /* styles for the product list container */ }
.productlist-item { /* item container in the list */ }
.productlist-itemimage { /* image inside a product list item */ }
不列颠帝国勋章
OOCSS 和 SMACSS 可以被看作是一个考虑结构化 CSS 的框架,并结合一些方便的经验法则,BEM 是一个关于如何创作和命名你的样式的更加严格的系统。
BEM 最初是来自搜索引擎公司 Yandex 的一种应用程序开发方法。它包括几个关于如何在大型 web 应用程序中构造 UI 的约定、库和工具。这些应用程序中使用的命名约定已经成为 HTML 和 CSS 上下文中 BEM 一词的同义词。
首字母缩写 BEM 代表块、元素、修饰语。块是顶层抽象,相当于 SMACSS 中的模块或 OOCSS 中的对象。任何远程独立的东西都可以被描述为一个块。元素是块的子组件,不要与纯 HTML 元素混淆。最后,修饰符是块或元素的不同状态或变体。
BEM 中的块、元素和修饰符用小写字母书写,用破折号分隔多字项:
.product-list { /* this is a block name */ }
块中的元素用两条下划线分隔:
.product-list__item { /* this is the item element inside the product list */ }
修饰符用单下划线添加,可以修改块或元素:
.product-list_featured { /* product list variation */ }
.product-list_featured__item { /* item inside featured product list */ }
.product-list__item_sold-out { /* Sold out item inside normal product list */ }
这种语法还有几种变体。开发人员 Harry Roberts 使用了一种变体,用双破折号而不是单下划线来分隔修饰符:
.product-list__item--sold-out {}
哈利还在他的网站(csswizardry.com)上写了大量关于如何使用这些类型的命名方案的文章,包括将 BEM 语法与前缀结合的各种方法。(他还写了一个关于编写高质量 CSS 的综合资源,包括如何构造和命名事物,可在 cssguidelin.es 获得。)
不管您选择哪种语法,BEM 的主要思想是,只要您知道所使用的命名方案,就能够立即识别与某个类名相关联的规则类型。这也有助于保持代码的焦点。如果一个类承担了太多的责任,它将开始与名字的明确目的相冲突。这是重新思考抽象的一个信号。没有什么可以阻止您将一个块的复杂部分分解成它自己的嵌套块,这反过来会带来进一步的可重用性。
管理复杂性
到目前为止,我们描述的所有指导方针和方法的底线可以总结为目标是管理复杂性。任何超出一小段代码的东西都会很快变得复杂,所以我们要么极大地限制范围(只允许更简单的设计),要么把复杂的部分分解成更简单的块。
使用代表 UI 行为的命名方案和类名使 CSS 更容易理解,但并没有降低整体的复杂性。相反,它将一些复杂性转移到了 HTML 上。对于网页或网站的不同部分,它可能在不同程度上这样做,但它总是这样做。为了理解这是为什么,我们需要回溯一下 CSS 被引入的时间。
关注点分离
随着网络从一大堆和
标签转变为一种由 CSS 决定表现形式的模式,有一股巨大的推动力促使公司开始使用 CSS 并保持 HTML 的纯净,不提及文档如何表现。像 CSS 禅宗花园(www.csszengarden.com/)这样的网站;见图 12-13 )做了大量的工作来说服设计者和开发者语义标记的价值和 CSS 的力量。将表示层视为独立于底层文档的标记,这很好地说明了关注点分离 (SoC)的软件设计原则:标记不应该包括表示层,也不应该依赖于表示层,两个层应该尽可能少地混合。这是内置于 Web 本身的东西:即使没有 CSS(或 JavaScript,就此而言),网页的基本表示也应该是有意义的。
图 12-13。CSS Zen Garden 展示了单个 HTML 页面的各种变化,只是通过操纵 CSS——有时非常聪明地使用伪内容
划分为不同的责任区域有助于最终用户体验网站,也有助于我们构建网站。最终用户应该能够使用 HTML 中表示的内容,而不管其能力如何,或者 CSS 是否由于某种原因无法加载。作为开发人员,我们应该能够专注于 CSS 文件,而不是在我们想要更新设计时更新每个 HTML 元素。
现在,考虑使用一个仅处理特定表示方面的类名的情况,比如水平布局结构的行和列(我们在第七章中使用过):
.row {
margin: 0 -.9%;
padding: 0;
}
.row:after {
content: '';
display: block;
clear: both;
}
.col {
float: left;
box-sizing: border-box;
margin: 0 .9% 1.375em;
}
即使我们将它们重命名为不太直观的名称,如 group 和 block,它们仍然有一个目的,那就是创建表示挂钩。我们已经在 HTML 中放置了表示信息,这是毫无疑问的。这违背了 SoC,那么为什么会被接受呢?
SoC 原理本身比 Web 要古老得多。它是由传奇计算机科学家 Edsger Dijkstra 在 1974 年创造的(www . cs . ute xas . edu/users/EWD/transcriptions/ewd 04 xx/ewd 447 . html),他在一篇文章中阐述了如何推进软件工程领域。
用 Dijkstra 的文章来概括,面对日益增加的复杂性的方法是“将一个人的注意力集中在某个方面”,而不是“同时处理这些不同的方面”。由于我们可能需要同时处理 HTML 和 CSS,我们在一个地方违反了这个原则。
事情是这样的:我们可以在代码的任何层次上自由地应用“关注点分离”原则,为了另一部分的利益而牺牲一部分的理论纯度可能是有益的。
像这样的规则的唯一目的。row 是 Dijkstra 所说的焦点类型的一个例子。当我们在一个地方解决了基于列的布局的一般问题时,它就解决了:当我们找到一个更好的方法时,我们不必更新我们在 CSS 中重复相同解决方案的所有地方。
代价是,当名称改变时,我们需要更新 HTML,或者代码的特定部分不再使用该解决方案。使用这种命名挂钩的支持者倾向于认为,在 HTML 中找到并替换我们使用类名的地方比重构一个混乱的 CSS 文件更容易。
HTML 语义与类语义
下面是 HTML 规范对类名的说明:
对于作者可以在 类 属性中使用的标记没有额外的限制,但是鼓励作者使用描述内容性质的值,而不是描述内容的期望表示的值。
谈到 CSS,CSS 2.1 规范对类名有如下描述:
CSS 赋予了“class”属性如此强大的力量,以至于作者可以基于几乎没有关联表示的元素(如 HTML 中的 DIV 和 SPAN)设计自己的“文档语言”,并通过“class”属性分配样式信息。作者应该避免这种做法,因为文档语言的结构元素通常有公认的含义,而作者定义的类可能没有。
首先要注意最后一句话使用的语言:“鼓励”、“应该”、“经常”、“可能。”使用可能被认为是表示性的类名并没有被禁止:它不会导致验证错误,并且它本身对文档的结构语义或可访问性没有影响。类名和其他标识符主要是为了开发人员,而不是用户。因此,我们应该明确区分:HTML 及其属性的语义不同于作者定义的值(如类)的语义。
关于设计你自己的“文档语言”的部分是很重要的:看起来像按钮的东西和使用像 Click me!,并使用点击我!。标签创建了一个可聚焦和可访问的元素,默认情况下,它是根据操作系统惯例设计的,并且可能与一个表单相关联。同样的原则也适用于 HTML 中任何具有语义意义的元素或属性——负责任地使用它们。
规范没有说的是,我们不能使用类作为这些元素的正确使用的扩展,或者用一些额外的语义上无意义的元素增加文档来帮助表示。
开发人员尼古拉斯·加拉格尔在一篇题为“关于 HTML 语义和前端架构”(nicolasgallagher . com/About-HTML-semantics-front-end-architecture/)的有影响力的文章中提出了重新评估类名语义的论点,这篇文章发表于 2012 年。它很好地总结了将表示性类名视为有效(如果不总是更可取)工具而不是被禁止的反模式背后的思想。
找到正确的平衡
通过与表示无关的名字来最小化样式和结构的混合显然是一个好的目标。但是,即使当类名代表“视觉语义”时,我们也可以尽量保持最小的表示性质。
如果你走上允许表示类名的道路,你可能会倾向于认为 arial-green-text 是一个好名字。也许你会发现一个可重复使用的规则对这种字体和颜色组合是有益的,因为它是品牌指南的一部分。如果你创建了这样一个规则,试着把实现细节从名字中去掉——也许 brand-primary-text 会是一个更好的选择?这样,即使不能完全避免哪些部分使用这种特定的样式,至少可以避免颜色和字体准则的变化。
我们还必须记住,无论我们如何命名我们的类或构建我们的规则,我们都不必做出二元选择。任何元素都可以有描述内容的类名,以及描述其作为 UI 一部分的功能的名。
类似地,我们可以完全放弃在标记的某些部分使用类名,尤其是非开发人员使用它的地方。应用程序 UI 中的标记很可能是在完全控制属性的情况下创作的,而博客文章的内容可能只是从内容管理系统中产生的纯 HTML。
如果您为基本排版设置了合理的规则,并避免过多地基于上下文进行样式设计(实际上,在适用的情况下,更多地基于类名),这些情况会自行解决。另一方面,将使用您编写的代码集合的人可能或多或少对标记的复杂性感到满意。
在博客文章“美国代码重构”(adactio.com/journal/7276)中,杰瑞米·基思写道,他和安娜·德本汉姆在“美国代码模式库”项目中不得不稍微放弃大量使用类名。简而言之,基于模式库创建页面的人不一定是编写 CSS 或理解所用命名约定的人。因此,它们受益于可读性更强、更简短的 HTML,而不是 CSS。
代码是给人用的
说到底,关于一种特定的 CSS 和 HTML 编写方式是好是坏的结论取决于情况和相关人员,只要它在技术上是合理的和可访问的。如果你或者和你一起工作的团队发现做事情的某些方式降低了事情的复杂性,那就去做吧。如果他们阻碍多于帮助,你应该考虑避开他们。
工具和工作流程
随着我们开始将 CSS 视为软件,并对我们如何编写和优化它提出了更高的要求,我们经常会寻找更好的工具来管理我们的工作流。近年来,像预处理器和构建系统这样的东西激增,以至于很容易迷失在术语丛林中。在这一节中,我们将简要介绍一些增强您的工具包的选项。
预处理器和 Sass
正如我们在本章开始时提到的,CSS 被有意设计成没有许多你在通用编程语言中所期望的构件。像循环、函数、列表和变量这样的东西是不可用的。正如我们已经得出的结论,有很好的理由让它们远离 CSS,但另一方面,有编程经验的人在编写 CSS 时也有很好的理由错过它们。它们允许您更容易地创建可重用的代码,有望降低创建和维护您的样式的总体工作量。
人们已经创造了其他语言,被称为预处理程序,在这些构建模块存在的地方,依次输出 CSS。有几种样式可供选择——Sass、Less、Stylus、PostCSS 等。在撰写本文时,最流行的是 Sass——语法上很棒的样式表。我们将看一个简单的例子来说明这种特殊样式的预处理器是如何工作的。
编写 Sass 最常见的方式是使用一个名为 SCSS 的 CSS 语法超集,这意味着您可以编写 CSS 中已经有效的任何内容,以及您选择使用的额外 Sass 功能。
安装和运行 SASS
Sass 编译器有多种版本,或者作为独立程序,或者作为许多流行代码编辑器的插件。您通常可以设置编辑器,以便在保存. scss 文件时,它会自动更新相应的 css 文件。
如果你进入 Sass 语言安装页面(sass-lang.com/install),你可以找到在各种平台上安装的说明,以及几个编辑器和内置支持插件的链接。
下面是使用了大量 Sass 特性的两个文件的片段。这远远不是 Sass 所能做到的全部,但它是语法外观和各种语言特性用途的一个尝试。如果你不能马上理解,不要担心。
首先,名为 library.scss 的文件:
$primary-color: #333;
$secondary-color: #fff;
@mixin font-smoothing($subpixel: false) {
@if $subpixel {
-webkit-font-smoothing: subpixel-antialiased;
-moz-osx-font-smoothing: auto;
}
@else {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
接下来,main.scss 文件:
@import 'library';
body {
color: $primary-color;
background-color: darken($secondary-color, 10%);
}
.page-header {
color: $secondary-color;
background-color: $primary-color;
@include font-smoothing;
}
.page-footer {
@extend .page-header;
background-color: #14203B;
a {
color: #fff;
}
}
浏览器不需要这些代码,所以 SCSS 文件总是通过输出普通 CSS 的预处理程序运行。结果如下:
body {
color: #333;
background-color: #e6e6e6;
}
.page-header,
.page-footer {
color: #fff;
background-color: #333;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: greyscale;
}
.page-footer {
background-color: #14203B;
}
.page-footer a {
color: #fff;
}
main.scss 文件中的许多内容您可能都很熟悉:选择器、规则集、属性/值对等都有相同的语法。使用常规的@import 语句将 library.scss 文件导入主文件。对于 Sass 编译器来说,这意味着文件包含在输出中。在这种情况下,库文件实际上不包含任何要输出的内容,而是支持材质的集合。
例如,Sass 允许我们定义变量,然后我们可以在多个地方重用它们。例如,secondary-color 变量可用于设置背景和文本颜色。
library.scss 文件还包含一个 mixin ,它是我们可以在任何地方输出的可重用 css 的集合。我们定义的字体平滑混合允许我们切换特定于浏览器的属性,这些属性跟踪哪种抗锯齿方法用于文本。
在 main.scss 文件中,我们使用了这个 mixin 来包含字体平滑属性,作为。页眉规则。使用@include 语法调用 Mixins。
那个。页脚规则又使用了另一种语言特性——@ extend 语法。这意味着它的选择器被添加到它所扩展的选择器中,在这种情况下意味着。页脚正在与共享样式。页眉。
最后,有嵌套:在。页脚规则里面有一个用于 a 元素的选择器。通过这样写规则:
.page-footer {
/* rules for page footer here */
a {
/* rules for links in footer here */
}
}
Sass 编译器会自动创建如下两组规则:
.page-footer {}
.page-footer a {}
虽然嵌套使我们不必键入完整的选择器,但嵌套选择器可能会太过分。为了某种视觉整洁,将特定组件的所有规则嵌套在彼此内部可能很有吸引力,但是输出会受到过于具体的规则的影响:
.my-component {
/*...rules here */
.subcomponent {
/*...rules here */
.nested-subcomponent {
/*...rules here */
h3 { /*...rules here */ }
}
}
}
/* ...will output... */
.my-component { /* ...rules here... */ }
.my-component .subcomponent { /* ...rules here... */ }
.my-component .subcomponent .nested-subcomponent { /* ...rules here... */ }
.my-component .subcomponent .nested-subcomponent h3 { /* ...rules here... */ }
经常检查你的输出 CSS 是一个好主意,这样你就不会无意中创建臃肿的样式表。
预处理程序的情况
预处理程序对你如何编写 CSS 有很大的影响。当代码开始变得庞大和复杂时,预处理程序可以帮助您实现代码的一致性和结构化,并加快开发速度。
当您习惯使用预处理器时,即使较小的项目也能受益;尤其是当您开始构建一个可以在项目间共享的约定、混合和功能的武库时。
..反对的理由是
但是在选择使用预处理器之前,还有一些事情需要考虑。在学习使用和正确使用它的过程中有一个明确的障碍。如果您编写了其他人将要维护或协作的代码,您也承诺让他们知道如何用您选择的预处理器语言编写。这也是对这种语言长期存在并得到支持的一种押注。
回顾关于代码质量的最初部分,我们看到好代码的主要敌人是复杂性。同样的规则也适用于这里:不要假设预处理程序总是让你的代码更容易处理,但是如果它们看起来能帮助你写出更好的代码,不要犹豫尝试使用它们。
还有其他工具可以帮助你以不同的方式检查你的代码。在下一节中,我们将看看其中的一些。
工作流工具
无论您使用 CSS 还是预处理程序语言,在开发时通常都有需要反复执行的任务。幸运的是,计算机擅长处理无聊和重复的东西。在本节中,我们将简要介绍一些有用的工具。
静态分析和棉绒
就代码的正确性而言,许多代码编辑器都有内置的语法检查器,可以突出显示任何看起来不正确的选择器或样式声明。这种错误检查通常被称为“静态分析”,这是程序员的一种时髦说法,意思是试图在代码运行之前发现其中的问题。
如果您想增强代码的静态分析,可以配置一些工具来检查语法错误以外的问题。这些工具被称为棉绒,专注于寻找“棉绒”——不应该在那里的碎屑。对于 CSS,有 CSS Lint(【csslint.net】)和 style Lint(【http://stylelint.io/】)。这些工具检查你的选择器和声明中可能不需要的语法错误和模式(见图 12-14 )。两者都是可配置的,你甚至可以编写自己的规则。
图 12-14。在崇高文本编辑器中使用 CSS Lint。左边空白处的圆点表示潜在的问题,解释信息打印在底部栏上
构建工具
除了林挺之外,在开发一个网站时,我们可能需要在编辑器之外反复做很多工作:
-
预处理 CSS
-
如果在多个较短的文件中工作,将 CSS 文件连接在一起
-
缩小 CSS,删除注释等等来节省空间
-
优化 CSS 中引用的图像
-
运行开发服务器
-
重新加载一个或多个浏览器来检查我们的更改
幸运的是,有许多工具可以帮助您在项目中自动完成这些任务。它们包括从命令行运行的更高级的工具,以及可以在图形用户界面中管理的更简单的设置,如考拉(见图 12-15 )。
图 12-15。考拉是一个可用于 Windows、Mac 和 Linux 的构建工具 GUI
也有很多应用程序允许你完全用代码来配置你的构建系统。这通常需要更多的设置,但是由于配置可以在开发人员和项目之间共享,它可以帮助保持开发环境的一致性和快速设置。
使用节点和吞咽设置 CSS 构建工作流
Node 是一种 JavaScript,可以在浏览器之外运行,并用于任何类型的编程任务。自从 Node 出现以来,许多前端开发工具都是基于 JavaScript 的,可以说,已经知道它的人可以自己动手了。
对于前端构建工作流,有许多专门管理构建任务的节点应用程序,如 Grunt、Gulp 和花椰菜(是的,这些是实际名称)。它们通过配置和链接独立任务的输出来提供帮助,每个任务负责工作流的一部分。
对于我们的示例工作流,我们将使用 Gulp,它又通过 NPM 来处理,这是 Node 附带的一个实用程序。NPM 是一个命令行工具,因此示例中的所有命令都在终端窗口中运行。
注意
如果您需要如何使用命令行的速成课程,请访问learnpythonthehardway . org/book/appendix-a-CLI/introduction . html并完成一些示例。这可能是一项艰巨的任务,但您将学到管理计算机和利用更高级工具的宝贵技能。
首先:你需要安装节点。前往nodejs.org下载并运行您平台的安装程序。接下来,导航到本书附带的代码中的 12 示例章节内的 workflow-project 文件夹。
这个文件夹有一个名为 package.json 的文件,这个文件记录了运行项目所需的内容。这些依赖关系存储在 www.npmjs.com 的一个中央代码库中,那里有成千上万的自由代码库被共享。通常,您可以通过调用命令 npm init 来创建自己的包文件,并回答一些关于您的项目的问题,但是这一次我们走了 TV-chef 的路线,提前准备了一个。
要安装程序包文件中列出的依赖项,请运行 npm install 命令。这将在本地项目文件夹中下载并安装以下小应用程序:
-
Gulp: 任务运行器,将其余的程序放在一起。
-
**Gulp-Sass:**Sass 预处理器库的一个版本。
-
Browser-sync: 运行轻量级开发服务器的工具,跨浏览器同步网页的重新加载和交互。还包括一个调试器,所以你可以调试,例如在移动设备上。
-
Autoprefixer: 一个非常有用的库,它检查你的 CSS,并根据你想要支持的浏览器列表为 CSS 属性添加相关的前缀和替换语法。
-
**Gulp-PostCSS:**PostCSS 预处理器的一个版本,运行 Autoprefixer 需要它。
这些应用程序将把它们的文件放在项目目录中名为 node_modules 的文件夹中。如果您对您的文件使用任何类型的版本控制,您可能会想要告诉它忽略这个文件夹:里面会有成千上万个小文件,您可以随时使用包文件重新创建它们。
下一个组件是 gulpfile.js 文件,它包含如何一起使用已安装的软件包的说明。我们不会深入讨论这是如何工作的所有细节,但是作为一个例子,下面是处理 CSS 预处理的部分:
gulp.task('styles', function(){
var processors = [autoprefixer()];
gulp.src(['*.scss'])
.pipe(sass())
.pipe(postcss(processors))
.pipe(gulp.dest('./'))
.pipe(browserSync.reload({stream:true}))
});
当告诉 gulp 程序运行样式任务时,任何。提取同一目录下的 scss 文件。然后,它通过 Sass 预处理器运行。之后,它运行通过 PostCSS 处理器,在那里 Autoprefixer 添加必要的前缀。它通过去caniuse.com并找出要使用的前缀和语法变化来做到这一点。默认情况下,它覆盖了所有主流浏览器的最新两个版本,以及大约超过 1%的市场份额的浏览器,但是这个设置是高度可配置的。最后,CSS 被保存到磁盘,浏览器同步程序被通知重新加载任何连接的浏览器窗口。
为了运行任务,我们使用 NPM。package.json 文件包含针对我们安装的应用程序的命令映射。当我们现在运行 npm run gulp 时,默认的任务集是 run,并且在我们保存文件时继续自动重新运行,直到我们停止它。
我们机器上的默认浏览器也在一个新的选项卡中启动,当前目录中的 index.html 文件就在这个选项卡中。CSS 文件一改变,浏览器(以及您指向同一地址的任何其他浏览器)就会重新加载。
在设置任务运行器和工作流时很容易迷路。当您将它归档,并准备好跨项目重用或共享时,您就赢了。新的合作者只需要安装 Node 并运行 npm install,然后他们的机器上就有了完全相同的设置。虽然对大多数人来说,开发工具可能是网站建设中最无趣的部分,但是一旦你建立并运行了网站,这通常是一种“设置好就忘了”的任务,从长远来看可以节省很多时间。如果你需要稍微详细一点的介绍(使用 Grunt 任务运行器而不是 Gulp),我们推荐克里斯·科伊尔的文章《Grunt 给那些认为 Grunt 这样的事情很怪异很难的人》(24ways.org/2013/grunt-is-not-weird-and-hard/)。
CSS 语法和结构的未来
在本书中,我们一直在使用 CSS 特性来支持不同层次的浏览器。我们在应用新功能时重复了渐进增强的咒语——这意味着今天你可以使用很多新功能,只要在它们还不被支持时你有一个合理的替代方案。这适用于很多情况,但不是所有情况。
对于我们如何编写 CSS 有一些基本的改变。其中一些已经得到了实验支持,但由于它们很难以渐进的方式应用,很可能需要几年时间我们才能在日常工作流中使用它们。然而,关注 CSS 的发展方向是很有用的。
自定义属性 CSS 中的变量
很长一段时间以来,向 CSS 中添加变量一直是最受欢迎的功能之一。一个规范已经准备了好几年,直到最近才成为候选推荐标准。在本书撰写之际,Chrome 和 Safari 即将发布支持版本。Firefox 已经支持变量很久了。
这些变量在技术上的正确名称是自定义属性:它们看起来非常像变量在 Sass 中的工作方式,但是略有不同。
自定义属性的声明语法与供应商前缀属性非常相似,只是供应商名称为空,导致两个破折号。要定义一个全局可用的变量,可以将它添加到:root 选择器中。您还可以定义(或重新定义)一个变量,使其在特定的选择器上下文中具有特定的值。
:root {
--my-color: red;
}
.myThing {
--my-color: blue;
}
我们可以在自定义属性值级联的任何地方访问该值。var()函数表示法提取自定义属性值,如果属性未定义或无效,还可以选择回退值。在下面的代码片段中,颜色声明将被设置为蓝色,因为前面的示例将其设置在。神话祖先:
.myThing .myInnerThing {
/* second (optional) argument is the fallback */
color: var(--my-color, purple);
}
自定义属性不仅可以作为整个值与 var()一起使用,还可以作为另一个子值的一部分:
:root {
--max-columns: 3;
}
.myThing {
columns: var(--max-columns, 2) 12em;
}
既然像 Sass 这样的预处理程序有内置的变量,为什么我们要选择在客户端完成这项工作,而不是事先通过构建脚本或服务器进程呢?因为自定义属性是在浏览器中计算的,而不是预先计算的,所以它们可以访问动态 DOM 树和整个层叠。如果有变化,样式可以重新计算。如果我们在页面加载后使用 JavaScript 为 html 元素设置- my-color 变量,那么依赖于该颜色值的每个元素都会立即更新其颜色。
在广泛的浏览器支持自定义属性之前,对代码的重要部分使用自定义属性是很棘手的。任何对常规值的回退都必然会导致大量重复的声明。然而,它们无疑是 CSS 的一个强大补充。
HTTP/2 和服务器推送
我们今天使用的许多与性能相关的模式都与 HTTP 的工作方式有关。更具体地说,我们试图应对当前版本 HTTP/1.1 在一次获取多个内容时相对较慢的事实,因此我们将所有样式混合到一个文件中,并避免像瘟疫一样的外部请求。
在 HTTP/2 中,底层协议为一次交付许多小资产而优化。一个连接可以承载多个文件,这大大减少了发出新请求的开销。HTTP/2 中也有一些关于 web 页面如何交付的聪明想法。使用所谓的“服务器推送”,服务器可以自动发送包含 HTML 和 CSS 的单个响应,除非请求表明浏览器的缓存中已经有了 CSS 文件。
这些新的进步可能会帮助我们摆脱像内联样式或制作图像精灵这样的模式。@import 语句长期以来一直是一种反模式,它可以获得新生,允许我们按照自己的意愿分割文件,而不用担心额外的请求开销。
HTTP/2 已经在很多地方得到了支持,并且今天还可以使用,因为当它在浏览器中不受支持时,它会退回到旧的行为。但是,它要求您使用的 web 服务器支持它。与许多基础设施的变化一样,这意味着在它真正成为主流并内置到我们的工具中之前,还需要一段时间。
Web 组件
Web Components 是一系列标准的名称,这些标准允许 Web 开发人员将 HTML、CSS 和 JavaScript 打包成真正独立且易于重用的组件,就像它们是本地元素一样。当使用一个 Web 组件时,您应该能够将它直接放入您的项目中,而不用担心您的样式(或脚本)和那些属于该组件的样式之间的命名冲突。
如果我们创建了一个假想的 Web 组件来包含来自互联网电影数据库的缩略图预览(www.imdb.com),我们将使用一个 JavaScript 文件来告诉浏览器我们打算在标记中使用一个定制的 imdb-preview 元素。然后可以在 HTML 中像这样调用该元素:
<imdb-preview>
<a href="http://www.imdb.com/title/tt0118715/>The Big Lebowski</a>
</imdb-preview>
在页面上,结果可能如图 12-16 所示,有几个部分。
图 12-16。调用我们的元素的假设结果
在幕后,这个定制元素可以根据链接的 URL 获取数据,并用自己隐藏的 DOM 片段(称为影子 DOM)替换其内容。标题、评论分数和图片在这个片段中都有自己的元素。在某种意义上,这有点像 iframe——内容被屏蔽在页面的常规 DOM 树之外,它们使用单独的上下文来编写脚本和样式。
在 Shadow DOM 片段中,样式元素的范围自动限制在 web 组件的根元素,就像我们使用 scoped 属性一样。父文档中的样式也不会渗透到元素中,因此组件样式被完全封装。诀窍在于定制属性可以级联到组件中——因此组件作者可以准确地指定允许您覆盖哪些属性。有了这种机制,可以安全地更改排版和配色方案等内容,以匹配使用组件的站点。
到目前为止,许多提议的 Web 组件标准还不受支持,但是封装到影子 DOM 中已经在具有类似的元素的浏览器中发生了。即使标记中的元素是空的,它也隐藏了视频控件之类的元素——例如,你可以激活 Chrome DevTools 中的“显示用户代理阴影 DOM”选项来查看它的运行情况(参见图 12-17 )。
图 12-17。如果我们打开阴影 DOM 检查并检查一个,我们会看到它由许多其他元素组成——例如音量控制和播放器按钮
在 Web 组件思维中,我们获得了许多其他方法所追求的模块化和可组合性——不仅仅是在 CSS 中,而是在 HTML、CSS 和 JavaScript 中。所有主要的浏览器供应商都在实现 Web 组件,但是对于应该使用哪些特性集和语法仍然存在分歧。
它们会在多大程度上影响我们编写 CSS,现在说还为时过早。也许它们会改变我们一起构建网站的方式,也许它们只是我们偶尔会遇到的另一种做事方式。
CSS 和可扩展 Web
Web 组件背后的一个想法是,它们可以用作新的本机功能的各种试验场。如果某个组件成为创建某种类型的小部件或内容的事实方式,并在数百万个网站上使用,那么也许该元素应该成为 HTML 标准的一部分?
基于开发人员实际使用的东西来创建标准的想法并不新鲜。例如,jQuery JavaScript 库使用 CSS 选择器方法来选择 DOM 元素,并且变得非常流行。如今,我们有了原生 JavaScript 中的 querySelector API,其工作方式非常相似:
// jQuery code:
jQuery('.myThing p');
// Standardized way:
document.querySelectorAll('.myThing p');
同样,像预处理程序这样的东西会影响 CSS 未来的样子。有一些关于 CSS 标准的建议,包括 native @extend 指令、原生嵌套、自定义命名媒体查询等等。
有一个文档,由网络社区中的许多人签名,名为“可扩展的网络宣言”(extensiblewebmanifesto.org/),它推动了这样一个想法,即标准需要朝着让开发人员更好地访问较低层次的浏览器内部的方向发展,这样他们就可以探索新的构建方式。然后,他们提出的解决方案会反馈给标准组织,以进行更高级别的构建块标准化。
这种模式与当前的情况有所不同,在当前的情况下,浏览器制造商和其他行业利益相关者提出了拟议的高级标准,开发者只有在浏览器中实现后才能尝试这些标准。
对于 CSS 来说,可扩展 Web 的想法并不一定意味着这种语言会被底层特性所拖累。相反,JavaScript APIs 将有望向渲染和自定义语法等事物开放,这样,任何提议的新 CSS 功能都可以在构思时作为脚本 polyfills 实现。如果成功了,CSS 的创新率只会增加。我们最好准备好。
摘要
在这最后一章,我们已经涵盖了从浏览器如何解释你的 CSS,到作者如何编写灵活和可维护的代码,任何称职的前端开发人员都可以理解。即使你写的 CSS 没有错误,包装良好,可维护,开发仍然是一件苦差事。像预处理器和构建脚本这样的强大工具可以在这个过程中帮助你,但是不要让它们本身变得过于复杂。毕竟,权力越大,责任越大。
最后,我们已经看到了 CSS 的一些前景,以及我们如何规划未来。我们面前还有许多发展,太多了,一本书无法详细介绍。当这本书的第一个版本写出来的时候,曾经被认为是普遍的和不可改变的概念,随着语言(以及对它的要求)的成熟,已经变得更加微妙和复杂。
最好的 CSS 作者总是一只眼睛看着现在,另一只眼睛看着未来,不断质疑今天的趋势如何可能成为明天的瓶颈。与其全力支持某个特定的工具或技术,不如花时间和精力去理解其基本原理。这是掌握 CSS 的真正途径。**