CSS-精通指南-全-

CSS 精通指南(全)

原文:CSS Mastery

协议:CC BY-NC-SA 4.0

一、奠定基础

电子补充材质

本章的在线版本(doi:10.1007/978-1-4302-5864-3 _ 1)包含补充材质,可供授权用户使用。

人类是天生好奇的物种。我们只是喜欢摆弄东西。当我们在办公室拿到新的 Parrot AR 无人机时,我们甚至在看说明书之前就已经把它拆成了碎片。我们喜欢自己解决问题,创造自己的思维模式来思考事物的行为。我们蒙混过关,只有在出现问题或出乎我们意料时才求助于手册。

学习级联样式表(CSS)的最好方法之一是直接开始动手修改。事实上,这可能是你们中有多少人学会了编码;通过从博客中获取技巧,查看源代码以了解您最喜欢的设计者是如何实现特定效果的,以及通过浏览开放源代码库来获取代码片段。几乎可以肯定,你一开始并没有阅读完整的规范,这足以让人昏昏欲睡。

修补是一个很好的开始方式,但是如果你不小心的话,你可能会误解一个重要的概念或者为以后的工作埋下隐患。我们知道;我们已经这样做过好几次了。在这一章中,我们将回顾一些基本但经常被误解的概念,并向你展示如何让你的 HTML 和 CSS 保持清晰和良好的结构。

在本章中,您将了解:

  • 可维护性的重要性

  • HTML 和 CSS 的不同版本

  • 未来友好和向后兼容代码的策略

  • 给你的 HTML 增加意义并使用新的 HTML5 元素

  • 向 HTML 添加适当的样式挂钩

  • 用 ARIA、微格式和微数据扩展 HTML 语义

  • 浏览器引擎模式和验证

构建您的代码

大多数人不会考虑建筑物的地基。然而,如果没有坚实的基础,大多数建筑都不会屹立不倒。虽然这本书是关于 CSS 技术和概念的,但是如果没有一个结构良好且有效的 HTML 文档,你将要学习的很多东西是不可能的(或者至少是非常困难的)。

在本节中,您将了解为什么结构良好且有意义的 HTML 对于基于标准的开发至关重要。您将了解如何为您的文档添加更多的含义和灵活性,这样做可以使您作为开发人员的工作更加轻松。但是首先是一个非常重要的话题,不管我们碰巧用什么语言工作。

可维护性

可维护性可以说是任何好的代码库最重要的特征。如果你的代码开始失去结构,变得难以阅读,那么很多事情都会变得困难。如果您正在与不可读和脆弱的代码作斗争,添加新功能、修复 bug 和提高性能都会变得更加复杂和令人沮丧。在某些情况下,情况变得如此糟糕,以至于开发人员会完全拒绝进行更改,因为几乎每次他们这样做时,都会有一些东西出错。这可能导致没有人喜欢在网站上工作的情况,或者在非常糟糕的情况下,导致严格的变更控制过程,发布只能每周一次,甚至每月一次!

如果你正在构建一个要交给客户或其他开发团队的网站,可维护性就更加重要了。关键是你要提供易于阅读、意图明确的代码,并且针对变化进行了优化。“唯一不变的是变化”在这里是一个特别合适的陈词滥调,因为谁的项目没有不断变化的需求,以及不断的特性请求和错误修复?

随着代码库的增长,CSS 是最难维护的语言之一,即使是相对较小的网站的样式表也可能很快失控。其他现代编程语言具有内置的变量、函数和名称空间等特性;默认情况下,所有有助于保持代码结构化和模块化的特性。CSS 没有这些特性,所以我们需要将它们构建到我们使用语言和构建代码的方式中。当我们在整本书中讨论不同的主题时,你会发现可维护性的主题几乎贯穿了所有的主题。

标记简史

网络的力量在于它的普遍性。无论是否残疾,每个人都能获得服务是一个重要方面。

—蒂姆·伯纳斯·李

蒂姆·伯纳斯·李在 1990 年创造了 HTML,目的是格式化科学研究文件。它是一种简单的标记语言,能够赋予文本基本的结构和含义,如标题、列表和定义。这些文档通常很少或没有视觉修饰,可以很容易地被计算机索引,并由人们使用纯文本终端、网络浏览器或屏幕阅读器(如果需要)来阅读。

然而,人类是非常视觉化的生物,随着万维网的流行,HTML 开始获得创建演示效果的特性。人们将使用字体和粗体标签的组合来创建特定的视觉效果,而不是使用标题元素作为页面标题。表格被当作一种布局工具,而不是显示数据的方式,人们会使用块引用元素来缩进文本,而不是表示引用。很快,HTML 失去了给内容提供结构和意义的主要目的,变成了字体和表格标签的混乱。网页设计师为这种标记起了一个名字;他们称之为标记汤(见图 1-1 )。

A314884_3_En_1_Fig1_HTML.jpg

图 1-1。2000 年 8 月 14 日 abcnews.com 的头条新闻的标记使用表格作为布局,大而粗的文本作为标题。代码缺乏结构,难以理解

网络已经变得一团糟,CSS 被创造出来帮助整理东西。CSS 的主要目的是允许 HTML 中的表示规则被提取出来并放入它们自己的系统中;为了分离内容和呈现。这鼓励了意义和语义潜移默化地回到 HTML 文档中。像字体标签这样的表示性标签可以被抛弃,布局表格可以被慢慢取代。这对于网站的可访问性和速度来说是一个福音,但 CSS 也为网站设计者和开发者提供了许多好处:

  • 一种专门设计用来控制视觉样式和布局的语言

  • 可以更容易地在一个站点中重用的样式

  • 通过关注点分离改进代码结构

关注点分离

关注点分离的概念在软件开发中很常见。在 Web 上,它不仅可以应用于标记和样式的分离,还可以应用于样式的编写方式。事实上,这是确保代码可维护性的主要方法之一。

Unix 开发社区中有一个常见的短语,通过“小块、松散连接”来表达这个概念“一小块”是专注于做好一件事的代码模块。因为它与其他组件是“松散连接”的,所以该模块可以很容易地在系统的其他部分重用。Unix 中的“小片段”可以是字数统计功能,它可以处理你输入的任何文本。在 web 开发中,“一小块”可以是产品列表组件,如果松散耦合,它将可以在站点的多个页面上重用,或者在布局的不同部分重用。

你可以把这些小段代码想象成乐高积木。每一块砖都非常简单,但是它们可以以多种方式连接在一起,创造出极其复杂的物体。在本书的最后,在第十二章中,我们将回到这个主题,并研究如何以结构化的方式使用这个策略。

HTML 和 CSS 的不同版本

CSS 有各种不同的版本,或者说“层次”,了解一下这些版本的含义以及它们如何影响你应该或不应该使用的 CSS 特性是很有好处的。万维网联盟(W3C)是负责 Web 技术标准化的组织,它的每个规范在最终成为 W3C 推荐标准之前都经历了许多发展阶段。CSS 1 在 1996 年底成为 W3C 的推荐标准,包含非常基本的属性,如字体、颜色和边距。CSS 2 在 1998 年成为一个推荐标准,并增加了高级概念,如浮动和定位,以及新的选择器,如子、相邻兄弟和通用选择器。

CSS 3 是一个稍微不同的野兽。事实上,这里的并不是css3 本身,而是每个独立升级的模块集合。当一个模块规格继续改进一个现有的概念时,它从第 3 级开始。如果它是一种全新的技术,它从第 1 级开始。当使用 CSS 3 这个术语时,它通常指任何足以成为模块规范一部分的新东西。模块的例子包括“CSS 背景和边框级别 3”、“选择器级别 4”和“CSS 网格布局级别 1”这种模块化方法允许不同的规范以不同的速度发展,一些 3 级规范,如“CSS 颜色 3 级”,已经作为推荐标准发布。其他人处于候选人推荐状态,许多人仍处于工作草案状态。

虽然 CSS 3 的工作大约在 CSS 2 发布的时候就开始了,但是这些新规范的进展最初是缓慢的。因此,在 2002 年,W3C 发布了 CSS 2 修订版 1。CSS 2.1 修复了 CSS 2 中的错误,删除了浏览器中不太支持或不存在的功能,并在总体上进行清理,以提供更准确的浏览器实现情况。CSS 2.1 在 2011 年 6 月达到推荐状态,这是在 CSS 3 的工作开始十多年之后。这让你知道标准机构和浏览器制造商需要多长时间才能确定这些技术的工作原理。也就是说,当特性处于草案阶段时,浏览器通常会提供对特性的实验性支持,而在候选推荐阶段,事情通常是相当稳定的。某样东西成为可用技术的日期通常比它成为推荐的日期要早得多。

HTML 的历史同样复杂。1999 年,HTML 4.01 成为推荐标准,W3C 开始关注 XHTML 1.0。XHTML 1.1 本应遵循这一标准,但它强加的严格程度被证明是不切实际的,并且在 web 开发社区的成员中不受欢迎。实质上,网络主要语言的发展停滞了。

2004 年,一些公司成立了网络超文本应用技术工作组(WHATWG ),并开始制定一套新的规范。W3C 在 2006 年认识到这项工作的必要性,并加入了这项工作。2009 年,W3C 完全放弃了 XHTML,正式接受了 WHATWG 的新标准,即 HTML5。最初,WHATWG 和 W3C 都协调了他们在标准方面的工作,但是随着时间的推移,他们的关系变得复杂了。今天,他们编辑了两个独立的标准,一个来自 WHATWG,被称为 HTML,另一个来自 W3C,被称为 HTML5。是的,我们知道,这有点疯狂。幸运的是,这两个标准非常接近,所以将 HTML5 作为一个单独的东西来说还是有意义的。

我应该使用什么版本?

设计师和开发人员经常问他们应该使用哪个版本的 HTML 或 CSS,但是这个问题没有简单的答案。尽管规范为标准和 web 技术开发工作提供了一个焦点,但它们在很大程度上与设计人员和开发人员的日常工作无关。重要的是知道 HTML 和 CSS 的哪些部分已经在浏览器中实现,以及这些实现有多健壮和无错误。它们是应该谨慎使用的实验特性吗?或者,它们是否健壮且经过良好测试,在大量浏览器上实现匹配?

理解浏览器支持的状态是当今编写 CSS 和 HTML 最棘手的部分之一。有时候事情似乎进展得很快,你必须努力工作才能跟上。在其他时候,它会感觉慢得令人沮丧。在这本书里,你会看到关于 HTML 和 CSS 各种特性的浏览器支持说明,以及关于如何和何时应该考虑使用它们的提示。不可避免地,这里打印的信息会过时,所以你自己跟上这些信息是很重要的。

有几个学习浏览器支持的好地方。对于 CSS 属性,“我可以使用”网站(caniuse.com)允许你搜索一个属性或一套属性,包括桌面和移动浏览器中支持它的浏览器的百分比统计。另一个雄心勃勃的计划是 http://webplatform.org 的 ??,这是 W3C 和几个浏览器制造商和行业巨头之间的合作,试图收集和合并他们各自支持 CSS、HTML、JavaScript APIs 等的文档。然而,正如大型项目往往会做的那样,将这种规范的 web 技术文档放在一起需要花费大量的时间。在这种情况下,Mozilla 的开发者文档 MDN(【http://developer.mozilla.org】)被普遍认为是黄金标准。

当讨论浏览器支持时,重要的是要接受并非所有的浏览器都是生来平等的;他们永远也不会。如今只有极少数浏览器支持 CSS 3 的一些特性。例如,直到 11 版的 Internet Explorer 和 6.1 版的 Safari 才支持灵活框布局(或简称为 flexbox )。即使需要支持遗留浏览器,也不代表 flexbox 一点用都没有。您可能会避免在站点的核心布局中使用 flexbox,但是您仍然可以选择在特定的组件中使用它,因为它的强大功能非常有用,并且确保在不理解属性的浏览器中有一个可接受的后备。判断向后兼容性和未来友好代码的能力是定义真正 CSS 大师的一部分。

渐进增强

平衡向后兼容性与最新 HTML 和 CSS 特性的能力涉及到一种称为渐进增强的策略。这代表的基本意思是“从使它在最小公分母上工作良好开始,但是在它们被支持的地方自由地进一步发展。”使用渐进式增强意味着您将在“层”中编写代码,其中每个连续的增强只有在被支持或被认为合适时才被应用。这听起来可能很复杂,但好消息是 HTML 和 CSS 都部分内置了这一点。

对于 HTML,这意味着未知的元素或属性一般不会给浏览器带来麻烦;它会毫无怨言地将它们一扫而光,但可能不会将结果应用到页面的工作方式上。例如,您可以使用 HTML5 中定义的新型输入元素。假设您有一个电子邮件地址的表单域,标记如下:

<input type="text" id="field-email" name="field-email">

您可以像这样更改 type 属性的值:

<input type="email" id="field-email" name="field-email">

没有实现新字段类型的浏览器将简单地回答“我不知道那是什么意思”,并退回到默认的类型值,即“文本”,就像第一个例子一样。理解“电子邮件”类型的新浏览器会知道用户应该在这个字段中输入什么类型的数据。在许多移动设备上,软件键盘会调整以显示为输入电子邮件地址而优化的视图,如果你在较新的浏览器中使用内置的表单验证支持,这也会起作用。我们已经逐步增强了页面,对老版本浏览器的用户没有负面影响。

另一个简单的变化是将文档类型声明从 HTML5 标准更新为新的、更短的版本。文档类型,或简称为 doctype,是 HTML 文档顶部的一位,它被认为是关于文档中使用的标记语言版本的机器可读提示。在旧版本的 HTML 和 XHTML 中,这曾经是一件漫长而复杂的事情,但在 HTML5 中,它被简化为:

<!DOCTYPE html>

您可以安全地切换到使用此 doctype 编写 HTML 文档,因为 HTML5 语法和 doctype 向后兼容。在接下来的章节中,我们将进一步了解 HTML5 中的一些新元素,但如果你需要更多关于如何开始编写 HTML5 标记的深入信息,请查看 Jeremy Keith 在 http://html5forwebdesigners.com的为网页设计师编写的 HTML5。

当涉及到浏览器如何解释新属性时,CSS 中的渐进式增强以类似的方式工作。浏览器无法识别的任何属性或值都会导致它丢弃该声明,因此添加新属性不会有任何不良影响,只要您提供合理的后备。

例如,许多现代浏览器支持颜色值的 rgba 函数符号。它允许您使用红色、绿色和蓝色通道的单独值以及透明度值(称为 alpha 通道)来指定颜色。我们可以这样使用它:

.overlay {
    background-color: #000;
    background-color: rgba(0, 0, 0, 0.8);
}

该规则规定,具有覆盖类名的元素应该具有黑色背景色,但是随后立即使用 rgba 将背景色重新声明为略微透明的黑色。对于不理解 rgba 表示法的浏览器,第二条语句将被忽略,元素将具有纯黑色背景色。对于那些理解 rgba 符号的浏览器来说,第二条语句覆盖了第一条语句。因此,即使 rgba 表示法并不是在所有地方都受支持,我们仍然可以使用它,只要我们先使用后备声明。

供应商前缀

浏览器制造商使用同样的原理在他们的浏览器中引入实验性的特性。他们通过在属性名或属性值前添加一个特殊的字符串来实现这一点,这样只有他们自己的浏览器引擎会应用它,而其他浏览器会忽略它。这使得浏览器制造商可以在规范缺失或不成熟时引入新功能。如果不同的浏览器对新特性有不同的解释,样式表作者可以在不破坏页面的情况下尝试它们。例如:

.myThing {
  -webkit-transform: translate(0, 10px);
  -moz-transform: translate(0, 10px);
  -ms-transform: translate(0, 10px);
  transform: translate(0, 10px);
}

这将一个转换应用到带有几个不同前缀的元素(我们将在第十章中讨论)。那些以-webkit-开头的应用于基于 webkit 的浏览器,比如 Safari。Google Chrome 和 Opera 基于 Blink 引擎,而 Blink 引擎最初是基于 WebKit 的,所以-WebKit-前缀通常也适用于它们。前缀-moz 适用于基于 Mozilla 的浏览器,如 Firefox,前缀-ms 适用于微软的 Internet Explorer。

最后,我们添加了无前缀版本,这样支持该属性标准化版本的浏览器就不会错过。从历史上看,开发人员在添加标准化版本时一直很马虎。这已经走得很远,一些浏览器制造商已经开始支持竞争引擎的前缀,只是为了确保流行的网站能在他们的浏览器上工作。这种混乱的结果是,大多数浏览器制造商不再使用厂商前缀。实验性的特性被隐藏在偏好标志后面,或者在特殊的预览版本中。

书中的例子大多只使用不带前缀的标准化属性,所以建议你去像caniuse.com这样的网站查看,以确保当前的支持情况如何。

条件规则和检测脚本

对于更高级的情况,我们需要基于 CSS 支持的完全不同的解决方案,有@supports-block。这个特殊的块称为条件规则,它检查括号内的声明,并且仅在声明受支持时才应用该块内的规则:

@supports (display: grid) {
  /* rules for when grid layout is supported go here */
}

这样做的问题是,这条规则本身相当新,所以我们只能将它用于任何传统浏览器都没有实现的前沿功能(例如,我们将在第七章中查看网格布局)。对于其他情况,我们可以使用 JavaScript 来判断某样东西是否受支持。这种类型的特性测试在几个 JavaScript 库中都有,最流行的是 Modernizr(【http://modernizr.com】??)。它的工作原理是将支持提示附加到 HTML 中,然后您可以将它作为 CSS 的基础。

我们将在接下来的章节中更仔细地研究类似的策略和工具,但是重要的是渐进式增强可以帮助我们摆脱对版本号和规范的过多担心。通过谨慎的应用,我们可以在适当的地方使用新的闪亮玩具,而不会把用户留在旧的浏览器上。

创建结构和语义丰富的 HTML

语义标记是任何好的 HTML 文档的基础。语义学是对意义的科学研究。在具有一组正式符号(如 HTML 及其元素和属性)的虚构语言的上下文中,语义指的是我们通过使用某个符号所表达的意思。简单地说,语义标记就是在正确的地方使用正确的元素,从而产生有意义的文档。

有意义的文档有助于确保尽可能多的人可以访问内容,无论他们是使用最新版本的谷歌浏览器,运行 Lynx 等纯文本浏览器,还是依赖屏幕阅读器或盲文显示器等辅助技术。无论项目后期可能需要什么样的图形或交互,文档的基本语义都不应该也不需要被破坏。

良好的结构化标记也意味着你的内容更容易被机器使用,特别是搜索引擎蜘蛛,如 Googlebot,它对页面进行索引和排序,以纳入谷歌的搜索结果。Googlebot 从您的页面中获取的数据越丰富,它就越有可能对这些页面进行正确的索引和排名。因此,你很可能会受益于搜索排名中更高的位置。

更重要的是,在 CSS 的上下文中,有意义的标记为您提供了一种简单的方法来定位您想要样式化的元素。它为文档增加了结构,并为您创建了一个基础框架。

事实上,许多制作 CSS 的现代方法都建议从网站的一组“基础”样式开始。图 1-2 所示的保罗·劳埃德的样式指南页面包含了他在个人博客上可能需要的所有看似合理的元素。它描述了如何以及何时使用它们,并且他的样式表确保了无论他在页面上添加什么元素,都将被适当地样式化,而不必做进一步的工作。

A314884_3_En_1_Fig2_HTML.jpg

图 1-2。paulrobertlloyd.com 样式指南,位于paulrobertlloyd.com/about/styleguide/

保罗的样式指南包含了所有明显有意义的元素,例如:

  • h1、h2 等等

  • p、ul、ol 和 dl

  • 强壮和 em

  • 批量引用和引证

  • 预编码和编码

  • 时间、图片标题和标题

它还包括表单和表格及其相关元素的基本样式,包括:

  • 字段集、图例和标签

  • 标题、thead、tbody 和 tfoot

拥有这套基本样式的价值怎么强调都不为过。很明显,在设计和开发过程中,您需要很快开始继承和覆盖它们,但是拥有一个坚实的元素样式基础可以为您将来的工作做好准备。它也可以作为校样。当您对 CSS 进行更改时,您可以浏览样式指南中的组件,并验证您在处理其他样式时没有无意中覆盖某些样式。

类别和 ID 属性

有意义的元素提供了一个很好的基础,但是它们不会为你提供应用每一个视觉效果所需的所有“钩子”。十有八九你会希望根据上下文来调整基本元素的样式。我们需要一种在文档中提供其他样式“挂钩”的方法,一种常见的方法是使用 ID 和 class 属性。

添加一个 ID 或类属性并不会固有地给你的文档添加意义或结构。添加这些属性是一种通用的方式,允许其他东西与您的文档交互并解析您的文档,CSS 就是一种可以利用它们的东西。这些属性的价值在于它们可以包含您定义的名称。

这听起来很琐碎,但是命名是编写代码中最重要(通常也是最困难)的部分之一。选择一个名字可以让你陈述某个东西是什么,并暗示它的用途或者应该如何使用它。当你写代码时,清晰和明确是绝对重要的。所以让我们取一个简单的链接列表,并给它一个 class 属性,这个属性有一个很好的可读性和有用的值:

<ul class="product-list">
  <li><a href="/product/1">Product 1</a></li>
  <li><a href="/product/2">Product 2</a></li>
  <li><a href="/product/3">Product 3</a></li>
</ul>

这里,我们使用 class 属性在文档中创建了一个产品列表模块。在 CSS 中,我们认为类名是定义事物的一种方式。product-list 类名为我们提供了一种方法来指定我们希望成为这种类型的任何列表。一旦我们创建了 CSS 来样式化我们的产品列表,我们不仅可以在这里使用它,还可以在网站的任何其他上下文中使用它——比如蓝图或模板。

即使我们添加了一个类名作为样式的显式挂钩,我们通常也应该避免使用一个在视觉上表明它看起来像什么的名字(我们将在第十二章讨论是否以及何时打破这个规则)。相反,我们应该选择一个表明组件类型的名称。例如,这里我们选择了 product-list,而不是一个通用名称,比如 large-centered-list。

您会注意到,在前面的例子中,我们选择了使用 class 属性,而不是 ID 属性。在用于样式化时,ID 和 class 属性之间有一些重要的区别,但是此时最适用的是一个 ID 名称只能应用于页面上的一个元素。这意味着它不能像为我们的产品列表这样的模块定义一个可重用的“模板”那样简单。如果我们使用了 ID 属性,我们就不能在每页上重用产品列表一次以上。

我们更喜欢使用 ID 属性来标识特定模块的单个实例。例如,我们的产品列表模块的一个实例可能如下所示:

<ul id="primary-product-list" class="product-list">
  <li><a href="/product/1">Product 1</a></li>
  <li><a href="/product/2">Product 2</a></li>
  <li><a href="/product/3">Product 3</a></li>
</ul>

这是我们的 product-list 的另一个实例,它根据 class 属性选择样式,但是这里它也被定义为 primary product-list。每页只能有一个主要产品列表,这似乎是合理的,因此 ID 属性可能是一个合适的选择。然后,该 ID 可以用于向模块添加额外的覆盖样式,或者可以用于向模块添加一些与 JavaScript 的交互,或者作为导航的页面内锚。

实际上,使用 ID 属性作为 CSS 的挂钩通常并不特别有价值。如果您喜欢使用类进行样式化,并且只使用 id 来标识文档中的元素,而不是用于样式化,那么您通常会创建更简单、更易于维护的代码。我们将在第十二章更详细地讨论这个话题。

结构元素

HTML5 引入了一系列全新的结构元素:

section
header
footer
nav
article
aside
main

引入这些元素是为了创建 HTML 文档的逻辑部分。您可以使用它们来表示包含独立内容(文章)、导航组件(nav)、特定部分的标题(标题)等的部分。main 元素是最新添加的元素,突出显示了包含页面主要内容的区域。http://html5doctor.com 是一个很好的资源,可以让你深入了解所有这些新元素的正确用法。

除了 main 元素之外,所有这些新元素都可以在一个文档中多次使用,这给了机器和人类更好的解释机会。在这些新元素出现之前,您经常会看到具有相似类名的 div 元素,例如在标记博客帖子时:

<div class="article">
  <div class="header">
    <h1>How I became a CSS Master</h1>
  </div>
  <p>Ten-thousand hours.</p>
</div>

那些 div 元素没有为文档提供任何真正的语义值;它们可能只是作为样式挂钩,使用类名。前面片段中唯一有实际意义的部分是 h1 和 p,但是使用我们新发现的 HTML5 元素,我们可以改进一些东西:

<article>
  <header>
    <h1>How I became a CSS Master</h1>
  </header>
  <p>Ten-thousand hours.</p>
</article>

我们通过这一改变改进了 HTML 的语义,但是带来了意想不到的副作用。现在,我们唯一可以用来设计样式的挂钩是文章和标题元素。用于样式化它们的 CSS 选择器可能如下所示:

article {
  /* styles here */
}
article header {
  /* other styles here */
}

文章和标题元素都可以在页面的其他地方重用,用于显示博客文章之外的其他目的。如果它们现在被重用,并且我们将样式直接附加到选择器中的元素上,那么它们将会选择我们为博文设计的样式规则,不管它们是否适合新的情况。一种更灵活、更具前瞻性的方法是将这两个例子结合起来:

<article class="post">
  <header class="post-header">
    <h1>How I became a CSS Master</h1>
  </header>
  <p>Ten-thousand hours.</p>
</article>

关联的 CSS 规则然后可以使用类名来挂钩到这个结构:

.post {
  /* styles here */
}
.post-header {
  /* other styles here */
}

通过这个简单的改变,我们实际上展示了一个非常重要的概念。我们已经将文档的语义从样式化的方式中分离出来,使其更易于移植,目的更清晰,因此更易于维护。如果我们现在决定一篇文章不是包含该内容的最合适的元素,或者我们发现我们的内容管理系统(CMS)出于某种原因限制我们使用 div,我们不需要做任何进一步的更改。无论我们选择(或被迫)使用什么元素,我们与类属性挂钩的样式都会工作得很好。

旧互联网探索者和新元素

在大多数浏览器中,使用这些新元素可以很好地工作,但 Internet Explorer 8 和更早版本不会将样式应用于它不知道的元素。幸运的是,这可以通过使用一个叫做“填充”或“多填充”脚本的 JavaScript 片段来解决。

你可以在 https://github.com/aFarkas/html5shiv 找到这样一个脚本的版本。

它也包含在前面提到的 Modernizr 库中,我们将在接下来的章节中回到这个库中。

如果您预计很大一部分用户使用的是非常旧的浏览器,那么在严重依赖这些新元素时应该小心,因为这会导致额外的 JavaScript 依赖,从而使事情按预期工作。

使用 div 和跨度

所有这些新奇的语义元素并不意味着我们的老工具 div 元素是多余的。当没有其他更符合您的目的的语义正确的元素时,div 是用于对内容进行分组的合适元素。有时,纯粹出于样式目的,您需要向文档中添加额外的元素,例如围绕整个页面的包装,以帮助创建居中布局。

如果您可以使用一种更具语义的元素来构建您的内容,那么总是这样做,如果需要样式化,就给它一个合适的 class 属性。但是,如果您需要一个非语义元素作为额外的样式挂钩,请使用 div。有一个古老的术语叫做“divitis ”,指的是 HTML 作者倾向于在他们的标记中加入 div,或者对任何事情都使用 div,而不管是否有更合适的元素。只在必要的地方添加 div,以提供简单明了的造型挂钩,但不要因为不得不添加一些 div 而感到尴尬或羞愧。我们将在后面的章节中看到一些具体的例子,在这些例子中,一些额外的无意义的 div 对于创建干净的、可维护的代码变得非常有价值。

div 元素的一个关联是 span。像 div 一样,它没有语义意义,可以纯粹用于向文档添加表示挂钩。span 不同于 div,因为它是一个文本级元素,用于在一段文本流中提供结构。同样,在使用无意义的 span 之前,一定要确保没有语义丰富的 HTML 元素可以代替它。例如,time 应该用来标记时间和日期,q 用来标记引文,而通常的疑点 em 用来强调重音,strong 用来表示非常重要:

<p>At <time datetime="20:07">7 minutes past eight</time> Harry shouted, <q>Can we just end this, now!</q> He was <strong>very</strong> angry.</p>

重新定义的表示性文本元素

元素是表示标记时代的残余,分别用来代表粗体和斜体文本。你可能会认为它们已经从新的 HTML5 规范中剔除了,但是它们实际上仍然存在。由于它们广泛出现在网络上的旧内容中,或者通过亚标准的 WYSIWYG 编辑器创建的内容中,HTML5 规范的编辑决定将它们留在那里,而不是更新它们的定义。

今天,元素代表了不同于其周围环境的内容,通常会被排版为斜体。HTML5 规范中的例子包括不同语言的表达式或船名。

元素有几乎完全相同的定义,但是对于传统上是粗体的内容。这里的例子包括产品名称或类别。

这听起来有点模糊,但重要的是,这两个元素与它们的表亲和**不同,因为它们没有说明其中内容的重点。大多数时候你会想要,因为它们是在一段文本中强调和强调的语义上正确的选择。****

扩展 HTML 的语义

长期以来,web 开发人员一直在探索向 HTML 有限的词汇中添加新的语义和结构的方法。内容中更丰富的意义表达为网络和围绕它构建的工具提供了各种可能性。尽管迈向语义网天堂的进程还没有快到令人眼花缭乱的地步,但在允许 HTML 作者向他们的文档中添加更细粒度和更具表达性的语义方面已经取得了一些积极的进展。

ARIA 角色属性

许多新的 HTML5 元素为可访问性优势开辟了可能性。例如,如果屏幕阅读器等辅助技术能够理解导航元素在页面中的位置和内容,它们就可以帮助用户跳过这个导航来找到内容,或者在需要时返回导航。

实现这一点的另一种方法是使用可访问的富互联网应用程序(ARIA ),它作为 HTML 的补充规范。ARIA 允许您为辅助技术提供更多的语义含义,通过指定文档包含的不同元素或它们提供的功能。例如,role="navigation "是所谓的" landmark role "属性,它声明一个元素具有导航角色。其他标志性角色包括:

  • 旗帜

  • 形式

  • 主要的

  • 搜索

  • 补充的

  • 内容信息

  • 应用

ARIA 角色及其定义的完整列表可以在 ARIA 规范www.w3.org/TR/wai-aria/roles#role_definitions中找到。

如果你需要一个何时使用里程碑式角色的简短分类,Paciello Group 的 Steve Faulkner 已经在blog . paci ello Group . com/2013/02/using-wai-aria-landmarks-2013/上发布了如何以及何时使用它们的概述。

ARIA 还允许开发人员指定更复杂的内容或界面元素。例如,在 HTML 中重新创建音量控制滑块小部件时,它将包含一个值为 slider 的角色属性:

<div id="volume-label">Volume</div>
<div class="volume-rail">
  <a href="#" class="volume-handle" **role="slider" aria-labelledby="volume-label" aria-valuemin="1" aria-valuemax="100" aria-valuenow="67"**></a>
</div>

额外的属性 aria-labelledby、aria-valuemin、aria-valuemax 和 aria-valuenow 都提供了额外的信息,辅助技术可以使用这些信息来帮助有视觉障碍、运动障碍或不同能力的用户使用滑块小部件。

添加关于 HTML 页面各种组件所扮演角色的额外语义信息也是为脚本和样式提供钩子的一个好方法,所以这是一个典型的双赢。

微格式

到目前为止,最广泛采用的扩展 HTML 语义的方式是微格式,这是一组标准化的命名约定和标记模式,用于表示特定类型的数据。这些命名约定基于现有的数据格式,如 vCard 和 iCalendar。例如,以下是一些以 hCard 格式标记的联系方式:

<section class="h-card">
  <p><a class="u-url p-name" href="http://andybudd.com/">Andy Budd</a>
     <span class="p-org">Clearleft Ltd</span>
     <a class="u-email" href="mailto:info@andybudd.com">info@andybudd.com</a>
  </p>
  <p class="p-adr">
    <span class="p-locality">Brighton</span>,
    <span class="p-country-name">England</span>
  </p>
</section>

用微格式标记的联系信息使开发人员更容易编写提取这些数据的工具。例如,浏览器插件可以在您浏览的页面中找到微格式,并允许您将联系人下载到您的地址簿中,或者将事件添加到您的日历应用程序中。一系列数据类型都有微格式:联系方式、事件、食谱、博客文章、简历等等。微格式也可以用来表达一段内容和该内容链接到的另一个 URL 之间的关系。

微格式之所以受欢迎,部分是因为它们易于实现,并且已经被包括 Yahoo!以及直接添加到 WordPress 和 Drupal 等出版工具中。2012 年一项关于结构化数据实现的研究(microformats.org/2012/06/25/microformats-org-at-7)发现,微格式在网络上的应用最为广泛,但也有一些替代品在最近开始取得重大进展,比如微数据。

微观数据

微数据是 HTML5 中引入的,它提供了另一种向 HTML 添加结构化数据的方式。它的目的和目标与微格式非常相似,但是将微数据嵌入内容的细节有些不同。让我们看看如何使用微数据标记上一节中相同类型的联系信息:

<section itemscope itemtype="http://schema.org/Person">
  <p><a itemprop="name" href="http://thatemil.com/">Emil Björklund</a></p>
     <span itemprop="affiliation" itemscope             itemtype="http://schema.org/Organization">
       <span itemprop="name">inUse Experience AB</span>
     </span>
     <a itemprop="email" href="mailto:emil@thatemil.com">emil@thatemil.com</a>
  </p>  <p itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">
    <span class="addressLocality">Malmö</span>,    <span class="addressCountry">Sweden</span>  </p>
</section>

如本例所示,微数据语法比相应的微格式略显冗长;但这是有原因的。微数据被设计为可扩展的,因此它可以表示所需的任何类型的数据。它只是提供了一些表达数据结构的语法,但本身并没有定义任何特定的词汇表。这与微格式相反,微格式定义了特定类型的结构化数据,如 hCard 或 hCalendar。

微数据留给其他人来定义和记录特定的格式。我们在前面的例子中使用的格式是由 http://schema.org 定义的词汇表之一,它是由必应、谷歌和雅虎的代表创建的。这些搜索引擎使用它来帮助他们索引和排列页面,这意味着这些词汇表是帮助搜索蜘蛛丰富有效地索引您的内容的另一种方式。

确认

即使您的标记的核心是经过深思熟虑的,语义上也是合理的,但是仍然存在输入错误或格式错误会给您带来不可预见的麻烦的风险。这就是验证的用武之地。

现实世界中的大多数 HTML 文档实际上都不是有效的 HTML。用规范编写者的话说,他们是不符合规范的。这些文档的元素嵌套不正确,包含未编码的&符号,并且缺少必需的属性。浏览器非常优雅地处理这类错误,并且总是试图猜测作者的意图。事实上,HTML 规范中包含了处理无效 HTML 的规则,以确保浏览器开发者以一致的方式处理错误。

浏览器如此善于处理我们的错误,这一事实对整个网络来说是一件幸事,但这并不能免除我们在这方面的责任。我们应该尽可能尝试创建有效的文档。这样做将有助于我们更快地捕捉错误,或者完全阻止它们被引入。如果您有一个呈现或布局错误,但没有立即和明显的修复方法,一个好的第一步是验证 HTML,以确保您尝试的是一个格式正确的文档。

有许多工具可以帮助您验证 HTML。您可以使用 W3C 站点上的 HTML 验证器(validator.w3.org/),或者与它通信的许多浏览器插件中的一个。例如,Web Developer extension(【http://chrispederick.com/work/web-developer/】??)可用于 Mozilla Firefox、Opera 和 Google Chrome,并具有验证公开可用网站和本地网站的选项(以及其他真正有用的功能!).或者,如果您的项目有任何类型的自动化构建或测试过程,您可以在这里包含 HTML 验证作为一个步骤。

CSS 验证也是可能的。W3C 在 http://jigsaw.w3.org/css-validator/有一个 CSS 验证器。有人可能会说验证 CSS 文件不如验证 HTML 重要——CSS 中的错误不太可能导致 JavaScript 失败或使使用辅助技术(如屏幕阅读器)的人无法访问您的页面。尽管如此,你应该确保不时地检查你的 CSS,以确保你没有犯任何简单的错误,比如忘记在度量中添加单位。

根据您在 CSS 验证器中选择的设置,您会收到许多关于在代码中使用供应商前缀的警告或错误。这些是非标准的属性或值,浏览器制作者在实现对 CSS 特性的实验性支持时,允许您将其设置为真实事物的替身。例如,display 属性的-webkit-flex 值是基于 webkit 的浏览器中 flex 属性的实验版本。这很可能被验证器标记为警告或错误,但是即使验证器对您大喊大叫,您的文件也能正常工作。你只需要确保你明白为什么它会把事情标记为有问题。

验证本身并不是目的,由于来自第三方的内容、笨拙的 CMS 系统或您可能想要使用的实验性 CSS 功能,许多本来很好的页面都无法通过验证。还有一个风险是,验证器实际上没有跟上标准和浏览器实现。所以不要在验证上较劲,而是把它作为一种手段,在错误引起太多连锁反应之前,捕捉那些容易修复的错误。

摘要

在这一章中,我们看了一些可以确保你在 HTML 和 CSS 方面有一个坚实基础的方法。您学习了一些关于 HTML 和 CSS 的历史,如何跟上变化,以及如何使您的代码既向后兼容又对未来友好。现在,您已经知道了编写可维护代码的重要性,以及一些构建 HTML 的方法,这样就可以轻松、一致地使用 CSS 样式。

在下一章中,我们将回顾一些基本的 CSS 选择器,然后从第 3 级和第 4 级选择器规范转移到一系列更高级的选择器。您将了解特殊性、继承性和级联,以及如何将它们用于创建高效的样式表。

二、让你的样式达到目标

一个有效的、结构良好的文档为你的样式的应用提供了基础。您可能已经在 HTML 中添加了适当的样式“挂钩”,或者随着页面设计需求的发展,您可能还会添加更多的样式。在这一章中,我们将看看我们可以用来定位 HTML 的选择器的范围,以及我们可以用来获得更多控制的额外钩子。我们将涵盖:

  • 公共选择器

  • 面向现在和未来的前沿选择器

  • 特异性和级联的奇妙世界

  • 将样式应用到页面

CSS 选择器

最基本的选择器是后代选择器。类型选择器用于定位特定类型的元素,比如一个段落(如下图所示)或者一个标题元素。您只需简单地指定想要样式化的元素的名称。类型选择器有时也被称为元素选择器。

p {
  color: black;
}

Descendant 选择器允许您定位特定元素或元素组的后代。后代选择器由两个其他选择器之间的空格表示。在此示例中,只有作为块引号后代的段落元素才会缩进,而所有其他段落将保持不变:

blockquote p {
  padding-left: 2em;
}

这两个选择器非常适合全面应用基本样式。为了更具体和有针对性地选择元素,可以使用 ID选择器。顾名思义,这些选择器将以具有相应 ID 属性或类名值的元素为目标。ID 选择器使用散列字符来标识;类选择器用句点标识。本示例中的第一条规则将使介绍性段落中的文本加粗,第二条规则将使日期变灰:

#intro {
  font-weight: bold;
}
.date-posted {
  color: #ccc;
}
<p id="intro">Happy Birthday, Andy</p>
<p class="date-posted">20/1/2013</p>

有时,将 ID 和类选择器与类型和后代选择器结合起来会很有用,而不是将 ID 或类属性添加到每个要作为目标的元素中:

#latest h1 {
  font-size: 1.8em;
}
#latest .date-posted {
  font-weight: bold;
}
<article id="latest">
<h1>Happy Birthday, Andy</h1>
<p class="date-posted"><time datetime="2013-01-20">20/1/2013</time></p>
</article>

这些都是非常简单明显的例子。然而,您会惊讶地发现,仅仅使用到目前为止讨论的四个选择器,就可以成功地定位到多少个元素。通常,这些是可维护 CSS 系统的真正主力。其他高级选择器可能非常有用,但是它们没有这些更简单和更常用的选择器灵活和强大。

子代和同级选择器

在这些基本的选择器之上,CSS 包含了许多更高级的选择器。这些高级选择器中的第一个是选择器。一个后代选择器将选择一个元素的所有后代,而一个子代选择器只针对元素的直接后代,或者子代。在下面的例子中,外部列表中的列表项将被赋予一个自定义图标,而嵌套列表中的列表项将保持不变(见图 2-1 )。

A314884_3_En_2_Fig1_HTML.jpg

图 2-1。 child 选择器设计列表的子元素,而不是它的孙元素
#nav > li {
  background: url(folder.png) no-repeat left top;
  padding-left: 20px;
}
<ul id="nav">
  <li><a href="/home/">Home</a></li>
  <li><a href="/services/">Services</a>
  <ul>
      <li><a href="/services/design/">Design</a></li>
      <li><a href="/services/development/">Development</a></li>
      <li><a href="/services/consultancy/">Consultancy</a></li>
    </ul>
  </li>
  <li><a href="/contact/">Contact Us</a></li>
</ul>

有时,您可能希望根据一个元素与另一个元素的接近程度来设置该元素的样式。相邻兄弟选择器允许你定位一个元素,这个元素的前面是另一个共享相同父元素的元素。使用相邻的 同级 选择器,你可以使顶层标题后的第一段加粗,灰色,并且比后续段落稍大(见图 2-2 ):

A314884_3_En_2_Fig2_HTML.jpg

图 2-2。h2 后面的第一段有不同的样式
h2 + p {
  font-size: 1.4em;
  font-weight: bold;
  color: #777;
}

这可能是一种有用的技术,但是请记住,用自己的类值(如 intro-text)来设计开头段落的样式可能会使 CSS 更简单、更灵活。然后,这个 intro-text 类可以用来设计不紧跟 h2 的其他段落的样式。

和+标记被称为组合符,因为它们描述了规则两边组合的方式。我们已经看到了子组合符 ( >)和相邻兄弟组合符 (+)的例子,但是我们还应该看看第三个组合符——一般兄弟组合符(∾)。回到上一个例子,您可以使用通用兄弟组合符来定位每一个具有前一个兄弟 h2 的段落元素。

h2 ∼ p {
  font-size: 1.4em;
  font-weight: bold;
  color: #777;
}
注意

你可能已经注意到相邻兄弟普通兄弟组合符不允许你选择前面的兄弟——例如,后面不是 h2 的段落。对这样一个有用的选择器有抵触的原因有点复杂,但与页面呈现性能有关。

一般来说,当元素出现在页面上时,浏览器会对它们进行样式化,而在应该对段落进行样式化时,HTML 源代码中的 h2 可能还不存在。先前的兄弟组合符意味着浏览器必须跟踪这些选择器,然后在处理文档时执行应用样式的附加步骤。

然而,有一个以前的兄弟选择器的建议版本正在考虑标准化,但到目前为止,这个想法是将它的有效性限制在 CSS 选择器的特殊用途上,比如当它们在 JavaScript 中被评估时,所以即使该标准在浏览器中发布,它也可能不会按照您希望的方式工作。

通用选择器

通用选择器就像一个通配符,匹配任何元素。像其他语言中的通配符一样,通用选择器用星号表示。单独使用时,通用选择器匹配页面中的每个元素。使用以下规则来删除每个元素的默认浏览器填充和边距可能很有吸引力:

* {
  padding: 0;
  margin: 0;
}

这可能会有许多不可预见的情况,特别是在表单 UI 元素(如按钮和选择元素)的格式化方面。最好更明确地说明您要重置的内容,如下例所示:

h1, h2, h3, h4, h5, h5, h6,
ul, ol, li, dl, p {
  padding: 0;
  margin: 0;
}

幸运的是,有许多小型的开源库可以帮你解决这个问题。埃里克·迈耶的 CSS 重置(meyerweb.com/eric/tools/css/reset/)和尼古拉斯·加拉格尔的 normalize . CSS(necolas.github.com/normalize.css/)就是很好的例子。后者采用了一种略有不同的方法:Normalize.css 确保所有元素在不同浏览器中以一致的样式开始,而不是将边距和填充重置为 0。我们认为这是一组比简单地将所有内容重置为 0 稍微安全的默认设置。

当然,您不必只使用通用选择器来设置文档中每个元素的属性。您还可以将它与组合子一起使用,以特定的嵌套级别为目标,其中嵌套级别很重要,但元素的类型不重要。举个例子:

.product-section > * {
    /* ...  */
}

这将针对作为具有 product-section 类名的元素的直接后代的任何元素,但是不关心 product-section 的后代的类型或属性。当您想要针对这些元素而不增加特异性时,这种技术是有用的——我们将在本章前面进一步讨论特异性。

属性选择器

顾名思义,属性选择器允许您基于属性或属性值的存在来定位元素。这让你可以做一些非常有趣和强大的事情。

例如,当您将鼠标悬停在具有 title 属性的元素上时,大多数浏览器都会显示工具提示。您可以使用这种行为来扩展事物的含义,如首字母缩写词和缩写词,由元素表示:

<p>The term <abbr title="self-contained underwater breathing apparatus">SCUBA</abbr> is an acronym rather than an abbreviation as it is pronounced as a word.</p>

但是,如果不将鼠标悬停在元素上,就无法知道这些额外的信息是否存在。为了解决这个问题,您可以使用属性选择器来设计标题不同于其他元素的 abbr 元素的样式——在本例中,通过给它们一个底部虚线边框。当光标悬停在元素上时,通过将光标从指针变为问号,可以提供更多的上下文信息,表明此元素不同于大多数元素。

abbr[title] {
  border-bottom: 1px dotted #999;
}

abbr[title]:hover {
  cursor: help;
}

除了根据属性的存在来设置元素的样式之外,还可以根据特定的值来应用样式。例如,这可以用来修复光标悬停在提交按钮上时浏览器显示的不一致性。有了下面的规则,所有类型属性值为 submit 的 input 元素在鼠标经过时都会显示一个手形指针:

input[type="submit"] {
  cursor: pointer;
}

由于我们可能对属性值的模式感兴趣,而不是对确切的值感兴趣,所以属性选择器允许以更细粒度的方式匹配这些属性。通过在等号前添加一个特殊字符,我们可以指出我们感兴趣的匹配类型。

要匹配属性开头的值,请在等号前使用脱字符(^)。

a[href^="http:"]

要匹配属性末尾的值,请使用美元符号($)。

img[src$=".jpg"]

要匹配属性中任何位置的值,请使用星号(*)。

a[href*="/about/"]

要匹配以空格分隔的字符串列表中的值(如 rel 属性中的值),请使用波浪号字符(∩)。

a[rel∼=next]

还有一个属性选择器,可以选择值的开头匹配的元素,或者是单独匹配,或者后面紧跟一个破折号。对于这种匹配,请使用管道字符(|)。

a[hreflang|=en]

这个例子将匹配属性值 en 和 en-us,并提示使用这个选择器的意图:这对于在属性值中选择具有特定语言代码的元素很方便,因为它们是用破折号分隔的。从技术上讲,您可以将它与 class 属性一起使用来匹配类名,例如分别匹配 message 和 message-error,但是这样做的可移植性不是很好:如果您在 HTML 中的 message 类之前放置了另一个类,例如 class="box message ",那么选择器将不起作用。

伪元素

有时,您希望将页面的一部分作为目标,而该部分不是由元素表示的,但是您又不想在页面上添加额外的标记。CSS 为一些最常见的情况提供了一个简短的列表。这些被称为伪元素

首先,您可以通过使用::first-letter 伪元素来定位一段文本的第一个字母。每段文本的第一行可以用::first-line 版本作为目标。

还有一些伪元素,分别使用::before 和::after 伪元素,对应于存在于一段内容的开头和结尾的假设元素。这对于插入小符号和印刷修饰非常有用,并且通常作为创建视觉效果的挂钩,否则您会附加到真实元素上。一种方法是使用 content 属性以文本的形式插入内容,但是可以随意使用背景、边框等来设置伪元素的样式,就像设置任何其他元素的样式一样。

警告

使用伪元素注入内容时要小心!不要用它们来添加任何形式的文本内容,如果你的 CSS 不能正确加载的话,你的用户就离不开它们。还要注意,屏幕阅读器没有一个标准的方法来解释伪元素的内容:有些人忽略它,有些人阅读它。

将这些伪元素放在一个例子中,我们可以用最少的标记得到类似图 2-3 的东西。

A314884_3_En_2_Fig3_HTML.jpg

图 2-3。夏洛克·福尔摩斯小说的开头一段,的血字研究,借助伪元素做了一些排版处理

下面是实现这一点的 HTML 和 CSS 的缩略版本。

HTML:

<h1>A Study In Scarlet</h1>
<section class="chapter">
    <p>In the year 1878 I took my degree of Doctor of Medicine of the University of London, and proceeded to Netley to go through the course prescribed for surgeons in the army. Having completed my studies there, I was duly attached to the Fifth Northumberland Fusiliers as Assistant Surgeon.</p>
</section>

CSS:

.chapter::before {
    content: '”';
    font-size: 15em;
}
.chapter p::first-letter {
    float: left;
    font-size: 3em;
    font-family: Georgia, Times, "Times New Roman", serif;
}

.chapter p::first-line {
    font-family: Georgia, Times, "Times New Roman", serif;
    text-transform: uppercase;
}

正如您所看到的,我们使用::first-letter 伪元素在段落的开头创建了一个不同字体的首字下沉字母。第一行被转换成大写字母,并使用不同的字体和::first-line 伪元素。我们还使用::before 伪元素在章节容器中添加了一个装饰性的引号。所有这些都不需要添加任何多余的元素!确实方便。

我们将在第四章中仔细研究更多的印刷技术。

小费

伪元素应该使用我们到目前为止看到的双冒号语法,以区别于伪类,在下一节中您将看到伪类使用单冒号。然而,伪元素是在旧浏览器中用单冒号语法引入的,并且仍然以这种方式编写。因此,为了兼容起见,您仍然可以对一些伪元素使用单冒号语法,我们已经在本书中的示例中适当地这样做了。

伪类

有些情况下,您可能希望根据文档结构之外的内容来设置元素的样式,例如,超链接或表单元素的状态。这可以使用一个伪类选择器来完成。这些选择器以冒号(:)开头,用于定位应用它们的元素中的特定状态或关系。

一些最常见的伪类选择器可以用于样式化链接,如下所示,并且应该总是包含在针对最常见 HTML 元素的基本样式集中:

/* makes all unvisited links blue */
a:link {
  color: blue;
}
/* makes all visited links green */
a:visited {
  color: green;
}
/* makes links red on mouse hover, keyboard focus */
a:hover,
a:focus {
  color: red;
}
/*...and purple when activated. */
a:active {
  color: purple;
}

这些伪类选择器的顺序很重要。首先需要:link 和:visited 规则,然后是与用户交互相关的规则。当用户悬停在链接上或将键盘焦点给予链接时,: hover 和:focus 选择器将覆盖:link 和:visited,最后当用户单击或用键盘选择链接时,后跟:active。链接是交互式内容,默认情况下可以聚焦和激活。默认情况下,还有很多其他的交互元素,比如表单域和按钮,所以这些伪类也适用于它们。您还可以通过使用 JavaScript 使其他元素具有交互性。

最后,您可以对几乎任何元素使用:hover 伪类,但是请记住,像触摸屏和键盘这样的输入方法实际上没有悬停状态,所以不要对基本功能使用:hover。

目标和否定

另一个有用的伪类是:target,它匹配任何具有 ID 属性的元素,该属性当前在页面的 URL 哈希中表示。如果我们去example.com/blog/1/#comment-3并在那个页面上找到一个标记为<的评论文章 class = " comment " id = " comment-3 ">...</文章>,我们可以使用以下规则用淡黄色背景突出显示评论:

.comment:target {
  background-color: #fffec4;
}

现在,如果我们想要突出显示那个评论,但前提是它不是那些内容被隐藏的灰显的、被否决的评论中的一个呢?还有一个选择器,用来排除某些选择器。遇到否定伪类,或者:not()选择器!假设我们在标记为“否决”的注释上有一个特殊的类名,我们可以将规则改为:

.comment:target:not(.comment-downvoted) {
  background-color: #fffec4;
}

除了伪元素和它本身之外,negation 伪类几乎可以与您放入括号中的任何类型的选择器一起工作。

结构伪类

CSS 3 引入了大量与文档结构相关的新伪类。其中最常见的是第个子选择器,它可以用来设置表格中交替行的样式:

tr:nth-child(odd) {
  background: yellow;
}

这将使表格中的第一行以及随后的每一行都具有黄色背景。第 n 个子选择器充当一个函数,它可以接受许多不同的表达式作为参数。它将接受关键字 odd 和 even,如前面的示例所示。它也可以是表示目标元素的序号位置的数字,例如在下面的示例中,它将所有表格的第三行设置为粗体:

tr:nth-child(3) {
  font-weight: bold;
}

当我们看到对数字表达式的支持时,事情开始变得有点复杂;例如:

tr:nth-child(3n+4) {
  background:  #ddd;
}

上一个表达式中的数字 4 与我们要定位的第一个元素的序号位置有关,在本例中是第四个表行。数字 3 与第一个元素之后的每个目标元素的顺序位置相关。所以在前面的例子中,第个子选择器将匹配表中的第四、第七、第十等行,如图 2-4 所示。为了更好地理解这里的数学,表达式中括号内的 n 被替换为一个数字,从零开始,然后增加 1,直到不再有匹配的元素。

A314884_3_En_2_Fig4_HTML.jpg

图 2-4。一个表,其中的行使用:n-child(3n+4)设置样式。表达式以 n 为数字进行计算,从 0 开始,只要有匹配项,就增加 1

你可以用这些表达式做各种疯狂的事情。例如,您可以减去一个数字,而不是添加一个数字,因此我们可以这样做:n-child(3n-4)并得到不同的结果。这同样适用于 n 之前的数字,或者 n 本身——将它改为负数可以得到一些有趣的结果。例如,表达式:n-child(-n+3)将只选择前三个元素!

还有其他伪类选择器支持这些类型的表达式,如下所示:

:nth-last-child(N)

:n-last-child 选择器的操作方式与:n-child 选择器非常相似,只是它从最后一个子元素开始计数,而不是从第一个子元素开始计数。

回到 CSS 2.1 中,第一个孩子有一个伪元素,名为 first-child,因此,使用它可以更简单地编写第 n 个孩子(1)。三级选择器规范为最后一个孩子添加了一个对应的,名为(你猜对了):last-child,对应于:n-last-child(1)。还有:独生子女和独生子女。如果一个元素是特定元素类型的唯一子元素,则:only-of-type 选择器适用于该元素。通过使用这些伪类选择器,我们可以更好地定位特定类型的元素:

:nth-of-type(N)
:nth-last-of-type(N)

这两个伪类选择器的行为方式与:n-child 选择器相同,只是它们忽略(并且不计算)任何不属于它们所应用的元素类型的元素。这让我们有机会创建一些非常高效的模式,而不会将选择器过多地绑定到标记上。

巧妙使用结构伪类

仅仅使用结构化的伪类就可以做很多事情:当根据元素在文档中的位置和它们的环境来选择元素时,它们给了你很大的精确性。例如,可以根据某种类型的子元素的数量来选择项目,这使得可以根据项目的总数来设计网格列等样式。这是通过结合使用:n-last-of-type 伪选择器和:first-child 选择器来实现的。下面是一个匹配包含四个“列”的内容的示例,假设这些列具有相同的元素类型:

.column:nth-last-of-type(4):first-child,
.column:nth-last-of-type(4):first-child  ∼ .column {
  /* Rules for when there is exactly four .column elements */
}

当这个选择器匹配时,意味着从末尾算起的第四个元素也是第一个元素,因此有四个与。列类。我们还包括相邻兄弟选择器,以确保我们选择了所有其余的列。很整洁,是吧?

请注意,编号匹配不仅仅计算具有类名列的元素:它选择具有该类名的所有元素,然后根据它们具有相同的元素类型来计算项目。在选择器级别 4 规范中,有一个过滤匹配项的建议,使用 of 关键字,后跟括号内的选择器:

:nth-child(2 of .column):first-child {}

可悲的是,这种更有用的结构化伪类还没有在浏览器中广泛使用。

结构选择器通常有广泛的支持,但在 Internet Explorer 8 和更早的版本中却没有。如果您需要支持这些遗留浏览器,您可能希望将这种技术限制在小的增强上,而使用标记挂钩来定位整体布局模式的元素。

有关基于元素计数的样式的更多启发,请参见海登·皮克林的文章“CSS 的数量查询”(alistapart.com/article/quantity-queries-for-css)。

形成伪类

有许多专门针对表单中元素的伪类。这些可以用来反映特定表单输入的状态,这取决于用户如何与它们交互。

例如,HTML5 为表单输入引入了几个新的属性,其中一些我们在第一章中看到了。这些新属性之一是必需属性:

<label for="field-name">Name: </label>
<input type="text" name="field-name" id="field-name" **required**>

如果您想在视觉上突出显示必填字段,使其对用户更明显,您可以使用:required 伪类来定位具有必填属性的表单元素,并使输入的边框具有不同的颜色(参见图 2-5 ):

A314884_3_En_2_Fig5_HTML.jpg

图 2-5。使用:required 伪类为必填字段提供深色边框
input:required {
  outline: 2px solid #000;
}

类似地,我们可以对那些没有所需属性的输入使用

input:optional {
  border-color: #ccc;
}

我们还有伪类来帮助设计有效和无效字段的样式。如果 input 元素需要特定的有效类型,比如电子邮件地址,那么我们可以在 type 属性中使用 HTML5 中定义的一系列不同的输入类型:

<input type="email" />

然后,可以使用以下样式基于输入的当前值的有效性对该元素进行样式化(图 2-6 显示了无效输入的示例):

A314884_3_En_2_Fig6_HTML.jpg

图 2-6。最后一个字段不是一个有效的电子邮件地址,并被赋予了一个明显不同的轮廓:invalid 伪类。在屏幕上,您会看到这个无效的电子邮件地址用红色标出
/* When the field contains a valid email address: */
input[type="email"]:valid {
  border-color: green;
}
/* When the contents are not a valid email address: */
input[type="email"]:invalid {
  border-color: red;
}

还有许多其他的表单伪类,例如:in-range 和:out-of-range,用于数字类型的目标输入;readonly 属性的只读输入;以及 read-write,用于没有 readonly 属性的输入。要了解这些伪类的更多信息,可以在developer.mozilla.org/docs/Web/CSS/Pseudo-classes从 MDN 拿货。

瀑布

即使是中等复杂的样式表,也可能有两个或更多的规则针对同一个元素。CSS 通过一个被称为级联的过程来处理这种冲突,这个概念非常重要,以至于它的名字就叫级联样式表。级联通过给每个规则分配一个重要性来工作。作者样式表是由站点开发人员编写的,被认为是最重要的。用户可以通过浏览器设置应用他们自己的样式,这些样式被认为是次重要的。最后,您的浏览器或用户代理使用的默认样式表是最不重要的,因此您可以随时覆盖它们。为了给用户更多的控制权,他们可以通过将规则指定为!重要的是,即使规则标记为!作者认为重要。那个!重要注释添加到属性声明的末尾,在规则中使用时如下所示:

p {
  font-size: 1.5em !important;
  color: #666 !important;
}

让用户覆盖规则的原因是!重要的是支持特定的可访问性需求,比如允许有某种形式阅读障碍的用户使用中等对比度的用户样式表。

因此,级联按以下重要性顺序工作:

  • 用户样式标记为!重要的

  • 作者样式标记为!重要的

  • 作者样式

  • 用户样式

  • 浏览器/用户代理应用的样式

然后根据选择器的具体程度对规则进行排序。具有更具体选择器的规则会覆盖那些不太具体的规则。如果两个规则同样具体,则最后定义的规则优先。

特征

为了计算规则的具体程度,每种类型的选择器都被赋予一个数值。然后,通过累加每个选择器的值来计算规则的特异性。不幸的是,特异性不是以 10 为基数计算的,而是一个很高的、未指定的基数,这意味着 10 个类选择器(或者 43 个)的特异性不等于或大于 1 个 ID 选择器。这是为了确保高度特定的选择器(如 ID)永远不会被许多不太特定的选择器(如类型选择器)覆盖。但是,如果一个特定规则中的选择器少于 10 个,为了简单起见,可以用 10 进制计算特异性。

选择器的特异性被分解成四个组成级别:a、b、c 和 d。

  • 如果样式是内联样式,则 a 等于 1。

  • b 等于 ID 选择器的总数。

  • c 等于类、伪类和属性选择器的数量。

  • d 等于类型选择器和伪元素选择器的数量。

使用这些规则,可以计算任何 CSS 选择器的特异性。表 2-1 显示了一系列选择器,以及它们相关的特性。

表 2-1。特异性示例
|

选择器

|

特征

|

碱基 10 的特异性

|
| --- | --- | --- |
| Style= " " | 1,0,0,0 | One thousand |
| #包装#内容{} | 0,2,0,0 | Two hundred |
| #内容。发布日期{} | 0,1,1,0 | One hundred and ten |
| div #内容{} | 0,1,0,1 | One hundred and one |
| #内容{} | 0,1,0,0 | One hundred |
| p .评论。发布日期{} | 0,0,2,1 | Twenty-one |
| 页:1 | 0,0,1,1 | Eleven |
| div p {} | 0,0,0,2 | Two |
| p {} | 0,0,0,1 | one |

乍一看,所有这些关于特异性和高但未定义的基数的谈论似乎有点令人困惑,所以这里是你需要知道的。本质上,用样式属性编写的规则总是比任何其他规则更具体。有 ID 的规则比没有 ID 的规则更具体,有类选择器的规则比只有类型选择器的规则更具体。最后,如果两个规则具有相同的特征,由于级联生效,最后定义的规则优先。

注意

通用选择器(*)的特异性始终为 0,不管它在选择器链中出现多少次或出现在什么位置。稍后我们将在“继承”一节中展示一个例子,说明这是如何产生一些意想不到的结果的。

解析级联时的规则顺序

当两个规则具有相同的特性时,最后定义的规则优先,这是一个重要的事实。这意味着您必须考虑将规则放在样式表中的什么位置,以及选择器的顺序。

级联的一个很好的例子是在 link 元素上使用伪类,如前所述。因为每个选择器都有相同的特性,所以声明它们的顺序变得很重要。如果在 a:hover 选择器之后有 a:visited 选择器,一旦访问了链接,悬停样式就不会再出现,因为它被:visited 样式覆盖了。这看起来并不直观,直到你理解了特异性和级联的细节。记住链接伪类的顺序的一个方便的记忆方法是“维德勋爵讨厌毛茸茸的动物。”所以你应该从:link 伪类开始,然后是:visited,:hover,:focus,最后是:active。

管理特异性

理解特殊性对于编写好的 CSS 至关重要,这也是大型网站中最难控制和管理的方面之一。特异性允许您为公共元素设置常规样式,然后为更具体的元素覆盖它们。在下面的例子中,我们为不同类型的介绍性文本设置了一些规则。我们有一个灰色的基本介绍性文本颜色,覆盖了正文的默认黑色。在主页上,介绍文本是浅灰色背景的黑色,里面的链接是绿色的。

body {
  color: black;
}
.intro {
  padding: 1em;
  font-size: 1.2em;
  color: gray;
}
#home .intro {
  color: black;
  background: lightgray;
}
#home .intro a {
  color: green;
}

这就引入了一系列对他们具有广泛针对性的规则。这在较小的网站上不太可能造成任何问题,但是随着代码库的增长和越来越多的样式影响页面,这种规则可能会变得难以管理,因为要对主页介绍文本应用任何进一步的规则,需要一个至少具有一个 ID 和一个类的选择器。

例如,假设我们有另一个组件,它有一个行动号召链接,通过简单地使用背景颜色和一些填充,它看起来有点像一个按钮:

a.call-to-action {
    text-decoration: none;
    background-color: green;
    color: white;
    padding: 0.25em;
}

如果我们想在主页简介中添加一个行动号召链接,会发生什么?好吧,说得委婉一点,这会看起来很糟糕,因为文本是不可见的:主页介绍中的链接选择器会覆盖我们的“按钮”样式,并在绿色背景上创建绿色文本(见图 2-7 )。

A314884_3_En_2_Fig7_HTML.jpg

图 2-7。主页简介中的行动号召组件。介绍的链接样式(#home。intro a)比组件的样式(a.call-to-action)更具体,在绿色背景上给出绿色文本

为了减轻这一点,我们需要以某种方式增加特异性,可能是在行动号召组件上使用另一个更强大的选择器:

a.call-to-action,
#home .intro a.call-to-action {
    text-decoration: none;
    background-color: green;
    color: white;
    padding: 10px;
}

随着样式表的增长,必须像这样改进规则,这可能导致特异性军备竞赛,最终导致代码过于复杂。

更好的方法是简化您的选择器,减少它们的特异性:

body {
    color: black;
}
.intro {
    font-size: 1.2em;
    color: gray;
}

.intro-highlighted {
    color: black;
    background: lightgray;
}
.intro-highlighted a {
    color: green;
}
a.call-to-action {
    text-decoration: none;
    background-color: green;
    color: white;
    padding: 10px;
}

我们通过重写前面的代码做了两件事。首先,我们移除了 ID 选择器,这将这些选择器的特异性降到了最低。我们还删除了对简介的上下文的任何引用。我们没有在主页上谈论介绍,而是将主页介绍(改名为“高亮介绍”)作为原始介绍的一个更具体的版本。您现在可以像这样使用这些介绍类:

<p class="intro">A general intro</p>
<p class="intro intro-highlighted">We might need to use this on the homepage, or in the future, on a <a href="/promo-page" class="call-to-action">promo page</a>.</p>

这种更简单、更有针对性的方法让作者可以更好地控制自己的样式。突出显示的链接不再覆盖行动号召链接的颜色,您还可以在其他页面上重用突出显示的组件,而无需更改 CSS。

特异性和调试

在修复 bug 时,特殊性可能是极其重要的,因为您需要知道哪些规则优先以及为什么优先。例如,假设您有以下一组规则。快速浏览一下,你认为这两个标题会是什么颜色?

#content #main h2 {
    color: gray;
}

div > #main > h2 {
    color: green;
}

#content > [id="main"] .news-story:nth-of-type(1) h2.first {
    color: hotpink;
}
:root [id="content"]:first-child > #main  h2:nth-last-child(3) {
    color: gold;
}

HTML:

<div id="content">
  <main id="main">
    <h2>Strange Times</h2>
    <p>Here you can read bizarre news stories from around the globe.</p>
    <div class="news-story">
      <h2 class="first">Bog Snorkeling Champion Announced Today</h2>
      <p>The 2008 Bog Snorkeling Championship was won by Conor Murphy
    with an impressive time of 1 minute 38 seconds.</p>
    </div>
  </main>
</div>

令人惊讶的是,答案是两个标题都是灰色的。第一个选择器具有最高的特异性,因为它由两个 ID 选择器组成。一些后来的选择器可能看起来更复杂,但是因为它们只包含一个 ID,所以它们总是比不上更具体的选择器。值得注意的是,即使一些选择器包含对 HTML 的 ID 属性的引用,它们仍然只是属性选择器,并且具有较低的特异性。如果您只有 ID 属性来挂钩您的样式,并且不想让您的规则具有太高的特异性,那么这可能是一个有用的工具。

调试特殊性问题可能是一件棘手的事情,但幸运的是,有一些工具可以帮助您。所有现代浏览器都内置了开发人员工具,可以非常清楚地看出特定性是如何应用于特定元素的。在 Chrome 中,开发者工具(DevTools)允许你“检查一个元素”,并会列出所有的 CSS 选择器和与之匹配的规则,包括浏览器默认设置。图 2-8 显示了前一个示例代码中的第二个 h2,证明第二个标题实际上是灰色的,这是因为第一个最具体的选择器。

A314884_3_En_2_Fig8_HTML.jpg

图 2-8。通过谷歌 Chrome 开发者工具,看看实际应用了哪些规则

遗产

人们经常混淆遗传和级联。虽然乍看之下,这两个概念似乎有关联,但实际上是完全不同的。幸运的是,继承是一个更容易理解的概念。某些属性(如颜色或字体大小)由应用这些样式的元素的后代继承。例如,如果您将 body 元素的文本颜色设置为黑色,那么 body 元素的所有后代也将具有黑色文本。字体大小也是如此。

如果您设置了正文的字体大小,您会注意到页面上的任何标题都不会选择这种样式。你可以假设标题不继承文本大小。但实际上是浏览器默认样式表设置了标题大小。任何直接应用于元素的样式都会重写继承的样式。这是因为继承的样式具有空特性。

继承是非常有用的,因为它让您不必为元素的每个后代添加相同的样式。如果您试图设置的属性是一个继承属性,您也可以将其应用于父元素。毕竟,写作的意义是什么:

p, div, h1, h2, h3, ul, ol, dl, li {color: black;}

当你可以写下面的内容时。

body {color: black;}

继承的属性值根本没有特异性,甚至为零。这意味着通过通用选择器设置的属性(其特异性为零)将覆盖继承的属性。这给了我们图 2-9 所示的可能意想不到的情况,其中通用选择器设置的颜色覆盖了从标题继承的颜色:

A314884_3_En_2_Fig9_HTML.jpg

图 2-9。通用选择器的特异性为 0,但它仍然优于继承属性
* {
  color: black;
}
h2 {
  color: red;
}

<h2>The emphasized text will be <em>black</em></h2>

相反,在 body 元素上设置一个基本颜色对于这种情况来说是一个更好的选择,所以这个颜色是继承的,而不是为所有其他元素设置的

正如明智地使用级联可以帮助简化 CSS 一样,良好地使用继承可以帮助减少代码中选择器的数量和复杂性。如果您有许多继承不同样式的元素,那么确定样式的来源可能会变得很混乱。

将样式应用到您的文档

当你在写 CSS 的时候,你需要知道如何将这些样式应用到一个给定的 HTML 文档中。有各种方法可以做到这一点,每种方法都有自己的优点和缺点。

链接和样式元素

通过将样式放置在 style 元素中,可以直接将样式添加到文档的标题:

<style>
 body {
   font-family: Avenir Next, SegoeUI, sans-serif;
   color: grey;
 }
</style>

如果您有少量的样式想要立即应用到页面,并且您不希望浏览器下载单独的文件的开销,有时这很有用。但是,您通常希望应用外部样式表中的样式,以便在其他页面中重用。有两种方法可以将外部样式表附加到网页。最常见的方法是使用 link 元素:

<link href="/c/base.css" rel="stylesheet" />

这将指导浏览器下载 base.css 文件,并将其包含的任何样式应用于页面。您可以将它添加到任意数量的 HTML 页面中,因此这是跨多个页面甚至跨多个站点重用一组样式的好方法。

您还可以使用@import 指令来加载外部 CSS 文件:

<style>
  @import url("/c/modules.css");
</style>

@import 指令可以用在 HTML 文档头部的样式块中,也可以用在外部样式表本身中。后一种用法意味着在页面上包含一个外部 CSS 文件可能会导致浏览器加载后续的 CSS 文件。

从表面上看,使用 link 元素或@import 指令可以获得几乎相同的结果,但是有一些重要的考虑因素使 link 优于@import,我们将在下一节的性能中讨论这些因素。

当向页面添加样式时,不要忘记顺序对层叠很重要:当两个或多个具有相同特征的规则在设置元素的属性上竞争时,最后声明的规则获胜。

当您向 HTML 添加几个引用样式表的链接元素或添加样式元素时,它们在声明顺序方面的位置由它们在 HTML 源代码中的顺序决定。考虑来自 HTML 元素头部的以下片段,其中所有引用的样式表和样式元素为 h1 元素声明了不同的颜色,具有相同的特异性:

<link rel="stylesheet" href="css/sheet1.css">
<style>
    @import 'css/sheet3.css';

    h1 {
        color: fuchsia;
   }
</style>
<link rel="stylesheet" href="css/sheet2.css">

在这种情况下,声明的顺序如下:

  1. 来自 sheet1.css 的声明

  2. sheet3.css 中的声明,在 style 元素中导入

  3. style 元素内部的声明

  4. 来自 sheet2.css 的声明

胜出的声明将是 sheet2.css 中的声明,因为它是列表中的最后一个。

表演

选择哪种方式将 CSS 加载到页面中是控制浏览器显示页面速度的最大选择(假设 HTML 页面本身加载速度很快!).

web 性能的一个重要指标是内容开始显示在屏幕上所需的时间。这有时被称为“渲染时间”或“玻璃时间”

现代浏览器在开始在屏幕上呈现内容之前至少需要两样东西:HTML 和 CSS。这意味着让浏览器尽快下载 HTML 和所有 CSS 是非常重要的。

不要试图通过将 CSS 放在主体中或靠近页脚来延迟它的加载。当浏览器预先拥有了布局页面所需的所有 CSS 信息时,它们的反应最好。这样,他们就可以开始理解页面的外观,并一次性将页面呈现到屏幕上,而不是不断地在加载新样式时进行调整。

减少 HTTP 请求

当链接到外部样式表时,将文件数量保持在最小是很重要的。每个额外的文件都会导致一个额外的 HTTP 请求,从服务器请求文件的行为会大大增加浏览器下载和应用所有样式的时间开销。额外的 HTTP 请求意味着从浏览器发送额外的数据,比如 cookies 和请求头。然后,服务器必须为每个请求发回响应头。两个文件总是比一个具有相同 CSS 内容的文件在浏览器和服务器之间发送更多的数据。

总是试着把你在一个实时网站上发布的 CSS 文件的数量保持在一到两个。只使用一个 link 元素加载 CSS 文件,然后在其中使用@import,这并不意味着浏览器只使用一个请求:相反,这意味着它需要一个对链接文件的请求,然后是获取所有导入文件的后续请求。所以避免使用@import(至少在直播网站上)。

压缩和缓存内容

同样非常重要的是,你在现场使用的任何文件都是用 GZIP 压缩的。CSS 压缩非常有效,因为它有许多重复的模式,如属性名和值。在许多情况下,可以将 CSS 文件的大小减少 70–80 %,这可以显著减少用户的带宽和加载时间。大多数 web 服务器都有一种机制,可以将内容自动压缩到支持它的浏览器中。

同样,指导 web 服务器为 CSS 文件设置适当的缓存时间也很重要。理想情况下,你希望用户的浏览器下载一次 CSS 文件,并且在它改变之前不再下载。这方面的策略包括设置各种 HTTP 头,指示浏览器将文件缓存很长时间,然后在有任何变化时通过更新文件名来“破坏缓存”。

这是如何工作的细节已经超出了本书的范围。您可能需要您的主机提供商或您公司的系统管理员的支持来帮助配置服务器,但是压缩和正确缓存内容是您可以做的提高网站性能的两件最重要的事情。

避免呈现阻塞 JavaScript

当您在 HTML 文档的元素中添加一个

这导致了在 HTML 页面的底部,紧接在结束的

  <!-- Load scripts last -->
  <script src="/scripts/core.js"></script>
</body>

一种更现代的方法是在中使用

<head>  
  <!-- will load asynchronously, but execute immediately when downloaded -->
  <script src="/scripts/core.js" async></script>
  <!-- will load asynchronously, but execute after HTML is done-->
  <script src="/scripts/deferred.js" defer></script>
</head>

通过使用这两种方法加载 JavaScript,您可以确保浏览器能够解析和显示 HTML 内容和 CSS,而不会因 JavaScript 文件请求而延迟。选择哪种方法主要是浏览器支持的问题:async 和 defer 属性是 HTML5 标准的一部分,因此是较新的。最明显的是,10 版之前的 Internet Explorer 缺少或部分支持。

摘要

在这一章中,你已经重新熟悉了常见的 CSS 选择器,并且了解了一些你以前可能没有遇到过的强大的新选择器。现在,您对特异性如何工作以及如何使用级联来构建 CSS 规则并帮助它们达到目标有了更好的理解。我们已经初步了解了如何避免陷入特异性军备竞赛,以及如何利用你对特异性、级联和遗传的理解来获得优势。您还了解了如何将 CSS 应用于文档,以及这会影响网页性能的一些方式。

在下一章,你将学习 CSS 盒子模型,边距如何以及为什么折叠,以及浮动和定位是如何工作的。

三、可视化格式模型概述

一些需要掌握的最重要的 CSS 概念是浮动、定位和盒子模型。这些概念控制元素在页面上的排列和显示方式,并构成许多布局技术的基础。最近,专门为控制布局而设计的新标准已经被引入,我们将在接下来的章节中单独讨论这些标准。但是,您在本章中学习的概念将帮助您完全掌握盒模型的复杂性、绝对和相对定位之间的差异以及浮动和清除实际上是如何工作的。一旦你牢牢掌握了这些基础知识,使用 CSS 开发网站就变得容易多了。

在本章中,您将了解:

  • 盒子模型的复杂性

  • 利润崩溃的方式和原因

  • 不同的定位属性和值

  • 浮动和清算是如何工作的

  • 什么是格式化上下文

盒子模型概述

盒子模型是 CSS 的基石之一,它决定了元素是如何显示的,并且在一定程度上决定了元素之间的交互方式。页面上的每个元素都被认为是一个由元素内容、填充、边框和边距组成的矩形框(见图 3-1 )。

A314884_3_En_3_Fig1_HTML.gif

图 3-1。盒子模型的插图

内容区域周围应用了填充。如果向元素添加背景,它将应用于由内容和填充形成的区域。因此,填充通常用于在内容周围创建一个装订线,使其看起来不会与背景边缘齐平。添加边框会在填充区域的外部应用一条线。这些线条有多种样式,如实线、虚线或虚线。边框之外是边距。边距是框的可见部分之外的透明空间,允许您控制页面中元素之间的距离。

另一个可以应用于框但不影响其布局的属性是 outline 属性,它在元素的边框周围绘制一条线。它不影响框的宽度或高度,在调试复杂布局或演示布局效果时会很有用。

填充、边框和边距是可选的,默认值为零。然而,用户代理样式表将为许多元素提供边距和填充。例如,默认情况下,标题总是有一些边距,尽管这些边距因浏览器而异。您当然可以在自己的样式表中覆盖这些浏览器样式,或者在特定的元素上,或者通过使用一个重置的样式表,如第二章所讨论的。

盒子尺寸

默认情况下,框的宽度和高度属性指的是内容框的宽度和高度——由元素的呈现内容的边缘形成的矩形。添加边框和填充不会影响内容框的大小,但会增加元素框的整体大小。如果您希望一个边框为 5 像素、两边填充为 5 像素的框的总宽度为 100 像素,则需要将内容的宽度设置为 80 像素,如下所示。如果盒子周围也有 10 像素的边距,它将占据总共 120 像素宽的空间(见图 3-2 )。

A314884_3_En_3_Fig2_HTML.gif

图 3-2。默认的盒子模型。width 属性适用于内容区域
.mybox {
**width: 80px;** 
  padding: 5px;
  border: 5px solid;
  margin: 10px;
}

您可以使用 box-sizing 属性更改计算框宽度的方式。框大小的默认值是 content-box,并应用到目前为止描述的行为。然而,让宽度和高度属性不仅仅影响内容框是非常有用的,尤其是在响应式布局中。

注意

在某些浏览器中,某些表单控件元素(如 input)可能有不同的框大小默认值。这是由于与传统行为的兼容性,在传统行为中,不可能更改填充或边框等内容。

如果将 box-sizing 属性的值设置为 border-box,如下所示,那么 width 和 height 属性将包括框的填充和边框所需的空间(参见图 3-3 )。边距仍然影响元素在页面上占据的总大小,但仍然不包括在宽度中。使用这些规则,您可以实现如图 3-2 所示的整体布局:

A314884_3_En_3_Fig3_HTML.gif

图 3-3。当 box-sizing 属性设置为 border-box 时的框模型。width 属性现在对应于元素可见部分的整个宽度
.mybox {
  box-sizing: border-box;
**width: 100px;** 
  padding: 5px;
  border: 5px;
  margin: 10px;
}

那么这为什么有用呢?从很多方面来说,这是一种更直观的处理盒子的方式,事实上,在 IE6 之前的旧版本的 Internet Explorer 中,盒子模型就是这样工作的。它是“直观的”,因为当你仔细想想,这就是盒子在现实世界中的工作方式。

想象一个 CSS 盒子就像一个包装箱。盒子的壁作为一个边界,并提供视觉定义,而填充物在里面保护内容。如果框需要特定的宽度,添加更多的填充或增加墙的厚度会侵蚀可用的内容空间。现在,如果您需要在堆叠盒子之前将它们隔开,每个盒子之间的空间(实际上是边距)对盒子本身的宽度没有影响,或者实际上对可用内容空间的大小没有影响。这感觉像是一个更合理的解决方案,所以很遗憾浏览器开发者,包括微软在 IE 后续版本中,决定走不同的方向。

幸运的是,box-sizing 属性允许我们覆盖默认行为并简化 CSS 布局中的一些常见模式。举以下例子:

<div class="group">
  <article class="block">
  </article>
</div>

如果我们想确保任何。块在我们的。组始终是其包含列宽度的三分之一,我们可以应用以下规则:

.group .block {
  width: 33.3333%;
}

这将工作得很好,直到我们开始添加水槽使用 adding 对我们的边。块,使其内容远离可见边缘。现在我们的。块元素是父元素的三分之一。group 元素的宽度加上填充,这可能会破坏我们想要的布局。图 3-4 说明了不同之处。

A314884_3_En_3_Fig4_HTML.gif

图 3-4。假设我们想要。块元素是。组元素,当我们向它添加填充时,可能会得到意外的结果

例如,我们可以通过添加额外的内部元素来解决这个问题,或者我们可以选择不同的框大小属性来改变宽度的计算方式(参见图 3-5 ):

A314884_3_En_3_Fig5_HTML.gif

图 3-5。添加框尺寸:即使添加了填充,border-box 也将我们的框保持在 33.3333%的宽度
.group .block {
  width: 33.3333%;
**box-sizing: border-box;** 
**padding: 20px;** 
}

现在我们的。block 元素的宽度正好是父元素宽度的三分之一,正如我们声明的那样,无论我们向它添加多少填充或边框。

填充、边框和边距可以应用于元素的所有边或单个边。边距也可以为负值。这可以以多种有趣的方式用于将元素拉入或拉出页面的位置。我们将在后面的章节中探讨这些技术。

您可以使用 CSS 规范中的任何长度度量(如像素、ems 和百分比)来为元素添加填充和边距。使用百分比值有一些值得一提的特点。假设加价和上一个例子中的一样,那么这个例子中的 5%实际上代表了什么?

.block {
  margin-left: 5%;
}

答案是,在这种情况下,它是父项宽度的 5%。组元素。如果我们假设。组元素的宽度为 100 像素,它的左边有 5 像素的边距。

当使用这些尺寸来测量元素顶部和底部的填充或边距时,猜测百分比是从父元素的高度得出的是情有可原的。这乍一看似乎很合理——然而,由于高度通常没有声明,并且可以随着内容的高度而变化,CSS 规范规定填充和边距的顶部和底部值也从包含块的宽度中取值。在这种情况下,包含块是父块,但是这是可以改变的——我们将在本章的前面一点说明这意味着什么。

最小值和最大值

有时,对元素应用最小宽度和最大宽度属性可能很有用。这样做在实践响应式设计时特别有用,因为它允许块级框默认情况下自动填充其父元素的宽度,但不会收缩到小于 min-width 中指定的值,也不会增长到大于 max-width 中指定的值。(我们将在第八章回到响应式网页设计,以及它与 CSS 的关系。)

类似地,也存在最小高度和最大高度属性,尽管在 CSS 中应用任何高度值时应该小心,因为元素从它们包含的内容中隐式地导出它们的高度几乎总是更好。否则,如果内容量增加或文本大小改变,内容可能会流出固定高度的框。如果你因为某种原因需要设置一个默认的高度,使用最小高度通常会更好,因为它可以让你的框随着内容扩展。

可视化格式模型

了解了盒子模型之后,我们可以开始探索一些可视化的格式和定位模型。

人们通常将 p、h1、article 等元素称为块级元素。这意味着它们是可视地显示为内容块或块框的元素。相反,诸如 strong、span 和 time 之类的元素被描述为内联级元素,因为它们的内容在行内显示为内联框

使用 display 属性可以更改生成的框的类型。这意味着您可以通过将 span 等内联级元素的 display 属性设置为 block,使其行为类似于块级元素。通过将元素的 display 属性设置为 none,也可以使元素根本不生成任何框。该框以及所有内容不再显示,也不占用文档空间。

CSS 中有许多不同的定位模型,包括浮动、绝对定位和相对定位。除非指定,否则所有盒开始时都位于正常流中,并具有默认的静态属性。顾名思义,元素框在正常流中的位置将由元素在 HTML 中的位置决定。

块级框将一个接一个地垂直出现;框之间的垂直距离由框的垂直边距计算。

内联框水平排列成一行,跟随文本流向,当文本换行时换行。可以使用水平填充、边框和边距来调整它们的水平间距(参见图 3-6 )。但是,垂直填充、边框和边距对内嵌框的高度没有影响。类似地,在内联框上设置显式的高度或宽度也不会有任何效果。

A314884_3_En_3_Fig6_HTML.gif

图 3-6。段落块框内的内嵌组件

由一行文本形成的水平框被称为行框,一个行框对于它可能包含的所有行内框来说总是足够高。改变行框尺寸的唯一方法是更改行高,或者设置行框内任何行框的水平边框、填充或边距。图 3-6 显示了具有两行文本的段落的块框,其中一个单词在内联显示的< strong >元素内。

还可以将元素的 display 属性设置为 inline-block。顾名思义,该声明使元素水平排列,就像它是一个行内框一样。但是,盒子内部的行为就好像盒子是块级的,包括能够显式设置宽度、高度、垂直边距和填充。

当您使用表格标记(table、tr、th 和 td 元素等)时,表格本身表现为一个块,但是表格的内容将根据生成的行和列进行排列。还可以设置其他元素的 display 属性,以便它们采用表格的布局行为。通过以正确的方式应用值 table、table-row 和 table-cell,您可以实现 HTML 表格的一些属性,而无需在标记中使用表格。

像 Flexible Box Layout(也称为 flexbox )和 Grid Layout(我们将在后面的章节中介绍)这样的模块进一步扩展了显示属性。通常,这些新的布局模式创建的框在其外部环境中充当块,但创建了如何处理框内内容的新规则。

外部和内部显示模式之间的这种划分(在内嵌块、表格和新值(如 flex 或 grid)中都可以看到)现在正在显示级别 3 模块中进行标准化。在那里,显示模式的现有属性和关键字被扩展,以允许更细粒度的控制。重要的一点是,行内级别的框和块级别的框仍然是 HTML 元素默认行为的基础,但实际情况稍有不同。

匿名信箱

与 HTML 元素可以嵌套的方式一样,框可以包含其他框。大多数盒子都是由明确定义的元素组成的。但是,有一种情况,即使没有显式定义块级元素,也会创建块级元素—当您在块级元素(如 section)的开头添加一些文本时,如下所示。即使您没有将“some text”位定义为块级元素,它也会被视为块级元素。

<section>
  some text
  <p>Some more text</p>
</section>

在这种情况下,盒子被描述为一个匿名 盒子,因为它不与一个特别定义的元素相关联。

块级元素中的文本行框也会发生类似的情况。假设您有一个包含三行文本的段落。每行文本形成一个匿名行框。您不能直接设置匿名块框或行框的样式,除非通过使用:first-line 伪元素,该元素显然用途有限,仅允许您更改与版式和颜色相关的某些属性。然而,理解你在屏幕上看到的一切都创造了某种形式的盒子是很有用的。

边距折叠

对于普通的块盒,有一种行为被称为边距折叠。边距折叠是一个相对简单的概念。然而,在实践中,当你设计一个网页时,它会引起很多混乱。简单来说,当两个或两个以上的垂直边距相遇时,它们将折叠形成一个单独的边距。此边距的高度将等于两个折叠边距中较大的一个的高度。

当两个元素在另一个之上时,第一个元素的底边将与第二个元素的上边一起折叠(见图 3-7 )。

A314884_3_En_3_Fig7_HTML.gif

图 3-7。元素的上边距与前一个元素的下边距一起折叠的示例

当一个元素包含在另一个元素中时,假设没有填充或边界分隔边距,它们的顶部和/或底部边距也将折叠在一起(参见图 3-8 )。

A314884_3_En_3_Fig8_HTML.gif

图 3-8。元素的上边距与其父元素的上边距一起折叠的示例

乍一看,这似乎很奇怪,但利润率甚至会自行下降。假设您有一个空元素,它有边距,但没有边框或填充。在这种情况下,上边距接触下边距,它们一起折叠(见图 3-9 )。

A314884_3_En_3_Fig9_HTML.gif

图 3-9。元素的上边距与其下边距一起折叠的示例

如果此页边空白接触到另一个元素的页边空白,它将自行折叠(见图 3-10 )。

A314884_3_En_3_Fig10_HTML.gif

图 3-10。一个空元素的折叠边距与另一个空元素的边距一起折叠的示例

这就是为什么一系列空的段落元素占用很少的空间,因为它们所有的边距都折叠在一起形成一个小的边距。

利润率下降乍一看似乎很奇怪,但实际上很有意义。取一个由几个段落组成的典型文本页面(见图 3-11 )。第一段上方的空间将等于段落的上边距。如果没有边距折叠,所有后续段落之间的间距将是它们两个相邻的上边距和下边距的总和。这意味着段落之间的间距将是页面顶部间距的两倍。随着页边距的折叠,每个段落之间的顶部和底部页边距也会折叠,使间距与其他地方的间距相同。

A314884_3_En_3_Fig11_HTML.gif

图 3-11。边距折叠以保持元素之间的间距一致

在文档的正常流动中,边距折叠仅发生在块框的垂直边距上。内联框、浮动框或绝对定位框之间的边距永远不会缩小。

包含块

赋予元素包含块的的概念很重要,因为它决定了如何解释各种属性,就像我们前面看到的用百分比设置填充和边距的情况。

元素的包含块取决于元素的定位方式。如果元素有一个静态位置(与没有声明 position 属性相同)或相对位置,则它的包含块被计算到其最近的父元素的边缘,该父元素的 display 属性被设置为导致类似块的上下文的内容,包括 block、inline-block、table-cell、list-item 等。

默认情况下,当以百分比设置时,宽度、高度、边距和填充的声明是根据父元素的尺寸计算的。当您将元素更改为具有绝对或固定的定位模型时,这种情况会发生变化。接下来,我们将介绍不同的模型,以及它们如何与包含块的概念进行交互。

相对定位

当您将元素的 position 属性设置为 relative 时,它最初将完全停留在原来的位置。然后,通过使用 top、right、bottom 和 left 属性设置垂直或水平位置,可以相对于元素的起点移动元素。如果您将顶部位置设置为 20 像素,该框将出现在其原始位置顶部下方 20 像素处。将左侧位置设置为 20 像素,如下所示,将在元素左侧创建一个 20 像素的空间,将元素向右移动(参见图 3-12 )。

A314884_3_En_3_Fig12_HTML.gif

图 3-12。相对定位元素
.mybox {
  position: relative;
  left: 20px;
  top: 20px;
}

使用相对定位,无论是否偏移,元素都会继续占据页面流中的原始空间。因此,偏移元素会导致它与其他框重叠。

绝对定位

相对定位实际上被认为是正常流程定位模型的一部分,因为元素是相对于其在正常流程中的位置的。相比之下,绝对定位将元素从文档流中移除,因此不占用空间。文档正常流程中的其他元素将表现为绝对定位的元素从未出现过(见图 3-13 )。

A314884_3_En_3_Fig13_HTML.gif

图 3-13。绝对定位元素

绝对定位元素的包含块是其最近的定位祖先,这意味着 position 属性设置为 static 以外的任何值的任何祖先元素。如果元素没有定位的祖先,它将相对于文档的根元素 html 元素定位。这也被称为初始包含块。

与相对定位的框一样,绝对定位的框可以从其包含块的顶部、底部、左侧或右侧偏移。这给了你很大的灵活性。您可以将一个元素放在页面上的任何位置。

因为绝对定位的框被从文档流中取出,所以它们可能会与页面上的其他元素重叠。您可以通过设置一个名为 z-index 的数值属性来控制这些框的堆叠顺序。z 索引越高,盒子在堆栈中的位置就越高。当用 z-index 堆叠物品时,有各种各样的复杂情况需要考虑:我们将在第六章中对它们进行分类。

尽管绝对定位是布局页面元素的有用工具,但它很少用于创建高级布局。绝对定位的框不参与文档的流动,这使得创建适应和响应不同宽度和不同长度内容的视口的布局变得相当麻烦。网络的本质不允许我们精确地测量元素在页面上的位置。随着我们越来越精通 CSS 中的其他布局技术,绝对定位在页面布局中的使用已经变得相当少见了。

固定定位

固定定位是绝对定位的一个子类。不同之处在于固定元素的包含块是视口。这使您可以创建始终停留在窗口中相同位置的浮动元素。许多网站使用这种技术,通过将它们固定在侧栏或顶栏中的位置,使它们的导航部分始终可见(图 3-14 )。这有助于提高可用性,因为用户不必看很远就能回到界面的重要部分。

A314884_3_En_3_Fig14_HTML.jpg

图 3-14。当你向下滚动时,谷歌开发者文档的顶部栏和侧边导航保持不变

浮动的

另一个重要的视觉模型是浮动模型。浮动框可以向左或向右移动,直到其外边缘接触到其包含块或另一个浮动框的边缘。因为浮动框不在正常的文档流中,所以正常文档流中的块框几乎就像浮动框不存在一样。我们一会儿会解释“几乎”。

如图 3-15 所示,当您向右浮动框 1 时,它被从文档流中取出并向右移动,直到其右边缘接触到包含块的右边缘。它的宽度也会收缩到包含其内容所需的最小宽度,除非您通过设置特定的宽度或最小宽度/最大宽度明确地告诉它。

A314884_3_En_3_Fig15_HTML.gif

图 3-15。元素右浮动的示例

在图 3-16 中,当您向左浮动框 1 时,它被从文档流中取出并向左移动,直到其左边缘接触到包含块的左边缘。因为它不再在流中,所以它不占用空间,实际上位于盒子 2 的顶部,从视图中隐藏了它。如果将所有三个框都向左浮动,框 1 将向左移动,直到接触到其包含的块,另外两个框将向左移动,直到接触到前一个浮动的框。

A314884_3_En_3_Fig16_HTML.gif

图 3-16。向左浮动的元素示例

如果包含元素对于所有浮动元素来说太窄,以至于不能水平放置,那么剩余的浮动将下降,直到有足够的空间(参见图 3-17 )。如果被浮动的元素有不同的高度,当它们落下时,浮动可能会被其他浮动“卡住”。

A314884_3_En_3_Fig17_HTML.gif

图 3-17。如果没有足够的可用水平空间,浮动的元素将向下移动,直到有足够的水平空间

线盒和清除

在上一节中,您了解了浮动一个元素会将它从文档流中移除,不再对非浮动项目产生影响。实际上,这并不完全正确。如果文档流中的一个浮动元素后面跟着一个元素,则该元素的框将表现为好像该浮动元素不存在一样。但是,框中的文本内容保留了浮动元素的一些内存,并移开以腾出空间。在技术术语中,浮动元素旁边的行框被缩短,以便为浮动元素腾出空间,从而围绕浮动框流动。事实上,创建浮动是为了让文本围绕图像流动(见图 3-18 )。

A314884_3_En_3_Fig18_HTML.gif

图 3-18。行框在靠近浮动时会变短

要阻止线条框环绕浮动框的外部,需要对包含这些线条框的元素应用 clear 属性。clear 属性可以是 left、right、both 或 none,它指示框的哪一侧不应该靠近浮动框。许多人认为 clear 属性只是删除了一些否定先前浮动的标志。然而,现实要有趣得多。当你清除一个元素时,浏览器会在元素的顶部添加足够的边距,将元素的上边框边缘垂直向下推,越过浮动(见图 3-19 )。当您尝试将自己的边距应用于“已清除”的元素时,这有时会令人困惑,因为该值只有在达到并超过浏览器自动添加的值时才会生效。

A314884_3_En_3_Fig19_HTML.gif

图 3-19。清除元素的上边距,为前面的浮动创建足够的垂直空间

正如您所看到的,浮动元素被从文档流中取出,除了将行框缩短到足以为浮动框腾出空间之外,对周围的元素没有任何影响。但是,清除一个元素实质上是为所有前面的浮动元素清除一个垂直空间。这在使用浮动作为布局工具时很有用,因为它允许周围的元素为浮动的元素腾出空间。

让我们看看如何使用浮动创建一个简单的组件布局。假设您有一张图片,您希望它浮动在标题的左侧,一小块文本浮动在标题的右侧,这通常被称为“媒体对象”,因为常见的模式是有一段媒体(如图形、图像或视频)和一段伴随的文本。您希望此图片和文本包含在另一个具有背景颜色和边框的元素中。你或许可以试试这样的东西:

.media-block {
  background-color: gray;
  border: solid 1px black;
}
.media-fig {
  float: left;
  width: 30%; /* leaves 70% for the text */
}
.media-body {
  float: right;
  width: 65%; /* a bit of "air" left on the side */
}
<div class="media-block">
  <img class="media-fig" src="/img/pic.jpg" alt="The pic" />
  <div class="media-body">
    <h3>Title of this</h3>
    <p>Brief description of this</p>
  </div>
</div>

但是,因为浮动元素是从文档流中取出的,所以具有。media-block 不占用空间,它只有浮动的内容,因此在文档流中没有给它一个高度。如何可视化地让包装器包含浮动的元素?您需要在该元素内部的某个地方应用 clear,正如我们前面看到的,这在被清除的元素上创建了足够的垂直边距,以便为浮动的元素留出空间(参见图 3-20 )。不幸的是,由于示例中没有要清除的现有元素,您可以在结束 div 标记之前添加一个空元素,并清除:

A314884_3_En_3_Fig20_HTML.gif

图 3-20。添加一个清除 div 会强制容器包含浮动
/* Added CSS: */
.clear {
    clear: both;
}
<div class="media-block">
      <img class="media-fig" src="/img/pic.jpg " alt="The pic" />
      <div class="media-body">
        <h3>Title of this</h3>
        <p>Brief description of this</p>
      </div>
      <div class="clear"></div><!-- added extra empty div -->
</div>

这得到了我们想要的结果,但代价是在我们的标记中添加了额外的代码。通常会有一个现有的元素,您可以对其应用 clear,但有时您可能不得不硬着头皮为布局的目的添加无意义的标记。然而,在这种情况下,我们可以做得更好。

我们可以这样做的方法是使用:after 伪元素模拟额外的 clearing 元素,如下所示。通过将此应用到浮动元素的包含元素,将创建一个额外的框,您可以将清除规则应用到该框。

.media-block:after {
  content: " ";
  display: block;
  clear: both;
}

这种方法和它的一些变体在尼古拉斯·加拉格尔的一小段代码中得到了最好的展示,这段代码被称为 micro clearfix,在nicolasgallagher.com/micro-clearfix-hack/展示。

格式化上下文

CSS 有许多不同的规则,适用于元素在页面上水平或垂直流动时如何相互交互。这些规则集之一的技术名称是格式化上下文。我们已经看到了内联格式上下文的一些规则——例如,垂直边距对内联框没有影响。类似地,某些规则适用于块盒如何堆叠,就像我们在折叠边距一节中看到的那样。

其他规则定义了页面必须如何自动包含任何在末尾突出的浮动(否则浮动元素内的内容可能会在可滚动区域之外结束),并且默认情况下所有块框的边缘都与包含块的左边缘对齐(或右边缘,取决于文本方向)。这组规则被称为块格式化上下文

一些规则允许元素建立自己的内部块格式上下文。其中包括以下内容:

  • 元素的 display 属性被设置为一个值,该值为元素的内容创建一个类似块的上下文,如 inline-block 或 table-cell。

  • float 属性不是 none 的元素。

  • 绝对定位的元素。

  • 将 overflow 属性设置为 visible 以外的任何内容的元素。

正如我们之前讨论的,块的边缘接触其包含块的边缘的规则甚至适用于前面有浮动的内容。浮动从页面流中移除,并通过触发跟随它的元素中的行框来缩短,从而产生为自己腾出空间的视觉效果。元素本身仍然在浮动之下伸展到它需要的程度。

当一个元素有触发一个新的块格式上下文的规则,并且在一个 float 旁边时,它将忽略这条规则,即它的边缘必须靠着它包含的块的边。相反,它会缩小以适应——不仅仅是线路盒,而是整个系统。这可用于重新创建。上一节中的媒体块示例,但规则更简单:

.media-block {
  background-color: gray;
  border: solid 1px black;
}
.media-fig {
  float: left
  margin-right: 5%;
}
.media-block, .media-body {
  overflow: auto;
}
<div class="media-block">
  <img class="media-fig" src="/img/pic.jpg" alt="The pic" />
  <div class="media-body">
    <h3>Title of this</h3>
    <p>Brief description of this</p>
  </div>
</div>

在设置溢出:自动;在两个容器上。媒体块和我们的。媒体主体元素,我们为它们建立了新的块格式化上下文。这有几个影响(见图 3-21 中的比较):

  • 它包含在中的浮动图像。无需清除规则的媒体块组件,因为块格式化上下文也自动包含浮动。

  • 作为一个额外的好处,它允许我们抛弃宽度的规则以及。media-body 元素,如果我们需要的话——它将简单地调整到浮动旁边的剩余空间,并且仍然在图像旁边保持一个漂亮的直边。如果没有新的格式上下文,并且文本有点长,则下面的任何行框都会浮动。media-fig 将在它的下面伸展,最后在图像的左边齐平。

A314884_3_En_3_Fig21_HTML.gif

图 3-21。要是。media-fig 元素是浮动的,并且文本足够长,一些行将在浮动下换行并结束于左侧。创建新的块格式化上下文强制。要收缩的媒体主体

创建尽可能具有可预测和简单行为的布局降低了代码的复杂性,增加了布局的健壮性,因此知道何时应用这样的技巧来避免浮动和清除元素之间的复杂交互是一件好事。幸运的是,更好的布局技术正在迅速普及。

内在和外在规模

CSS 模块“内部和外部大小调整级别 3”定义了一个关键字列表,可以应用于(最小和最大)宽度和高度属性,而不是以像素或百分比等为单位的长度。这些表示从周围上下文(外部)或元素内容(内部)派生的显式长度,但让浏览器计算出最终值,而不是将属性设置为 auto 或使用 floats 或 block 格式上下文创建收缩到适合的场景而根本不设置宽度的隐式值。

我们不会在这里深入讨论不同关键字的细节,但是有趣的是,我们在其中发现了包含浮动。这个关键字应该和你预期的差不多;例如,您可以使用以下代码使元素包含任何浮点:

.myThing {
  min-height: contain-floats;
}

到目前为止,对这个模块中各种关键字的支持还很弱——最值得注意的是,在撰写本文时,没有任何版本的 IE 支持其中的任何一个。尽管如此,它在未来创建健壮的规模估算时可能非常有用,而不需要求助于更复杂的技术。

其他 CSS 布局模块

我们已经讨论了 CSS 可视化格式模型的基础和最常见的部分,但是还有一些其他的地方需要简单的提一下。

您可能会认为健壮灵活的布局模型是 CSS 这样的可视化表示工具的关键部分。你是对的。但不幸的是,我们花了很长时间才得到一个。从历史上看,我们使用语言中任何可用的特性来实现我们的目标,即使它们远不是工作的理想工具。最初这包括采用数据表,因为它们有用的布局特征——尽管它们有臃肿的标记和不恰当的语义。最近,我们一直在强制使用浮动和绝对定位来实现大多数复杂的页面布局,但同样,这些功能都不是为布局网页而设计的。两者都有严重的限制,其中大部分我们只是训练自己去忍受。

幸运的是,最近的 CSS 模块引入了新的内容模型,专门用于创建灵活和健壮的页面布局。在撰写本文时,这些模块都处于不同的就绪状态,有些还没有可互操作的跨浏览器实现。我们将在接下来的章节中详细讨论其中的一些,以及它们带来的一些更有用的技术,但这只是对它们所提供的功能的一个快速总结。

灵活的盒子布局

我们之前提到的 Flexible Box 布局模块,或者说 flexbox ,是 CSS 3 中引入的布局模型。Flexbox 允许您水平或垂直布局一个盒子的子项,并确定这些子项的大小、间距和分布。它还允许您更改元素在页面上呈现的顺序,而不管它们在 HTML 源代码中的位置。Flexbox 作为正常流模型(内嵌和块)的升级,在内容本身及其如何影响大小方面提供了精确控制和灵活性的平衡。

Flexbox 得到了广泛的应用,但是在旧版本的 Internet Explorer 中,这种支持明显缺失或不完整。好消息是,它的构造方式可以让你把它和其他方法结合起来,比如 floats,来创建非常健壮的布局。我们将在第六章中详细了解 flexbox。

网格布局

Grid layout 是第一个成熟的 CSS 高级布局工具,其目标是取代复杂的页面布局,这些页面布局过去是用浮动和定位元素创建的。它提供了布局与源代码顺序的完全分离,并从内容结构和单个模块的表示中抽象出网格系统的概念。其中 flexbox 是“微观”,网格布局是“宏观”,所以这两种方法很好的互补。

网格布局还没有得到广泛的支持,但是在本书编写的时候,浏览器制造商正在竞相实现它。我们将在第七章中了解这个强大的新模块。

多栏布局

多栏布局模块是允许内容流入不同栏的一种相当简单的方式;例如,创建一个类似报纸的布局,其中段落的文本排列成多个垂直的列。该模块允许您选择一组列数或一个首选宽度,并根据可用空间选择列数。您还可以控制列之间的间距,并对这些间距应用类似边框的视觉效果。由于多栏布局更多的是一种排版工具,而不是一般布局,我们将在第四章中讨论它。

地区

CSS 区域允许您指定内容如何在页面上的元素之间流动。一个元素充当内容的来源,但是该内容可以流入页面上其他位置的其他占位符元素,而不是正常的块流。这意味着布局不再受 HTML 源代码顺序的影响,布局表示也不再受内容结构的影响。

CSS 区域允许以前单独使用 CSS 不可能实现的布局,并可能在未来推动某些基于打印的布局模式的采用。然而,很少有浏览器制造商对 CSS 区域表现出任何喜爱,而且这种类型的布局在一段时间内可能还不够成熟。出于这个原因,我们在本书中将不再详细讨论区域。

摘要

在本章中,您学习了盒子模型以及填充、边距、宽度和高度如何影响盒子的尺寸。您还了解了边距折叠的概念以及它如何影响布局。向您介绍了 CSS 的各种格式模型,如正常流、绝对定位和浮动。您了解了内联框和块框之间的区别,如何在相对定位的祖先中绝对定位元素,以及清除是如何工作的。

既然你已经掌握了这些基本原则,让我们开始好好利用它们吧。在本书接下来的章节中,你将会了解到一些核心的 CSS 概念,你将会看到它们是如何被应用到各种实用的技术中的。所以,启动你最喜欢的编辑器,让我们开始编码吧。

四、网络排版

自从印刷机发明以来,排版一直是平面设计的基本部分,所以你会期望它在网页设计领域发挥核心作用。有些人甚至说网页设计 95%是排版。因此,令人惊讶的是,浏览器直到最近才允许我们完全接受网页上的排版和排版。这为我们提供了从数百年的印刷历史中学习的可能性,并创造出样式丰富的内容,阅读起来令人愉悦。

以前版本的 CSS 掌握不包含一个单独的网页排版章节,所以也许这给了你一些过去几年在这个领域的进步。在本章中,我们将涉及许多领域:

  • 如何使用基本的 CSS 字体和文本属性应用实心排印规则

  • 控制度量、多栏文本和断字

  • 使用自定义 web 字体和高级字体功能

  • 使用阴影和其他技巧的文本效果

CSS 中的基本排版

大多数设计师要做的第一件事就是添加基本的印刷样式。从 body 元素开始,向下发展到越来越具体的规则,我们为可读性、清晰性和音调设置了基础。作为本章的第一个例子,我们将这样做:选取一个示例页面并应用基本的排版处理。

图 4-1 显示了一个非常简单的 HTML 文档(关于月球的文本,转载自维基百科),显示在浏览器中,没有添加样式。它仍然呈现为可读文档的事实是由于浏览器中的默认样式表,其中设置了一些相对合理的印刷规则。

A314884_3_En_4_Fig1_HTML.jpg

图 4-1。一个简单的 HTML 文档,还没有应用样式

我们的简单文档包含几个标题和一些文本段落(在适用的地方有一些行内元素来增强文本),位于 article 元素中:

<article>
  <h1>The Moon</h1>
  <p> The <strong>Moon</strong> (in Greek: σελήνη...</p>
  ...
  <h2>Orbit</h2>
  <p>The Moon is in synchronous…</p>
  ...
  <h3>Gravitational pull &amp; distance</h3>
  <p>The Moon's gravitational...</p>
  <h2>Lunar travels</h2>
  <p>The Soviet Union's Luna programme...</p>
  <p class="source">Text fetched from...</p>
</article>

虽然未样式化的文档是可读的,但它远非理想。我们的目标是创建一个相对较短的样式表,以帮助提高页面的可读性和美观性。在图 4-2 中,我们看到了我们想要的最终结果。

A314884_3_En_4_Fig2_HTML.jpg

图 4-2。应用了新字体属性的文档

让我们仔细检查每一个规则,分解术语,为什么制定规则,以及基本排版属性背后的 CSS 机制是如何工作的。

文本颜色

文本颜色可能是我们为文档设置的最基本的东西之一,但是我们很容易忽略它的效果。默认情况下,浏览器将大多数文本呈现为黑色(当然,链接除外;那些是充满活力的蓝色),这与白色背景形成了非常高的对比度。足够的对比度对于可访问性至关重要,但也可能在另一个方向走得太远。事实上,屏幕的对比度非常高,以至于对于较长的文本来说,白底黑字会显得过于密集,影响可读性。

我们将标题设为默认的黑色,并将段落设置为非常暗的蓝灰色阴影。链接也仍然是蓝色的,但是我们会稍微调低一点活跃程度。

p {
    color: #3b4348;
}
a {
   color: #235ea7;
}

字体系列

font-family 属性允许您按优先顺序列出您想使用的字体:

body {
  font-family: 'Georgia Pro', Georgia, Times, 'Times New Roman', serif;
}
h1, h2, h3, h4, h5, h6 {
  font-family: Avenir Next, SegoeUI, arial, sans-serif;
}

body 元素(以及几乎所有其他元素,因为 font-family 是继承的)有一个字体堆栈,包括“Georgia Pro”,Georgia,Times,“Times New Roman”,serif。Georgia 是一种几乎普遍可用的衬线字体,其中较新的 Georgia Pro 变体安装在一些版本的 Windows 10 上。如果两个版本的 Georgia 都不可用,那么 Times 和 Times New Roman back 也存在于许多系统中。最后,我们回到通用系统 serif 字体。

对于标题,我们将 Avenir Next 列为我们的首选,这种字体有许多变化,预装在现代 Mac OS X 电脑和 iOS 设备上。如果这种字体不可用,浏览器会查找 Segoe UI,这是一种类似的多功能无衬线字体,存在于大多数版本的 Windows 电脑和 Windows Phone 设备上。如果浏览器找不到,它将尝试使用 Arial 字体(可在各种平台上使用),最后使用任何通用的 sans-serif 字体作为当前平台的默认字体。

图 4-3 显示了这些字体在 Mac OS X 上的 Safari 9 中的外观,与 Windows 10 上的 Microsoft Edge 进行了比较。

A314884_3_En_4_Fig3_HTML.jpg

图 4-3。我们的页面在 Safari 9 上使用 Avenir Next 和乔治亚进行渲染(左)与在微软 Edge 上使用 Segoe UI 和乔治亚进行渲染(右)
注意

衬线是字形笔画末端的小角度形状,常见于许多经典字体。无衬线只是指没有衬线的字体。

这种回退机制是 font-family 属性的一个重要特性,因为不同的操作系统和移动设备并不都有相同的可用字体。字体的选择也比字体是否存在更复杂:如果首选字体缺少文本中使用的字形,如重音字符,浏览器也将退回到字体堆栈中查找这些单独的字符。

关于各种操作系统上可用的默认字体的一些研究可以帮助您为项目选择正确的堆栈。你可以在 http://cssfontstacks.com 找到一个好的起点。

列表末尾定义的 sans-serif 和 seriffont 族被称为通用族,作为一个总括选项。我们也可以选择草书、幻想和等宽字体。serif 和 sans-serif 通用族可能是最常用于文本的族。当选择预格式化文本(如代码示例)的字体时,monospace 会尝试选择所有字符都具有相同宽度的字体,使字符跨行对齐。fantasy 和草书属类稍微不常见一些,但它们分别对应于更精致的装饰字体或类似笔迹的字体。

注意

严格来说,您不需要将包含空格的字体系列名称放在引号中,但这样做是个好主意。该规范只要求在字体系列名称与通用系列名称相同的情况下使用引号,但是对于包含可能会在浏览器中出错的非标准符号的名称,也建议使用引号。如果不考虑其他因素,代码编辑器中的语法高亮器通常会更好地处理带空格的名字,如果它们被引用的话。

字体和字体之间的关系

关于字体、字体系列和字体的术语会变得非常混乱。字样(也称为字体族)是字母、数字和其他共享一种样式的字符的形状(也称为字形)的集合。对于每个字形,字体可以有几种不同的变体,包括粗体、正常体和轻体、斜体、不同的数字显示方式、将几个字符组合成一个字形的连字以及其他变体。

最初,字体(或字体面)是一种字体的特定变体的所有字形的集合,被铸造成金属片。这些收藏品随后被用于机械印刷机。在数字世界中,我们用这个词来表示保存字体表示的文件。假设的字体“CSS Mastery”可以只是一个字体文件,也可以由包含“CSS Mastery Regular”、“CSS Mastery Italic”、“CSS Mastery Light”等的几个字体文件组成。

字体大小和行高

几乎所有浏览器的默认字体大小都是 16 像素,除非用户改变了他们的偏好。我们保留了默认的字体大小,而是选择使用 em 单位来调整特定元素的大小:

h3 {
  font-size: 1.314em; /* 21px */
}

用于 font-size 时,em 单位是继承了 font-size 的元素的比例因子。例如,对于我们的 h3 元素,大小是 1.314 * 16 = 21px。我们也可以将字体大小设置为 21px,但是 ems 更灵活一些。大多数浏览器允许用户缩放整个页面,即使是像素也能很好地工作。使用 ems,如果用户仅改变他们偏好中的默认字体大小,测量也可以缩放。

由于 em 单位基于继承的大小进行缩放,我们也可以通过调整父元素的大小来缩放页面的一部分的字体大小。另一方面——也是使用 ems 的棘手之处——是我们不希望仅仅因为它在标记中的位置就意外地扩展它。考虑以下假设的样式规则:

p {
  font-size: 1.314em;
}
article {
  font-size: 1.314em;
}

前面的规则意味着默认情况下,p 和 article 元素的字体大小都是 21px。但这也意味着,作为 article 元素的子元素的 p 元素的字体大小将为 1.314em × 1.314em,大约为 1.73em 或 28px。这可能不是设计中的意图,所以当使用相对长度时,您需要跟踪尺寸计算。

当谈到字体大小时,我们可以用百分比来代替 ems。设置 133.3%和用 1.333em 完全一样,用哪个是个人喜好问题。作为最后的灵活测量,我们可以使用 rem 单位。它是一个缩放因子,就像 em 一样,但是总是基于根元素 em 的大小进行缩放(因此得名 rem),这意味着在 html 元素上设置的字体大小。我们使用了 rem 单位来为所有标题获得一致的页边距上限值:

h1, h2, h3, h4, h5, h6 {
  margin-top: 1.5rem; /* 24px */
}

当 ems 用于盒子模型尺寸时,它与继承的字体大小无关,而是与元素本身的计算字体大小有关。因此,这种测量对于所有标题级别都是不同的。为了获得一致(但灵活)的值,我们需要使用 rem,或者在 ems 中为每个标题级别单独计算边距。

rem 单元相对较新,可以在所有现代浏览器中工作。作为 Internet Explorer 8(以及更早版本)等旧浏览器的后备,我们可以利用 CSS 的容错能力,在基于 rem 的声明之前声明一个边距的像素度量:

h1, h2, h3, h4, h5, h6 {
  **margin-top: 24px;** /* non-scalable fallback for old browsers */
  margin-top: 1.5rem; /* 24px, will be ignored by old browsers */
}
警告

还有基于物理尺寸的绝对测量单位,如 mm、cm、in 和 pt,主要用于打印样式表。这些不应该用于屏幕样式。我们不会在这里讨论打印样式表,但是会在第八章中讨论如何针对不同的媒体类型。

带刻度的字体大小

在决定使用哪种字体时,并没有硬性规定选择哪种字体。最重要的是要确保文本足够大以便阅读,然后努力找到在当前上下文中有意义的大小。一些人喜欢观察它,而另一些人相信测量是基于数学关系的。

我们将三个标题的大小松散地建立在一个被称为“完美第四”的数学尺度上每个增加的标题级别都是其自身大小比前一个级别大四分之一,或者(以反比关系表示)是其下一个级别的 1.3333333 倍。然后将尺寸四舍五入以匹配最接近的像素尺寸,并截断到三位小数:

h1 {
  font-size: 2.315em; /* 37px */
}
h2 {
  font-size: 1.75em; /* 28px */
}
h3 {
  font-size: 1.314em; /* 21px */
}

当开始设计工作时,像这样的规模可以是一个很大的帮助,即使你最终通过感觉设置最终测量。你可以在www.modularscale.com/的模块化比例计算器中摆弄一堆不同的预设比例(见图 4-4 )。

A314884_3_En_4_Fig4_HTML.jpg

图 4-4。模块化比例计算器允许您使用字体和数学大小比例的组合

行距、对齐和线盒的剖析

当我们为文本设置额外的尺寸时,我们将开始看到各种印刷概念之间的关系。因此,有必要更深入地了解 CSS 内联格式模型,以及更多的印刷术语——至少当它应用于西方书写系统时。图 4-5 展示了组成一行文本的各个部分,使用了我们例子中第一段的前两个单词。

A314884_3_En_4_Fig5_HTML.gif

图 4-5。内联格式模型的组成部分和技术术语
<p>The <strong>Moon</strong>…[etc]</p>

我们在第三章中看到了内联格式的高级视图。每行文本生成一个行框。这个框可以通过表示内联元素(比如本例中的<强>元素)或者它们之间的匿名内联框,进一步分成几个内联框

文本绘制在内嵌框的中间,即所谓的内容区域。内容区域的高度是字体大小测量的定义——在图 4-5 中“月亮”一词的后面,我们看到一个 1em × 1em 的正方形,以及它与字形本身大小的关系。给 em 单元命名的传统印刷术语“em”源于大写字母“M”的大小,但正如我们所见,这不是 web 印刷中的正确定义。

小写字母如“x”的上边缘决定了所谓的 x 高度。不同字体之间的高度差异很大,这解释了为什么很难给出准确的字体大小的一般建议——你需要用实际的字体进行测试,看看感觉大小是多少。在我们这里使用的 Georgia 中,x-height 相当高,使得它看起来比其他相同字体尺寸的字体要大。

然后,实际的字形被放置成在内容区域内的某个地方垂直平衡出现,这样,默认情况下,每个行内框在靠近底部的一条公共线上对齐,这条公共线被称为基线。字形也不一定受内容区域的约束:例如,在某些字体中,小写的“g”可能会突出显示在内容区域的下面。

最后,行高定义了行框的总高度。这通常被称为行距,或者在排印术语中,行距(发音为“ledding”),这是由于在印刷机上用于分隔字符行的铅字排字机块。与 mechanical 类型不同,CSS 中的前导始终应用于行框的顶部和底部。

从总行高中减去字体大小,得到的尺寸分成两个相等的部分(称为半前导)。如果字体大小为 21px,行高为 30px,每条半行距将为 4.5px 高。

注意

如果一个行框包含不同行高的行框,行框的行高作为一个整体将至少与最高的行框一样高。

设置行高

当设置行高时,我们需要考虑什么对当前字体有意义。在我们的文章示例中,我们为 body 元素设置了基本字体系列 Georgia 和 1.5 的行高:

body {
    font-family: Georgia, Times, 'Times New Roman', serif;
    line-height: 1.5;
}

行高通常在 1.2 到 1.5 之间。当你调整这个值时,你需要找到这些线既不太拥挤也不太间隔和不连接的地方。一般来说,x 高度较大的文本可以容忍更大的行间距,就像我们在 Georgia 的文本集一样。文本的长度和字体大小也很重要:较短的较小文本通常可以处理更紧凑的行高值。

我们用一个无单位的 1.5 来设置行高,简单来说就是当前字体大小的 1.5 倍。正文的字体大小为 16px,默认行高为 24px。

可以使用像素、百分比或 ems 来设置行高,但请记住,body 的所有子体都将继承该值。一个可能的“陷阱”是,即使对于百分比和 em,继承的行高也是行高的计算的像素值,而对于无单位值则不是这样。通过省略单位,我们确保特定元素的行高作为乘数被继承,总是与其字体大小成比例。

竖向定线

除了行高之外,内联框还会受到垂直对齐属性的影响。默认值为 baseline,这意味着元素的基线将与父元素的基线对齐。在文章的最后,我们引用了在维基百科上查找的日期,其中序数“rd”后缀用一个 span 标记:

<time datetime="2016-02-23">the 23**<span class="ordinal">**rd**</span>** of February 2016.</time>

我们将使用 vertical-align 和 super 关键字为该文本设置一个上标对齐方式(以及一个稍小的字体大小):

.ordinal {
**vertical-align: super;** 
  font-size: smaller;
}

其他可能的关键字有 sub、top、bottom、text-top、text-bottom 和 middle。它们都或多或少地与内容区域和父行框有着复杂的关系。仅举一个例子,text-top 或 text-bottom 将内容区域的顶部或底部与父行框的内容区域对齐,这仅在内联框的字体大小或行高与父行框不同时才有效。就像我们说的:复杂。

也许更直观的方法是将元素基线的垂直对齐方式从父基线向上或向下移动一个设定的长度——以像素为单位,或者相对于字体大小的长度(例如 em 或%)。值得注意的是,不仅行高值会影响一段文本的最终行距。如果行框中有一个使用垂直对齐移动的项目,该元素将推出最终的行框高度。图 4-6 显示了当通过不同的垂直对齐值移动元素时,我们文章中的一行文本会发生什么。

A314884_3_En_4_Fig6_HTML.jpg

图 4-6。可以与 vertical-align 一起使用的各种关键字和值。请注意线条框的顶部和底部是如何被最大值推出的,从而增加了该线条的整体行高
注意

与内联文本相比,内联块和图像对垂直对齐的反应略有不同,因为它们不一定有自己的单一基线。当我们在第六章中查看一些布局技巧时,我们将利用这一点。

字体粗细

接下来,我们使用 font-weight 属性设置标题的权重。一些字体有许多变化,如 Helvetica 新光,Helvetica 新光粗体,Helvetica 新光黑色,等等。我们没有声明字体变体的名称,而是使用关键字(普通、粗体、粗体和浅体)或数值。数值被写成偶数,从 100 开始,然后是 200、300、400 等等,直到 900。

normal 的默认值映射为 400,bold 为 700,这是大多数字体中最常见的粗细。关键字 bolder 和 light 的工作方式稍有不同,用于使文本比继承的值更重或更轻。

值 100–300 通常分别映射到名称中带有“细”或“细线”、“超轻”和“轻”的字体。相反,值 800 或 900 将映射到名称中包含“超粗体”、“粗体”或“黑色”字样的字体。介于 500(中等)和 600(半粗体或半粗体)之间。

作为标题的默认设置,我们将中等粗细设置为 500,对于超粗体 h1 元素和半粗体 h2 元素有所不同:

h1, h2, h3, h4, h5, h6 {
**font-weight: 500;** 
}
h1 {
**font-weight: 800;** 
}
h2 {
**font-weight: 600;** 
}

Avenir Next 和 Segoe UI(我们最喜欢的字体)都包含很多粗细变化。如果字体缺少所需的粗细,它可能会尝试模仿更粗的粗细,但不会比正常粗细。遗憾的是,人工加粗字体的效果往往不太理想。

字体样式

设置声明字体样式:italic 从字样中选择斜体样式,如果有字样的话。如果没有,浏览器会试图通过倾斜字体来伪造——同样,结果往往不太理想。斜体通常用于强调或区分不同语调的事物。在我们的例子中,我们用标签包装了月球的拉丁和希腊名字。这个标记最初是早期 HTML 实现的表示标记的残余,但在 HTML5 中被重新定义,用于标记传统的斜体文本,如名称。

<p>The <strong>Moon</strong> (in Greek: σελήνη **<i lang="el">Selene</i>**, in Latin: **<I lang="la">Luna</i>**)

虽然标签不表示斜体,但是浏览器默认样式表将字体样式设置为斜体:

i {
**font-style: italic;** 
}

如果我们愿意,我们可以重新定义这个元素,使其显示为粗体、非字母化的文本:

i {
  font-weight: 700;
**font-style: normal;** 
}

除了斜体和默认的正常值,您还可以使用倾斜关键字(倾斜文本的另一种变体),但这很少使用,因为很少有字体附带倾斜样式。

转换大小写变体

有时设计要求文本以不同于 HTML 源代码的形式显示。CSS 允许您通过文本转换属性对此进行一些控制。在我们的示例中,h1 在标记中被写成大写(首字母大写),但是通过 CSS 被强制显示为大写(参见图 4-7 ):

A314884_3_En_4_Fig7_HTML.jpg

图 4-7。我们的 h1 显示为大写
h1 {
**text-transform: uppercase;** 
}

除了大写值之外,您还可以指定 lowercase 使所有字母小写,大写使每个单词的第一个字母大写,或 none 将大小写恢复为 HTML 中创作的默认大小写。

使用字体变体

CSS 还有一个名为 font-variant 的属性,它允许你为你的字体选择所谓的小型大写字母。Small-caps 是字体的一种变体,其中小写字母显示为大写(或大写)字母的形状已经“缩小”到它们的大小。适当的小型大写字母变体对字母形状有更大的尊重,而不仅仅是简单地缩小它们,但这些变体大多出现在更独特的字体系列中。如果没有这样的字体,浏览器会试图为你伪造。我们可以在我们的文档中包含缩写“NASA”的缩写标签上说明这一点(见图 4-8 ):

A314884_3_En_4_Fig8_HTML.jpg

图 4-8。使用字体变量关键字 small-caps 使浏览器将大写字形向下收缩到 x 高度
<abbr title="National Aeronautics and Space Administration">NASA</abbr>

我们将把它与 text-transform: lowercase 规则一起应用,因为 HTML 源代码中的字母已经是大写的了。最后一个调整是将 abbr 元素设置为稍微小一点的行高,因为在某些浏览器中,小型大写字母似乎会将内容框向下推,从而影响整个行框的高度。

abbr {
  text-transform: lowercase;
  font-variant: small-caps;
  line-height: 1.25;
}

CSS 2.1 规范将 small-caps 定义为 font-variant 属性的唯一有效值。在 CSS 字体模块级别 3 规范中,这一点已经扩展到包括大量表示选择替代字形的方式的值。浏览器在采用这些方面进展缓慢,但幸运的是有更好的支持方式来实现这一点;我们将在下一节高级排版技术中讨论它们。

改变字母和单词之间的间距

改变单词和单个字符之间的间距通常最好留给字体的设计者。CSS 确实允许你用一些粗糙的工具来改变这一点。

word-spacing 属性很少使用,但正如您可能猜到的那样,它会影响单词之间的间距。你给它的值指定了从默认的间距增加或减少多少,由当前字体中的空白字符宽度决定。以下规则将0.1 毫米添加到段落中单词之间的默认间距:

p {
  word-spacing: 0.1em;
}

同样,您可以使用 letter-spacing 属性影响每个字母之间的间距。对于小写文本,这通常不是一个好主意——大多数字体都是为了让你在阅读时一次识别整个单词的形状而设计的,所以打乱间距会使文本难以阅读。大写(或小型大写)字形更适合单独解释,就像首字母缩写词的情况一样。一点额外的间距实际上可以使它们更容易阅读。让我们通过给我们的缩写标签增加一点字母间距来尝试一下(见图 4-9 ):

A314884_3_En_4_Fig9_HTML.jpg

图 4-9。应用于 abbr 元素的少量字母间距
abbr {
  text-transform: lowercase;
  font-variant: small-caps;
**letter-spacing: 0.1em;** 
}

这是我们最后的字体相关设置和小的排版调整。接下来,我们将关注文本如何布局,以进一步确保良好的阅读体验。

小节、节奏和节奏

我们下一个关注的领域是使文本读起来愉快的一个关键因素:行的长度。在印刷术语中,这被称为度量。过长或过短的线条会扰乱读者在文本中的眼球运动,会导致读者迷失方向,甚至完全放弃文本。

对于什么是完美的线长度,没有确切的答案。这取决于字体的大小、屏幕的大小以及正在显示的文本内容的类型。我们所能做的是参考关于行长度一般规则的研究和历史建议,并尝试将它们合理地应用到我们的页面中。

Robert Bringhurst 的经典著作The Elements of Typographic Style指出,正文通常设置在 45 到 75 个字符之间,平均约为 66 个字符。在将这个建议应用到网络上时,排版专家 Richard Rutter 发现这个范围在网络上也很有效——至少在大屏幕上是这样。在非常小的屏幕(或在远处观看的大屏幕,如电视或投影)的情况下,尺寸与到屏幕的距离相结合可能保证短至 40 个字符的度量。

注意

我们将在第八章回到响应式网页设计的排版挑战。

可以通过在包围文本的元素上或者在标题、段落等上设置宽度来实现对行长度的约束。他们自己。

在我们的正文中,Georgia 字体有相对较宽的字母形式,这是 x 高度较大的结果。这意味着我们可以在更高的范围内进行测量。我们选择了简单的选项,将 article 元素的最大宽度设置为 36em(一个字符平均为 0.5em),使其在页面上居中。如果视窗比这个更窄,元素会自动缩小。

article {
  max-width: 36em;
  margin: 0 auto;
}

这使得我们的段落文本在更宽的视窗中的行长度约为 77 个字符,如图 4-10 所示。我们选择使用 ems 来应用宽度,这样即使我们或者用户决定改变字体大小,也可以很好地缩放尺寸。

A314884_3_En_4_Fig10_HTML.jpg

图 4-10。article 元素的最大宽度被限制为 36em,即使我们增加了字体大小

文本缩进和对齐

默认情况下,我们的文本将被设置为左对齐。文本的左边缘保持平直有助于眼睛找到下一行,保持阅读速度。对于一段接一段的段落,通常要么在一行之间使用空白,要么少量缩进文本,以强调从一个段落到下一个段落的转换。在设置文章文本时,我们选择了后者,使用相邻的兄弟组合符和 text-indent 属性:

p + p {
**text-indent: 1.25em;** 
}

文本的右边缘非常不均匀(见前面的图 4-9 ),我们暂时让它保持原样。这种不均匀的形状在印刷术语中被称为“rag”(如“ragged”)。末端边缘参差不齐并不是一种灾难,但是在对除了非常短的文本之外的任何内容使用居中对齐之前,您应该仔细考虑。居中文本最适合小块用户界面文本(如按钮)或短标题,但两边参差不齐会破坏可读性。

然而,我们已经将样本页面的 h1 居中。我们还给它加了一个底部边框,让它在视觉上锚定到下面的文章文本,如图 4-11 所示。

A314884_3_En_4_Fig11_HTML.jpg

图 4-11。我们已经将 h1 居中对齐
h1 {
**text-align: center;** 
  border-bottom: 1px solid #c8bc9d;
}

text-align 属性可以接受几个关键字值,包括 left、right、center 和 justify。CSS 文本级别 3 规范定义了一些额外的值,包括开始和结束。这两个逻辑方向关键字对应于文本的书写模式:大多数西方语言是从左向右书写的,因此如果文档语言是英语,则起始值将表示左对齐,结束值将表示右对齐。在从右向左的语言中,如阿拉伯语,这将是相反的。如果您在父元素上设置了 dir="rtl "属性,大多数浏览器也会自动反转默认的文本方向,以指示从右向左的文本。

text-align 属性也可以使用值 justify,分布单词之间的间距,以便文本与左右边缘对齐,消除不规则的右边。这是印刷媒体中的一种常见技术,在这种情况下,可以调整副本、断字和字体属性,以匹配页面上的空间。

网络是一种不同的媒介,它的准确呈现取决于我们无法控制的因素。不同的屏幕尺寸、不同的字体安装和不同的浏览器引擎都会影响用户查看我们页面的方式。如果你使用对齐的文本,它可能会看起来很糟糕,变得很难阅读,如图 4-12 所示。空白的“河流”可能会在你的文本中形成,尤其是当小节减少的时候。

A314884_3_En_4_Fig12_HTML.gif

图 4-12。文本对齐的文本段落:对齐会导致单词之间出现“河流”

浏览器用来对齐文本的默认方法是一种相当笨拙的算法,其结果不如桌面出版软件中的精确。使用的算法类型可以通过 text-justify 属性进行更改,但是对各种值的支持很少,并且主要涉及如何调整除大多数西方书写系统之外的其他语言类型的字母形式和单词。

有趣的是,Internet Explorer 支持该属性的非标准值报纸,它似乎使用了一种更聪明的算法,在字母和单词之间分配空白。

用连字符号连接

如果你仍然想让页面中的文字对齐,断字可以在一定程度上帮助消除河流。您可以使用­ HTML 实体在标记中手动插入软连字符。该连字符只有在浏览器需要断开它以适合该行时才可见(见图 4-13 ):

A314884_3_En_4_Fig13_HTML.jpg

图 4-13。用软连字符手动断字
<p>The <strong>Moon</strong> […] is Earth's only natural satel**&shy;**lite.[…]

对于像一篇文章这样的较长文本,你不太可能仔细检查并手动断字。使用连字符属性,我们可以让浏览器来完成这项工作。它仍然是一个相对较新的特性,所以大多数支持它的浏览器都需要厂商前缀。Internet Explorer 版之前的版本,Android 设备上的股票 WebKit 浏览器,以及令人惊讶的是,Chrome 和 Opera(在编写本文时)等基于 Blink 的浏览器根本不支持断字。

如果你想激活自动断字,你需要两段代码。首先,您需要确保设置了文档的语言代码,通常是在 html 元素上:

<html **lang="en"**>

接下来,将相关元素的连字符属性设置为 auto。图 4-14 显示了在 Firefox 中出现的结果。

A314884_3_En_4_Fig14_HTML.jpg

图 4-14。激活自动断字会在 Firefox 中显示更直的右边横条
p {
 hyphens: auto;
}

要关闭断字功能,可以将连字符属性设置为手动值。手动模式仍然尊重软连字符。

在多列中设置文本

虽然对文章整体宽度的 36em 限制有助于限制尺寸,但它确实会在较大的屏幕上浪费很多空间。这么多未使用的空白!有时,为了更有效地使用更宽的屏幕,同时保持合理的度量,将文本设置在多列中是有意义的。CSS 多列布局模块的属性为我们提供了实现这一点的工具,将内容分成相等的列。

“多列布局”这个名称可能会有点误导,因为这组属性并不是指为一个页面的不同部分创建具有列和间距的通用布局网格,而是指页面的一部分内容像报纸一样以列的形式流动。尝试将它用于其他目的肯定是可能的,但也许不可取。

如果我们将最大宽度增加到 70em,我们可以放入三列。我们可以通过将 columns 属性设置为所需的最小列宽来告诉文章自动将内容排列成列(参见图 4-15 )。列间距由列间距属性控制:

A314884_3_En_4_Fig15_HTML.jpg

图 4-15。文章内容现在会自动流入 70em 最大宽度范围内的尽可能多的列中,只要它们的最小宽度为 20em
article {
**max-width: 70em;** 
**columns: 20em;** 
**column-gap: 1.5em;** 
  margin: 0 auto;
}

columns 属性是设置列数和列宽属性的简写。如果只设置列数,浏览器将生成一定数量的列,而不考虑宽度。如果设置列宽和计数,列宽作为最小值,而计数作为最大列数。

columns: 20em; /* automatic number of columns as long as they are at least 20em */
column-width: 20em; /* same as above */

columns: 3; /* creates 3 columns, with automatic width */
column-count: 3; /* same as above */

columns: 3 20em; /* at most 3 columns, at least 20em wide each */

/* the following two combined are the same as the above shorthand: */
column-count: 3;
column-width: 20em;

后退宽度

为了避免在不支持多列属性的浏览器中出现过长的行,我们可以添加一些规则来设置段落本身的最大宽度。旧的浏览器将会显示一个单独的列,但是仍然容易阅读:

article > p {
  max-width: 36em;
}

列跨度

在前面的示例中,文章包装中的所有元素都流入列中。我们可以选择从那个流中剔除一些元素,迫使它们跨越所有列。在图 4-16 中,文章标题和最后一段(包含源链接)跨越所有列:

A314884_3_En_4_Fig16_HTML.jpg

图 4-16。第一个标题和最后一个段落跨越所有列
.h1,
.source {
  column-span: all; /* or column-span: none; to explicitly turn off. */
}

如果我们选择让流中间的元素跨越所有列,文本将被分成几个垂直堆叠的基于列的流。在图 4-17 中,h2 元素被添加到先前的规则中,显示了标题前后的文本如何在它自己的一组列中流动。

A314884_3_En_4_Fig17_HTML.gif

图 4-17。具有柱跨的元素:all 会将柱流分成几个垂直堆叠的柱集

几乎所有浏览器都支持多栏布局属性,IE9 和更早版本除外。尽管如此,一些警告仍然适用:

  • 几乎每个浏览器都需要适当的供应商前缀来应用列属性。

  • 在编写本文时,Firefox 根本不支持列跨度规则。

  • 各种浏览器之间存在相当多的错误和不一致。大多数情况下,当元素跨列流动时,像边距折叠和边框呈现这样的事情会奇怪地发生。Zoe Mickley Gillenwater 有一篇关于这个和其他陷阱的文章:

垂直节奏和基线网格

我们已经提到了排版中尺寸之间的一些数学关系是如何帮助它走到一起的。例如,我们使用“完美的第四”尺寸作为标题尺寸的基础。我们还为所有标题设置了一个通用的页边距-上限值 1.5 雷姆,等于一行正文的高度,并对各列之间的间距再次使用了相同的度量。一些设计师坚信这些和谐的尺寸,让基线高度作为设计其余部分的节拍器。

在印刷设计中,紧密遵循这种节奏是很常见的,这样正文的行就会落在一个基线网格上,即使标题、引用或其他部分不时打破这种节奏。它不仅有助于扫描页面时的眼球运动,还有助于防止(薄)纸另一面上的打印线条在双面打印中穿透,因为相同的基线网格适用于两者。

在 Web 上,获得正确的基线网格要挑剔得多——尤其是在处理可变大小和用户生成的内容(如图像)时。至少在某些情况下尝试是有意义的,比如多栏文本。在图 4-18 中,我们可以看到,由于标题的原因,各列的基线并没有完全对齐。

A314884_3_En_4_Fig18_HTML.jpg

图 4-18。我们的多栏布局,叠加了基线网格。有些部分不合节奏

让我们调整标题的边距,使两个标题级别的上边距、行高和下边距之和等于基线行高值的倍数。这样,基线应该在所有三列中对齐。

h2 {
  font-size: 1.75em; /* 28px */
  line-height: 1.25; /* 28*1.25 = 35px */
  margin-top: 1.036em; /* 29px */
  margin-bottom: .2859em; /* 8px */
}
h3 {
  font-size: 1.314em; /* 21px */
  line-height: 1.29; /* 1.29*21 = 27px */
  margin-top: .619em; /* 13px */
  margin-bottom: .38em;/* 8px */
}

最初,所有标题的行高值都是 1.25,但是为了简化计算,我们已经覆盖了这个值。总的来说,上边距和下边距的划分多少是通过感觉来完成的。重要的是,这两条规则的总和是基线高度的倍数:h2 为 72px,h3 为 48px。所有三列中正文文本的基线现在应该对齐了(参见图 4-19 )。

A314884_3_En_4_Fig19_HTML.gif

图 4-19。多栏文章,现在设置了垂直节奏,以便所有段落都落在基线网格上

网页字体

到目前为止,在这一章中,我们已经把自己限制在本地安装在用户计算机上的字体。常见的网络字体如 Helvetica 字体、佐治亚字体和 Times New Roman 字体之所以常见,恰恰是因为它们传统上可以在流行的操作系统如 Windows 和 Mac OS X 上使用

多年来,设计者希望能够从网络上嵌入远程字体,就像他们可以将图像嵌入网页一样。自 1997 年 Internet Explorer 4 发布以来,这种技术就已经存在,但直到 2009 年 Firefox、Safari 和 Opera 引入类似技术后,才出现了良好的跨浏览器支持。

从那以后,网络字体被大量采用。最初在小型博客和个人网站上进行试验,随后大公司甚至政府组织(例如,见图 4-20 )都采用了定制的网络字体。

A314884_3_En_4_Fig20_HTML.jpg

图 4-20。http://www.gov.uk 网站使用了由 Margaret Calvert 和 Henrik Kubel 设计的自定义字体

批准

处理网络字体的另一个复杂因素是许可。最初,字体代工厂对于允许个人浏览器下载他们的字体非常谨慎。人们担心这会导致对他们字体的不可控制的盗版,这种担心需要几年时间才能消除。

大多数在网上提供字体的铸造厂都要求对如何提供字体有一定的安全限制。例如,他们可能只允许从具有特定域名的站点下载链接到的字体,或者要求服务器上的字体名称定期更改以避免字体的热链接。

网络字体托管服务

如果您还没有开始尝试自定义字体,最简单的方法就是使用 web 字体服务。像 Adobe type kit(typekit.com)、cloud . typography(【http://www.typography.com/cloud】)和 Fonts.com()这样的商业服务负责托管和提供网络字体的所有细节。还有谷歌字体(www.google.com/fonts),谷歌从一系列字体代工厂收集并托管免费使用的字体。

这些在线服务处理与铸造厂的不同许可交易,以及将字体转换为正确文件格式的困难工作,确保包含正确的字符集并进行良好的优化。然后,他们从可靠的高速服务器上托管和提供这些字体。

选择托管服务允许您单独许可字体以供一次性使用,或者作为字体资源库订阅的一部分。托管服务消除了处理网页字体的大量痛苦,让你可以专注于网页中字体的设计和使用。

@font-face 规则

嵌入式 web 字体的关键是@font-face 规则。此规则允许您指定供浏览器下载的字体在 web 服务器上的位置,然后允许您在样式表的其他地方引用该字体:

@font-face {
  font-family: Vollkorn;
  font-weight: bold;
  src: url("fonts/vollkorn/Vollkorn-Bold.woff") format('woff');
}
h1, h2, h3, h4, h5, h6 {
  font-family: Vollkorn, Georgia, serif;
  font-weight: bold;
}

前面的@font-face 块中的代码声明,当样式表使用字体系列值 Vollkorn 和粗体字时,应用此规则,然后提供一个 URL 供浏览器下载包含粗体字的 Web 开放字体格式(WOFF)文件。

一旦声明了这个新的 Vollkorn 字体,就可以在样式表的普通 CSS 字体系列属性中使用它。在前面的例子中,我们选择对页面上的所有标题元素使用粗体 Vollkorn 字体。

字体文件格式

尽管现在所有主流浏览器对 web 字体的支持都非常好,但对一致字体文件格式的支持却不太好。字体格式的历史悠久而复杂,并且与微软、苹果和 Adobe 等公司的历史紧密相连。幸运的是,所有的浏览器制造商现在都支持标准化的 WOFF 格式,有些甚至支持新的更高效的 WOFF2。如果你需要支持 IE8 和更早的版本,Safari 的旧版本,或者旧的 Android 设备,你可能需要用额外的文件格式来补充你的代码,比如 SVG 字体,EOT 和 TTF。

小费

如果你有网络字体使用许可的字体,你可以使用在线工具创建这些额外的格式,如字体松鼠(fontsquirrel.com)。

为了处理这种不一致的支持,@font-face 规则能够接受 src 描述符的多个值(很像 font-family 的工作方式)以及 format()提示,让浏览器来决定哪个文件最适合下载。

使用这个特性,我们可以通过@font-face 规则获得对 web 字体的几乎通用的跨浏览器支持,如下所示:

@font-face {
  font-family: Vollkorn;
  src: url('fonts/Vollkorn-Regular.eot#?ie') format('embedded-opentype'),
       url('fonts/Vollkorn-Regular.woff2') format('woff2'),
       url('fonts/Vollkorn-Regular.woff') format('woff'),
       url('fonts/Vollkorn-Regular.ttf') format('truetype'),
       url('fonts/Vollkorn-Regular.svg') format('svg');
}

这涵盖了所有支持 EOT、WOFF(包括 WOFF2)、TTF 和 SVG 的浏览器,也就是说几乎现在使用的所有浏览器。它甚至解释了 IE6–8 中的古怪行为,通过声明带有 querystring 参数的第一个 src 值。这种模式被称为“Fontspring @font-face 语法”,在www . font spring . com/blog/further-hardening-of-bullet-syntax中有详细的记录,以及它所考虑的格式和边缘情况。

注意

在 IE6–8 中使用 web 字体时,尤其是在使用同一字样的几种变体时,还会遇到一些问题。这里我们就不细说了,但是你可以从 Typekit 的这篇文章中找到更多的背景:blog . type kit . com/2011/06/27/new-from-type kit-variation-specific-font-family-names-in-ie-6-8/。我们还在随书附带的代码示例中记录了变通方法。

在其余使用 web 字体的示例中,我们将只使用 WOFF 和 WOFF2 格式——通过使用这些格式,我们可以支持大多数浏览器,同时保持代码简单。

字体描述符

@font-face 规则接受许多声明,其中大部分是可选的。最常用的有

  • font-family:必选,字体系列的名称。

  • src:必选,URL 或 URL 列表,可以在其中获得字体。

  • 字体粗细:可选的字体粗细。默认为正常。

  • 字体样式:可选的字体样式。默认为正常。

重要的是要明白,这些不是你应用于常规规则集的相同字体属性——它们实际上根本不是常规属性,而是字体描述符。我们并没有改变任何关于字体的东西,而是解释了当在样式表中使用时,这些属性的哪些值应该触发这个特定字体文件的使用。

如果字体粗细在这里设置为粗体,这意味着“当这个字体系列中的某个设置的字体粗细设置为粗体时,使用这个块中的文件。”一个缺陷是,如果这是 Vollkorn 唯一可用的实例,它也将用于其他权重,尽管不是正确的权重。这是浏览器如何加载和选择字体的规范的一部分:正确的字体系列高于正确的权重。

许多 typefaces 有不同的字体以适应不同的粗细、样式和变体,所以您可能有几个不同的@font-face 块引用指向不同文件的字体族名称 Vollkorn。在下面的例子中,我们加载了两种不同的字体,并声明了每种字体的粗细和样式:

@font-face {
  font-family: AlegreyaSans;
  src: url('fonts/alegreya/AlegreyaSans-Regular.woff2') format('woff2'),
       url('fonts/alegreya/AlegreyaSans-Regular.woff') format('woff');
  /* font-weight and font-style both default to "normal". */
}
@font-face {
  font-family: Vollkorn;
  src:  url('fonts/vollkorn/Vollkorn-Medium.woff') format('woff'),
        url('fonts/vollkorn/Vollkorn-Medium.woff') format('woff');
  font-weight: 500;
}
@font-face {
  font-family: Vollkorn;
  font-weight: bold;
  src:  url('fonts/vollkorn/Vollkorn-Bold.woff') format('woff'),
        url('fonts/vollkorn/Vollkorn-Bold.woff') format('woff');
}

然后,我们可以在样式表的其他地方使用正确的字体文件,方法是声明我们需要的变体:

body {
  font-family: AlegreyaSans, Helvetica, arial, sans-serif;
}
h1, h2, h3, h4, h5, h6 {
  font-family: Vollkorn, Georgia, Times, 'Times New Roman', serif;
  font-weight: bold; /* will use the Vollkorn Bold font. */
}
h3 {
  font-weight: 500; /* will use the Vollkorn Medium font. */
}

将这些样式应用于我们在月球文章示例中使用的相同标记,我们得到了不同的外观,其中用于正文的 Alegreya sans-serif 字体系列与用于标题的 serif Vollkorn 形成了对比(见图 4-21 )。h1 和 h2 现在使用 Vollkorn Bold 字体文件,而 h3 自动使用 Vollkorn Medium,因为字体粗细匹配 500。

A314884_3_En_4_Fig21_HTML.jpg

图 4-21。应用了新字体的文章示例
警告

在加载 web 字体时,一个常见的错误是在@font-face 块中加载粗体字体,并将其字体粗细描述符设置为 normal,然后将其用于 font-weight 属性设置为 bold 的元素。这导致一些浏览器认为这种字体没有合适的加粗变体,让他们在原来的加粗字体上加上“假加粗”。

在前面的例子中,我们可以看到字体系列的机制是如何与我们的新字体相结合的:原来 Alegreya Sans 字体不包含任何希腊字母,这些字母出现在月亮的翻译名称中(见图 4-22 )。对于这些字形,使用备用字体—在本例中为 Helvetica。从两种字体中不同的 x 高度可以明显看出这一点。

A314884_3_En_4_Fig22_HTML.jpg

图 4-22。希腊字形使用字体系列堆栈中的备用字体。请注意 x 高度略有不同

坏消息是,我们没有为 Alegreya 加载斜体字体文件,对于缺失的字体样式,浏览器使用基于正常样式的“假斜体”。当我们查看文章最后的来源参考段落时,这一点变得更加清晰(见图 4-23 )。

A314884_3_En_4_Fig23_HTML.jpg

图 4-23。我们文章底部的假斜体文本

幸运的是,Alegreya 包含了各种各样的变体,所以如果我们添加一个指向正确文件的新的@font-face 块,这个问题应该会为任何已经设置为 font-style: italic 的正文文本自行解决(见图 4-24 ):

A314884_3_En_4_Fig24_HTML.jpg

图 4-24。现在用真正的斜体
@font-face {
  font-family: AlegreyaSans;
  src: url('fonts/alegreya/AlegreyaSans-Italic.woff2') format('woff2'),
       url('fonts/alegreya/AlegreyaSans-Italic.woff') format('woff');
  font-style: italic;
}

Web 字体、浏览器和性能

虽然网页字体为网页设计提供了一个相当大的飞跃,但它们的应用带有一定的免责声明。

很明显,下载额外的字体会增加用户的页面总重量。你首先要考虑的是限制你需要加载多少字体文件。同样非常重要的是,如果您托管自己的自定义字体,您必须应用适当的缓存头以最小化网络流量。但是,关于浏览器如何将字体实际呈现到屏幕上,还有其他一些考虑因素。

下载 web 字体时,浏览器有两种文本内容选择。首先,它可以阻止在屏幕上显示文本,直到网络字体已经下载并可供使用,这被称为不可见文本的闪光(或 FOIT)。这是 Safari、Chrome 和 Internet Explorer 默认显示的行为,它可能会导致用户无法阅读您站点的内容,因为字体下载速度很慢。对于在慢速网络连接上浏览的用户来说,这可能是一个特别的问题,如图 4-25 所示。

A314884_3_En_4_Fig25_HTML.jpg

图 4-25。等待字体下载时,www.nike.com上的一个页面

浏览器的另一个选择是在等待浏览器下载 web 字体时,以备用字体显示内容。这避免了缓慢的网络阻止内容的问题,但也有一个权衡,你得到了回退字体的闪光。这种闪现有时被称为无样式文本的闪现,或 FOUT。

这种无样式文本的闪烁会影响可感知的性能,尤其是当备用字体的规格与您试图加载的首选 web 字体不同时。如果在下载和应用字体时页面内容跳跃太多,用户可能会失去他们在页面中的位置。

如果您使用 web 字体,您可以选择通过 JavaScript 加载字体,以进一步控制使用哪种方法,以及如何显示 web 字体和回退字体。

用 JavaScript 加载字体

在最近的 CSS 字体加载规范中定义了一个用于加载字体的实验性 JavaScript API。遗憾的是,浏览器支持还不是特别广泛。相反,我们需要使用第三方库来确保一致的字体加载体验。

Typekit 维护一个开源的 JavaScript 工具,叫做 Web Font Loader(github.com/typekit/webfontloader)。这是一个小的库,在后台使用本地字体加载 API,并在其他浏览器中模拟相同的功能。它支持一些常见的 web 字体提供商,如 Typekit、Google Fonts 和 Fonts.com,但也允许您自己托管字体。

你可以下载这个库,或者从谷歌自己的服务器上下载,详情见developers . Google . com/speed/libraries/# we B- font-loader

Web 字体加载器提供了许多有用的功能,但最有用的功能之一是确保字体加载的跨浏览器行为一致的能力。在这种情况下,我们希望确保缓慢加载的字体永远不会阻止用户阅读我们的内容。换句话说,我们希望在其他受支持的浏览器上启用 FOUT 行为。

Web 字体加载器为以下事件提供挂钩:

  • 加载:字体开始加载时

  • 活动:当字体完成加载时

  • 无效:如果字体加载失败

在这个实例中,我们将把所有的@font-face 块移动到一个名为 alegreya-vollkorn.css 的单独样式表中,并把它放在一个名为 css 的子文件夹中。然后,我们将在示例页面的头部添加一小段 JavaScript 代码:

<script type="text/javascript">
  WebFontConfig = {
    custom: {
        families: **['AlegreyaSans:n4,i4', 'Vollkorn:n6,n5,n7']**,
        urls: ['css/alegreya-vollkorn.css']
    }
  };
  (function() {
    var wf = document.createElement('script');
    wf.src = 'https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js';
    wf.type = 'text/javascript';
    wf.async = 'true';
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(wf, s);
  })();
</script>

这段代码将下载 Web 字体加载器脚本本身,并配置我们使用的字体和变体(在代码中以粗体突出显示)。我们想要的变体在字体系列名称之后描述:n4 代表“正常样式,权重为 400”,等等。当在这个样式表中找到的字体被加载时,脚本会自动将生成的类名添加到 html 元素中。这样,您可以根据字体加载的当前状态来定制 CSS。

body {
  font-family: Helvetica, arial, sans-serif;
}
.wf-alegreya-n4-active body {
  font-family:  Alegreya, Helvetica, arial, sans-serif;
}

这两个 CSS 规则意味着在 Alegreya 字体加载之前,我们将显示备用字体堆栈。然后,一旦 Alegreya 完成加载,加载器脚本将 wf-alegreya-n4-active 类添加到 html 元素,浏览器开始使用我们新下载的字体。现在,我们不仅可以看到跨浏览器的一致行为,还可以调整后备字体和网页字体的排版细节。

匹配后备字体大小

当字体正在加载但尚未完成时,应用类似的规则,我们可以避开 web 字体和 fallbacks 之间的字体度量差异。这一点很重要,因为当 web 字体替换后备字体时,您希望这种大小变化尽可能地谨慎和不引人注意。

在我们的例子中,Alegreya 字体的 x 高度明显小于 Helvetica 字体和 Arial 字体(两者具有相似的度量)。通过稍微调整字体大小和行高,我们可以非常接近地匹配高度。类似地,我们可以通过稍微调整单词间距来调整字符宽度的差异。这样,我们最终得到的结果更接近于网页字体加载后的文本外观。

.wf-alegreyasans-n4-loading p {
  font-size: 0.905em;
  word-spacing: -0.11em;
  line-height: 1.72;
  font-size-adjust:
}
小费

如果您使用的是垂直节奏,那么在使用这种技术时,您可能需要在几个地方调整这些类型的属性,以便不同的字体大小仍然与基本尺寸相对应。

我们将使用 web 字体加载器的另一个目的是在 Web 字体加载后设置字体大小调整属性。此属性允许您指定 x 高度和字体大小之间的纵横比。在首选字体中缺少字形的情况下,将调整备用字体的大小以匹配该比例。这通常会降低到大约一半高(值为 0.5),但它可能会有一点不同,使您的备用字体和您的首选 web 字体之间的差异非常明显。我们可以设置关键字 auto,让浏览器为我们完成这项工作,而不是手动测量并将这个值设置为一个数字:

.wf-alegreyasans-n4-active body {
  font-size-adjust: auto;
}

在撰写本文时,Firefox 是唯一支持字体大小调整的浏览器,Chrome 在偏好标志后提供实验性支持。如果我们在 Firefox 中查看文章示例,如图 4-26 所示,我们可以看到希腊字形(在 Helvetica 看到的)现在与周围的 Alegreya 具有相同的高度。

A314884_3_En_4_Fig26_HTML.jpg

图 4-26。Firefox 显示了调整了 x 高度的 Helvetica 希腊字形

高级排版功能

OpenType 是微软和 Adobe 在 20 世纪 90 年代开发的一种字体格式,允许在字体文件中包含字体的附加特征和功能。如果您使用的字体文件包含 OpenType 特性(可以包含在。ttf,。otf 或者。woff/.woff2 文件),您可以在大多数现代浏览器中控制一系列 CSS 功能。这些特征包括字距调整、连字和替代数字,以及装饰性特征,如图 4-27 所示。

A314884_3_En_4_Fig27_HTML.jpg

图 4-27。“胖脸”字体上带有花式符号的“&”会议发言人的名字

CSS 字体规范有许多 OpenType 特性的目标属性,如字体字距调整、字体变体数字和字体变体连字。对这些目标属性的支持目前还不能跨浏览器使用,但有一些方法可以通过另一个更低级的属性(字体功能设置)来访问这些功能,该属性在许多现代浏览器中都有支持。通常,您最好两者都指定,因为有些浏览器可能支持目标属性,但不支持低级设置。

font-feature-settings 属性通过向其传递四个字母的 OpenType 代码(可选地带有一个数值)来接受切换某些功能集的值。例如,我们可以启用连字——将两个或更多字符组合成一个的字形——如图 4-28 所示。

A314884_3_En_4_Fig28_HTML.jpg

图 4-28。Vollkorn 中的两个文本集,第一个没有连字,第二个启用了连字。请注意“fi”、“ff”和“fj”对的区别

字体设计者可以指定几种类型的连字,这取决于它们是用于一般情况还是特殊情况。要启用 Vollkorn 字体中的两种连字,即标准连字和 ?? 自选连字,我们将使用以下规则:

p {
  font-variant-ligatures: common-ligatures discretionary-ligatures;
  font-feature-settings: "liga", "dlig";
}

在支持 OpenType 的浏览器中,使用 font-variant-ligations 时,默认情况下总是启用标准连字,因此它们在第一个声明中被忽略。某些浏览器支持字体功能设置属性,但语法略有不同,而其他浏览器需要该属性的供应商前缀版本,因此打开常用和任意连字的完整规则是

h1, h2, h3 {
  font-variant-ligatures: discretionary-ligatures;
  -webkit-font-feature-settings: "liga", "dlig";
  -moz-font-feature-settings: "liga", "dlig";
  -moz-font-feature-settings: "liga=1, dlig=1";
  font-feature-settings: "liga", "dlig";
}

语法差异需要解释一下:

  • 影响 OpenType 特性的标准方式是在引号中使用它的四个字母的代码,后面可选地跟着一个关键字(开或关)或一个数字。这些代码指示功能的状态,如果您忽略它们(如前面的示例),它们将默认为 on。

  • 使用 0 表示状态也会关闭该功能。如果该功能只有开和关两种状态,则值为 1 会将其打开。一些功能有几个“状态”,可以通过为每个状态使用适当的数字来选择这些状态,这意味着什么取决于您想要激活的单个字体和功能类型。

  • 当几个特性同时受到影响时,它们需要用逗号隔开。

  • 大多数浏览器仍然将这些特性作为厂商前缀来实现,所以要确保包含这些特性。

  • 一些 Mozilla 浏览器的旧语法略有不同:所有受影响的功能都以逗号分隔的方式命名在一个带引号的字符串中,状态受等号和数字部分的影响。

OpenType 特性代码的完整列表可以在微软 http://www.microsoft.com/typography/otspec/featurelist.htm找到。在其余的例子中,我们将只使用字体特征设置的标准化形式以及目标特征属性。

数码

有些字体包含多种不同情况下使用的数字样式。许多字体,如 Georgia 或 Vollkorn,默认使用旧式数字,数字和字母一样有上升和下降。Vollkorn 还包括内衬数字,其中数字位于基线上,与大写字母具有相同的一般高度。在图 4-29 中,我们使用以下代码在旧式数字和衬里数字之间进行了明确切换:

A314884_3_En_4_Fig29_HTML.jpg

图 4-29。衬里数字(上)与 Vollkorn 字体的旧式数字(下)
.lining-nums {
  font-variant-numeric: lining-nums;
  font-feature-settings: "lnum";
}
.old-style {
  font-variant-numeric: oldstyle-nums;
  font-feature-settings: "onum";
}

大多数字体都有不同宽度的数字(比例数字),就像普通的字母一样。如果您在表格或列表中使用的数字需要垂直对齐,您可能想要切换到表格数字。在图 4-30 中,我们将它们与衬里数字结合使用,配置如下:

A314884_3_En_4_Fig30_HTML.jpg

图 4-30。Alegreya Sans 中设置的表格衬里数字。右边的价格垂直排列,尽管宽度不同
table {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum", "lnum";
}

字距调整选项和文本呈现

高质量的字体内部通常有数据来调整某些字形对之间的间距。这个微调间距的过程被称为字距调整。这意味着一些字母对之间可能需要额外的空间,以使彼此看起来不太拥挤,一些甚至需要稍微重叠,以使彼此看起来不太远。在图 4-31 中可以看到一些常见字距对的例子,这里我们已经激活了 Alegreya 字体的字距。

A314884_3_En_4_Fig31_HTML.jpg

图 4-31。一个没有(上)和有(下)详细字距调整的句子。注意像“AT”、“Ad”和“Ta”这样的词对之间的空间是如何缩小的

浏览器中的文本呈现大多试图基于已知的度量自动处理这一点,但您也可以在许多现代浏览器中激活从单个字体读取详细的字距调整数据。我们通过设置字体字距微调属性或激活 kern OpenType 特性来触发它:

.kern {
  font-kerning: normal;
  font-feature-settings: "kern";
}

关键字 normal 告诉浏览器从字体中获取字距调整数据(如果有)。auto 值允许浏览器在适当的时候打开它;例如,对于非常小的文本大小,它可能会被忽略。最后,您可以通过将该值设置为 none 来显式关闭它。

注意

在某些浏览器中,激活其他 OpenType 功能(如连字)可能会自动触发使用字体的字距调整数据,因此如果您想要关闭字距调整但仍使用连字,则需要明确指定。相反,使用“字距”功能也可能会触发常用或标准连字的应用。

避免文本呈现属性

设置声明 text-rendering:optimize legability 是同时激活字距调整和常用连字的另一个技巧。它不是任何 CSS 标准的一部分,而是 SVG 规范中的一个属性,告诉浏览器选择一种在 SVG 中呈现字母形状的方法。它可以优先考虑性能(optimizeSpeed)、更精确的形状(optimizeGeometricPrecision)或更可读的形状(optimizeLegibility)。

这个属性已经存在了一段时间,并且得到了很好的支持,所以经常可以看到站点使用它——在基于 WebKit 的旧浏览器支持字体功能设置之前,这是激活这些功能的唯一方法。但是,您应该知道,使用该属性会产生很多严重的渲染错误,所以您最好避免使用它。

文本效果

当涉及到网页排版的基础知识时,仍然有很多需要探索的地方,有些情况下,你可能会对标题和标识之类的东西着迷。在这一节中,我们将看看一些让你超越自我的技巧的例子,这些技巧可以创造出引人注目的效果,使你的网站与众不同。

使用和滥用文本阴影

CSS 文本阴影属性允许你在一段文本后面画一个阴影。对于较长的正文来说,这通常不是一个好主意,因为这会降低文本的可读性。对于标题或其他短文本,它确实有一些很好的用途,特别是用于创建“凸版印刷”一样的效果或重新创建传统绘画标志的阴影。

文本阴影的语法非常简单。您需要提供 x 轴和 y 轴相对于原始文本的偏移长度(正或负)、模糊距离的长度(其中 0 表示完全清晰的阴影)和颜色,所有这些都用空格分隔(参见图 4-32 ):

A314884_3_En_4_Fig32_HTML.jpg

图 4-32。一个简单的文本阴影 应用了一些扩散。除 0 之外的任何扩散值都意味着阴影模糊
h1 {
  text-shadow: -.2em .4em .2em #ccc;
}

除此之外,您可以使用逗号分隔的阴影列表为一段文本创建多个阴影。应用多个阴影时,它们会堆叠在一起,定义的第一个阴影显示在顶部,其他阴影堆叠在它的后面,按照定义的顺序逐渐向下堆叠。

给一个文本添加多个阴影的能力使它成为一个多才多艺的效果。这可以让你模拟一种“凸版印刷”效果,通过添加一个较暗和一个较亮的阴影,在文本上方或下方突出来,字体看起来要么被压入页面,要么被凸起(见图 4-33 )。浅色和深色阴影的偏移量取决于文本是比背景更亮还是更暗:上面有浅色阴影、下面有深色阴影的深色文本通常会出现在页面中,反之亦然。

A314884_3_En_4_Fig33_HTML.jpg

图 4-33。简单的“凸版印刷”效果

以下代码示例说明了两种不同的效果:

.impressed {
  background-color: #6990e1;
  color: #31446B;
  text-shadow: 0 -1px 1px #b3d6f9, 0 1px 0 #243350;
}
.embossed {
  background-color: #3c5486;
  color: #92B1EF;
  text-shadow: 0 -1px 0 #243350, 0 1px 0 #def2fe;
}

在多重阴影技术的基础上,我们可以创建看起来像伪 3D 阴影样式的字体,模仿手绘标牌的样式。添加大量清晰的阴影,其中每个阴影之间的对角线偏移为一个像素或更少,使我们能够实现这种效果:

h1 {
  font-family: Nunito, "Arial Rounded MT Bold", "Helvetica Rounded", Arial, sans-serif;
  color: #d0bb78;
  text-transform: uppercase;
  font-weight: 700;
  text-shadow:
    -1px 1px 0 #743132,
    -2px 2px 0 #743132,
    -3px 3px 0 #743132,
    /* …and so on, 1px increments */
    -22px 22px 0 #743132,
    -23px 23px 0 #743132;
}

这给了我们在图 4-34 中看到的时髦的 70 年代样式。文本设置在 Nunito 中,从 Google 字体加载。

A314884_3_En_4_Fig34_HTML.jpg

图 4-34。随着偏移量的增加,大量的文本阴影会从文本中创建一个对角线阴影

为了进一步增加手绘标牌的感觉,我们可以应用一些效果。首先,我们可以用第一批白色阴影创建一个轮廓效果,因为标志画家通常会在字母和阴影之间留一些空间,这样他们可以更快地工作,因为字母中的油漆不必干燥,就可以移动到阴影中。我们将需要复制白色的阴影,并在所有方向上使用偏移,使其完全围绕字母。

其次,我们可以使用另一个技巧,使阴影看起来随着它的方向改变颜色,创造一个更加伪 3D 的外观,模拟照明方向。这是通过以交错的方式偏移各个阴影来实现的,其中颜色在较亮和较暗的颜色之间交替。这样,我们就利用它们的叠加来使一种颜色在水平方向更加突出,而另一种颜色在垂直方向更加突出。最终结果如图 4-35 所示。

A314884_3_En_4_Fig35_HTML.jpg

图 4-35。我们完成的阴影标题

下面是所描述的两个技巧的结果代码:

h1 {
  /* some properties left out */
  text-shadow:
      /* first, some white outline shadows in all directions: */
       -2px 2px 0 #fff,
       0px -2px 0 #fff,
       0px 3px 0 #fff,
       3px 0px 0 #fff,
       -3px 0px 0 #fff,
       2px 2px 0 #fff,
       2px -2px 0 #fff,
       -2px -2px 0 #fff,
       /* …then some alternating shades that increasingly stick out in either direction: */
       -3px 3px 0 #743b34,
       -4px 3px 0 #a8564d,
       -4px 5px 0 #743b34,
       -5px 4px 0 #a8564d,
       -5px 6px 0 #743b34,
        /* ..and so on… */
       -22px 21px 0 #a8564d,
       -22px 23px 0 #743b34,
       -23px 22px 0 #a8564d,
       -23px 24px 0 #743b34;
}

在 Typekit Practice 网站(practice.typekit.com/lesson/using-shades/)上可以找到一篇关于这种网络阴影和旧式标牌技术的深入文章,该网站也有大量其他学习网络排版艺术的资源。

几乎所有的浏览器都支持文本阴影属性,只有 IE9 和更早的版本没有支持。至于性能,如果支持的话,绘制阴影可能是非常昂贵的操作,所以你应该在你的设计中尽量少用阴影效果。

使用 JavaScript 增强排版

在某些情况下,纯粹的 CSS 并不能解决问题。例如,您可以使用:first-letter 伪元素定位一段文本的第一个字母,但是没有选择器可以单独定位其余的字母。例如,如果您希望每个字母都有不同的颜色,那么您唯一的选择就是用一个元素(比如一个)将每个字母包装起来,并以这些元素为目标。这种方法不太可行,尤其是如果您无法手动控制想要设置样式的元素的标记。

幸运的是,我们可以将这些视觉效果视为一种增强,并使用 JavaScript 自动创建额外的挂钩。lettering.js jQuery 插件(letteringjs.com)将会做到这一点。这个插件背后的一个人是设计者和开发者 Trent Walton。图 4-36 在他个人网站的一个标题中显示了在野外使用的 lettering.js。

A314884_3_En_4_Fig36_HTML.jpg

图 4-36。一个使用 lettering.js jQuery 插件的例子,来自trentwalton.com

有大量不同的基于 JavaScript 的解决方案可以帮助你调整文本。以下是一些例子:

  • fitText.js:来自 lettering.js(来自 agency Paravel)背后的同一个人的 jQuery 插件,用于根据页面大小调整文本大小( fittext.js )。

  • BigText.js:来自 Filament Group 的 Zach Leatherman 的一个脚本,它试图根据其容器(github.com/zachleat/BigText)使一行文本尽可能大。

  • 鳏夫:来自 Gridset.com 的内森·福特的一个脚本,它通过在距离段落结尾一定距离的单词之间插入不间断空格字符(github.com/nathanford/widowtamer)来确保防止意外的寡妇。

注意

SVG 实现了一些非常酷的文本效果,这通常超出了本书的范围。然而,在第十二章中,我们将会看到一些先进的视觉效果技术,其中包括使用 SVG 的可缩放文本的简要介绍。

进一步的类型灵感

网络上的字体设计是一个丰富的研究、实验和挑战极限的领域。有数百年的历史和传统可以探索,并研究我们可以应用什么以及我们如何在网络环境中明智地应用它。

罗伯特·布林赫斯特(Robert Bringhurst)所著的《排版样式的要素》(The Elements of Typographic Style)一书是排版方面的权威之一,该书记录并解释了这一传统的大部分内容。它谈到了我们在本章中讨论过的许多特性,比如垂直节奏,断字和单词间距的细微差别。

前面提到的 Richard Rutter 已经花时间思考如何将 Bringhurst 建立的一些最佳实践带到 Web 上。应用于 Web 的排版样式元素(【http://webtypography.net】)展示了如何使用 HTML 和 CSS 来应用排版传统的特性,如果你对如何为 Web 排版获得更详细的规则和实践感兴趣,这是非常值得一看的。

另一个很好的排版实践指南是 Buttericks 的实用排版,它解释了如何将建议翻译成 CSS,可以在 http://practicaltypography.com/的买到。

最后,杰克·吉尔特索夫的字体链接集“网络字体”(typographyontheweb.com),是一个关于设计和代码的绝佳资源。

记住,如果你在网页上添加任何文本,那么你就是在排版。

摘要

在这一章中,我们已经学习了 CSS 中文本和字体属性的基础知识,以及如何使用它们来获得最大的可读性和灵活性的一些技巧。使用多列布局模块,我们以类似报纸的格式创建了文本集。我们看到了行高和其他间距属性中的系统距离如何让您将文字设置为垂直节奏。

我们研究了如何使用@font-face 规则加载自定义字体,以及影响加载哪个字体文件的各种参数。我们还快速了解了如何使用 Web 字体加载器 JavaScript 库来控制字体加载的感知性能。

我们看了一些更详细的 OpenType 选项,这些选项可用于增强印刷控制—连字、数字和字距—以及字体功能设置属性如何允许我们对如何打开或关闭这些功能进行低级控制。

最后,我们探索了一些尝试更激进的标题和海报字体排版技术的方法,使用文本阴影和 JavaScript 的进一步帮助。

在下一章,我们将看看如何为你漂亮的排版页面搭建舞台:使用图像、背景色、边框和阴影。

五、漂亮的盒子

在前面的章节中,你学习了 HTML 文档的每个元素都是由矩形框组成的:从保存页面结构部分的容器到段落中的文本行。然后,在最后一章中,您学习了如何设计页面文本内容的样式。

如果我们不能增强这些盒子的外观,或者用颜色、形状和图像来补充它们,网页设计就不会有创造性或灵活性。这就是背景、阴影和边框的 CSS 属性,以及通过 img 元素的内容图像和其他嵌入对象的来源。

在本章中,您将了解

  • 背景颜色和不同种类的不透明度

  • 使用背景图像和不同的图像格式

  • 使用 calc()函数对长度进行数学计算

  • 给你的盒子添加阴影效果

  • 使用简单和高级边框效果

  • 用 CSS 生成渐变

  • 设置内容图像和其他嵌入对象的样式和大小

背景颜色

我们将从一个非常基本的例子开始,向整个页面的背景添加一种颜色。下面的代码将我们的背景设置为柔和的绿色:

body {
  background-color: #bada55;
}

我们还可以使用 shorter background 属性设置背景颜色:

body {
  background: #bada55;
}

这两个属性有什么区别?第二个是 background,它是一个简写属性,允许您同时设置许多其他与背景相关的属性。在前面的例子中,我们只在速记中声明了一个背景颜色,但是其他值(对于背景图像)也会受到影响——它们被重置为默认值。这可能会无意中覆盖您已经明确设置的内容,所以要小心使用它——我们将在本章的后面详细讨论它。

颜色值和不透明度

在前面的颜色示例中,我们用十六进制的符号设置值:一个哈希字符(也称为八位数英镑符号数字符号)后跟一个六字符的字符串。该字符串由三组两个字符组成,每组字符的范围为 0 到 F。十六进制意味着每个“数字”可以有 16 个不同的值,因此 0-9 由代表第 11 到第 16 个值的 A-F 来补充:

0123456789ABCDEF

这三对代表颜色的红色、绿色和蓝色(RGB)值。每个颜色通道有 256 个不同的可能值,因此每个颜色通道有两个字符(16 × 16 = 256)。

所有三对颜色在两个地方都有相同的值,允许缩短为三个字符:#aabbcc 变成#abc,#663399 变成#639,依此类推。

小费

您也可以使用许多可用的颜色关键字之一来指定颜色,如红色、黑色、蓝绿色、一枝黄花或暗海绿色。有一些非常奇怪的颜色关键字——它们源于一个名为 X11 的旧图形系统,开发人员依次从一盒蜡笔中选择一些颜色关键字!

很难找到任何好的理由来解释你为什么要使用这些关键字——除了可能为了调试的目的想快速地想出一种颜色。我们将通过使用更精确的方法向前推进。

设置 rgb 值可以用另一种方式完成,使用 RGB()函数符号。RGB 的每个值可以表示为数字(从 0 到 255)或百分比(0%到 100%)。以下是上一节中使用 rgb()符号的示例:

body {
  background-color: rgb(186, 218, 85);
}

十六进制和 rgb()符号从 CSS 1 开始就存在了。最近,我们有了一些处理颜色的新方法:hsl()、rgba()和 hsla()。

首先,有 hsl()函数符号。十六进制和 RGB 表示法都是指计算机如何利用颜色在屏幕上显示它们——红、绿、蓝的混合。hsl()符号是指使用色调-饱和度-亮度(HSL)模型描述颜色的不同方式。色调从一个假设的色轮中获得其值(见图 5-1 ),其中颜色根据您选择的度数逐渐相互转换:红色在顶部(0 度),绿色在周围的三分之一处(120 度),蓝色在三分之二处(240 度)。

A314884_3_En_5_Fig1_HTML.jpg

图 5-1。HSL 色轮

如果你使用过任何类型的图形设计软件,你可能在颜色选择器中看到过一个色轮。要使用 hsl()语法,您需要向它传递表示您想要选择的圆的角度的度数,以及两个百分比。这两个百分比首先代表你想要在颜色混合中使用的“颜料”(饱和度)的数量,然后是亮度。下面是如何用 hsl()符号编写前面的代码:

.box {
  background-color: hsl(74, 64%, 59%);
}

重要的是要注意,选择这两种方法中的任何一种来书写颜色值都没有本质上的区别:它们只是表示同一事物的不同方式。

下一个新的颜色符号是 RGB 的 turbo-powered 版本,称为 rgba()。“a”代表 alpha ,控制透明度的是 alpha 通道。如果我们想要和上一个例子一样的基本背景色,但是现在想要它有 50%的透明度,我们可以用下面的例子:

.box {
  background-color: rgba(186, 218, 85, 0.5);
}

rgba()函数参数中的第四个值是 alpha 值,它是一个介于 1.0(完全不透明)和 0(完全透明)之间的值。

最后,还有 hsla()符号。它与 hsl()的关系与 rgb()与 rgba()的关系相同:您为 alpha 通道传递一个额外的值来选择颜色的透明度。

.box {
  background-color: hsla(74, 64%, 59%, 0.5);
}

现在你知道了如何让颜色变得或多或少的透明,需要注意的是,在 CSS 中还有另外一种控制透明度的方法。这可以通过不透明度属性来实现:

.box {
  background-color: #bada55;
  opacity: 0.5;
}

这会让我们。框元素的颜色和透明度与上一个示例相同。那么这里有什么不同呢?在前面的例子中,我们只将背景色设为透明,但是在这里我们将整个元素设为透明,包括其中的任何内容。当一个元素使用 opacity 设置为透明时,不可能使其中的子元素变得不透明。

实际上,这意味着具有透明度的颜色值非常适合制作半透明的背景或文本,而较低的不透明度会使整个元素淡出。

警告

注意文字和背景颜色的对比!虽然这本书不是关于设计理论本身,但我们想强调的是,网页设计是关于你的用户能够接受你创建的网页上的信息。背景和文本颜色对比选择不当会影响在阳光下用手机访问你的网站的人,屏幕不好的人,视力受损的人等等。关于色彩对比的一个很好的资源是在 http://contrastrebellion.com/的现场对比反叛。

背景图像基础

添加背景色是创建更有趣页面的好工具。有时我们想更进一步,在我们的元素上使用图像作为背景,无论是微妙的图案,解释用户界面的象形图,还是给页面增加一些额外字符的更大的背景图形(见图 5-2 )。CSS 有很多工具可以做到这一点。

A314884_3_En_5_Fig2_HTML.jpg

图 5-2。https://teamtreehouse.com 上的博客使用了褪色和彩色的背景图片

背景图像与内容图像

首先:什么时候图像是背景图像?您可能知道有一个专门用于向网站添加内容图片的 HTML 元素:img 元素。我们如何决定在 CSS 中什么时候使用 img,什么时候使用背景图片?

简单的答案是,任何可以从网站上删除但仍然有意义的东西都应该作为背景图片来应用。或者换句话说:如果网站具有完全不同的外观和感觉,任何仍然有意义的东西都应该是内容图像。

可能会出现界限不清的情况,最终你为了达到特定的视觉效果而变通了规则。请记住,任何来自 img 元素的内容图片,如果纯粹是为了装饰你的网站,可能会出现在其他地方,在那里你的内容最好不要被打扰:例如,在 feed 阅读器和搜索结果中。

使用背景图像的简单示例

想象一下,我们正在设计一个页面,类似于 Twitter 或脸书等社交网站的个人资料页面上的大标题(见图 5-3 )。

A314884_3_En_5_Fig3_HTML.jpg

图 5-3。上一页简介https://twitter.com

相反,我们的页面将是一个猫的社交网络,在这一章中,我们将使用各种属性来创建一个类似图 5-4 的头部组件。

A314884_3_En_5_Fig4_HTML.jpg

图 5-4。带有文本和个人资料图片的巨型标题图像和个人资料框

首先,我们将添加一个默认的蓝灰色背景颜色和一个背景图像,以及一些尺寸到页面的大标题。如果图像加载失败,添加默认背景色很重要:

.profile-box {
  width: 100%;
  height: 600px;
  background-color: #8Da9cf;
  background-image: url(img/big-cat.jpg);
}

该组件的 HTML 可能如下所示:

<header class="profile-box">
</header>

这样做的结果可以在图 5-5 中看到:我们的图像被加载并平铺在整个轮廓框中。

A314884_3_En_5_Fig5_HTML.jpg

图 5-5。背景图像沿两个方向平铺在轮廓框上

为什么它像那样平铺在整个盒子上?因为默认值的另一个属性与背景图像有关,名为 background-repeat。默认值 repeat 表示图像在 x 轴和 y 轴上重复。这对于包含图案的背景来说非常有用,但对于照片来说可能不是。我们可以通过将值设置为 repeat-x 或 repeat-y 来将其限制为任意方向,但现在我们将通过将其设置为 no-repeat 来完全移除平铺效果:

.profile-box {
  background-image: url(img/cat.jpg);
  background-repeat: no-repeat;
}

3 级背景和边框规范使用扩展的语法和新的关键字重新定义了该功能。首先,它允许您指定两个方向的重复值,用空格分隔关键字,因此下面的内容相当于设置 repeat-x:

.profile-box {
  background-repeat: repeat no-repeat;
}

其次,它定义了一些新的关键字。在支持的浏览器中,您可以将 space 或 round 设置为一个或两个关键字。使用空间意味着,如果背景图像两次或更多次适合元素内部(不进行裁剪或调整大小),它将重复适合的次数并隔开,以便背景图像的第一个和最后一个“副本”接触元素的边缘。使用 round 意味着将调整图像的大小,使其多次适合元素内部。

老实说,这些新的背景重复功能可能不是非常有用。如果你想使用一个符号或重复的图案作为背景,并想在设计中保持某种对称,它们会很方便,但它们也很难保持图像的纵横比。支持也参差不齐:老版本的浏览器被遗漏了,但是即使是现代版本的 Firefox 也缺少支持。

加载图像(和其他文件)

在使用 url()函数符号时,就像我们在前面的例子中所做的那样,我们可以使用一个相对 URL——例如 url(img/cat.jpg)。浏览器将尝试在 img 子目录中查找与保存 CSS 本身的文件相关的文件 cat.jpg。如果路径以斜杠“/img/cat.jpg”开头,浏览器将在顶级 img 目录中查找图像,该目录与加载 CSS 文件的域相关。

我们也可以使用绝对 URL。绝对 URL 的一个例子是,如果我们精确地指定哪种协议、域和路径的组合指向图像,就像example.com/img/my-background.jpg

除了绝对和相对 URL,我们可以选择加载图像(和其他资源)而不指向任何文件,而是将数据直接嵌入样式表中。这是通过一种叫做数据 URI 的东西来完成的,在这里,文件中的二进制编码数据被转换成一长串文本。有很多工具可以帮你做到这一点,包括在线版本,如 http://duri.me/的。

然后,您可以将该文本粘贴到 url()函数中,并将数据保存为样式表的一部分。它看起来像这样:

.egg {
  background-image:
    url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC gAAAAoAQAAAACkhYXAAAAAjElEQVR4AWP…
    /* ...and so on, random (?) data for a long time.. */
...4DwIMtzFJs99p9xkOXfsddZ/hlhiY/AYib1vsSbdn+P9vf/1/hv8//oBIIICRz///r3sPMqHsPcN9MLvn1s6SfIbbUWFl74HkdTB5rWw/w51nN8vzIbrgJDuI/PMTRP7+ByK//68HkeUg8v3//WjkWwj5G0R++w5WyV8P1gsxB2EmwhYAgeerNiRVNyEAAAAASUVORK5CYII=);
}

带数据的起始位:image/png;base64 告诉浏览器应该得到什么样的数据,其余的是转换成字符串的图像的实际像素数据。

使用嵌入式数据 URIs 有好有坏——使用它们的主要原因是为了减少 HTTP 请求的数量,但同时它们会增加样式表的大小,所以要谨慎使用。

图像格式

您可以在网上使用几种不同格式的图像文件,它们都可以作为内容图像或背景图像。这里有一个简短的介绍:

  • JPEG: 一种位图格式,可以高度压缩,但在细节上有一些质量损失,适合照片。不支持透明。

  • PNG: 一种无损压缩的位图格式,这种格式不适合照片(它会创建非常大的文件),但对于图标或插图之类的“扁平”图形,它可以实现非常小的文件大小。可以是透明的。

  • GIF: 一种旧的位图格式,类似于 PNG,主要用于猫的动画图片。说真的,除了动画图像之外,它在很大程度上已经被 PNG 所取代:PNG 也支持动画图像,但浏览器支持有点落后。GIF 支持透明度,但不支持 alpha 级别,因此边缘看起来经常呈锯齿状。

  • SVG: 一种矢量图形格式,也是它自己的标记语言。SVG 既可以直接嵌入网页,也可以作为背景图像或内容图像的来源。

  • WebP: 由 Google 开发的一种新格式,压缩效率极高,结合了 JPEG(高度可压缩)和 PNG(透明)的特点。到目前为止,浏览器支持非常不稳定(只有 Chrome 和 Opera 等基于 Blink 的浏览器),但这可能会很快改变。

除了 SVG 之外,所有这些都是位图格式,这意味着它们包含逐像素的数据,并且具有固有的尺寸(意味着“内置”的宽度和高度)。对于细节层次高的图形元素,如照片或详细的插图,这是有意义的。但是对于许多用途来说,真正有趣的格式是 SVG,它包含如何在屏幕上绘制特定形状的指令。这使得 SVG 图像可以自由调整大小或以任何像素密度显示在屏幕上:它们永远不会失去任何清晰度或细节水平。

SVG 本身是一个足以写几本书的主题(事实上,有许多这样的书存在),但是我们仍然希望在本书中让您对 SVG 的灵活性有所了解(特别是在第十一章,当我们看到 CSS 中一些更前沿的视觉效果时)。SVG 是一种古老的格式(它从 1999 年就出现了),但是近年来浏览器支持变得足够广泛,使得 SVG 成为一种可行的替代方案。唯一坚持的是有点古老的 Internet Explorer 版本(版本 8 和更早版本)和 Android 上的 WebKit browers 的早期版本(版本 2 和更早版本)。

背景图像语法

回到图 5-5 ,我们开始创建带有 JPEG 格式背景图像的个人资料页面示例,因为它是一张照片。到目前为止,我们已经把它放在了元素的背景中,但是它看起来还不是很好。我们将讨论让您调整背景图像的属性。

背景位置

我们可以试着把我们的图像放在元素的中心。背景图像的位置由 background-position 属性控制。

我们还使用了更大版本的图像文件,以确保它即使在更大的屏幕上也能覆盖元素(见图 5-6 )。在较小的屏幕上,边会被剪掉,但至少图像是居中的。

A314884_3_En_5_Fig6_HTML.jpg

图 5-6。我们的页面有一个更大的,居中的背景图片来覆盖整个元素
.profile-box {
  width: 100%;
  height: 600px;
  background-color: #8Da9cf;
  background-image: url(**img/big-cat.jpg**);
  background-repeat: no-repeat;
**background-position: 50% 50%;** 
}

您可以使用关键字或单位(如像素、ems 或百分比)来设置背景位置属性值。最简单的形式是,该值由两个子值组成:一个表示从左侧的偏移量,一个表示从顶部的偏移量。

注意

有些浏览器支持 background-position-x 和 background-position-y 属性,这两个属性分别在每个轴上定位图像。这些最初是 IE 中的非标准属性,但是正在被标准化。在撰写本文时,基于 Mozilla 的浏览器仍然不支持它们。

如果使用 pixels 或 ems 设置这些值,图像的左上角将从元素的左上角开始按指定的像素数定位。因此,如果您指定 20 个像素的垂直和水平位置,图像的左上角将出现在距离元素左边缘 20 个像素和距离元素上边缘 20 个像素的位置。使用百分比的背景定位略有不同。百分比定位不是定位背景图像的左上角,而是使用图像上的相应点。如果您将垂直和水平位置设置为 20%,那么您实际上是将一个点定位在距离图像的顶部和左侧边缘 20%的位置,距离父元素的顶部和左侧边缘 20%的位置(参见图 5-7 )。

A314884_3_En_5_Fig7_HTML.gif

图 5-7。当使用像素定位背景图像时,使用图像的左上角。使用百分比定位时,会使用图像上的相应位置

关键字对齐的工作方式是将 x 轴和 y 轴的一个或两个测量值替换为 x 轴的左侧、中间或右侧,或者 y 轴的顶部、中间或底部。你应该养成这样的习惯,总是先声明 x,再声明 y。这是为了一致性和可读性,也是为了避免错误:规范允许你改变顺序,如果你使用两个关键字(比如左上),但是当一个是关键字,一个是长度时就不允许了。以下内容将被破坏:

.box {
  background-position: 50% left; /* don’t do this */
}

背景定位的约束已经困扰设计师很久了。考虑图 5-8 中的设计:我们有一些未知长度的文本,在最右边有一个图标图像,周围有一些空白。使用像素或 ems 来定位图像是没有用的,因为我们不知道图像应该位于离左边多远的地方。

A314884_3_En_5_Fig8_HTML.gif

图 5-8。右边缘带有图标作为背景图像的一段文本

以前,除了为图标提供自己的包装元素和定位之外,唯一的解决方案是使用一个背景图像,将其定位在距离左边 100%的位置,并将右边的空白作为透明像素烘焙到图像文件中。这不是很优雅,因为它没有让我们通过 CSS 来控制这些空白。幸运的是,3 级背景和边框规范支持我们!

背景位置的新语法允许我们做我们所希望的事情,就像刚才描述的那样:我们可以用相应的 edge 关键字作为每个距离的前缀,我们想用它作为参考。看起来是这样的:

<p>
  <a href="/activate" class="link-with-icon">Activate flux capacitor</a>
</p>

.link-with-icon {
  padding-right: 2em;
  background-image: url(img/icon.png);
  background-repeat: no-repeat;
  background-position: **right 1em top 50%**;
}

前面的例子意味着我们将图像定位在距离右边缘 1 em,距离顶部 50%的位置。问题解决了!可悲的是,这个版本的语法在 IE8 或 7 版之前的 Safari 中不起作用。根据您的使用情况,它可以作为一种增强,但在不受支持的浏览器中很难优雅地降级。

介绍 Calc

实际上,我们可以通过引入另一个具有更广泛支持的 CSS 构造(calc()函数符号)来获得与上一节中的示例相同的结果。使用 calc 可以让浏览器为你计算任何类型的数字(角度、像素、百分比等)。).它甚至可以处理混合单元,直到页面被渲染才知道!这意味着您可以说“100% + x 像素数”,例如,这对于以百分比表示的大小或位置与以 ems 或像素设置的其他距离相冲突的任何情况都非常有用。

在我们之前讨论的“背景图像从右侧定位”问题的情况下,我们可以使用 calc()符号来表示 x 轴上的相同位置:

.link-with-icon {
  /* other properties omitted for brevity. */
  background-position: **calc(100% - 1em) 50%**;
}
注意

Internet Explorer 9 确实支持 calc()符号,但遗憾的是,当它专门用于背景位置时,有一个严重的错误,导致浏览器崩溃。因此,前面的例子主要是理论上的。calc()函数对于许多其他情况也很有用——元素大小、字体大小等等。

calc()函数符号与加法(+)、减法(-)、乘法(*)和除法(/)这四个运算符一起使用。calc()表达式中可以有多个值;以下规则集中的声明也完全有效:

.thing {
  width: calc(50% + 20px*4 - 1em);
}
注意

使用 calc()时,在使用加法和减法时,运算符两边需要有空格。这显然是为了更清楚地将运算符与数字上的任何符号区分开来,例如长度-10 px。

calc()符号是在 Level 3 值和单位规范中定义的,它有相当不错的支持。就像你之前看到的“四值”背景位置一样,IE8 和更早的版本,以及更老的 WebKit 浏览器,正在失去乐趣。一些稍旧版本的基于 WebKit 的浏览器确实支持它,但可能需要一个前缀,形式为-webkit-calc()。

背景剪辑和原点

默认情况下,用于背景的图像将绘制在元素的边框上,这意味着它们可能会覆盖元素的所有可见边缘。请注意,由于它们被绘制在任何边框的下面,半透明的边框可能会显示在图像的顶部。

背景剪辑属性可以改变这种行为。默认对应于背景剪辑:边框框。设置值 padding-box 将切换到裁剪边框内的图像,覆盖填充框,设置值 content-box 将裁剪任何填充内的图像,到内容框。图 5-9 显示了不同之处。

A314884_3_En_5_Fig9_HTML.jpg

图 5-9。剪裁到边框(左)、填充框(中)和内容框(右)的背景之间的差异
.profile-box {
  border: 10px solid rgba(220, 220, 160, 0.5);
  padding: 10px;
  background-image: url(img/cat.jpg);
**background-clip: padding-box;** 
}

即使更改了背景剪辑值,背景位置的默认原点(即图像开始定位的参考点)仍然是填充框的左上角,这意味着定位值从元素上任何边框的内部开始。

幸运的是,您也可以通过背景-原点属性来影响原点位置。它接受与背景剪辑相同的与框模型相关的值:边框、填充框或内容框。

背景剪辑和背景源都是前面提到的 3 级背景和边界规范的一部分。它们已经存在了一段时间,但仍然缺乏在真正老的浏览器中的支持:再次,IE8 是主要的落后者,但这次即使是老的 Android 浏览器也实现了这些属性,尽管带有-webkit-前缀。

背景附件

背景附加到显示在后面的元素上。如果你滚动页面,背景也随之滚动。可以通过 background-attachment 属性来更改此行为。如果我们希望标题图像的背景在用户向下滚动时“粘”在页面上,我们可以使用下面的代码:

.profile-box {
  background-attachment: fixed;
}

图 5-10 试图捕捉用户滚动页面时背景的行为:它给出了页眉隐藏在页面其余部分后面的外观,这可能是一个很酷的效果。

A314884_3_En_5_Fig10_HTML.jpg

图 5-10。带有固定背景附件的个人资料标题

除了固定和默认值,滚动,你可以设置背景附件到本地。很难在纸上说明,但是局部值影响元素滚动位置内的附件:当元素有滚动条时,它通过将溢出属性设置为 auto 或 scroll 并使内容足够高以突出元素,使元素内容一起滚动。如果我们在标题上这样做,那么当页面滚动时,背景图像将随着元素一起滚动,但是当内部滚动位置改变时,背景图像也会随着内容一起滚动。

本地值在桌面浏览器中得到相对较好的支持,但在移动浏览器中却不太可靠:有理由认为一些移动浏览器制造商忽略了这一属性(以及固定值),因为元素滚动是不常见的,并且会对使用触摸滚动的小屏幕产生可用性影响。事实上,该规范还允许实现者忽略背景附件,如果它被认为在设备上不合适的话。移动浏览器专家彼得-保罗·科赫(Peter-Paul Koch)在他的网站 QuirksMode.org(http://www . quirksmode . org/blog/archives/2013/03/new _ CSS _ tests _ c _ 2 . html)上有一篇关于这个主题的文章(以及其他移动浏览器测试的宝库)。

背景尺寸

在上一节的例子中,我们使用了一个更大的图像来覆盖轮廓框。这意味着在较小的浏览器窗口中查看时,它会被剪切。当窗户变得很大时,它的侧面也可能会有缝隙。假设我们希望防止这种情况,并在缩放页面时保持内容的纵横比,我们需要利用 background-size 属性。

通过将 background-size 设置为显式长度度量,您可以将背景图像的大小调整为新的固定度量,或者让它随元素一起缩放。

如果我们仍然有一个大文件,并且出于某种原因想要显示得小一些,我们可以给它新的像素度量:

.profile-box {
  background-size: 400px 240px;
}

让图像和框一起缩放意味着我们需要切换到使用百分比。您可以设置宽度和高度的百分比,但是这些百分比与图像的固有大小无关,而是与容器的大小有关:如果容器的高度随内容而变化,这可能会扭曲图像的纵横比。

使用百分比的一个更明智的方法是对一个值使用 percent,对另一个值使用关键字 auto。例如,如果我们希望图像的宽度为 100%(x 轴,第一个值),并保持其纵横比(见图 5-11 ),我们可以使用以下方法:

A314884_3_En_5_Fig11_HTML.jpg

图 5-11。使用百分比和 auto 关键字设置背景大小允许背景覆盖元素的宽度,而不管视口大小如何
.profile-box {
  background-size: **100% auto**;
}

使用百分比给了我们一些灵活性,但并不适用于所有情况。有时,我们可能希望确保背景永远不会被裁剪,在 profile header 示例中,我们可能希望确保背景总是覆盖元素的整个区域。幸运的是,有一些神奇的关键字为我们解决了这个问题。

首先,我们可以使用关键字 contain 作为背景尺寸。这意味着浏览器将试图在不扭曲其纵横比或裁剪图像的情况下尽可能放大图像:这几乎与上一个示例类似,但它会自动确定哪个值应该是 auto,哪个值应该是 100%(见图 5-12 )。

A314884_3_En_5_Fig12_HTML.gif

图 5-12。使用 contain 关键字作为背景大小可以防止裁剪
.profile-box {
  background-size: contain;
}

在又高又窄的元素中,正方形背景最多 100%宽,但可以留有垂直间隙;在宽元素中,它最多 100%高,但会留下水平间隙。

我们可以使用的第二个关键字值是 cover:这意味着图像的大小可以完全覆盖元素的每个像素,而不会扭曲图像。这就是我们想要在我们的个人资料页面的例子。图 5-13 显示了一个窄而高的元素上的正方形背景如何填充高度但剪裁侧面,一个宽的元素如何在填充元素宽度的同时剪裁顶部和底部,配置如下:

A314884_3_En_5_Fig13_HTML.gif

图 5-13。使用 cover 关键字在裁剪背景时完全覆盖元素的表面
.profile-box {
  background-size: cover;
}

与 clip 和 origin 的属性一样,background-size 是一个相对较新的背景属性,支持级别也类似。

背景速记

正如我们在本章开始时看到的,有一个后台简写语法,用于同时设置许多与后台相关的属性。一般来说,你可以随意指定不同的值——浏览器会从不同的关键字和语法中判断出你的意思。不过,还是有一些问题。

首先,由于长度对可用于背景位置和背景大小,您需要将它们写在一起,首先是背景位置,然后是背景大小,并用斜杠(/)字符分隔它们。

第二个是用于背景源和背景剪辑的*-box 关键字。以下规则适用:

  • 如果只有一个*-box 关键字(border-box、padding-box 或 content-box),则两个值都被设置为声明的值。

  • 如果有两个*-box 关键字,第一个设置背景-原点,第二个设置背景-剪辑。

下面是一个将各种背景属性组合在一起的例子:

.profile-box {
 background: url(img/cat.jpg) 50% 50% / cover no-repeat padding-box content-box #bada55;
}

正如我们在本章开始时所说的,要小心后台简写:它会自动将您没有提到的所有值设置回它们的默认值。如果您确实要使用它,首先放入速记声明,然后根据需要覆盖特定的属性。尽可能多地使用快捷键来节省一些击键次数可能很诱人,但作为编写代码的一般规则,显式代码通常比隐式代码更不容易出错,也更容易理解。

多重背景

到目前为止,我们已经处理了背景图像,就好像你总是使用一个单独的图像作为背景。过去是这样,但是 3 级背景和边框规范中定义的背景属性现在允许您为单个元素指定多个背景,并为每个属性指定相应的多值语法。多个值用逗号分隔。下面是一个例子,如图 5-14 所示:

A314884_3_En_5_Fig14_HTML.jpg

图 5-14。一个元素上的多个重叠背景
.multi-bg {
  background-image: url(img/spades.png), url(img/hearts.png),
                    url(img/diamonds.png), url(clubs.png);
  background-position: left top, right top, left bottom, right bottom;
  background-repeat: no-repeat, no-repeat, no-repeat, no-repeat;
  background-color: pink;
 }

背景层在声明时从上到下堆叠,第一个在顶部,最后一个在底部。颜色层在它们后面结束(见图 5-15 )。

A314884_3_En_5_Fig15_HTML.jpg

图 5-15。多个背景层按照声明的顺序从上到下堆叠。颜色层总是在底部

您还可以声明多个后台简写值:

.multi-bg-shorthand {
  background: url(img/spades.png) left top no-repeat,
              url(img/hearts.png) right top no-repeat,
              url(img/diamonds.png) left bottom no-repeat,
              url(img/clubs.png) right bottom no-repeat,
              pink;
}

使用这种语法,你只允许在最后一个背景层上声明一种颜色,考虑到图 5-15 中的顺序,这是有意义的。

如果任何背景属性的值列表短于背景图像的数量,则值列表会循环。这意味着如果它们的值都相同,你只需要声明一次:如果它在两个值之间交替,你只需要声明两个,以此类推。因此,前一个示例中的循环不重复可以写成如下形式:

.multi-bg-shorthand {
  background: url(img/spades.png) left top,
              url(img/hearts.png) right top,
              url(img/diamonds.png) left bottom,
              url(img/clubs.png) right bottom,
              pink;
  background-repeat: no-repeat; /* goes for all four */
}

因为多重背景的东西来自于 3 级规范,所以它在一些老的浏览器中也不可用。很多时候,通过使用单值后台语法的组合,您可以为较旧的浏览器实现完全可以接受的回退:

.multi-fallback {
  background-image: url(simple.jpg);
  background-image: url(modern.png), url(snazzy.png), url(wow.png);
}

就像书中的其他例子一样,旧的浏览器将获得更简单的第一个图像并丢弃第二个声明,而新的浏览器将忽略第一个,因为第二个会覆盖它。

边框和圆角

我们在第三章中提到了边界是盒子模型属性的一部分。在现代浏览器中,我们对边框有了进一步的控制,允许我们用图像和圆角给它们增添情趣——所以我们终于可以创建除了尖锐矩形之外的东西了!

首先快速回顾一下旧的边界属性:

  • 您可以分别设置边框各边的属性,也可以同时设置所有边的属性。

  • 您可以使用 border-width 设置整个边框的宽度,或者使用 border-top-width 设置特定的边。请记住,边框的宽度决定了框的整体大小,除非 box-sizing 属性另有说明。

  • 同样,您可以用 border-color 设置整个边框的颜色,或者用 border-left-color 设置特定的边。

  • 边框的样式、边框样式(或右边框样式等。)是由关键字设置的:实线、虚线或虚线是非常常用的。还有一些更奇特的,比如 double(在由 border-width 指定的表面上画两条平行线)、groove 和 inset。老实说,这些很少有用:一是因为它们看起来很时髦,二是因为你让浏览器来控制它们的外观——这在标准中并没有明确规定。您也可以通过设置 border-style: none 来完全移除边框。

  • 最后,您可以使用 border 速记来设置所有的边框属性。简写将所有边的宽度、样式和颜色设置为相同的值,如下所示:border:2px solid # 000;。

边框半径:圆角

很长一段时间,圆角一直是开发者的首选。我们花了无数的时间,使用可扩展的、跨浏览器工作的图像来想出新的方法。事实上,这本书以前的版本详细描述了它们。今天,幸运的是,我们已经过了那个阶段。目前唯一不支持边框半径属性的浏览器是老版本的 IE 浏览器(8 及以下版本)和 Opera Mini。关于圆角的事情是,它们通常是一个很好的特性,而不是页面可用性的关键,所以我们认为使用标准化的属性是有意义的,而不是让一些最弱的浏览器(就性能而言)负担更多的代码,以模拟所有其他浏览器中存在的东西。

边框半径速记

这一次,我们将从速记属性开始——因为这是最常见的用例——使盒子上的所有角都变圆。

border-radius 属性允许您通过简单地声明一个长度值来一次设置所有的角。让我们添加一个侧面照片框到我们的例子,并使角落变圆。首先,一些标记:

<header class="profile-box" role="banner">
  <div class="profile-photo">
    <img src="img/profile.jpg" alt="Charles the Cat">
    <h1 class="username">@CharlesTheCat</h1>
  </div>
</header>

下面是添加的 CSS 来调整我们的个人资料照片框的大小和位置,以突出标题区域的底部,并给它一个边框以突出背景(参见图 5-16 中的结果):

A314884_3_En_5_Fig16_HTML.jpg

图 5-16。轮廓照片组件上的圆角
.profile-box {
  position: relative;
  /* other properties omitted for brevity */
}

.profile-photo {
  width: 160px;
  min-height: 200px;
  position: absolute;
  bottom: -60px;
  left: 5%;
  background-color: #fff;
  border: 1px solid #777;
**border-radius: 0.5em;** 
}

更复杂的边框半径语法

您也可以使用速记属性来单独设置每个值。首先从左上角开始,然后顺时针旋转:

.box {
  border-radius: 0.5em 2em 0.5em 2em;
}

这个声明中的每个长度值已经是一个简写,因为它在每个角的水平轴和垂直轴上表示相同的半径。如果需要不同的值(即不对称的角形状),可以将每个轴指定为一个值列表(先水平,再垂直),并用斜杠分隔两个轴:

.box {
  border-radius: 2em .5em 1em .5em / .5em 2em .5em 1em;
}

如果值对角地反映在角上,您可以省略右下角和左下角;如果只有两个或三个值,其余的将被填入:

.box {
  border-radius: 2em 3em; /* repeated for bottom right and bottom left. */
}

在前面的示例中,第一个值设置左上角和右下角,第二个值设置右上角和左下角。如果我们为右下角包含第三个值,左下角将获得与右上角相同的值。

在单个角上设置边界半径

当然,您可以使用边框左上半径、边框右上半径等来设置单个角的值。

为这些单个拐角属性提供与前面速记示例中相同的半径长度:一个长度值(创建对称拐角)或两个长度值(用斜线分隔,第一个设置水平半径,第二个设置垂直半径)。

下面是应用于我们的个人资料照片框的单个对称圆角的代码,如图 5-17 所示:

A314884_3_En_5_Fig17_HTML.jpg

图 5-17。我们的个人资料照片框的一个版本,只有左上角是圆形的
.profile-photo {
  border-top-left-radius: 1em;
}

创建具有边框半径的圆形和药丸形状

到目前为止,我们一直在谈论使用长度值来设置半径,但您也可以使用百分比。以百分比设置边框半径时,x 半径与元素的宽度相关,y 半径与元素的高度相关。这意味着我们可以很容易地创建一个正方形元素,然后将其边界半径设置为至少 50%的圆形。

为什么是“至少”?嗯,确实没有理由为所有拐角设置高于 50%的值,但是知道当两条拐角曲线开始重叠时,两轴都减小,直到它们不再重叠,这可能是有用的。对于正方形上的对称角,任何高于 50%的值都会产生一个圆(见图 5-18 )。注意,具有相同边界半径的矩形元素将变成椭圆形,因为半径与该方向的尺寸成比例减小:

A314884_3_En_5_Fig18_HTML.jpg

图 5-18。圆形和椭圆形使用边界半径:50%
<div class="round"></div>
<div class="round oval"></div>

.round {
  width: 300px;
  height: 300px;
  border-radius: 50%;
  background-color: #59f;
}
.oval {
  width: 600px;
}

圆形通常是理想的,但椭圆形就不那么理想了。有时,我们想要一个“药丸形状”——一个带半圆的长方形元素。这种形状的专业术语(如图 5-19 所示)是一个 obrund 。百分比或精确的长度测量不会帮助我们创建这样的形状,除非我们知道元素的精确测量,这在网页设计中很少出现。

A314884_3_En_5_Fig19_HTML.jpg

图 5-19。使用大的边界半径来创建药丸形状

然而,我们可以使用一种奇怪的边界半径计算来创建这个形状。我们看到,当半径不再合适时,半径会减小。但是当它被设置为一个长度时(不是一个百分比),半径与元素的大小无关,相反它们最终是对称的。因此,为了创建一个 obrund 的半圆边,我们可以欺骗和使用一个长度,我们知道这个长度比创建一个半圆边所需的半径长,并且这个形状会自己创建:

.obrund {
  border-radius: 999em; /* arbitrarily very large length */
}

关于边框半径的最后一点,你应该知道它们是如何影响页面上元素的形状的。我们终于找到了一种方法来创建矩形之外的东西,但是唉:就布局而言,它们仍然会表现得好像它们是覆盖盒子原始表面的矩形。在如何解释元素的形状方面,改变了的一点是,元素的可点击(或“可触摸”)表面遵循了角的形状。在创建圆角按钮、链接等时,请记住这一点。,以便可点击的表面不会变得太小。

边框图像

级别 3 背景和边框规范还允许您定义一个图像作为元素的边框。你可能会问,单一图像有什么好处?border-image 属性的美妙之处在于,它允许您根据“切割”位置的规则将图像分割成九个独立的部分,浏览器将自动为相应的边框部分使用正确的部分。这种技术被称为九切片缩放,有助于避免在调整图像大小以覆盖框时通常会出现的失真。这有点难以想象,所以举个例子吧。

使用边框图像的典型例子可能是为元素创建类似图片框的东西。图片框架的来源是一个边长为 120 像素的正方形图像。如果你从盒子的上、右、下、左边缘画 40 像素的线,你将把盒子分成九个部分(见图 5-20 )。

A314884_3_En_5_Fig20_HTML.gif

图 5-20。我们的边框图像的源文件,为了便于说明,分割点画在上面

border-image 属性将自动使用每个扇区中的图像作为相应边框部分的背景。左上角的图像切片将用作该角的图像,中上位的切片将用于上边框,右上角的切片用于该角,依此类推。默认情况下,中间的切片会被丢弃,但是您也可以更改这种行为。

您还可以告诉浏览器在覆盖边框时如何处理上、右、下和左位。它们可以拉伸、重复或间隔,四舍五入显示的完整重复次数:它的工作方式很像更新的背景重复关键字。默认情况下,每一侧的中间切片都被拉伸,这很好地满足了我们的目的。

为了显示边界图像,还需要设置边界宽度——测量将根据特定线段的边界宽度拉伸每个切片。

将此图形用作边界图像,我们可以创建类似于我们在图 5-21 中看到的“座右铭”的东西。

A314884_3_En_5_Fig21_HTML.jpg

图 5-21。拉伸边框图像以适合元素

这个组件的 CSS 如下所示:

.motto {
  border-width: 40px solid #f9b256;
  border-image: url(picture-frame.png) 40;
  /* ...same as border-image: url(picture-frame.png) 40 40 40 40 stretch; */
}

前面的代码将加载图像 picture-frame.png,从四个边中的每个边对其进行 40 像素的切片,并在顶部、右侧、底部和左侧拉伸中间的切片。请注意,切片辅助线的“20 像素”测量值是在没有 px 单位的情况下给出的;这是一个与矢量图像(SVG)和位图图像之间的差异有关的怪癖。

关于前面的例子,另一件值得一提的事情是,您需要在 border-image 属性之前放置 border 速记(如果使用的话)。规范要求速记重置所有的边界属性,而不仅仅是它自己设置的属性。

如您所料,有特定的边框图像属性来分别设置每个值。事实上,有一大堆值可以让你控制边框图像的工作方式。事情是这样的,我们一方面可以计算出在我们的职业生涯中使用边框图像的次数,所以我们不会在这里做更多的细节。

几年前,边框图像支持在许多网页设计师的愿望清单上排在第一位,主要是因为它可以方便地创建圆角而无需黑客攻击。现在我们有了边界半径,这种需求就不那么迫切了。当然,根据项目的设计,边框图像可能是一个很好的选择——例如,很容易看出位图图像作为边框如何有利于古灵精怪的美感。

如果你想更深入地了解边框图像属性的复杂性,可以看看诺拉·布朗关于 CSS 技巧的文章:css-tricks.com/understanding-border-image/。对边框图像属性的支持相当广泛——主要是 Internet Explorer 10 和更早版本缺少支持。可悲的是,即使在支持浏览器中也存在相当多的错误和怪癖。

箱形阴影

暂时把背景图片和边框放在一边,我们将探索另一种给页面添加视觉效果的方法:阴影。过去,设计师不得不通过使用额外的元素和图像来为他们的设计添加阴影。不再是了!

CSS 允许您使用 box-shadow 属性添加阴影。它得到了很好的支持。事实上,几乎只有老版本的 IE(8 版及更早版本)和 Opera Mini 被遗漏了。为了支持旧的 Android WebKit 浏览器(和其他一些旧的 WebKit 版本),您需要-webkit-前缀。Firefox(和其他基于 Mozilla 的浏览器)已经有了足够长的时间来安全地跳过-moz 前缀。

你已经在前一章看到了文本阴影的语法:框阴影有一个非常相似的语法,但是增加了一些额外的东西。

让我们添加一个阴影到个人资料照片框来说明,使用以下标记和 CSS(图 5-22 显示结果):

A314884_3_En_5_Fig22_HTML.jpg

图 5-22。添加了细微阴影的轮廓图像框
.profile-photo {
  box-shadow: .25em .25em .5em rgba(0, 0, 0, 0.3);
}

本例中的语法与文本阴影版本完全相同:x 和 y 偏移的两个值,然后是模糊半径值(阴影边缘模糊的程度),最后是颜色,使用 rgba()。注意阴影是如何跟随圆形盒子的拐角形状的!

扩散半径:调整阴影的大小

box-shadow 属性比 text-shadow 更灵活一些。例如,您可以在模糊半径后添加一个值来指定扩散半径:阴影应该有多大。默认值为 0,表示与其应用的元素大小相同。增加该值会使阴影变大,负值会使阴影变小(参见图 5-23 )。

A314884_3_En_5_Fig23_HTML.jpg

图 5-23。显示有不同扩散半径值的框
.larger-shadow {
  box-shadow: 1em 1em .5em **.5em** rgba(0, 0, 0, 0.3);
}
.smaller-shadow {
  box-shadow: 1em 1em .5em **-.5em** rgba(0, 0, 0, 0.3);
}

嵌入阴影

另一个比文本阴影更灵活的额外的方框阴影功能是 inset 关键字。应用插入阴影意味着元素被假定为阴影投射到的表面,从而产生它被从背景中“剔除”的效果。例如,我们可以使用插入阴影效果,使它看起来像我们的个人资料标题的背景有点沉入页面,在个人资料照片和其余内容的后面。我们将向配置文件框规则集添加以下内容(参见图 5-24 ):

A314884_3_En_5_Fig24_HTML.jpg

图 5-24。轮廓标题组件的详细信息,显示了大背景底部边缘上的嵌入框阴影
.profile-box {
  box-shadow: inset 0 -.5em .5em rgba(0, 0, 0, 0.3);
}

多重阴影

就像文本阴影一样,您可以对单个元素应用多个阴影,用逗号分隔不同的值。我们将看一个例子来说明如何将它与“平面”阴影技术相结合,并完全消除模糊半径。

如果你忽略模糊半径或将其设置为 0,你将得到一个边缘非常清晰的阴影。这可能是有益的,因为它允许你远离伪现实阴影的心理模型,并开始更多地将它们视为在应用它们的元素后面生成的“额外的盒子”,这些盒子不会影响布局——对于各种效果来说非常方便。

一个有用的例子是在一个元素上创建多个“假边框”。border 属性只允许你画一个边框(诡异的 double 关键字除外,但那不算)。使用 0 模糊半径和不同扩散半径的阴影,你可以创建几个类似边界的区域(见图 5-25 )。因为它们不影响布局,所以它们的行为更像 outline 属性。

A314884_3_En_5_Fig25_HTML.jpg

图 5-25。使用多重阴影和扩散半径绘制假轮廓
.profile-photo {
  box-shadow: 0 0 0 10px #1C318D,
              0 0 0 20px #3955C7,
              0 0 0 30px #546DC7,
              0 0 0 40px #7284D8;
}

使用 CSS 渐变

设计中的一个常见用例是使用颜色渐变作为元素的背景,为页面添加微妙的深度感。加载包含渐变的图像文件可以很好地工作,但是 CSS 也有一个为您绘制渐变图像的机制。这是通过各种样式的梯度函数符号,结合任何接受图像(包括背景图像)的属性来实现的。假设我们有一个用户尚未上传背景图片的个人资料页面(见图 5-26 ),我们希望默认显示渐变背景:

A314884_3_En_5_Fig26_HTML.jpg

图 5-26。应用于轮廓框背景的线性渐变
.profile-box {
  background-image: linear-gradient(to bottom, #cfdeee 0%, #8da9cf 100%);
}

由于用 CSS 创建的渐变图像没有特定的大小,这个渐变最初会覆盖整个元素,除非你特别指定使用背景大小来度量它。

浏览器支持和浏览器前缀

大多数现代浏览器都支持渐变。Internet Explorer 9(及更早版本)和 Opera Mini 是最明显的例外。一些稍旧的基于 WebKit 的浏览器只支持线性渐变版本。在接下来的章节中,我们将会看到不止一种类型的渐变。

注意

CSS 渐变的语法在 Safari 中首次作为非标准属性引入以来,已经发生了几次变化。有三种不同的语法,根据您需要的浏览器支持级别,您可能需要同时使用多个版本,并带有不同的供应商前缀。为了使这一部分易于管理并且不会太混乱,我们将使用最新的无前缀语法来浏览它们。你可以仔细阅读本文中的各种语法:www . site point . com/using-un fixed-css3-gradients-in-modern-browsers/

线性渐变

前面的示例使用 linear-gradient()函数沿着从元素顶部到底部的假想线绘制渐变。这条线的角度,在本例中是一个关键字对(到底部),是函数的第一个参数,后面是逗号分隔的色标列表。色标定义了渐变线上颜色发生变化的点,在这种情况下,我们从 0%的较亮蓝灰色开始,以 100%的较暗蓝色结束,这意味着元素的底部。

我们可以使用 to 关键字指定方向,后跟一个边(上、右、下、左)或一个角(左上、右下等)。),后者使梯度成对角线。它从对角或对边开始,渐变线总是穿过图像区域的中心。我们也可以使用以度为单位的角度,其中 0 度表示向上/向北,然后顺时针增加到 360 度,就像 HSL 色轮一样。在这种情况下,度数意味着渐变的绘制方向,所以它仍然从我们指向的相反方向开始。这是一个 45 度的梯度:

.profile-box {
  background-image: linear-gradient(45deg, #cfdfee, #4164aa);
}

这里,渐变线不是从背景图像区域的边缘开始。相反,它会自动缩放,以便 0%和 100%的任何颜色都与图像的角重合。图 5-27 解释了其工作原理。

A314884_3_En_5_Fig27_HTML.gif

图 5-27。对角线渐变中渐变线的位置和比例

默认值和颜色停止位置

由于从上到下(180 度)是默认设置,0%和 100%分别隐含在第一个和最后一个色标中,我们实际上可以将第一个示例缩短如下(参见图 5-26 ):

.profile-box {
  background-image: linear-gradient(#cfdfee, #8da9cf);
}

任何没有指定位置的附加色标将以 0%到 100%之间的比例间隔结束,如果有五种颜色,它们将分别为 0%、25%、50%、75%和 100%:

.profile-box {
  background-image: linear-gradient(red, green, blue, yellow, purple);
}

我们可以使用百分比之外的其他测量值来表示色标,从而进一步控制渐变的绘制方式:

.profile-box {
  background-image: linear-gradient(#cfdfee, #8da9cf **100px**);
}

这将绘制一个渐变,从顶部的浅蓝色开始,然后经过 100 个像素转换到深蓝色,然后保持这种颜色,直到背景图像区域的底部边缘。

径向梯度

您还可以使用径向渐变来创建沿着假想的渐变光线发生的颜色偏移,以圆形或椭圆形的形式从中心点向所有方向延伸。

径向渐变的语法稍微复杂一些。您可以指定以下属性:

  • 哪种形状:圆形或椭圆形。

  • 渐变光线的半径,决定渐变区域的大小。圆形只接受一个尺寸测量(对于半径),而椭圆分别接受 x 轴和 y 轴上的两个尺寸测量。椭圆可以使用任何长度或百分比,其中百分比是相对于该轴上的背景图像大小。圆只接受长度,不接受百分比。还有表示渐变区域边缘结束位置的关键字,以便渐变可以延伸到距离中心最远或最近的一侧(最近侧和最远侧)的内,或者渐变形状的边缘接触图像区域的最近或最远角(最近角或最远角)。

  • 使用与背景位置属性非常相似的位置值来确定形状中心的位置。这些值前面有 at 关键字,以区别于大小。

  • 随着形状的扩展,颜色以逗号分隔的方式停止(尽可能多)。

一个例子可能是这样的:

.profile-box {
  background-image: radial-gradient(circle closest-corner at 20% 30%, #cfdfee, #2c56a1);
}

这将给我们一个圆形径向梯度,其中心位于 x 轴上的 20%和 y 轴上的 30%,延伸使得圆的圆周接触最近的角。在圆圈之外,最终颜色停止颜色继续覆盖整个背景图像区域(参见图 5-28 )。

A314884_3_En_5_Fig28_HTML.gif

图 5-28。我们的个人资料页面标题有一个圆形径向梯度,定位在 20% 30%,大小扩展到最近的角落

考虑到我们的轮廓框示例形状,我们可能需要一个居中的径向渐变,椭圆形状。让我们尝试一些更迷幻的东西(见图 5-29 ):

A314884_3_En_5_Fig29_HTML.jpg

图 5-29。放射状渐变中的几个重复色标
.profile-box {
  background-image: radial-gradient(#cfdfee, #2c56a1, #cfdfee, #2c56a1, #cfdfee, #2c56a1);
}

我们实际上忽略了声明它是一个椭圆的部分,该椭圆居中并覆盖整个元素(通过延伸到最远的角);在这种情况下,所有这些属性都包含在默认值中。但是像那样重复这些颜色停止似乎有点乏味,不是吗?这就是重复渐变的由来。

重复渐变

在沿着线(或光线)的某一点,法线渐变在最终颜色处停止。还有线性和径向的重复渐变函数(见图 5-30 ),只要它们的大小允许(通过背景大小属性或元素大小),就重复颜色停止序列。例如,这里有一个重复的线性渐变:

A314884_3_En_5_Fig30_HTML.jpg

图 5-30。重复渐变功能会在整个背景图像区域重复颜色停止列表
.linear-repeat {
  background-image: repeating-linear-gradient                              (#cfdfee, #2c56a1 20px);
}

这是一个重复的径向梯度:

.radial-repeat {
  background-image: repeating-radial-gradient(#cfdfee, #2c56a1 20px);
}

渐变作为图案

渐变不一定需要在几个像素上平滑过渡。它们也可以从一个像素变化到下一个像素,让我们可以创建更清晰的线条和圆圈。将这一点与在彼此之上层叠多个背景图像的能力相结合,为我们提供了一种工具来声明性地创建简单的背景图像模式,而无需打开图像编辑软件!

创造清晰图案的诀窍是以正确的方式放置色标。例如,要画一条简单的垂直线,我们需要将相邻的色标放在一起,这样就没有颜色逐渐变化的空间(见图 5-31 ):

A314884_3_En_5_Fig31_HTML.gif

图 5-31。第二个和第三个色标都位于 50%,在颜色之间产生明显的变化
body {
    background-color: #fff;
    background-image: linear-gradient(
        transparent,
        transparent **50%**,
rgba(55, 110, 176, 0.3) **50%** 
      );
    background-size: **40px 40px**;
  }

根据浏览器的不同,您可能会发现它并没有完全清晰地显示线条,而是实际上向两边渐变了 1 px。随着浏览器在渲染渐变方面变得更好,这可能会得到改善,但对于更微妙的模式来说应该足够好了。

我们没有在整个元素上使用重复的线性渐变,而是使用了单一渐变,然后使用背景属性调整和重复生成的图像。这让我们可以控制线条的比例,而不会影响颜色停止。通过添加另一个渐变图像,这次是水平运行,我们可以构建一个“桌布”图案(见图 5-32 ):

A314884_3_En_5_Fig32_HTML.jpg

图 5-32。用两条线性渐变线绘制背景图案
body {
    margin: 0;
    background-color: #fff;
    background-image: linear-gradient(
          transparent,
          transparent 50%,
          rgba(55, 110, 176, 0.3) 50%
        ),
      linear-gradient(
          to right,
          transparent,
          transparent 50%,
          rgba(55, 110, 176, 0.3) 50%
        );
    background-size: 40px 40px;
  }

想象一下你可以用线条、三角形(半填充对角线渐变)、圆形和椭圆形的基本形状(重叠)的倍数想象出丰富的形状,这并不是一个很大的进步。

一个很大的灵感来源是 Lea Verou 在lea.verou.me/css3patterns/的 CSS3 模式画廊(见图 5-33 )。

A314884_3_En_5_Fig33_HTML.jpg

图 5-33。Lea Verou 的 CSS3 模式库

使用 CSS 绘图

将渐变图案与方框阴影和伪元素结合起来,你就有足够的机会创造出创造性的效果,而无需加载一张图像。另一个鼓舞人心的资源是艺术家兼设计师林恩·费希尔的项目“一个单独的格子”(【http://a.singlediv.com)。这是一个用 CSS 完成的插图集合,其中每幅插图只需要标记中的一个元素,没有图像(见图 5-34 )。

A314884_3_En_5_Fig34_HTML.jpg

图 5-34。《一个格子》的插图

请记住,在某些时候,这些 CSS 绘图的代码可能会变得比创建一个 SVG(或 PNG)图像文件并使用它更难理解和维护。还值得记住的是,尽管渐变避免了加载外部图像资源,但它们本身也会对性能产生相当大的影响——尤其是在手机等资源受限的设备上。径向梯度尤其值得保持最小。

设计嵌入图像和其他对象的样式

在设计文档流中的图像样式时,您处理的内容不同于构成页面的其他框。这是因为图像可能有固有的宽度和高度(以像素为单位)、需要考虑的设定纵横比,或者两者都有。在灵活的设计中,内容取决于浏览器窗口的宽度,您需要使用 CSS 来驯服图像和其他嵌入对象。

注意

为当前渲染尺寸加载不同的图像——被称为响应图像——对于性能来说是一个非常重要的话题,但我们现在把它放在一边。我们将回到第八章的响应技术。

灵活的图像模式

使用理查德·鲁特(clagnut.com/blog/268/)发明的一项技术,可以使图像变得灵活,而不会显示得比其固有尺寸大,也不会扭曲纵横比。在其核心,你只需要以下规则:

img {
  max-width: 100%;
}

应用于图像的 max-width 属性意味着图像将根据其所在容器的边界缩小,但如果容器更宽,它将不会增长到其固有大小之外(参见图 5-35 )。

A314884_3_En_5_Fig35_HTML.jpg

图 5-35。320 像素宽的位图图像,最大宽度:100%显示,容器宽度为 100 像素,而容器宽度为 500 像素

我们可以通过将此规则扩展到以下内容来扩展它,以涵盖更多的基础:

img {
  width: auto;
  max-width: 100%;
  height: auto;
}

为什么有额外的规则?嗯,有时标记作者或内容管理系统在 HTML 源代码中把宽度和高度属性与图像尺寸放在一起。

将宽度和高度设置为 auto 部分是为了覆盖这些属性,但也是为了解决 IE8 中的一个错误,即没有声明宽度属性的图像有时无法正确缩放。

新的对象大小调整方法

有时,您最终希望将大小应用于 img 元素和其他嵌入对象(如视频或对象元素),这些对象与它们内部显示的媒体具有不同的纵横比。例如,您可能有一个矩形图像文件作为用户头像占位符(见图 5-36 ),但您希望使用 CSS 将其显示为正方形。

A314884_3_En_5_Fig36_HTML.jpg

图 5-36。矩形用户化身图像

一些新的神奇属性和关键字最近已经被标准化,并正在进入浏览器,允许您以更灵活的方式调整这些类型元素的内容的大小和位置。使用 object-fit 属性,我们可以调整图像内容的大小,就像使用较新的背景大小关键字一样,同时保持纵横比:

img {
  width: 200px;
  height: 200px;
}
img.contain {
  object-fit: contain;
}
img.cover {
  object-fit: cover;
}
img.none {
  object-fit: none;
}
img.scaledown {
  object-fit: scale-down;
}

图 5-37 说明了当以与固有尺寸不匹配的设定尺寸显示图像时,这些关键字之间的区别。

A314884_3_En_5_Fig37_HTML.gif

图 5-37。固定大小图像的示例,其内容使用对象适合属性的不同关键字来调整大小

对象适配的默认行为是填充,这意味着图像的内容将随着元素尺寸而拉伸,这可能会导致纵横比失真。

cover 和 contain 关键字的作用与它们在 background-size 属性中的作用相同。当使用 none 时,使用原始图像的精确尺寸,而不管元素的大小。最后,还有缩小,它会在无和包含之间自动选择,选取最小的结果尺寸。生成的图像居中,但可以使用对象位置进行定位,就像定位背景图像一样。

到目前为止,支持仅限于 Chrome、Opera、Safari 和 Firefox 的最新版本,尽管在编写本文时 Safari 还不支持对象位置。没有任何版本的 IE 或 Edge 支持这种行为,尽管 Edge 很可能会紧随其后,很快支持这些属性。

具有长宽比意识的柔性容器

对于位图图像,正如我们在前面几节中看到的,纵横比是内置的:它们有固定的宽度和高度,只要您将高度设置为自动,并且只更改宽度(反之亦然),事情看起来仍然正确。

但是,如果您正在设计的元素没有固有的纵横比,而您想给它一个纵横比,同时保持它的灵活性和可调整性,会发生什么情况呢?

iframe 和 object 元素就是这种情况,在某种程度上,SVG 内容也是如此。一个常见的例子是将来自 YouTube 或 Vimeo 等网站的视频嵌入页面时得到的标记:

<iframe width="420" height="315" src="https://www.youtube.com/embed/dQw4w9WgXcQ" frameborder="0" allowfullscreen></iframe>

如果我们像这样设置一个灵活的宽度:

iframe {
  width: 100%; /* or any other percentage, really…*/
}

…这将产生一个 100%宽的 iframe,但由于 height 属性,它仍然是 315 像素高。由于视频有一个设定的长宽比,我们希望高度自动调整。

设置自动高度或删除该属性不起作用,因为 iframe 没有固有的高度,它很可能变成 150 像素高。为什么是 150 像素?CSS 规范规定被替换的内容(比如 iframes、images、object 元素等。)没有指定的或固有的尺寸,退回到 300 像素宽和/或 150 像素高的尺寸。奇怪但真实。

为了解决这个问题,我们需要应用一些聪明的 CSS 技巧。首先,我们将 iframe 放在包装元素中:

**<div class="object-wrapper">** 
    <iframe width="420" height="315" src="https:////www.youtube.com/embed/dQw4w9WgXcQ" frameborder="0" allowfullscreen></iframe>
**</div>** 

然后,我们让包装框的大小与我们想要嵌入的对象的长宽比相同。为了弄清楚这一点,我们用原始高度(315 像素)除以原始宽度(420 像素)得到一个结果比率:315/420 = 0.75。所以高度是宽度的 75%。

接下来,我们将包装器的高度设置为 0,但是将底部填充设置为我们得到的数字—75%:

.object-wrapper {
  width: 100%;
  height: 0;
  padding-bottom: 75%;
}

你可能从第三章中记得,当垂直填充和边距以百分比设置时,它们实际上指的是包含块的宽度——在这种情况下,宽度是 100%(与包含块相同),所以填充是 75%。我们现在已经创建了一个具有设定纵横比的块。

最后,我们将嵌入的对象放在包装器中。即使包装器的高度为 0,我们也可以使用绝对定位将元素放置在“支持纵横比”的填充框中:

.object-wrapper {
  width: 100%;
  height: 0;
**position: relative;** 
  padding-bottom: 75%;
}
.object-wrapper iframe {
**position: absolute;** 
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

就是这样!现在,我们有办法将灵活的对象嵌入到页面中,并创建其他保持纵横比的元素。图 5-38 显示了该过程。

A314884_3_En_5_Fig38_HTML.gif

图 5-38。创建支持纵横比的容器

存在一个警告:如果我们希望包装器不是 100%宽,我们将不得不重新计算填充底部的尺寸。因此,使用另一个包装器来实现进一步的灵活性可能是一个好主意;然后,我们可以将外层包装的宽度设置为我们喜欢的宽度,将内层包装的宽度设置为 100%,这样就完成了。

这项技术是由开发者 Thierry Koblentz 率先提出的,你可以在alistapt . com/article/creating-intrinsic-ratios-for-video阅读对它的深入解释。

减小图像文件大小

当你使用图片作为设计的一部分时,你需要确保你不会向你的用户发送不必要的大图片。当然,您可以使用 CSS 来缩放和裁剪它们,但是每个不必要的像素都会导致性能损失。下载时间过长,电池耗尽,处理器浪费时间调整图像大小,这些都是良好用户体验的敌人。

减少不必要的文件大小的第一步是优化您的图像。图像文件通常包含大量的元数据,浏览器并不真正需要这些元数据来正确显示图像,有一些程序和服务可以帮助你从文件中剥离这些内容。艾迪·奥斯马尼在 https://addyosmani.com/blog/image-optimization-tools/做了一次精彩的综述。他提到的许多工具都是自动化任务运行器的一部分——我们将在第十二章中回头来看看这类工作流。

如果您正在处理 PNG 图像以获得更简单的图形,那么通过减少图像中的颜色数量,您也可以极大地减小文件大小。如果您在图像中使用 alpha 透明度,大多数图像编辑软件只会让您以 PNG24 格式导出它。事实是,即使是更简单(也小得多)的 PNG8 格式也可以包含 alpha 透明度,因此通过将图形转换为这种格式,您可以获得更多的收益。有基于网络的服务,比如帮助你在线转换 PNG 文件的tinypng.com,还有几个适用于所有操作系统的独立应用。一些专业图像编辑程序,如 Photoshop,在最新版本中内置了这一功能。

如果您正在使用 SVG 图形,您应该知道大多数处理 SVG 的图像编辑器导出的文件中有许多不必要的数据。一个非常有用的优化 SVG 的工具是杰克·阿奇博尔德的 OMG SVG(jakearchibald.github.io/svgomg/)——一个在线工具,让你调整一系列参数,使你的文件更精简,它甚至可以离线工作!

我们将在第十二章中深入分析和调试性能的技术。

摘要

在这一章中,我们已经学习了很多设计页面盒子样式的技巧。我们探讨了如何使用各种颜色语法,以及如何使用透明度。我们已经了解了如何控制背景图像,以及如何相对于元素框定位、调整大小、重复和裁剪它们。

我们还向您展示了如何使用边框,以及如何通过使用边框半径来创建圆角,甚至是圆形,从而打破默认的四方形。

我们尝试使用阴影,既作为一种在页面中创造深度的手段(作为盒子上的插入或开始阴影),也作为一种绘制“额外矩形”来创造其他视觉效果的手段。此外,我们还研究了如何使用线性和径向渐变,既作为微妙的效果,也作为让浏览器为您绘制图像模式的一种方式。

我们讨论了内容图像和背景图像之间的差异,以及如何灵活地设计内容图像的样式,还有其他嵌入的内容,包括支持纵横比的容器。

我们将在第十一章回到一些更高级(但不太受支持)的视觉效果。同时,在下一章中,我们将最终结合我们的尺寸、样式、定位框和文本的知识,使用新旧技术和属性为 Web 做适当的布局。

六、内容布局

一个网页,在最基本的层面上,是由不同的内容块组成的:标题、段落、链接、列表、图像、视频等等。这些要素可以按主题分组;构成新闻故事的标题、一些文本和图像。通过控制每个组件中项目的位置、大小、顺序和间距,我们可以更好地传达它们的功能和意义。

这些内容通常会进一步组合到整个页面的布局中。我们将在下一章中探讨如何系统地编排整个页面。在本章中,我们将继续关注各个内容块以及如何对它们进行布局。

我们已经简单地提到了使用定位和浮动来布局,它们都有优点和缺点。您还可以引导其他属性,如表格显示模式和内联块,在布局中发挥它们的作用,各有利弊。新的柔性盒布局模块——或简称为flexbox——提供了一整套属性来控制排序、方向、对齐和大小。Flexbox 是一个强大的工具,我们将详细介绍它。

在本章中,我们将了解以下内容:

  • 绝对与相对定位以及 z 索引的常见使用案例

  • 使用浮动、内联块和表格显示进行布局

  • 掌握垂直对齐和垂直居中

  • 使用 flexbox 进行定位、调整、订购和调整

使用定位

在第三章中,我们提到定位并不是高级布局的最佳工具,因为它将元素从页面流中剔除。另一方面,这使得定位成为 CSS 的一个重要部分。在这一节中,我们将简要分析一些定位是有用工具的场景。

从第章到第章,快速回顾一下:

  • 元素最初被定位为静态,这意味着块级元素垂直堆叠。

  • 我们可以给元素相对定位,允许我们相对于它们的原始位置轻推它们,而不改变它们周围的元素流。这样做也为后代元素创建了一个新的定位上下文。最后一个事实使得相对定位非常有用。从历史上看,移动元素的能力是许多老派布局技巧的重要组成部分,但现在我们没有它们也能应付过去。

  • 绝对定位允许我们给一个元素一个关于最近的定位上下文的精确位置,它或者是一个定位非静态的祖先,或者是 html 元素。在这个模型中,元素被从页面流中提取出来,并相对于它们的定位上下文放回原处。默认情况下,它们会停留在原本应该停留的位置,但不会影响周围的元素。然后,我们可以选择更改它们相对于定位上下文的位置。

  • 固定定位基本与绝对相同,但定位上下文自动设置为浏览器视口。

绝对定位用例

绝对定位的本质使它成为创建覆盖、工具提示和对话框等位于其他内容之上的东西的理想选择。它们的位置可以用 top、right、bottom 和 left 属性给出。关于绝对定位,有几件事是值得了解的,可以帮助您编写更高效的代码。

使用初始位置

对于这个例子,我们使用的是一篇关于宇宙飞船的文章,我们想引入一些行内注释。我们希望将它们显示为空白处的小注释气泡,如图 6-1 所示。

A314884_3_En_6_Fig1_HTML.jpg

图 6-1。在文章旁边显示页面内评论

每个注释都是位于注释所指段落之后的旁注元素:

<p>This is a fake article[...]</p>
<aside class="comment"> I've never done this. Is that really true?</aside>
<p>You may think[...]</p>

为了让评论显示在它所指的段落的末尾,我们需要绝对定位它。诀窍是我们不需要给它一个从物品容器顶部的精确的偏移量来在垂直方向上正确定位它。

当定位上下文的偏移量未定义时,绝对定位的元素将保留它们作为静态元素的位置,因此第一步是将注释留在原处(参见图 6-2 ):

.comment {
 position: absolute;
}

A314884_3_En_6_Fig2_HTML.jpg

图 6-2。对注释应用绝对定位会将它从流中提升出来,但默认情况下会将它留在原本会以静态位置结束的地方

现在,我们需要将注释向左上方移动,使其位于上一段末尾的空白处。这种轻推听起来像是相对定位的工作,但是我们不能让一个元素同时被绝对定位和相对定位。如果我们使用方向偏移(顶部、右侧、左侧和底部)来定位它,我们将依赖于父定位上下文和周围元素的确切大小。幸运的是,我们不需要!相反,我们可以使用负边距来微调元素:

.comment {
  position: absolute;
**width: 7em;** 
**margin-left: -9.5em;** 
**margin-top: -2.5em;** 
}

负边距在 CSS 中完全有效,并且有一些有趣的行为:

  • 左边距或上边距为负会将元素拉向那个方向,与它旁边的任何元素重叠。

  • 负的右边距或下边距会拉近任何相邻的元素,使它们与具有负边距的元素重叠。

  • 在浮动元素上,与浮动方向相反的负边距将减小浮动区域,导致相邻元素与浮动元素重叠。浮动方向上的负边距会将被浮动的元素拉向那个方向。

  • 最后,当在没有定义宽度的非浮动元素上使用时,侧面的负边距行为会稍微缓和一些。在这种情况下,左边和右边的负边距都将元素拉向那个方向。这个扩展了元素,潜在地覆盖了任何相邻的元素。

在我们的注释气泡的例子中,我们使用负的左边距和上边距来将元素拉到适当的位置,就像我们使用相对定位一样。

额外收获:在 CSS 中创建三角形

在图 6-1 所示的评论气泡中,指向前一段的小三角形状依次相对于评论气泡绝对定位。它被创建为一个伪元素,并使用一个带有边框的老技巧赋予了一个三角形。(这至少可以追溯到 2001 年——请看坦泰克·切利克的这篇文章:【http://tantek.com/CSS/Examples/polygons.html。)图 6-3 显示了它是如何工作的。

.comment:after {
  position: absolute;
  content: '';
  display: block;
  width: 0;
  height: 0;
  border: .5em solid #dcf0ff;
  border-bottom-color: transparent;
  border-right-color: transparent;
  position: absolute;
  right: -1em;
  top: .5em;
}

A314884_3_En_6_Fig3_HTML.gif

图 6-3。创建具有零尺寸元素和边框的箭头。由于右边缘和下边缘是透明的,所以留下了一个三角形

在这里,我们创建一个 0 × 0 像素的块,它有一个 5 em 的边界,但是只有顶部和右侧的边界边缘有颜色,所以我们最终得到一个三角形,因为角的边界边缘是倾斜的。一种不用图像就能生成三角形的简便方法!然后我们定位三角形,使其突出于评论框的右上角(见图 6-4 )。

A314884_3_En_6_Fig4_HTML.jpg

图 6-4。相对于注释内容定位三角形

使用偏移量自动调整大小

在标尺的另一端,它有助于了解当元素被绝对定位并声明了许多或所有的偏移量时,元素是如何反应的。没有任何声明的大小,绝对定位的元素将回落到包含其内容所需的大小。当我们从定位上下文的相对两侧声明偏移量时,元素将拉伸以适应满足这些规则所需的大小。

例如,我们可能会遇到这样的情况,我们希望在距离另一个元素的边缘一定距离处调整某个元素的大小,但不在任何一个元素上使用特定的大小。例如,我们可能在一幅图像的上面有一个包含文本的框,如图 6-5 所示。

A314884_3_En_6_Fig5_HTML.jpg

图 6-5。图像顶部的半透明框相对于右侧、底部和左侧绝对定位。到顶端的距离由内容决定
<header class="photo-header">
  <img src="img/big_spaceship.jpg" alt="An artist's mockup of the "Dragon" spaceship">
  <div class="photo-header-plate">
    <h1>SpaceX unveil the Crew Dragon</h1>
    <p>Photo from SpaceX on <a href="https://www.flickr.com/photos/spacexphotos/16787988882/">Flickr</a></p>
  </div>
</header>

假设我们不希望容纳标题的半透明“板”占据特定的宽度,我们可以从右侧、底部和左侧放置它,让它自己计算出它的尺寸和顶部边缘位置:

.photo-header {
  position: relative;
}
.photo-header-plate {
  position: absolute;
**right: 4em;** 
**bottom: 4em;** 
**left: 4em;** 
  background-color: #fff;
  background-color: rgba(255,255,255,0.7);
  padding: 2em;
}

无论图像的尺寸如何,板现在将位于图像的底部,距离底部和侧面 4 ems。这为我们提供了一些在不同屏幕尺寸下都能很好工作的东西——如果有换行符,板的上边缘会根据内容高度进行调整(见图 6-6 )。

A314884_3_En_6_Fig6_HTML.jpg

图 6-6。在较小的屏幕尺寸下,文字会随着框的向上增长而换行

定位和 z 索引:堆叠上下文陷阱

聪明地使用定位的最后一个要素是很好地掌握 z 索引:元素的堆叠顺序。我们在第三章中提到了基础知识:位置不是静态的元素根据它们在源代码树中的深度排列成堆栈,就像扑克牌叠放在一起一样。更改 z 索引会更改它们在堆栈中的顺序。

任何具有设置为正值的显式 z-index 声明的元素在堆栈中的位置都比没有 z-index 声明的元素高。具有负值的元素显示在没有 z 索引的元素后面。

但是 z 索引并不是唯一控制元素堆叠方式的东西。我们还有一个堆栈上下文的概念。将卡片组的类比延伸一点,每张卡片也可以是它自己的卡片组,卡片只能根据当前的卡片组级别进行排序。总是有一个根堆栈上下文开始,z 索引不是 auto 的定位元素在其中排序。随着其他上下文的形成,它们创建了堆栈的层次结构。

特定的属性和值创建了这些新的堆叠上下文。例如,position: absolute 和 z-index 声明设置为除 auto 之外的任何值的元素将为其中的后代元素形成堆栈上下文。

在堆栈上下文中,z-index 值的大小无关紧要:你不能相对于另一个堆栈上下文重新排序(见图 6-7 )。

A314884_3_En_6_Fig7_HTML.gif

图 6-7。容器 A、B、C 和 D 都是绝对定位的,其中 C 是 B 的子元素。容器 C 和 D 应用了 z-index,但是由于容器 B 的不透明度小于 1,它创建了一个新的堆叠上下文,与其他上下文分开。z-index 不会将 C 放在 D 的前面,不管数字有多高

这些触发规则之一是将不透明度设置为小于 1 的值。不透明度较低的元素在放置到页面上之前需要单独呈现(与其后代元素一起),因此这些规则是为了确保在这种情况下没有外部元素可以插入半透明元素之间。本书附带的文件中有一个代码示例,可以让您处理这种情况。

在本书的前面,我们将遇到其他的例子,比如 transform 和 filter 属性,它们也可以触发新的堆栈上下文的创建。在本章的最后,我们将了解使用 z-index 和 flexbox 的一些特性。

水平布局

一般来说,随着内容的增加,网页在垂直方向上增长。您添加的任何块容器(div、article、h1–h6 等。)将垂直堆叠,因为它们显示为具有自动宽度的块。正因为如此,当你想给内容块一个宽度,并让它们在水平方向彼此相邻时,最基本的布局挑战之一就出现了。

我们已经在第三章看到了一个使用浮动设计小型“媒体组件”的例子。这种一边是图像(或其他类型的媒体),另一边是一段文本的模式是布局的原子模式的一个很好的例子:“这个东西紧挨着另一个东西,它们属于一起。”如果你查看任何网站,你肯定会看到这种模式一次又一次地重复(见图 6-8 )。

A314884_3_En_6_Fig8_HTML.jpg

图 6-8。Wired.com 一部分的截图。你能发现多少“媒体对象”?

还有许多其他常见的模式出现在各种各样的网站上。很多都和横向布局有关。像 flexbox 这样的新标准是为了满足水平布局(以及更多)而创建的,但是在 flexbox 得到普遍支持之前,您可能需要采用浮动、内嵌块显示或表格显示模式来创建水平布局模式。

使用浮动

在飞船文章中,我们有一个关于浮动最基本用法的例子。该图浮动到右侧,允许文本的行框围绕其周围和下方流动(参见图 6-9 )。我们还使用了负的右边距来将图像从文本中拉出一段距离。

A314884_3_En_6_Fig9_HTML.jpg

图 6-9。使用一个浮动的数字,使用负边距-右拉出
<p>You may think[...]</p>
  <figure>
    <img src="img/spaceship.jpg" alt="The Dragon spaceship in orbit around Earth.">
    <figcaption>The "Dragon" spaceship, created by SpaceX. Image from <a href="https://www.flickr.com/photos/spacexphotos/16787988882/">Flickr.com</a></figcaption>
  </figure>
<p>There's various [...]</p>
figure {
  background-color: #eee;
  margin: 0;
  padding: 1em;
  float: right;
  max-width: 17em;
  margin-right: -8em; /* pull to the right */
  margin-left: 1em;
}

在图 6-10 中,我们移除了负边距,并限制该图占据宽度的 50%。我们还在第一个数字之后添加了第二个数字。这两个图形现在将水平并排放置。

A314884_3_En_6_Fig10_HTML.jpg

图 6-10。两个宽度为 50%的浮动图形,并排放置
figure {
  float: right;
  width: 50%;
}

这种效果——浮动项目充当“行”中的“列”——形成了无数 CSS 布局技术的基础。正如在第三章中所讨论的,浮动有一些奇怪的地方会让你犯错。请记住,浮动实际上并不在页面流中,所以您可能需要一个包含浮动的元素。通常,这是通过对容器内的(伪)元素应用 clear,或者通过一个规则使容器成为一个新的块格式上下文来实现的。如果需要的话,浮动也可以包装成多行,但是会卡在从上面的行伸出的前面的浮动上。

浮动还可以提供一些有限的水平内容的重新排序,与源顺序无关。例如,我们可以通过向左浮动而不是向右浮动来切换图形的位置(参见图 6-11 )。

A314884_3_En_6_Fig11_HTML.jpg

图 6-11。通过向另一个方向浮动来交换图形的位置

由于无处不在的浏览器支持和浮动的相对多功能性,它们已经成为许多水平布局变化的首选解决方案。当我们为高级页面布局构建一个小的网格系统时,我们将在第七章回到使用它们。但是还有其他 CSS 属性允许我们创建水平布局模式,它们有各自不同的优缺点,我们将在接下来的章节中看到。

作为布局工具的内嵌块

文本行本身就是一种水平布局,至少在从左到右或从右到左书写的语言中是这样。当我们使用行内元素(如 span、time 或 a)时,它们与文本在同一方向水平排列。我们还可以将内联块放入该流中,创建水平排列的元素,但在视觉格式方面充当块,并且可以在其中包含其他块。

例如,让我们在飞船文章的底部添加一些元数据,由一个作者姓名、一张照片和一个电子邮件地址组成。我们还增加了几个额外的跨度作为造型挂钩:

<p class="author-meta">
  <!-- image from Jeremy Keith on Flickr: https://flic.kr/p/dwFRgH -->
  <img class="author-image" src="img/author.jpg" alt="Arthur C. Lark">
  <span class="author-info">
    <span class="author-name">Written by Arthur C. Lark</span>
    <a class="author-email" href="mailto:arthur.c.lark@example.com">arthur.c.lark@example.com</a>
  </span>
</p>

的内容。作者-元段落现在将对齐,图像的底部边缘位于文本的基线上。任何空白字符,包括例如图像和作者信息开始的行之间的换行符,都将呈现为空白。该空间的宽度取决于字体系列和字体大小(参见图 6-12 )。

A314884_3_En_6_Fig12_HTML.jpg

图 6-12。我们的作者元数据。注意图像和文本之间的空白。

接下来,我们将把图像和作者信息转换成内嵌块:

.author-image,
.author-info {
  display: inline-block;
}

在渲染方面,组件在这个阶段看起来是一样的。不同之处在于,我们可以开始将图像和信息视为块。例如,我们可以将作者信息中的姓名和电子邮件地址放在图像旁边的单独行中,方法是将它们更改为块显示:

.author-name,
.author-email {
  display: block;
}

我们现在已经非常接近视觉效果了,例如,一个文本块旁边的浮动图像(如第三章中的“媒体块”示例)。一个区别是作者信息块的最后一条基线与图像的底部对齐。我们在图 6-13 中看到了结果,我们在图像和作者信息周围添加了一个虚线轮廓,以可视化这两个元素之间的关系。

A314884_3_En_6_Fig13_HTML.jpg

图 6-13。作者信息的基线现在与图像的底部对齐

我们现在可以通过改变 vertical-align 属性来相对于图像移动作者信息。当对齐设置为顶部时,作者信息块的顶部将与图像的顶部对齐(参见图 6-14 )。

A314884_3_En_6_Fig14_HTML.jpg

图 6-14。使用 vertical-align: top 将作者信息对齐到图像顶部

带内嵌块的垂直居中

现在,假设我们想要的设计是作者信息块相对于图像垂直居中。尝试这样的事情可能很有诱惑力:

.author-info {
  vertical-align: middle;
}

…但这可能不会达到您预期的效果!图 6-15 显示了结果。

A314884_3_En_6_Fig15_HTML.jpg

图 6-15。使用垂直对齐时作者信息的位置:中间

这就是事情变得有些棘手的地方。关键字 middle 应用于内联块时,意味着“将该内联块的垂直中心与文本行的 x 高度的中间对齐”在这种情况下,有就有没有内联文本。因此,图像(是行中最高的元素)决定了行框的高度和基线的结束位置。因此,x 高度的中心正好位于图像底部的上方。为了使作者信息在图像的垂直中心居中,我们需要使两个元素指向同一个“中间”:

.author-image,
.author-info {
  vertical-align: middle;
}

当图像是一个内嵌块时,它也在作者信息的同一个垂直点上垂直居中,产生了我们想要的布局,如图 6-16 所示。

A314884_3_En_6_Fig16_HTML.jpg

图 6-16。将 vertical-align: middle 应用于图像和作者信息,使它们在同一点上垂直居中

如何决定行框基线的规则,以及它如何影响行内和行内块元素,是相当复杂的。如果你想深潜,我们推荐克里斯托弗·奥伊的文章《垂直对齐:你需要知道的一切》(christopheraue.net/2014/03/05/vertical-align/)。为了将内嵌块显示用作布局工具,在垂直对齐方面有两个重要的要点:

  • 要使内联块顶端对齐(很像浮动),设置 vertical-align: top。

  • 要使内容彼此垂直居中,请确保它们都是内联块,然后使用 vertical-align: middle。

容器元素内的垂直居中

上一个列表中的最后一点使我们能够在任意高度的容器中垂直居中内容,这需要一点技巧。唯一的先决条件是容器的高度设置为一个确定的长度。

例如,让我们假设我们想要使作者信息块 10 米高,并在其中垂直和水平地居中放置作者图像和信息。首先,我们将高度应用于。作者-元块。我们还将添加一个边框,使变化更容易被发现(见图 6-17 )。

.author-meta {
  height: 10em;
  border: 1px solid #ccc;
}

A314884_3_En_6_Fig17_HTML.jpg

图 6-17。那个。添加了高度和边框的作者元块

照片和作者信息的垂直对齐与容器块无关,但与它们所在的假想文本行有关。为了垂直对齐它们,我们需要添加另一个内嵌块元素,它占据了 100%的高度。该元素将强制 middle 关键字的对齐点位于容器的中间。为此,我们将使用伪元素。图 6-18 显示了当添加这个“虚元素”时,假设基线是如何计算的。

A314884_3_En_6_Fig18_HTML.gif

图 6-18。使用 100%高的伪元素来强制 middle 关键字以表示容器的垂直中心结束
.author-meta:before {
  content: '';
  display: inline-block;
  vertical-align: middle;
  height: 100%;
}

至此,整个。author-meta 容器实际上将有一个占据整个高度的单行框。由于伪元素是垂直对齐设置为中间的内联块,所以其他内联块将垂直对齐容器的中心。我们现在需要做的就是将内容水平居中。由于内联块响应文本对齐,我们需要使用文本对齐:

.author-meta {
  height: 10em;
**text-align: center;** 
  border: 1px solid #ccc;
}
.author-info {
**text-align: left;** 
}

这将导致。author-meta 水平和垂直居中,如图 6-19 所示。

A314884_3_En_6_Fig19_HTML.jpg

图 6-19。内容现在水平和垂直居中

实际上,水平居中并不完全正确。请记住,行框中的任何空白字符都将显示为一个空格。伪元素将创建一个这样的空间,将内容向右推几个像素。我们可以通过对伪元素应用负边距来取消空白空间的宽度:

.author-info:before {
**margin-right: -.25em;** 
}

为什么是 0.25 毫米?在这种情况下,它恰好是当前字体中空白字符的宽度。这是一个“神奇的数字”,会随着使用的字体而变化。因此,它不是很健壮,我们不推荐任何系统化的布局工作。在我们的下一个水平布局例子中,我们将关注内联块作为布局工具的更详细的应用。

获取正确的细节:对抗空白

当处理水平布局时,每个块占用一个精确的宽度,空白问题变得更加明显。我们将通过构建另一个通用组件来强调如何在使用内联块时解决这个问题,使用更少的幻数。

这一次我们创建了一个导航栏,由四个链接项组成,每个链接项正好占宽度的四分之一。我们从标记开始:

<nav class="navbar">
  <ul>
      <li><a href="/home">Home</a></li>
      <li><a href="/spaceships">Spaceships</a></li>
      <li><a href="/planets">Planets</a></li>
      <li><a href="/stars">Stars</a></li>
    </ul>
</nav>

CSS 在颜色和字体方面为我们提供了一些基本的样式,以及突出项目之间边缘的轮廓。每个项目都设置为 25%的宽度,因此四个项目应该作为一个整体适合导航栏:

.navbar ul {
  font-family: Avenir Next, Avenir, Century Gothic, sans-serif;
  list-style: none;
  padding: 0;
  background-color: #486a8e;
}
.navbar li {
  text-transform: uppercase;
  display: inline-block;
  text-align: center;
  box-sizing: border-box;
  width: 25%;
  background-color: #12459e;
  outline: 1px solid #fff;
}
.navbar li a {
  display: block;
  text-decoration: none;
  line-height: 1.75em;
  padding: 1em;
  color: #fff;
}

我们使用 box-sizing: border-box 来确保每个项目的任何边框或填充都包含在每个项目的 25%宽度中。导航栏本身的背景是蓝灰色的,而项目的背景是略暗的蓝色,带有白色的链接文本。

现在来看结果,如图 6-20 所示。

A314884_3_En_6_Fig20_HTML.jpg

图 6-20。遗憾的是,这个列表不能放在一行中,而且项目是分开的

HTML 源代码中的换行符呈现为空格字符,增加到每个项目的 25%宽度,并导致换行。例如,我们可以通过将所有的

  • Tags are placed in a line to eliminate these blank characters, but this requirement for tag format is fragile.

我们解决这个问题的首选方法有点残忍。它的工作原理是将容器本身的字体大小设置为 0(从而导致空格字符的宽度为零),然后重新设置项目的大小:

.navbar ul {
**font-size: 0;** 
}
.navbar li {
**font-size: 16px;** 
**font-size: 1rem;** 
}

这以一种可预测的方式消除了空白,使项目很好地适应容器,如图 6-21 所示。

A314884_3_En_6_Fig21_HTML.jpg

图 6-21。具有四个等宽项目的导航栏

这种技术有几个缺点。第一个与继承的字体大小有关。假设我们在导航条上使用 16 像素的字体大小,我们不能再使用 em 单位或百分比来继承列表项的灵活字体大小——它只会变成 0 的倍数。相反,我们可以通过使用 rem 单位将大小基于根字体大小来保留灵活的大小调整。对于不支持 rem 单位的浏览器(大多数是 Internet Explorer 8 和更早版本),基于像素的测量作为一种后备措施。

第二个缺点与稍旧的基于 WebKit 的浏览器有关,在这种浏览器中,字体大小为 0 并不总是受欢迎的——例如,早期版本的 Android 4 上基于 WebKit 的浏览器。正如我们将在本章前面看到的,我们通常只使用内嵌块显示作为旧浏览器的后备,然后在 flexbox 等更现代的技术上再加一层。因为即使是这些旧的 Android 浏览器也支持 flexbox——尽管是旧版本——空白问题可能会成为一个问题。

小费

如果你出于某种原因需要在这些旧的 Android 浏览器中使用内嵌块技术,还有一个关于字体的技巧。它的工作原理是在父元素上使用一个很小的自定义字体,只包含一个宽度为零的空格字符。然后在子元素上重置原始字体系列。详见开发者 Matthew Lein 的这个演示:Matthew Lein . com/articles/inline-block-no-space-font/

将表格显示属性用于布局

表格中的行具有我们在 navbar 示例中寻找的精确质量:许多“单元格”划分它们之间的空间,从不在多行上滑落。这也是为什么在网络早期,实际的 HTML 表格被用于布局的原因之一。现在,我们可以通过 CSS 借用表格的显示模式,而不需要借助基于表格的标记。

如果我们将导航栏示例更改为使用 ul 元素的表格显示模式,并将每个项目设置为显示为表格单元格,我们将获得与使用内联块时相同的外观:

.navbar ul {
  /* some properties omitted for brevity. */
**width: 100%;** 
**display: table;** 
**table-layout: fixed;** 
}
.navbar li {
  width: 25%;
**display: table-cell;** 
}

这给了我们与前面的内嵌块示例完全相同的外观(如图 6-21 所示)。

请注意,我们已经将 ul 元素设置为 100%宽。这是为了确保 navbar 扩展以填充其父级。与常规块不同,没有设置宽度的表格具有“收缩以适应”宽度,除非单元格的内容将它们推出以填充其父容器。

对于如何计算表格行中每列的宽度,有两种算法。默认情况下,浏览器将使用“自动”算法。从标准的角度来看,它有些不明确,但是它基本上允许表格根据整个表格的单元格内容来调整列宽。

另一种算法是“固定的”表格布局。使用表格布局:固定,列宽根据表格的第一行确定。第一行上声明的任何宽度都会遇到“win”,如果后续行有更宽的内容,该内容将在单元格内换行,或者溢出。

虽然在这个示例中,将表格布局设置为 fixed 在技术上不是必需的,但是在使用表格显示模式作为布局工具时,通常会使用它,以避免自动模式带来的任何意外。

当使用表格显示模式进行布局时,您应该知道表格呈现的其他特点也适用。例如,不可能对呈现为表格单元格的元素应用边距,应用于表格单元格的定位行为充其量也是不可靠的。我们将在第九章回到 HTML 表格和 CSS 表格显示模式。

表格单元格中的垂直对齐

表格显示模式的另一个有用的方面是,在这种情况下,垂直对齐的工作方式略有不同。在显示为表格单元格的元素上设置 vertical-align: middle 将使单元格的内容垂直居中对齐,没有任何额外的技巧。图 6-22 显示了如果我们给以表格形式显示的列表添加一个设定的高度,并将列表项垂直居中对齐会发生什么。

.navbar ul {
  display: table;
  height: 100px;
}
.navbar li {
  display: table-cell;
  vertical-align: middle;
}

A314884_3_En_6_Fig22_HTML.jpg

图 6-22。为显示为表格单元格的列表项添加高度和垂直居中

不同技术的优缺点

当考虑将浮动、内联块和表格显示模式作为水平布局和垂直对齐的工具时,我们如何确定使用哪一种呢?每种方法都有利弊:

  • 与内联块一样,float能够换行。浮动还根据其内容“收缩包装”到一个大小,这是一个有用的行为。消极的一面是,当包含或清除浮动时,浮动可能会给你带来痛苦,当浮动的项目卡在更高的浮动上时。另一方面,浮动在某种程度上是独立于源代码顺序的,因为您可以将一些元素浮动在一行的右边,而将其他元素浮动在左边。

  • 内联块有空白问题,但是这些问题是可以解决的,尽管有一些蹩脚的解决方案。从积极的一面来看,内联块也可以换行到多行上,它们给你一些对垂直对齐的控制,并且它们有和浮动一样的“收缩换行”大小行为。

  • 使用表格显示模式进行水平布局也很有效,但是只适用于非换行的内容行。它们和表格有相同的怪癖,也就是说,例如,它们不受边距的影响,并且里面的项目不能被重新排序。它们还允许其内容简单地垂直居中。

flex box(flex box)的缩写形式

灵活框布局模块,称为 flexbox,是我们可以用来创建布局的一组较新的 CSS 属性。它由许多与容器元素( flex 容器)及其直接子元素( flex 项目)以及这些子元素的行为相关的属性组成。Flexbox 可以控制 flex 项目的几个方面:

  • 大小,基于内容和可用空间

  • 流向:水平或垂直,向前或向后

  • 在两个轴上对齐和分布

  • 订购,不考虑来源订单

如果对布局使用内联块、浮动和表格属性让你感到不舒服,flexbox 可能是你想要的解决方案。它是作为对我们在本章中已经看到的各种常见场景的直接响应而开发的。

浏览器支持和语法

所有主流浏览器的最新版本都支持 Flexbox。通过对语法和供应商前缀的一些调整,您也可以让它适用于各种稍旧的浏览器。

为了在 IE10 和更老的 WebKit 浏览器中实现支持,您需要用厂商前缀和稍微不同的属性来补充我们在本章中使用的标准语法,因为 flexbox 的语法在规范的各种迭代中已经改变了很多。有许多工具和文章描述了如何做到这一点,比如“Flexy Boxes”代码生成器(the-echoplex.net/flexyboxes/)。

请注意,Internet Explorer 9 和更早版本根本不支持 flexbox。我们将在本章后面讨论这些浏览器的一些回退策略。

了解挠曲方向:主轴和横轴

Flexbox 允许您定义页面的一个区域,在这个区域中,可以根据顺序、大小、分布和对齐来控制一组元素。该空间内的框按两个方向排列:默认情况下,水平(作为一行)或垂直(作为一列)。该方向被称为主轴

内部的方框也可以在垂直于主轴的方向上移动和调整大小:这被称为横轴(见图 6-23 )。通常,使用 flexbox 创建布局的最重要的度量是沿着主轴的尺寸:水平布局的宽度和垂直布局的高度。我们称这个尺寸为物品的主尺寸

A314884_3_En_6_Fig23_HTML.gif

图 6-23。定义行与列模式下的主轴和横轴,以及它们各自的主要大小属性

回到我们第一次在图 6-20 中看到的导航栏例子(一个包含链接的无序列表的包装器),我们可以很容易地将其转换成一个水平的 flex 容器。假设其余的样式(颜色,字体,链接样式,边框)是相同的,我们需要最少的 CSS。我们还不需要列表项目本身的任何特定属性,并且项目上没有声明宽度(见图 6-24 ):

.navbar ul {
  display: flex;
  /* this also implies flex-direction: row; unless told otherwise */
}

A314884_3_En_6_Fig24_HTML.jpg

图 6-24。使用 flexbox 创建的导航条

如图 6-24 所示,项目水平排列,并根据其中的内容缩小到最小尺寸。看待它的一种方式是,好像我们把块流旋转了 90 度。

项目也聚集在左侧,这是语言方向从左向右时的默认行为。如果我们将 flex-direction 属性更改为 row-reverse,项目将从右边缘开始,从右向左流动(参见图 6-25 )。注意顺序也是颠倒的!

.navbar ul {
  display: flex;
**flex-direction: row-reverse;** 
}

A314884_3_En_6_Fig25_HTML.jpg

图 6-25。行中的流动项目-反向

如果没有其他尺寸,flex 容器中的项目会缩小到这个尺寸。这意味着行中的项自动获得最小宽度,列中的项获得最小高度,这两者都基于每个项中内容的最小大小。

对齐和间距

我们可以使用 flexbox 以各种方式沿着行分布项目。Flexbox 称沿主轴分布对齐,横轴分布称对齐。(为了便于记忆,请记住水平方向是默认的,在水平书写系统中,文本对齐也在水平方向上进行。诀窍是当方向改变时要记住哪个是哪个。)

现在我们可以用各种关键字和 justify-content 属性在主轴上分布项目。使项目按当前文本方向(在本例中是从左到右)对齐的默认值称为 flex-start。使用弯曲端使它们移动到另一侧(见图 6-26 ),但这次保持相同的顺序。图 6-27 、 6-28 和 6-29 分别显示了其他关键字:中心、间隔和环绕。

A314884_3_En_6_Fig26_HTML.jpg

图 6-26。使用 justify-content: flex-end 将项目向右移动

A314884_3_En_6_Fig27_HTML.jpg

图 6-27。使用 justify-content: center 使 flex 项目居中。额外的空间放置在边缘项目的外侧

A314884_3_En_6_Fig28_HTML.jpg

图 6-28。使用对齐内容:空格。在项目之间放置额外的空间

A314884_3_En_6_Fig29_HTML.jpg

图 6-29。使用对齐内容:空格。空间被平均划分并放置在每个项目的两侧。请注意,项目之间的空间不会折叠

Flexbox 不允许您使用这些关键字来调整单个项目。然而,当应用于 flexbox 项目时,用关键字 auto 设置边距值有稍微不同的含义,我们可以在这里使用它。如果某个元素的一侧的 margin 设置为 auto,并且容器中还有剩余空间,则该边距会扩展以填充可用空间。这可用于除一件物品外所有物品都需要放在一边的模式。例如,我们可以将所有项目放在右边,但将“Home”项目放在左边(见图 6-30 ):

.navbar li:first-child {
  margin-right: auto;
}

A314884_3_En_6_Fig30_HTML.jpg

图 6-30。在第一个项目上使用 margin-right: auto 会耗尽所有剩余的空间,将其余的项目推到右边

请注意,像这样使用自动边距会抵消其他项目上任何对齐的影响,因为没有剩余的空间来分配。不过,您仍然可以在其他元素上设置单独的边距。

横轴对齐

到目前为止,我们只处理了水平布局的基本问题,这对于 flexbox 来说轻而易举。Flexbox 还允许我们控制另一个轴的工作方式。如果我们增加 flex 容器本身或其中一个项目的高度,我们会发现默认属性会产生有趣的效果(参见图 6-31 ):

.navbar ul {
**min-height: 100px;** 
}

A314884_3_En_6_Fig31_HTML.jpg

图 6-31。默认情况下,项目将拉伸以填充跨轴维度中的伸缩容器

似乎我们自动拥有了等高的物品!控制跨轴对齐的 align-items 属性的默认值是 stretch。这意味着所有 flex 项目将填满可用空间。我们还可以设置 flex-start、center 或 flex-end 的值(分别参见图 6-32 到 6-34 ),以使项目收缩回其原始大小,并与导航栏的顶部、中部或底部对齐。

A314884_3_En_6_Fig32_HTML.jpg

图 6-32。使用对齐项目:弹性开始

A314884_3_En_6_Fig33_HTML.jpg

图 6-33。使用对齐-项目:居中

A314884_3_En_6_Fig34_HTML.jpg

图 6-34。使用对齐-项目:柔性端

最后,可以使用 baseline 关键字将项目内文本的基线与容器的基线对齐,类似于内联块的默认工作方式。如果您有不同大小的盒子,您希望它们在横轴上放置不同的位置,但它们之间是对齐的,这将非常有用。

在图 6-35 中,我们添加了一个代表当前活动项目的类名:

A314884_3_En_6_Fig35_HTML.jpg

图 6-35。使用一个 navbar-active 类来显示选中的状态
<ul>
      <li><a href="/home">Home</a></li>
      <**li class="navbar-active"**><a href="/spaceships">Spaceships</a></li>
      <li><a href="/planets">Planets</a></li>
      <li><a href="/stars">Stars</a></li>
</ul>

活动项目被赋予了更大的字体大小和 1:

.navbar .navbar-active {
    font-size: 1.25em;
}

较大的活动项目现在决定了基线,其他项目也相应地自行对齐。

对齐单个项目

除了将所有项目作为一组对齐之外,您还可以在横轴上为每个项目设置单独的对齐方式。例如,我们可以让“Home”项与左上方对齐,其余项与右下方对齐(参见图 6-36 ):

.navbar ul {
   min-height: 100px;
   align-items: flex-end;
}
.navbar li:first-child {
**align-self: flex-start;** 
  margin-right: auto;
}

A314884_3_En_6_Fig36_HTML.jpg

图 6-36。使用自对齐进行单独对齐

与 Flexbox 垂直对齐

最后,flexbox 对齐用很少的代码解决了垂直对齐问题。当容器中有单个项目时,我们只需要将父容器设置为 flex 容器,然后将我们希望居中的项目的边距声明设置为自动。请记住,在 flex 项目上设置为自动的边距将向所有方向扩展以“填充”。

<div class="flex-container">
    <div class="flex-item">
      <h2>Not so lost in space</h2>
      <p>This item sits right in the middle of its container...<p>
    </div>
  </div>

我们现在可以将。使用下面的 CSS 水平伸缩项目和垂直伸缩项目,不管容器或项目的大小。在这个例子中,我们让容器和视窗一样高(在 html、body 和。flex-container 元素),只是为了可视化结果,如图 6-37 所示。

html, body {
  height: 100%;
}
.flex-container {
  height: 100%;
  display: flex;
}
.flex-item {
  margin: auto;
}

A314884_3_En_6_Fig37_HTML.jpg

图 6-37。带 flexbox 和自动边距的垂直和水平居中

当 flex 容器中有几个项目时——就像我们的作者元数据示例中一样——我们可以使用对齐属性将它们聚集到水平和垂直中心(参见图 6-38 )。为此,我们将对齐和对齐都设置为居中。(顺便说一下,这也适用于单个项目,但是 margin: auto 方法需要的代码要少一些。)

.author-meta {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

A314884_3_En_6_Fig38_HTML.jpg

图 6-38。使用 flexbox 轻松实现多个元素的垂直居中

灵活的尺寸

Flexbox 让我们能够更好地控制规模。这是 flexbox 在详细内容布局方面如此出色的部分原因,但也是迄今为止 flexbox 最复杂的部分。不要担心这一部分一开始会让人不知所措——灵活的规模调整是您在“点击”之前需要做的事情之一

灵活的上浆性能

这就是 flexbox 中“flex”的用武之地,正如三个属性 flex-basis、flex-grow 和 flex-shrink 中定义的那样。这些属性是在每个 flex 项目上设置的,而不是在容器上。

  • flex-basis 在根据可用空间进行修正之前,规定项目在主轴上的“首选”大小(宽度或高度)。它可以设置为长度(例如 18em)、百分比(基于容器的主轴大小)或关键字 auto(默认值)。

auto 关键字听起来像是将 width 或 height 设置为 auto,但事实并非如此。相反,这意味着项目将从相应的属性(宽度或高度)中获取其主要大小(如果设置了该属性的话)。如果没有设置主大小,元素将根据其内容调整大小,有点像浮点或内联块,

您还可以将值设置为 content,这也将根据项目的内容设置大小,但是将忽略任何设置了宽度或高度的主轴大小(与 auto 不同)。请注意,content 关键字是 flexbox 的新增功能,在撰写本文时对它的支持还不稳定。

  • flex-grow 规定当每个元素通过 flex-basis 被赋予其首选大小时,如果还有剩余空间会发生什么:你向它提供一个数字,称为伸缩因子,它作为额外空间的一部分计算出来。我们马上会解释分数是如何工作的。flex-grow 的默认值为 0,这意味着项目的增长不会超过它们从 flex-basis 获得的大小。

  • flex-shrink 的工作方式与 flex-grow 类似,但方向相反:如果没有足够的空间,元素将如何收缩?当伸缩开始起作用时,计算就有点复杂了——我们将在后面进一步讨论。默认值为 1,这意味着如果没有足够的空间,所有项目都将与其首选大小成比例缩小。

理解 flex-basis 如何处理 flex-grow 和 flex-shrink 是棘手的部分。Flexbox 使用一种相当复杂的算法来计算大小,但是如果我们将其简化为两个步骤,就会更容易处理:

  1. 通过查看弹性基准来确定假设的主要尺寸。

  2. 确定实际主要尺寸。如果在将项目放入具有假设主要大小的容器后,容器中还有任何剩余空间,它们可以增长。这一增长基于弹性增长因素。出于同样的原因,如果空间太小无法容纳它们,这些项目可以根据伸缩系数缩小。

我们可以通过一个例子把这些属性拼凑起来。在这个例子中,我们想象一个 1000 像素宽的容器。标记中的容器内有两个子项。其中一个包含一个短单词,导致这个特定的元素占据 200 像素的宽度。另一个包含一个长单词,宽度为 400 像素(见图 6-39 )。这些物品还没有放入容器中。

A314884_3_En_6_Fig39_HTML.gif

图 6-39。一个 1000 像素宽的 flex 容器,以及两个尚未放入容器的 flex 项目

如果这些项目的 flex-basis 设置为 auto,并且没有声明显式宽度值,如下所示,则当它们被放入容器时,它们将保留基于内容的大小(参见图 6-40 ),总共占用 600 个像素的可用宽度。这是 flex-basis 的默认行为,与我们到目前为止在导航栏示例中看到的一样。

.navbar li {
  **flex-basis: auto;** /* default value. */
}

A314884_3_En_6_Fig40_HTML.gif

图 6-40。这些项目总共占用了可用的 1000 个像素中的 600 个像素,留下了 400 个像素的未使用空间

由于还有空间可供分配,灵活增长开始发挥作用。默认情况下,flex-grow 设置为 0,这不会改变项目的大小。但是当我们将两个项目的 flex-grow 都设置为 1 时会发生什么呢?

.navbar li {
  flex-basis: auto;
**flex-grow: 1;** 
}

1 和 0 代表什么?嗯,这有点像鸡尾酒配方:1 份这个,2 份这个,3 份苏打水。它并不代表一个特定的度量,只是整体的一部分。

在这种情况下,有两个项目。两者现在将相等地增加可用空间的1%,这意味着它们都将增加剩余空间的一半,即 200 个像素。这意味着第一个项目将被调整到 400 像素的最终大小,第二个项目将是 600 像素,加起来正好填满容器,如图 6-41 所示。

A314884_3_En_6_Fig41_HTML.gif

图 6-41。两个项目都增加了剩余 400 像素的一部分,即每个项目增加 200 像素

我们还可以为项目设置单独的弹性增长系数:

.navbar li:first-child {
**flex-grow: 3;** 
}
.navbar li:last-child {
**flex-grow: 1;** 
}

这将导致第一个项目获得可用空间的四分之三,第二个项目获得四分之一。结果,这两个项目最终都是 500 像素宽!图 6-42 显示了在这种情况下,布局算法如何调整尺寸。

A314884_3_En_6_Fig42_HTML.gif

图 6-42。第一项将增加可用空间的四分之三,而第二项仅增加四分之一

本例中的项目碰巧以同样的宽度结束。如果我们希望项目之间按比例划分整个空间,而不考虑内容,有更好的 flexbox 技术,我们接下来会发现。

纯粹根据灵活因素进行规模调整

在我们在上一节中使用的简化 flexbox 布局算法的第一步中,项目的大小基于其内容的宽度,使用 flex-basis of auto,没有显式的宽度声明。如果我们假设弹性基数为 0,那么结果将是在第一步中没有空间被分配。容器内的所有空间将保留在步骤 2 中,根据伸缩系数进行划分,并设置项目的最终大小。

在图 6-43 中,项目的弹性基数为 0,弹性增长设置为 1。这意味着要划分的总空间由两部分组成,因此每个项目将正好占据分配空间的一半。这种效果接近于为布局计算和使用百分比,额外的好处是 flexbox 不在乎有多少个项目,它们会自动调整大小以适应总宽度。

A314884_3_En_6_Fig43_HTML.gif

图 6-43。弹性基础设置为 0 的两个项目在算法的第一步中将占用零空间。然后,将完全根据它们的伸缩系数来确定它们的大小

这一次,我们将使用 flex 简写来同时设置 flex-grow、flex-shrink 和 flex-basis,按顺序声明并用空格分隔:

.navbar li {
**flex: 1 0 0%;** 
}

注意值中 flex-basis 最后一个后面的百分号:速记中的 flex-basis 不能是无单位的,所以在这个实例中必须使用 0%或另一个单位,如 0px。

如果我们希望第一个项目占用的空间是任何其他项目的两倍,我们可以给它一个伸缩因子 2:

.navbar li {
  flex: 1 0 0%;
}
**.navbar li:first-child {** 
**flex-grow: 2;** 
**}** 

将这一点应用到前面的带有四个项目的 navbar 标记中,我们得到一个 navbar,其中第一个项目占宽度的五分之二(或 40%),接下来三个项目各占五分之一(或 20%)(见图 6-44 )。

A314884_3_En_6_Fig44_HTML.jpg

图 6-44。navbar 示例,其中第一个项目设置为增长 2 个单位,其余项目设置为增长 1 个单位

收缩弹性项目

当要放入 flex 容器中的项目加起来超过可用空间时,我们可以允许它们基于 flex-shrink 属性进行收缩。机制比 flex-grow 更复杂一些。缩小项目的更复杂规则背后的想法是防止小项目缩小到零,只是因为较大的项目导致总宽度过大。允许一个项目有更多的空间是非常简单的(正如我们在 flex-grow 中看到的),并且与可用空间成比例。当收缩发生时,它的方式略有不同。

回到我们假设的 1000 像素宽的导航栏,让我们假设有两个子项目,每个项目都有一个通过 flex-basis 设置的首选大小。它们一起超出容器宽度 300 个像素,如图 6-45 所示。

.navbar li:first-child {
   flex: 1 1 800px;
}
.navbar li:last-child {
  flex: 1 1 500px;
}

A314884_3_En_6_Fig45_HTML.gif

图 6-45。两个弹性物料,其组合弹性基础超出了容器宽度

组合的首选宽度(800 + 500 = 1300)超出了容器的大小 300 个像素,并且两项的 flex-shrink 值都为 1。你可能会认为这两个项目都缩小了 150 像素以腾出空间,但这不会发生。取而代之的是,每一项都将按伸缩系数和伸缩基数的比例收缩。从技术上讲,每个项目的伸缩系数都要乘以伸缩基数。接下来,该值除以每个项目的伸缩因子与其伸缩基准的乘积之和。最后,除法的结果乘以负的空间,给出我们要缩小的空间量。

这是你要记住的很多东西,但要点是:首选尺寸较大的项目比首选尺寸较小的项目收缩更多(相对于伸缩系数)。即使我们两个项目的伸缩系数都是 1,它们也会收缩不同的量。如果我们完成第一项的计算,结果如下:

((800 × 1) / ((800 × 1) + (500 × 1))) * 300 = 184.6

第一项会缩小 184.6 像素。对第二个项目进行同样的计算,我们应该得到余数:

((500 × 1) / ((800 × 1) + (500 × 1))) * 300 = 115.4

…这意味着第二个项目将缩小 115.4 个像素,总共减少了 300 个像素,以适应 flex 容器中的两个项目(参见图 6-46 )。

A314884_3_En_6_Fig46_HTML.gif

图 6-46。更复杂的伸缩计算

在使用 flexbox 时,您需要牢记这一点吗?最有可能的答案是“不”。但是如果你正在努力使一个布局工作,意识到伸缩与伸缩的工作方式不同可能会防止你抓狂。

包装 Flexbox 布局

在导航栏和作者元数据示例中,我们只处理了一行内容。就像内联块或浮动一样,flexbox 允许我们将内容分成几行(或几列),但是增加了控制。

警告

包装成多行或多列的属性来自较新版本的规范。支持老的 flexbox 规范的浏览器,比如 Safari 的老版本,4.4 版之前的存量 Android 浏览器,28 版之前的 Firefox,都不支持换行。

这一次,我们将使用一个标签列表,代表行星的类别。这是一个带有链接的无序列表,很像导航栏,但是条目的数量可能会大得多,因此无法将它们都放在一行中。我们给每个项目一个背景颜色,和一个物理行李标签的形状外观,使用我们用于评论气泡的相同类型的伪元素技巧(见图 6-47 )。

A314884_3_En_6_Fig47_HTML.jpg

图 6-47。我们的标签列表

标记非常简单:

<ul class="tags">
  <li><a href="/Binary_planet">Binary planet</a></li>
  <li><a href="/Carbon_planet">Carbon planet</a></li>
  <!-- …and so on… -->
</ul>

标签的样式稍微复杂一点,但是没有我们以前没见过的:

.tags {
  border: 1px solid #C9E1F4;
  margin: 0;
  padding: 1em;
  list-style: none;
}
.tags li {
  display: inline-block;
  margin: .5em;
}
.tags a {
  position: relative;
  display: block;
  padding: .25em .5em .25em .25em;
  background-color: #C9E1F4;
  color: #28448F;
  border-radius: 0 .25em .25em 0;
  line-height: 1.5;
  text-decoration: none;
  text-align: center;
}
.tags a:before {
  position: absolute;
  content: '';
  width: 0;
  height: 0;
  border: 1em solid transparent;
  border-right-width: .5em;
  border-right-color: #C9E1F4;
  left: -1.5em;
  top: 0;
}

使用前面的样式,标记被声明为内联块,并且将很好地换行。现在是时候对 flexbox 进行增强了。首先,我们将列表转换成一个 flex 容器,并告诉它允许使用设置为 wrap 的 flex-wrap 属性换行:

.tags {
**display: flex;** 
**flex-wrap: wrap;** 
  margin: 0;
  padding: 0;
  list-style: none;
}

在这一点上,这个列表看起来和它最初的样子非常相似。但是现在我们拥有 flexbox 的所有功能来控制行的方向、大小和对齐。

包装和方向

首先,我们可以反转行的方向,就像我们最初对导航栏所做的那样。当 flex-direction 更改为 row-reverse 时,项目将从右上方开始,从右向左流动,绕成右对齐的行,如图 6-48 所示。

A314884_3_En_6_Fig48_HTML.jpg

图 6-48。用弯曲方向反转流动:行反转

我们也可以反转垂直流,使行从底部开始,向上缠绕!在图 6-49 中,flex-direction 设置为 row-reverse,flex-wrap 设置为 wrap-reverse。

A314884_3_En_6_Fig49_HTML.jpg

图 6-49。使用 wrap-reverse 关键字从下到上排列内容
注意

Flexbox 方向是逻辑方向,这意味着它们依赖于文本方向作为开始和结束边缘。例如,如果您正在构建一个具有从右向左文本的阿拉伯语网站,水平方向将被反转(假设您在标记中设置了正确的 dir 属性),而垂直方向保持不变。

包装布局中的灵活大小调整

多行 flexbox 布局的另一个好处是灵活的尺寸允许我们均匀地填充行(见图 6-50 )。flex-grow 计算是基于每一行进行的,因此项目将仅增长到填充当前行所需的量。

.tags li {
**flex: 1 0 auto;** 
}

A314884_3_En_6_Fig50_HTML.jpg

图 6-50。应用伸缩因子创建完美填充的行

当以稍微不同的尺寸观看时,最后一个项目包裹在最后一行上,变得不舒服地宽(参见图 6-51 )。不幸的是,在包装 flexbox 布局中没有处理特定行的机制。例如,如果项目在最后一行,我们不能让它们变得不灵活。

A314884_3_En_6_Fig51_HTML.jpg

图 6-51。当最后一个标签自己换行到最后一行时,它会变得非常宽,并不断增长以填充空间

我们将通过在标签上设置 max-width 属性来解决眼前的问题,以便它们在一定的限制内保持灵活性(参见图 6-52 ):

.tags li {
  display: inline-block;
  margin: .5em;
  flex: 1 0 auto;
**max-width: 14em;** 
}

A314884_3_En_6_Fig52_HTML.jpg

图 6-52。通过在标签项目上设置合理的最大宽度,我们可以防止项目增长到令人不舒服的长度

通常,填充可用空间的能力是 flexbox 的核心优势。将 flex-grow 与最小和最大宽度相结合,我们可以构建非常智能的包装 flexbox 布局,无论屏幕大小或物品数量如何,物品都保持在合理的尺寸范围内。我们将在第八章中深入探讨这方面的技术,在那里我们将讨论响应式网页设计,以及如何使布局适应它们的环境。

对齐所有行

在我们之前对跨轴对齐属性(align-items 和 align-self)的回顾中,我们看到了 flexbox 如何允许我们根据单行的 flex-start、center、baseline 和 flex-end 点来对齐。在换行布局中,我们可以根据容器对齐行或列本身。

如果我们将标记列表容器的最小高度设置为 300px,那么 align-content 属性的效果就很明显了。默认情况下,它被设置为 stretch,这意味着每一行都将被拉伸以填充其在容器高度中的份额。如果我们检查物品,我们可以看到每个 li 元素将伸展以填充三分之一的高度,如图 6-53 所示。

.tags {
  display: flex;
  flex-wrap: wrap;
**min-height: 300px;** 
**/* align-content: stretch; is implied here */** 
}

A314884_3_En_6_Fig53_HTML.jpg

图 6-53。拉伸每一行,以便所有行的组合填充容器

align-content 的效果与使用 justify-content 在主轴上分布内容的方式非常相似。我们现在可以将内容分布到 flex-start(容器的顶部)、flex-end(底部)、center(聚集到中间),或者使用 space-between 或 space-around 分隔。

列布局和单独排序

使用 flexbox 订单属性,您可以完全摆脱来源订单。你可以简单地告诉浏览器你想要的盒子的顺序。默认情况下,所有项的顺序值为 0,具有相同顺序值的项按照它们在源中出现的顺序排序。

Flexbox 让我们可以完全控制订单。在我们的下一个 flexbox 示例中,我们将离开水平布局技术,并创建一个小的“文章预告”组件,其中显示了我们的飞船文章的摘录,以及标题、图像和继续阅读全文的链接。我们将把它显示为单列。

从标记开始,我们将按重要性顺序排列每个组成部分:

  1. 带有文章标题的标题

  2. 摘要文本

  3. 文章题目的一幅图像插图

  4. 文章的链接

<div class="article-teaser">
  <h2>The Dragon and other spaceships</h2>
  <div class="article-teaser-text">
     <p>There are actual spaceships…</p>
  </div>
  <img src="img/medium_spaceship.jpg" alt="The Dragon spaceship in orbit around Earth.">
  <p class="article-teaser-more">
    <a href="/spaceships">Read the whole Spaceship article</a>
  </p>
</div>

文章预告如图 6-54 所示。我们为这个组件添加了一些基本的样式,主要是处理边距、颜色和排版。这种特殊的样式对于这个例子来说并不重要,所以我们暂时不考虑它。

A314884_3_En_6_Fig54_HTML.jpg

图 6-54。我们的文章摘要组件的第一次迭代

视觉上,把图像放在第一位,抓住潜在读者的眼球,也许对设计有益。但是就标记而言,把图像放在前面不太合理。例如,我们可能希望屏幕阅读器的用户将文章标题作为摘要中的第一个元素。

为了实现这种重新排序,我们需要将。文章摘要容器到 flexbox 列:

.article-teaser {
  display: flex;
  flex-direction: column;
}

接下来,我们给图像一个低于缺省值 0 的顺序值,这样它首先出现(见图 6-55 ):

.article-teaser img {
  order: -1;
}

A314884_3_En_6_Fig55_HTML.jpg

图 6-55。我们重新排序的文章预告

例如,如果我们首先需要标题,我们可以在标题和图像上设置顺序值:

.article-teaser h2 {
  order: -2;
}
.article-teaser img {
  order: -1;
}

…其余项目将保持不变,因为它们保留订单值 0。您设置的顺序值不必是连续的(我们可以分别为 header 和 image 使用-99 和-6),它们可以是正数也可以是负数。只要它们是可以比较的数字,这些项目就会相应地重新排序。请记住 0 是默认值。

警告

值得强调的是,使用 flexbox 重新排序项目只是一种视觉上的转变。像 tab 键顺序和屏幕阅读器朗读内容的顺序这样的事情不会被 order 属性改变。由于这个原因,确保 HTML 源代码仍然是合乎逻辑的,而不是将 flexbox 作为草率标记实践的借口是很重要的。

嵌套 Flexbox 布局

作为我们的最后一个例子,我们将展示 flexbox 布局可以嵌套,并带来一些真正有用的结果。

我们将重复使用文章预告的例子,但是这一次有两个预告,我们想把它们放在一起展示。为此,我们将添加一个包装元素,设置为显示为 flexbox 行:

<div class="article-teaser-group">
  <div class="article-teaser">
    <!-- first article teaser contents… -->
  </div>
  <div class="article-teaser">
    <!-- second article teaser contents… -->
  </div>
</div>

包装元素设置为显示为 flexbox 行:

.article-teaser-group {
   display: flex;
 }

在图 6-56 中,我们可以看到现在熟悉的效果,默认情况下,flex 项目将在横轴方向拉伸,创建两个等高的文章预告。

A314884_3_En_6_Fig56_HTML.jpg

图 6-56。我们的两篇文章摘要现在是嵌套的 flexbox 列,也作为 flexbox 行中的项目

我们以前见过使用 flexbox 的等高物品。但是当物品也是像这样的 flexbox 容器时,我们可以玩最后一个把戏。我们可以看到,第二个预告中的内容比第一个短得多,这在两个预告中的高对比度“阅读更多”链接组件之间造成了不平衡的印象。在这种情况下,Flexbox 也可以帮助我们。

还记得在伸缩项上设定为自动的边距会在每个方向上耗尽所有剩余空间吗?在“read more”元素上设置 margin-top: auto 会将其推到列的底部,使其与其旁边的组件对齐(参见图 6-57 ):

.article-teaser-more {
  margin-top: auto;
}

A314884_3_En_6_Fig57_HTML.jpg

图 6-57。在链接上使用 margin-top: auto 会将它推到列的底部,创建一个更整洁的印象

这是一种动态内容的布局,如果使用浮动、内联块和定位等旧技术,这种布局会很麻烦。当 flexbox 不受支持时,它会退回到更简单但完全可行的设计——这很好地引导我们进入下一个主题。

Flexbox 倒回

虽然 flexbox 在理论上得到广泛支持,但是仍然会有一些情况,您可能希望依靠像 floats 或 inline blocks 这样的技术。您可能需要支持旧版本的 Internet Explorer(IE10 之前)。可能会有一些浏览器错误阻止您实现 flexbox 布局,即使是在声称支持的浏览器之间。或者,您可能想要一种与旧的 Android 手机一致的包装行为。你明白了。

幸运的是,在 flexbox 的设计中有一些智慧,可以让你实现这些回退。

首先,由于 flexbox 是容器上的一种显示模式,不理解 flex 关键字的浏览器会忽略它。这意味着您可以让不支持的浏览器将容器本身显示为普通块。

其次,您可以向 flex 项目添加浮点声明,或者将它们设置为显示为内嵌块,而不会影响 flexbox 布局。float 和 clear 属性对 flex 项目没有影响,设置不同的显示值不会影响框的布局。这给了你一个很好的机会开始使用 flexbox 进行水平布局。首先,您将创建一个适用于任何地方的简单布局,然后使用 flexbox 增强它——例如,利用自动边距、垂直对齐或其他漂亮的修饰。

在某些情况下,您可能想单独区分理解 flexbox 的浏览器和不理解 flexbox 的浏览器。在这种情况下,我们建议您使用像 Modernizr(modernizr.com)这样的 JavaScript 库来检测浏览器的功能,为您提供作为样式基础的类名挂钩。我们将在第七章中更仔细地看看 Modernizr 技术。

如果您只关心该规范在最新浏览器中的最新实现,您也可以使用@supports 符号,这是专门为区分基于浏览器支持的样式而设计的:

@supports (flex-wrap: wrap) {
  /* flexbox rules here */
}

在这种情况下,我们将@supports 块限制在既理解条件规则语法又理解 flex-wrap: wrap 声明的浏览器中,该声明只存在于实现最新语法的浏览器中。有很多浏览器理解 flexbox 的变体,但不支持@supports,反之亦然。当您只想应用 flexbox 的一些非常新的方面,或者解决旧实现中的错误时,这种构造会非常方便。

这种技术的重要方面是将更简单的后备解决方案作为基线,然后在此基础上构建 flexbox 增强功能。

Flexbox 错误和陷阱

由于 flexbox 相当新,并且已经经历了不同语法的多次迭代,所以有相当多的错误和不一致需要考虑。

为了跟踪稍老的浏览器中的 flexbox bugs,请查看 Philip Walton 的社区管理的“Flexbugs”库(github.com/philipwalton/flexbugs),其中列出了 bug 和解决方法。

除了纯粹的 bug,还有一些其他的东西可能会让你犯错:

  • 当图像、视频和其他具有固有长宽比的对象成为 flex 项目时,调整它们的大小是很棘手的。随着时间的推移,规范在这方面已经发生了变化,所以最好的办法可能是在这些对象周围添加一个包装器元素,让包装器充当 flex 项目。

  • Flex 项目也有所谓的隐含最小宽度。实际上,这意味着 flex 项目可能会拒绝缩小到基于内容的大小以下,尽管它被告知可以通过灵活调整大小来这样做。重写 min-width 属性或设置显式主尺寸会重写此行为。

  • flex 项目的绘制顺序由 order 属性决定(如果存在)。这可能会影响项目的重叠,就像 z 索引一样。

  • 此外,与普通块不同,flex 项可以被赋予一个 z 索引,而不必赋予它们一个非静态的位置。如果给定 z 索引,该索引将覆盖堆叠顺序。设置了 z 索引的伸缩项还会创建新的堆叠上下文。

  • 有些元素的渲染模型有点不正常。例如,button 和 fieldset 元素的默认呈现并不完全遵循 CSS 样式的一般规则。试图让这些元素充当 flex 容器可能会严重失败。

摘要

在这一章中,我们已经研究了几种常见的内容布局模式和它们的各种用例。我们已经看到了如何将内联块、表格显示模式和浮动用于布局目的,以及它们各自的利弊。

我们还研究了一些使用绝对或相对定位的有用模式,并结合边距来实现一些有效的模式。

最后,我们深入研究了 flexbox 标准,找到了更有效的方法来横向和纵向分发、调整、对齐和排序项目。

在接下来的两章中,我们将在布局方面加大努力。首先,我们将看看如何将布局技术应用到整个页面的布局系统中,并了解专门为该场景创建的新网格模块。然后,我们将会看到如何使用响应式网页设计的技术,使我们的设计适应不同的屏幕尺寸。

七、页面布局和网格

这一章是关于创建页面布局的系统方法。在前一章中,我们从单个页面组件的角度关注布局。当设计页面的整体布局时,各个组件之间的优先级和关系是一个很好的起点。但是在某个时候,你会开始发现整体结构的循环模式。本章是关于将这些结构整理成可重用的解决方案:容器,你可以将你的内容“倒入”其中。

创建这些容器时,很可能会使用预先确定大小和比例的网格系统。本章将探索用 CSS 创建这样一个系统的不同方法。首先,我们将着眼于更传统的技术,然后用 flexbox 增强它们。在下半部分,我们将关注即将到来的 CSS 网格布局规范。

在本章中,您将了解

  • 页面布局的系统方法

  • 关于页面网格的术语

  • 使用浮动和内嵌块构建健壮的页面布局,并由 flexbox 增强

  • 使用网格布局模块

规划您的布局

当开始将设计转化为功能齐全的模板时,很容易让人直接跳入,开始标记页面并编写 CSS。风险在于,你可能会很快将自己逼入绝境。少量的计划可以在以后省去很多麻烦。或者说,俗话说“量两次;割一次。”

在这个阶段重要的事情是找到重复的模式和你试图翻译成代码的设计系统的本质。

网格

当谈到一个网站的整体布局系统时,网格是一个经常出现的词。它是指设计者用来将布局划分为若干行和列的一组基本规则(参见图 7-1 中的示例)。行和列之间的空间被称为间距。通过讨论一个跨越三列、左右两边有檐槽的元素,设计人员和开发人员对正在构建的内容有了更清晰的了解。这种系统的布局方法提供了一定的可预测性和稳定性。它仍然允许你离开网格,拥有非对称的布局——但这通常是例外而不是规则。

A314884_3_En_7_Fig1_HTML.jpg

图 7-1。在上使用的列和装订线尺寸的图解叠加视图。这个视图主要使用总共五列的组合,最右边的四列中有三个嵌套列

*#### 传统网格术语

网格远不是网页设计师的发明;几个世纪以来,它们以各种形式出现在平面设计中。在网页设计中,我们经常将术语简化为行、列和间距,但是在传统印刷设计中使用的网格有更丰富的词汇。

在更传统的意义上,行和列是整个网格的名称,横跨整个宽度或高度。横跨一列和一行的网格的单个单元被称为单元模块。然后,这些单元以一定的比例组合成更大的区域,例如,一个区域可以是三列宽、两行高。这些垂直和水平的单元组合,传统上被称为区域区域

整个网格中的总单元数通常基于一个数字,该数字可以用多种方式划分,以创建不同的比率。例如,24 列网格可以进一步分为 4 列,每列 6 个单位宽,或者 3 列,每列 8 个单位,依此类推。

这些术语的传统含义可能并不是为 Web 构建网格所必需的知识。另一方面,当与同事交流或为代码创建命名约定时,掌握它们不会有什么坏处。拥有一个通用的命名方案对于从一开始就创建一个结构化的代码库非常有帮助——通常您会使用它们来为您的设计创建助手类。

布局助手类

类名显然是悬挂布局样式的钩子。对于一个非常简单的站点,你可能最终只有几个类名来决定你布局的基础。用于控制两栏博客布局的类名可能看起来很简单,如下所示:

.main-content {}
.secondary-content {}

当您在更复杂的站点上工作时,您可能会发现重复的模式,这些模式无法清楚地识别为属于特定的内容层次结构。这使得以一种可重用的方式命名你的类变得更加困难。为了创建可重用的样式,很多人使用更直观的描述性类名,如下所示:

.column { /* general column styles */ }
.column-half { /* taking up 50% of the row width */ }
.row { /* general row styles */ }

这些类名在严格意义上是表示性的,这意味着你把关于表示的信息放在你的 HTML 中。另一方面,它们具有高度的可读性、可重用性,并且允许你一次性解决布局问题。

另一种方法是在一个列表中收集所有具有某种共同样式的选择器:

.thing,
.other-thing,
.third-thing,
.fourth-thing {
  /* styles in common here */
}

这种组织的好处是,您不需要将这些样式与 HTML 中的任何单个名称联系起来,而是可以在选择器列表中添加和删除。风险在于,选择器的数量可能会失控,难以扫描。这也提出了代码组织方面的问题。当基于相似的样式而不是基于可重用的组件来划分样式时,当对站点的特定部分进行编辑时,您可能不得不在 CSS 中跳来跳去,达到令人不舒服的程度。

命名方案是创建高质量代码的一个极具挑战性的部分,将表示和标记结合在一起是一个棘手的权衡。在这一章中,我们将尝试走中间路线,使用少量的布局助手类,同时尽可能地减少与表示的联系。这是一种创建布局系统的高度紧凑的方式,允许快速原型和一致的样式。我们将在第十二章回到创建模块化和可重用 CSS 系统的挑战。

不管是你自己创造了视觉设计,还是你在为别人的设计编码,你都会感谢自己创造了一个健壮的、经过深思熟虑的东西。当您需要与团队中的其他设计人员和开发人员协作时,为布局系统的组件命名也非常有帮助。如果你的设计非常复杂,你甚至可以从整合现成的 CSS 布局框架中获益。

使用现成的设计网格和框架

由于 CSS 布局可能很复杂,而且你在规划它们时发现的模式经常会在许多网站设计中重复出现,所以有许多现成的 CSS 框架或库包含了某种网格系统。

它们中的许多工作得相当好,并允许您快速组装跨浏览器支持的设计。这是一件非常好的事情,它可能会节省您大量的精力。特别是对于尺寸之间有复杂关系的布局,像 Gridset(gridsetapp.com)这样帮助你生成 CSS 的工具真的很有帮助(见图 7-2 )。

A314884_3_En_7_Fig2_HTML.jpg

图 7-2。Gridset 是一个帮助你为你的布局生成网格规则的工具

不利的一面是,许多较大的 CSS 框架都附带了一套完整的布局规则,用于您的设计可能不需要的情况。这意味着在您的项目中包含那些从未使用过但仍然占用网络和大脑带宽的代码——在您的项目中包含您自己都不理解的 CSS 可能是一件坏事。

您是否应该选择第三方代码而不是您自己构建的代码,这总是取决于具体情况。如果您正在构建一个快速原型来测试一些东西,那么使用一个预构建的库可能是很好的。对于中等复杂程度的站点,您可能会发现自己必须对现有的库进行大量修改,以至于自己构建库更有意义。

固定的、流动的或弹性的

你可能以前遇到过固定布局流体布局弹性布局这些术语。这些是指我们如何在特定的布局中约束元素的大小。

  • 固定布局:我们在页面布局上强加特定度量的布局。例如,我们可以说“我们的页面是 960 像素宽,仅此而已。”这是很长一段时间的趋势,因为它给了设计师和开发人员对设计的很大控制权。多年来,设计师们一直在争论他们的布局应该基于哪个尺寸:“是大多数用户都有 1024 像素宽的屏幕,还是现在假设每个人都有 1280 像素宽的显示器是安全的?”

  • 弹性布局:这种布局的灵活性来自于在 ems 中调整布局组件的大小。这样,即使用户调整文本的大小,布局的比例也会保留。这可以与最小和最大宽度相结合,使页面更好地适应屏幕尺寸。即使弹性布局在今天有些过时,借用 ems 中使用最大宽度集的想法也是约束流体布局的好方法。

  • 流体布局:也称为流体布局,布局中的元素按百分比调整大小,大小之间的比例(有时还有它们之间的距离)保持不变。以像素为单位的实际大小因浏览器窗口的大小而异。这在某种程度上是 Web 的默认模式,其中块级元素没有定义宽度,而是根据可用空间进行动态调整。

人们仍然在构建固定的布局,因为从设计师的角度来看,他们给人一种控制感。但是这种控制是强加在访问网站的人身上的,固定宽度的网站在当今设备和屏幕尺寸多样化的情况下工作不佳。

正如你可能已经猜到的,最好避免固定宽度的设计,让你的布局变得流畅,适应你正在浏览的设备。这种让设计对环境做出反应的方法是所谓的响应式网页设计的基石之一。

注意

创建响应式布局需要更多的要素。我们将在第八章中讨论这些,但是在这一章中,为了保持例子的简单,我们假设我们正在处理大屏幕的布局。

创建灵活的页面布局

在这一节中,我们将介绍一些实用的技巧,告诉你如何创建一个样式系统,帮助你创建可靠的、灵活的、可重用的页面布局。

本节中的许多技术和 CSS 属性都是你在第六章中看到的相同技术的变体,但是从更高层次的角度来看。

我们将重新创建一个布局,类似于我们在www.theguardian.com的一些部分中发现的布局(见图 7-3 ),在列和水平部分有一些不同的变化。

A314884_3_En_7_Fig3_HTML.jpg

图 7-3。《世界新闻报》在上分版面。我们可以看到不同数量的柱子和不同尺寸的一些变化

*如果我们将这个设计分解成主要布局模式的简化草图,我们最终会得到类似于图 7-4 所示的东西。在本节的其余部分,我们将尝试重新创建这个布局。

A314884_3_En_7_Fig4_HTML.gif

图 7-4。布局中不同大小的列的草图

草图中没有显示的是,布局作为一个整体在页面中居中,并有一个最大宽度。我们将从创建这种包装元素的规则开始。

定义内容包装

使用某种保存页面内容的包装元素是很常见的,就像这样:

<body>
    <div class="wrapper">
        <h1>My page content</h1>
    </div>
</body>

你可以使用 body 元素吗——毕竟,它已经存在(或者应该存在!)——但很多时候,你最终想要的不仅仅是一张包装纸。可以有一个站点范围的导航栏,在包装器外面有不同的宽度,或者只是覆盖整个屏幕宽度的堆叠部分,在它们里面有居中的包装器。

接下来,我们需要为这个包装器的行为设置一些规则。我们会给它一个宽度和一个最大宽度,并使用自动边距将其居中。对于流体布局,通常使用百分比设置的宽度,略小于窗口的全宽。然后根据字体大小设置最大宽度,使用 ems:

.wrapper {
  width: 95%;
  max-width: 76em;
  margin: 0 auto;
}

body 元素也有自己的默认边距,所以我们需要删除它,否则它会影响我们的样式。在第二章中,我们提到了“重置”样式,比如埃里克·迈耶的 CSS 重置和尼古拉斯·加拉格尔的 Normalize.css:他们会为你创建一个一致的基线,但有时慢慢开始并建立你自己的样式是个好主意。我们将保持这个例子的简单性:

body {
  margin: 0;
}

图 7-5 中的结果为我们提供了一个良好的基础。我们在这几行代码中做出了许多决定:

  • 主包装器通常会占据 95%的视窗宽度。

  • 对于速记 margin: 0 auto,我们已经告诉它在顶部或底部没有边距,并自动将空白空间划分到它的左侧和右侧(每侧留 2.5%),这使它在页面上居中。

  • 包装材质的最大宽度应为 76 ems。基于 16 像素的默认字体大小,这相当于 1216 像素,但是如果用户在浏览器中增加字体大小设置,这将自动改变。这个 76em 的数字并不是什么硬性规定:它只是在尝试布局时看起来合适的数字。

A314884_3_En_7_Fig5_HTML.jpg

图 7-5。我们的内容包装器——我们暂时给了它一个背景颜色和一些高度来观察样式的效果

我们正在回避许多可能发生变化的因素。我们不知道屏幕有多大,所以我们不想为了整体宽度而将自己束缚在任何特定的像素大小上。我们也不知道用户的字体设置是什么。

我们所知道的是,无论屏幕大小如何,我们都需要一个居中的包装,在两侧至少留有一些空间。我们还希望在某个时候限制布局宽度,以防止像行长度这样的事情失控。如果用户的字体大小设置不同于正常的 16px 默认设置,将遵循最大宽度。

您选择的特定度量将会改变以适应您正在进行的设计,但是原则是相同的:找到您想要为整个内容包装器设置的基本约束,但是不要太严格地定义它们。相反,要确保针对不断变化的环境进行优化。

“为改变而优化”是你在软件设计的所有领域都会听到的口头禅。我们已经为我们的布局包装器应该做什么建立了原则,而没有过分具体的像素度量。现在可以一次性编辑使用包装类的元素。

我们可以直接利用包装类的一致性,在三个不同的地方使用它。我们将为虚构的报纸添加一个报头部分和一个导航栏。这两个元素都是全角元素,但是有一个内部包装器约束其中的内容。保存特定于页面的内容的主要元素位于这两个块之后。

<header class="masthead">
**<div class="wrapper">** 
    <h1>Important News</h1>
**</div>** 
</header>
<nav role="navigation" class="navbar">
**<div class="wrapper">** 
    <ul class="navlist">
        <li><a href="/">Home</a></li>
        <!-- ...and so on -->
    </ul>
**</div>** 
</nav>
**<main class="wrapper">** 
  <!-- the main content goes here -->
**</main>** 

我们不会在这里深入讨论报头和导航条的样式(见图 7-6 ),但是 CSS 在本书的代码示例中是可用的,并且我们已经在第六章中介绍了如何创建导航条组件。

A314884_3_En_7_Fig6_HTML.jpg

图 7-6。使用包装类将两个堆叠的页面部分中的元素居中

行容器

接下来,我们关注水平内容组的行为。在这个阶段,我们希望他们做的唯一一件事就是包含任何浮动的元素。在第三章中,我们看到了如何通过创建一个新的块格式化上下文,使用溢出属性来实现浮动包容。虽然溢出方法对于较小的组件来说通常是最简单的方法,但是在这种情况下,我们将使用清除伪元素来代替。页面中这些较大的部分更有可能将定位内容突出到行容器之外,所以弄乱溢出可能会反过来伤害我们。

.row:after {
  content: '';
  display: block;
  clear: both;
  height: 0;
}

创建列

我们有自己的行容器,我们想把它分成列。首先:选择一种水平布局的方法。正如我们在前一章看到的,有许多方法可以做到这一点。使用 floats 可能是最常用的技术,并且具有非常普遍的浏览器支持,所以让我们从这里开始。默认情况下,向左浮动项目对于从左到右的语言来说似乎是一个不错的选择。

如果我们想在不改变宽度的情况下直接在列容器上添加边框或填充,我们还应该将框大小调整方法设置为 border-box:

.col {
  float: left;
  box-sizing: border-box;
}

接下来,我们需要决定调整列大小的方法。许多 CSS 库使用明确的表示大小类来调整各个列的大小:

.col-1of4 {
  width: 25%;
}
.col-1of2 {
  width: 50%;
}
/* ...etc */

当您可能在台式机或笔记本电脑上工作时,这种方法对于快速原型制作非常有帮助。根据前面的规则,最左边的列占据一半宽度的三列布局变得非常容易在标记中声明:

<div class="row">
  <div class="col **col-1of2**"></div>
  <div class="col **col-1of4**"></div>
  <div class="col **col-1of4**"></div>
</div>

这种技术的缺点是过分强调一种特定的布局。当我们以后想要调整布局以适应不同的屏幕尺寸时,命名方案就没有什么意义了。

如果我们想保留可重用类名的策略来调整大小,我们将在标记和表示之间留下一些联系。我们可以选择通过使用其他类名使这些联系更松散,而不用提及具体的宽度或比例。使用音乐世界的一个比喻,我们可以,例如,为通常有四个相等部分的行容器创建一个规则——一个四重奏:

.row-quartet > * {
  width: 25%;
}

在这里,我们的目标是带有通用选择器的行容器的直接子容器。这是为了保持这个一般规则的特异性较低。因为通用选择器的特异性值为 0,所以我们可以稍后用一个类名来覆盖这个宽度。下面的标记现在将创建一个具有四个等宽列的行:

<div class="row row-quartet">
  <div class="col"></div>
  <div class="col"></div>
  <div class="col"></div>
  <div class="col"></div>
</div>

任何与这个“节奏”的偏差。row-quartet 现在将获得自己的覆盖类名,但没有特定于布局的类。之前的三列布局示例现在看起来略有不同:

<div class="row row-quartet">
  <div class="col my-special-column"></div>
  <div class="col"></div>
  <div class="col"></div>
</div>
**.my-special-column {** 
**width: 50%;** 
**}** 

我们现在可以根据需要用更多的“速度”类来补充行规则:

.row-quartet > * {
  width: 25%;
}
**.row-trio > * {** 
**width: 33.3333%;** 
**}** 

在我们构建的布局草图中,两个子类别部分都有一个占据页面最左边五分之一的标题区域,以及一个占据剩余五分之四的内容区域。在第一小节中,还有一个更大的文章栏,占据了 50%的内容区域。

.subcategory-content {
  width: 80%;
}
.subcategory-header {
  width: 20%;
}
.subcategory-featured {
  width: 50%;
}

HTML:

<section class="subcategory">
  <div class="row">
    <header class="col subcategory-header">
      <h2>Sub-section 1</h2>
    </header>
    <div class="col subcategory-content">
      <div class="row row-quartet">
        <div class="col subcategory-featured"></div>
        <div class="col"></div>
        <div class="col"></div>
      </div>
      <div class="row row-quartet">
        <div class="col"></div>
        <div class="col"></div>
        <div class="col"></div>
        <div class="col"></div>
      </div>
    </div>
  </div>
</section>
<section class="subcategory">
  <div class="row">
    <header class="col subcategory-header"></header>
    <div class="col subcategory-content">
      <div class="row row-trio">
        <div class="col"></div>
        <div class="col"></div>
        <div class="col"></div>
      </div>
    </div>
  </div>
</section>
使用额外的包装元素

在这个例子中,我们为“内部”列组使用了带有 row 类的额外嵌套元素。您还可以将 row 类添加到 col 项本身。虽然在许多情况下,减少标记是一种很好的感觉,通常也是一种很好的实践,但是如果概念上不同的规则开始冲突,也会适得其反。在某些地方添加额外的元素来分隔它们可以最小化发生这种情况的风险,尽管有些多余。

将这些与包装器和一个简单的标题放在一起,我们离“页面框架”还有很长的路要走如下面所列和图 7-7 所示,我们还添加了一些占位符内容标题和一些列的最小高度,以及一个轮廓。(轮廓是可视化和调试布局的一个便利技巧,因为它们不影响元素的大小。)

.col {
  min-height: 100px;
  outline: 1px solid #666;
}

A314884_3_En_7_Fig7_HTML.jpg

图 7-7。我们的页面布局现在开始成形

现在我们已经有了自己的网格类,我们可以轻松地用更多的度量来组合或扩展它们,以创建更复杂的布局模式。接下来,我们将在每个容器中添加一些虚拟内容,并添加细节。

以下是带有图像的文章在标记中的外观:

<div class="col">
  <article class="story">
    <img src="http://placehold.it/600x300" alt="Dummy image">
    <h3>Cras suscipit nec leo id.</h3>
    <p>Autem repudiandae...</p>
  </article>
</div>

图 7-8 显示了当我们加入这些虚拟内容时的样子。

A314884_3_En_7_Fig8_HTML.jpg

图 7-8。现在,我们已经在网格单元格中添加了一些内容,以查看我们的布局如何,以及一些轻微的印刷样式

我们在列容器中使用了一个类名称为 story 的 article 元素。额外的元素将布局从内容中分离出来,为我们提供了一个可移植的解决方案,而不是重载包装器本身。

虚拟内容样式只包含一种背景颜色、一点填充和一条规则,这条规则使故事中的任何图像变得流畅,以填充元素的宽度:

.story {
  padding: .6875em;
  background-color: #eee;
}
.story img {
  width: 100%;
}

流槽

现在很明显,我们需要在我们的列之间增加一些间距,让布局有一些喘息的空间。这就是水槽的用武之地。

在流体布局中,您可以选择按百分比设置的流体间距,也可以选择固定长度的间距,通常相对于字体大小进行设置。无论您选择哪种方式,最常用的技术之一是在列元素的两侧设置相等的间距,每侧的宽度为预期总装订线尺寸的一半(参见图 7-9 )。

A314884_3_En_7_Fig9_HTML.gif

图 7-9。在列的两侧添加等量的空间,每个空间是总装订线宽度的一半

如果您需要直接在列上设置背景颜色或图像,并且仍然希望它们间隔开,那么使用边距作为边距可能是有意义的。如果你需要迎合像 IE7 这样甚至不支持框定大小的老浏览器,这也是有意义的。考虑到这是一个不固定的布局,您可能希望使用设置了百分比的边距,因为如果不使用 calc(),混合百分比和其他长度会变得很棘手,而 calc()在旧的浏览器中也不受支持。

在任何情况下,知道如何计算百分比边距都是有用的,这样可以使它们与其他宽度很好地配合。在前面的例子中,我们使用了 16 像素的字体大小和 1.375 的行高,这相当于 22 像素。比方说,我们希望间距等于相当宽的屏幕上文本的行高,将排版尺寸连接到网格。我们从布局的最宽点开始,76 ems 或 1216 像素。

因为边距是相对于包含块的,所以我们用计算相对字体大小的相同方法来计算装订线与总宽度的比率:用所需的尺寸除以总宽度。用 22 除以 1216 得到 0.018092105,所以整个檐槽大约是 1.8%。最后,我们将它分成两半,得到每列两边的边距,最后得到 0.9%:

.col {
  float: left;
  box-sizing: border-box;
**margin: 0 0.9% 1.375em;** 
}

我们还添加了一个底部边距,以一行文本的高度来分隔各行内容。请注意,垂直间距是以 ems 而不是百分比设置的,因为行高与屏幕大小无关,所以我们希望保持这种关系不变。

看看正在进行的示例,我们会看到一个不完整的布局(见图 7-10 ),因为页边距增加了列的尺寸。甚至没有框的大小:边框可以让我们摆脱这一点,所以我们需要修改列宽。

A314884_3_En_7_Fig10_HTML.jpg

图 7-10。我们的布局现在被打破了,因为页边距使各列相加超过 100%

为了在使用流体槽边距时固定列宽计算,我们需要从每个列宽中减去 1.8%:

.row-trio > * {
  width: **31.53333%**;
}
.row-quartet > * {
  width: **23.2%**;
}

.subcategory-featured {
  width: **48.2%**;
}
.subcategory-header {
  width: **18.2%**;
}
.subcategory-content {
  width: **78.2%**;
}

这给了我们在图 7-11 中看到的工作版本。在截图中,我们稍微缩小了浏览器窗口,你可以看到边框也随之缩小。

A314884_3_En_7_Fig11_HTML.jpg

图 7-11。我们的页面现在有了流畅的装订线,可以随着页面的宽度而增长和收缩

否定外水槽

此时,我们有了一个表示行、流体列和流体槽的工作系统。剩下的工作就是把细节做好,把视觉差异的风险降到最低。

首先,我们用来创建檐槽的边距会在外部容器的左右两边造成额外的缩进,这可能是不希望的。在更多的行容器中嵌套列会加剧这个问题(参见图 7-12 )。我们可以取消第一项的左边距和最后一项的右边距来应对这种情况。但是这会使计算栏宽和装订线宽度的数学计算更加复杂。

A314884_3_En_7_Fig12_HTML.jpg

图 7-12。由于我们应用于每一列的外部边距来创建装订线,文章容器最终离部分边框的右边缘有一段距离

相反,我们将使用负边距的技巧来缓解这个问题。我们在第六章中提到过,没有指定宽度的非浮动块元素在给定负的左边距和右边距时会扩展它们的宽度。

由于我们选择使用一个单独的元素作为我们的行容器(而不是让列元素兼作任何嵌套列的行),我们可以很好地利用这一点。我们修改了网格规则,规定每一行容器的每一侧都有一个等于半个装订线的负边距(见图 7-13 ):

.row {
**margin: 0 -.9%;** 
}

A314884_3_En_7_Fig13_HTML.jpg

图 7-13。在行容器元素上使用负边距,我们可以抵消嵌套行的额外缩进和复合边距

备用排水沟策略

为了进一步简化列宽计算,我们可以利用 box-sizing 属性,并使用 padding 来设置间距。

如果我们继续使用流体檐槽,我们只需要将檐槽尺寸改为填充。现在,我们可以再次将柱的尺寸表示为整体的适当分数,而不考虑檐槽的宽度:

.col {
  float: left;
  box-sizing: border-box;
**padding: 0 .9% 1.375em;** 
}
.row-trio > * {
**width: 33.33333%;** 
}
.subcategory-featured {
**width: 50%;** 
}
/* ...etc */

这也为使用带有排印尺寸的装订线设置开辟了道路:我们可以使用 ems 来设置装订线,使其与字体大小相关,而不是与网格的宽度相关。在下面的例子中(见图 7-14 ),装订线的尺寸与行高相同,无论网格的宽度如何,都在列之间产生相等的垂直和水平间距。

A314884_3_En_7_Fig14_HTML.jpg

图 7-14。使用与字体大小相关的“弹性”边距设置,无论内容的宽度如何,边距都保持不变
.col {
  float: left;
  box-sizing: border-box;
**/* one half of the line-height as padding on left and right: */** 
**padding: 0 .6875em 1.375em;** 
}

增强的列:换行和等高

到目前为止,我们使用浮动作为创建布局的方法。正如我们在前一章中所看到的,我们还有一系列其他的工具可以使用。我们将简要地看一些以与浮动列相同的一般方式使用它们的例子。这将帮助我们创建更加灵活的布局。

用内嵌块环绕列行

如果你仔细看《卫报》的截图(见图 7-15 ,你会注意到最上面的小节底部实际上有两行标题链接。在我们的布局版本中,到目前为止我们只有一行稍微大一点的故事预览。

A314884_3_En_7_Fig15_HTML.jpg

图 7-15。顶部小节的底部包含两行故事

使用浮动来包装容器行是一件棘手的事情:如果其中一个项目较高,浮动可能会被卡住,产生一个丑陋的阶梯效果。

为了解决这个问题,我们可以创建一个通用的类名,在内容需要包装成几行的地方使用。对于具有这个类名的容器,我们将使用我们在第六章中使用的字体大小技术来使用内嵌块显示。这样做的时候,我们需要以 rem 为单位设置行容器的负边距,因为元素本身的字体大小现在是 0。为了完全向后兼容,我们在 IE 8 中使用了像素测量。

.row-wrapping {
  font-size: 0;
  margin: 0 -11px;
  margin: 0 -.6875rem;
}
.row-wrapping > * {
  float: none;
  vertical-align: top;
  display: inline-block;
  font-size: 16px;
  font-size: 1rem;
}

此时,我们可以添加任意多的故事预览,它们会在连续填充四项后整齐地换行。但是在我们查看结果之前,我们将使用 flexbox 进一步润色细节。

将 Flexbox 用于等高列

正如我们在第六章中看到的,flexbox 可以帮助创建等高的列。创建系统布局时,我们希望有一些特定的规则集,仅在支持 flexbox 时应用。

为了能够检测 flexbox 支持,我们将在页面顶部添加一个小脚本。为此,我们将使用 Modernizr,它为受支持的每个特性在 html 元素中添加类名。在modernizr.com上,你可以创建你自己的检测脚本文件,里面只有你需要的检测代码。对于本例,我们将只为各种 flexbox 特性添加检测,以保持文件较小。

创建检测脚本后,将它放入 JavaScript 文件中,在加载任何 CSS 文件之前,加载页面的 head 元素。顺序很重要,因为检测需要在加载时、应用样式之前进行。

<script src="modernizr.js"></script>

我们现在可以开始使用带前缀的类来编码我们的解决方案,并且确信只有支持的浏览器才能看到它。flexbox 类表示现代 flexbox 支持,而 flexwrap 类表示支持将 flexbox 项目包装到多行或多列中。

在完整的代码示例中,您会发现我们将这些与 flexboxtweener 类结合在一起,这表明支持 IE10 中发布的 flexbox 版本。

首先,我们将把标准行转换成 flexbox 行:

.flexbox .row {
  display: flex;
}

此时,我们已经创建了等高列,这是默认拉伸 flex 项以填充父项的直接结果。

由于我们在每一列的内容周围使用了一个包装器元素,所以我们需要再加点 flexbox 的魔力,让内容均匀地填充各列。每个列本身都是一个列 flexbox 容器,其中的直接子项被设置为在分配任何额外空间时均匀填充空间:

.flexbox .col {
  display: flex;
  flex-direction: column;
}
.flexbox .col > * {
  flex: 1;
}

简写 flex: 1 是 flex 简写的特例,它将 flex-grow 设置为 1,将 flex-shrink 设置为 1,将 flex-basis 设置为 0。

最后,我们扩充了用于包装行的类,这样它们也可以利用 flexbox 的等高机制:

.flexwrap .row-wrapping {
  display: flex;
  flex-wrap: wrap;
}

请看图 7-16 所示的布局示例,它向我们展示了整齐的行和列,完美地填满了空间。

A314884_3_En_7_Fig16_HTML.jpg

图 7-16。网格的行和列现在完美地填满了它们的容器,调整到每行中最高的内容

至此,我们已经为创建页面布局创建了一个灵活的小规则系统。我们可以通过重组简单的类名集合来实现行、列和间距的一致性。这基本上就是像 Bootstrap 和 Foundation 这样的 CSS 框架中现成的网格规则为您做的事情,但是通常更依赖于表示性的类名。

像我们在本章中所做的那样从简单开始,允许您创建项目特别需要的网格规则,保持代码小且易于管理(最后的示例文件有大约 80 行用于整个网格系统的大间距代码,包括所有浏览器前缀)。

Flexbox 作为页面布局的通用工具

在前一章中,我们看到 flexbox 是一个强大的工具,可以实现详细而灵活的内容布局。在这一章中,我们将它放在一个更加向后兼容的基于浮动的布局系统之上。这种策略非常强大,事实上,这与《卫报》在他们的页面布局中采用的策略完全相同——如果你深究他们的页面来源,你会发现很多相似之处!

我们在第六章中看到了为什么这种“在顶部喷洒 flexbox”的策略如此有效——flexbox 被设计成忽略浮动并显示 flex 项目的属性。这使得使用 flexbox 来修饰基于浮动的布局变得很容易。flex 项采用已经设置的属性中的宽度、边距、填充等。但是 flexbox 是创建全页面布局和网格状结构的合适工具吗?

没有任何东西(除了在旧浏览器中缺乏支持)阻止你使用 flexbox 作为页面布局的核心方法,尽管它并没有明确地为此目的而设计。毕竟,浮动也不是!不过,使用 flexbox 作为高级布局工具既有好处也有坏处。

利弊

从积极的一面来看,flexbox 很快——至少在实现最新规范的浏览器中是如此。现代的 flexbox 通常比 floats 更具性能。不过,最早的 flexbox 规范的实现通常表现很差,所以您应该小心地将它应用于较旧的浏览器。

Flexbox 还使得获取页面的一部分变得非常容易,只需要很少的代码行,就可以使用增长和收缩因子将它分成灵活的部分。这种容纳内容而不考虑项目数量的能力对于创建类似网格的布局来说是一个明显的优势。

不利的一面是,由于这种灵活性要求在内容加载到项目中时进行重新计算,因此在第一次加载页面时可能会导致紧张的体验。例如,加载到一个灵活项目中的图像可以随着项目的增长而“推动”其他项目以容纳新的内容。

我们之前处理的示例依赖于行的默认 flex 值(其中元素不会自动增长)结合显式宽度来最小化跳动效应。

一维或二维布局

到目前为止,我们研究的所有布局方法,包括 flexbox,都是通过排列来创建行和列的变体。即使它们中的一些允许将内容包装成几行(从而堆叠成垂直维度),它们基本上是一维的——内容从左到右、从右到左或从上到下流动(参见图 7-17 ),但是项目不能同时跨越行和列。这意味着我们需要使用包装元素来细分布局。

A314884_3_En_7_Fig17_HTML.gif

图 7-17。到目前为止,我们所看到的所有布局方法(甚至包括换行的方法)都是一维的,因为内容是单向流动的

在 web 布局的早期,创建布局的少数工具之一是使用实际的 HTML 表格元素。在 CSS 成为一种可行的替代方案之后,实践仍然存在的原因之一是它实际上使我们能够创建二维布局——表中的项目可以具有 colspan 和 rowspan 属性,允许它们参与复杂的布局场景,如图 7-18 所示。

A314884_3_En_7_Fig18_HTML.gif

图 7-18。二维布局——看看您是否能确定需要多少容器元素才能用 floats 或 flexbox 实现这种布局

对于目前的 CSS 布局,我们已经接受了布局中的任何子部分都可能需要自己的容器元素,并且这种布局是我们应用于单个元素的东西。但是即将到来的 CSS 网格布局模块旨在改变这一切。

CSS 网格布局模块:2D 布局

当谈到页面布局的宏观层面时,到目前为止,我们所看到的技术都不是控制二维网格中的顺序、位置和大小的完整解决方案。CSS 网格布局模块定义了第一组 CSS 属性来专门做这件事。

使用 Grid Layout 模块,我们可以删除许多添加到控件布局中的额外元素,从而极大地简化了我们的标记。它还将设置列或行维度的负担从元素本身转移到表示页面上网格的单个包含元素。

警告:前方实验性质!

应该注意,网格布局规范是本章中支持最少的布局技术,在撰写本文时仍处于实验阶段。

Google Chrome Canary、Firefox Developer Edition、Safari Technology Preview 和 WebKit Nightly prerelease 浏览器版本都相当全面地实现了网格布局。Chrome Canary 的实现往往是最新的——我们建议您尝试该版本中的示例。它要求您打开首选项标志“启用实验性 Web 平台特性”

令人惊讶的是,Internet Explorer 是第一个支持网格布局的浏览器。它随 Internet Explorer 10 一起提供,但当时的规范看起来有点不同,并不支持所有的功能。为了使基础工作正常进行,您需要稍微修改一下语法,并对网格属性使用-ms-前缀。Microsoft Edge 也支持这种旧语法。

在这一章中,我们将只看今天定义的标准语法。如果你想修改 IE10-11 和微软 Edge 的语法,看看微软开发者网络页面的网格布局(msdn . Microsoft . com/en-us/library/ie/hh 673533(v = vs . 85)。aspx )。

理解网格术语

图 7-19 向你展示了一个完全成熟的网格,就像 CSS 中定义的那样。

A314884_3_En_7_Fig19_HTML.gif

图 7-19。网格容器及其组成部分

事情是这样的:

  • 设置为显示为网格的元素被称为网格容器——图中较厚的外部部分。

  • 然后容器被网格线分割成几个部分,称为网格单元,穿过网格容器。

  • 这些线条形成水平和垂直延伸的条带,称为网格轨迹。水平轨迹是网格行,垂直轨迹是网格列

  • 由一组相邻单元覆盖的组合矩形表面被称为网格区域

  • 网格容器的直接子容器称为网格项目。这些可以放置在网格区域。

您可能会注意到,这些术语与我们在本章开始时概述的更传统的网格术语没有什么共同之处。像 Mark Boulton 这样的设计者批评了这种术语上的差异(markboulton . co . uk/journal/open-letter-to-W3C-CSS-working-group-re-CSS-grids),但是编写规范的人决定最好使用来自表格和电子表格概念的名称来让开发人员理解网格的概念。不管是好是坏,这些名字是我们所坚持的。

定义行和列

为了创建网格,我们需要告诉浏览器它的行和列的数量和行为。为了实现图 7-19 中的 4×2 网格,使用我们可信赖的旧包装器 div 作为容器,我们需要将显示模式设置为 grid。我们还提供行和列的度量,称为网格模板:

.wrapper {
    display: grid;
    grid-template-rows: 300px 300px;
    grid-template-columns: 1fr 1fr 1fr 1fr;
}

前面的代码为我们提供了一个网格,其中有两行各 300 像素高,四列等宽。它还在每一列和每一行的边缘生成网格线——我们稍后需要使用这些网格线。

我们使用的列宽单位是新的:fr 单位代表分数(可用空间)。它与我们在 flexbox flex-grow-factors 中看到的灵活单位非常相似,但在这里它有自己的单位符号,大概是为了避免与其他无单位数字混淆。可用空间是在用明确的长度或根据它们自己的内容来调整任何网格轨迹的大小时剩下的空间。

因此,这里的每个 fr 单元代表网格中可用空间的四分之一;如果我们增加第五列 1fr,每个单元将代表可用空间的五分之一。

我们还可以在行和列中混合和匹配单位:您几乎可以选择任何类型的长度测量。例如,可以将列声明为 200px 20% 1fr 200px,在边缘处给出两个固定宽度的 200 像素列,从左数第二列占总空间的 20%,第三列占用其后剩余的任何空间——fr 单元处理计算完其他长度后的剩余空间,就像 flexbox 中一样。

为我们的页面子部分制作网格

查看到目前为止我们一直在使用的示例页面部分,我们现在可以将每个子部分分割成一个网格。第一部分最简单的网格应该是三行五列。列需要是总宽度的五分之一,行可以有自动高度,完全取决于内容(见图 7-20 )。

A314884_3_En_7_Fig20_HTML.jpg

图 7-20。从第一个页面子部分创建一个网格容器需要我们将其分成五列和三行。数字表示生成的网格线

现在可以从根本上简化内容所需的标记。我们仍然要为网格容器使用一个包装器元素,将它与任何子部分样式分开,但是在里面所有的故事都是直接的子元素:

<section class="subcategory">
  <div class="grid-a">
    <header class="subcategory-header">
      <h2>Lorem ipsum</h2>
    </header>
    <article class="story story-featured">
      <!-- The slightly bigger article goes here -->
    </article>
    <article class="story">[...]</article>
    <article class="story">[...]</article>
    <!-- ...and so on, for all our articles. -->      
  </div>
</section>

接下来,我们将在 CSS 中定义这个特定的网格设置。正如我们从图 7-20 中的网格“切片”中看到的,我们需要三行自动高度和五列,每一列占据相等的空间:

.grid-a {
  display: grid;
**grid-template-rows: auto auto auto;** 
**grid-template-columns: repeat(5, 1fr);** 
  margin: 0 -.6875em;
}

您还可以看到 grids 附带的一个新的函数符号:能够对列或行重复指定次数的跟踪声明,而不是逐个输入每个跟踪。

由于网格轨道不是由文档对象模型(DOM)中的任何特定元素表示的,所以我们不能用最小宽度、最大宽度等来调整它们的大小。为了在网格轨迹声明中实现相同的功能,引入了 minmax()函数符号。例如,我们可以将最后两行设置为至少 4em 高,但除此之外,它们将占用等量的可用空间:

.grid-a {
  display: grid;
  grid-template-rows: auto **minmax(4em, 1fr) minmax(4em, 1fr)**;
  grid-template-columns: repeat(5, 1fr);
  margin: 0 -.6875em;
}

如果要将网格轨迹定义压缩成一个简写,可以使用 grid-template 属性,在该属性中可以提供行定义和列定义,用斜杠分隔:

.grid-a {
  display: grid;
**grid-template: auto minmax(4em, 1fr) minmax(4em, 1fr) / repeat(5, 1fr);** 
  margin: 0 -.6875em;
}

在网格上放置项目

要在网格上放置项目,我们需要引用项目开始和结束的网格线。例如,子节标题占据了整个最左边的列。最冗长的说法是设置两个维度的起始行和结束行的属性(参见图 7-21 ):

.subsection-header {
  grid-row-start: 1;
  grid-column-start: 1;
  grid-row-end: 4;
  grid-column-end: 2;
}

A314884_3_En_7_Fig21_HTML.gif

图 7-21。使用带编号的网格线将页眉放置在网格上

我们可以通过分别使用 grid-row 和 grid-column 属性在一个声明中设置起始行和结束行来简化这一过程。每个属性中的起始行和结束行用斜杠字符分隔。

.subsection-header {
  grid-row: 1/4;
  grid-column: 1/2;
}

如果我们不确定网格中有多少行,但仍然希望标题跨越所有行,我们会希望指定标题在最后一行结束。Grid Layout 允许您使用负索引向后计算行数,因此最后声明的轨道的结束行总是-1。默认范围始终是一个单元格,因此我们也可以省略网格列值的最后一部分:

.subsection-header {
  grid-row: 1/-1;
  grid-column: 1; /* equivalent to grid-column: 1/2 */
}

最后,我们可以将这些值进一步压缩到 grid-area 属性中:它最多包含四个用斜线分隔的值。它们依次指定网格-行-开始、网格-列-开始、网格-行-结束和网格-列-结束。

.subsection-header {
  grid-area: 1/1/-1;
}

在前面的代码片段中,我们省略了第四个参数,该参数指示列方向的末端位置。您可以使用两个 end-direction 参数来实现这一点,因为网格定位将默认为在任一方向上跨越一个网格轨迹的项目。

网格项目对齐

当您将项目放置在网格上时,它们会自动变得与您放置它们的网格区域一样宽和一样高。自动扩展高度与 flex 行项目在 flexbox 中的工作方式非常相似。这不是巧合。

flexbox 和 Grid 布局都根据 CSS Box 对齐规范指定了子项的行为,CSS Box 对齐规范是一种在多个 CSS 上下文中负责对齐和对齐的标准。

就像在 flexbox 行中一样,可以使用 align-items 和 align-self 来控制垂直对齐。对齐方式默认为拉伸,这会导致项目垂直扩展以填充该区域。这里使用与 flexbox 中相同的值(但没有 flex 前缀),例如开始、结束或中心——图 7-22 解释了不同之处。

A314884_3_En_7_Fig22_HTML.gif

图 7-22。网格项目对齐的一些可能值

网格项目的行为类似于块级元素,并自动填充它们所在的网格区域的宽度,除非您为它们提供另一种度量。宽度的百分比基于项目所在的网格区域,而不是网格容器。

如果网格项目没有填满它们所在区域的整个宽度,您也可以使用 justify-items 和 justify-self 属性将它们在该区域内左对齐、右对齐或居中对齐。

就像在 flexbox 中一样,您可以在单个项目上使用 align-self,但是在网格布局上下文中,您也可以设置 justify-self。在网格容器上,align-items 或 justify-items 设置项目的默认对齐方式。

对齐网格轨迹

同样,当网格区域内的项目没有占据整个区域时,您可以对齐网格区域内的项目,也可以在容器内对齐网格轨道本身。只要轨道大小加起来没有覆盖网格容器的整个大小,就可以使用 align-content(垂直)和 justify-content(水平)来移动轨道。

例如,以下网格声明中的列加起来不等于容器的总大小:

.grid {
  width: 1000px;
  grid-template-columns: repeat(8, 100px); /* 800px in total */
}

现在,您可以选择容器内剩余空间的最终位置。默认情况下,justify-content 计算开始。图 7-23 显示了可能的值及其影响。

A314884_3_En_7_Fig23_HTML.gif

图 7-23。使用对齐内容移动网格轨道

以类似的方式,您可以使用具有 align-content 属性的相同关键字垂直对齐轨道(如果容器具有固定的高度)。

网格布局中的檐槽

有几种方法可以在网格中创建檐槽。通过在项目本身上使用边距,可以避免将它们与网格属性一起声明。也可以使用栅格轨迹对齐(例如,请参见前面的间距示例),或者创建类似于檐槽的空栅格轨迹。

如果您需要在所有轨道之间保持相同的固定大小的装订线,最简单的方法是使用 grid-column-gap 和 grid-row-gap 属性,如下所示。这会创建固定大小的装订线,就好像网格线本身有宽度一样,相当于多列布局中的列间距或表格中的边框间距。

.grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
**grid-column-gap: 1.5em;** 
**grid-row-gap: 1.5em;** 
}

自动网格放置

在我们正在处理的新闻站点子部分中,最左边的列是为标题保留的,但是其余的空间只是用。故事元素。例如,使用:n-of-type()选择器和显式网格位置来定位它们不会太难,但那会相当乏味:

.story-featured {
  grid-area: 1/2/2/4;
}
.story:nth-of-type(2) {
  grid-area: 1/4/2/5;
}
/* ...and so on */

幸运的是,网格布局规范有一个叫做自动布局的东西。它是网格布局的属性默认值的一部分,在不做任何更改的情况下,项目按源顺序排列在第一行的第一个可用单元格中,其中有一个空列。当行填满时,网格将在下一行以及那里的任何空单元格上继续。

这意味着我们只需指定以下内容,网格布局算法就能完成其工作:

  • 网格定义

  • 标题区域

  • 这篇专题文章跨越了两个专栏

其他东西都打包好了。复制我们之前创建的基于浮动的网格的完整代码(但是使用了更干净的标记)如下所示:

.grid-a {
**display: grid;** 
**grid-template-rows: auto auto auto;** 
**grid-template-columns: repeat(5, 1fr);** 
}
.subcategory-header {
**grid-row: 1/-1;** 
}
.story-featured {
**grid-column: span 2;** 
}

那就是共五个 申报用于控制实际布局!不可否认,完整的代码示例有更多的填充和空白边规则,但这与之前基于浮动的示例是一样的。图 7-24 显示了。故事条目填满了网格。

A314884_3_En_7_Fig24_HTML.gif

图 7-24。只有子节标题具有任何显式位置,即使这样也使用第 1 列的默认位置。其余的项目一列一列、一行一行地放置

自动放置的顺序

在这个例子中,自动布局缺省值非常适合我们。有几件事允许我们进一步控制位置,而不需要明确项目的最终位置。

在我们到目前为止所玩的例子中,源顺序与网格放置项目的顺序整齐地排列在一起。我们还可以像使用 flexbox 一样,利用 order 属性来控制项目的处理顺序。项目默认为订单值 0,并且允许任何整数值,包括负数值。

.story:nth-of-type(2),
.story:nth-of-type(3) {
**order: -2;** 
}
.story-featured {
**order: -1;** 
}

这将更改布局,使专题文章成为放置在网格上的第三个项目:第二个和第三个文章排在前面(由类为。网格包装内的故事)。之后,放置所有默认为 order: 0 的其他故事,如图 7-25 所示。

A314884_3_En_7_Fig25_HTML.jpg

图 7-25。更改 order 属性决定了自动布局发生的顺序
注意

没有什么可以阻止你把几个项目重叠在同一个网格区域。在这种情况下,order 属性也会影响它们的绘制顺序。您可以使用 z-index 进一步控制网格项目的堆叠,而无需设置任何特定的定位属性,就像使用 flexbox 一样。每个网格项目也形成自己的堆叠上下文。

切换自动放置算法

默认情况下,自动放置逐行发生。您可以将其设置为逐列放置,这由网格自动流动属性控制:

.my-row-grid {
  grid-auto-flow: row; /* default value */
}
.my-columnar-grid {
  grid-auto-flow: column;
}

默认情况下,放置算法非常简单:它进行一次遍历,并试图找到要放置的项目的下一个网格单元序列。当项目跨越多个单元格时,这可能会导致网格中出现孔洞(参见图 7-26 )。

A314884_3_En_7_Fig26_HTML.gif

图 7-26。当项目跨越几个单元格时,默认的稀疏算法可能会导致间隔。使用密集算法时,可以更有效地打包项目

如果我们将算法更改为使用所谓的密集模式(稀疏模式是默认模式),自动放置算法会返回到每一遍的起点,尝试找到第一个空槽。这导致了更加密集的网格。

.grid {
  grid-auto-flow: row **dense**;
}

网格模板区域

CSS 网格布局中的“命名模板区域”语法可能是 CSS 中最奇怪的部分之一。它允许你以一种非常直观的方式指定事情将如何被安排。因为它可能更适合简单的网格,让我们看看我们一直在处理的例子的第二小节(见图 7-27 )。我们会说,我们希望在这个布局中放入两个故事和几个广告。

A314884_3_En_7_Fig27_HTML.jpg

图 7-27。第二小节,左边有一个标题,两个故事块,中间有几个广告

在这一部分的标记中,我们希望按照优先顺序列出内容,首先是标题,然后是文章,最后是广告:

<section class="subcategory">
  <div class="grid-b">
    <header class="subcategory-header"></header>
    <article class="story"></article>          
    <article class="story"></article>
    <div class="ad ad1"></div>
    <div class="ad ad2"></div>
  </div>
</section>

然后,我们可以使用 grid-template-areas 属性声明网格布局:

.grid-b {
  display: grid;
**grid-template-columns: 20% 1fr 1fr 1fr;** 
**grid-template-areas: "hd st1 . st2"** 
**"hd st1 . st2";** 
}

grid-template-areas 属性采用空格分隔的引用字符串列表,这些引用字符串本身由空格分隔的网格每行的自定义标识符组成。您可以自由地为这些标识符选择名称,只要它们不与现有的 CSS 关键字冲突。

列或行之间相邻的同名单元格组成了命名的网格区域。这些区域必须是矩形的。用点标记的区域是匿名单元格,没有名称。

我们已经可视化地排列了这些行,所以它们从上到下排成一行,这是可选的,但是很有帮助——注意它们是如何形成我们布局的可视化表示的?这就像描述网格的 ASCII 艺术(图 7-28 显示了生成的网格区域)。

A314884_3_En_7_Fig28_HTML.gif

图 7-28。基于我们的模板产生的命名网格区域

各列的模板为第一列提供 20%,其余各列使用 fr 单位各占剩余 80%的三分之一。

为了在这个网格上放置项目,我们现在可以再次使用 grid-area 属性,但是这次使用我们已经定义的自定义区域名称:

.grid-b .subcategory-header {
**grid-area: hd;** 
}
.grid-b .story:nth-child(2) {
**grid-area: st1;** 
}
.grid-b .story:nth-child(3) {
**grid-area: st2;** 
}

在这个例子中,我们没有任何命名区域或特定广告位置的原因是我们不需要这样做。它们简单地默认为自动放置算法,并结束剩下的两个空单元格。全部完成!

现在,当老板不可避免地要你在故事的前面和下面再插入五个广告时,你只需要将它们添加到标记的最后,并调整网格模板区域(见图 7-29 ):

.grid-b {
  display: grid;
  grid-auto-columns: 1fr;
**grid-template-areas: "hd ... ... ..."** 
**"hd st1 ... st2"** 
**"hd ... ... ...";** 
}

A314884_3_En_7_Fig29_HTML.jpg

图 7-29。更多的广告被放入网格布局中

此示例还显示了用于表示未命名单元格的点模式的变体。该规范允许多个相邻的点代表一个匿名单元格,以便让您更整齐地排列模板字符串。

网格布局上的结束语

我们已经了解了网格布局的最重要的特性,但是还有更多要学习的。网格布局规范庞大而复杂,因为它允许您选择多种方式来表达网格结构。

网格布局成为布局的默认方式可能还需要一段时间——不理解它的浏览器至少还会存在几年。因为它影响了我们页面的一个非常重要的部分,所以很难在不退回到一个简单的页面元素列的情况下逐步分层。至少可以找到一个基于 JavaScript 的 polyfill,它是由弗朗索瓦·雷米(github.com/FremyCompany/css-grid-polyfill)创建的。

和任何新技术一样,我们设计师和开发人员如何创造性地将网格布局应用到我们构建的网站中,还有待观察。但是因为它将很快出现在大多数浏览器中(如果不是在你读这本书的时候!),尽早开始使用网格布局将是一个好主意。

摘要

这一章是关于设计网页布局系统的系统方法,从行、列和间距的角度考虑。我们开始使用 floats 构建向后和向前兼容的网格系统,内嵌块和 flexbox 属性使设计更进一步。

在 CSS 的整个历史中,我们需要有嵌套的元素结构,以便创建结构来保存我们的布局。这甚至适用于 flexbox 布局,否则它将是一个非常强大的布局工具。我们在本章的后半部分讨论了 CSS 网格布局规范,其中涉及到了很多问题。使用网格属性的布局将网格创建从单个元素转移到网格容器,我们只需要将项目放置并对齐到正确的位置。

有了这种理解,我们现在准备好掌握网页设计中的另一层思维:使你的页面适应各种不同的设备和形式。所以,系好安全带,准备下一章:响应式网页设计& CSS。**

八、响应式网页设计和 CSS

当 iPhone 在 2007 年首次亮相时,它标志着在移动设备上浏览体验的重大飞跃。人们争先恐后地为手机和触摸屏开发不同的优化网站,导致了“手机网络”和“桌面网络”的概念。

今天,你可以在手机中找到小到可笑的超大尺寸的浏览器;小平板,大平板,大小电脑,电视,手表,各种游戏机。

为每一种外形和输入类型创建一个单独的站点是不可能的,界限只会变得更加模糊。建立一个适应人们观看的设备的网站——一个响应迅速的网站——已经成为一种规范。

响应式网站设计原则上很简单,但是当你深入细节时,它就变得复杂了。在这一章中,我们将看看 CSS 中的技术,在某种程度上也包括 HTML,这些技术让你从基本原则出发,对响应式网页设计有一个坚实的理解。

我们会掩护

  • 响应式网页设计背后的历史和推理

  • 视窗、媒体类型和媒体查询的工作方式

  • 创建响应式网站时的基本“移动优先”策略

  • 何时何地创建断点

  • 使用 flexbox、网格布局和多栏布局等现代技术的响应示例

  • 响应式排版和响应式媒体内容

一个有反应的例子

从 CSS 的角度来看,响应式 web 设计最实际的部分是使用根据视区大小进行调整的流畅布局。我们将从重写第七章的新闻站点示例的第一部分开始这一章,作为一个响应性布局。

开始简单

对于较窄的视口,如移动设备上的视口,简单的布局通常就足够了。一列条目,按照内容的优先级排序(它们应该在 HTML 源代码中),是一种常见的方法,如图 8-1 所示。

A314884_3_En_8_Fig1_HTML.jpg

图 8-1。适用于较窄屏幕的单列布局

就布局代码而言,这意味着从我们在第七章中使用的示例中移除样式,而不是添加样式。我们可以删除几乎所有提及特定宽度的内容。我们设置为基本样式的唯一内容是行和列的填充和边距。我们还将把列设置为浮动的和 100%宽,遵守确保行包含任何浮动子元素的规则。

.row {
  padding: 0;
  margin: 0 -.6875em;
}
.row:after {
  content: '';
  display: block;
  clear: both;
}
.col {
  box-sizing: border-box;
  padding: 0 .6875em 1.375em;
  float: left;
  width: 100%;
}

介绍我们的第一个媒体提问

如果我们以稍微宽一点的尺寸来看这个设计,我们可以同时在屏幕上显示更多的内容。例如,我们可以让第二层和第三层占据集装箱宽度的一半,如图 8-2 所示。

A314884_3_En_8_Fig2_HTML.jpg

图 8-2。两个故事并排放在略宽的屏幕上的专题报道下

通过调整窗口大小,并试图找到并排显示两个故事的合理位置,我们最终得到的最小宽度约为 560 像素,或 35 ems。这就是我们需要添加一个叫做媒体查询的东西的地方,只有当满足最小宽度要求时,它才会触发其中的规则:

**@media only screen and (min-width: 35em) {** 
  .row-quartet > * {
    width: 50%;
  }
  .subcategory-featured {
    width: 100%;
  }
**}** 

如果你曾经用 JavaScript、PHP、Python、Java 等做过任何编程。,你可能见过 if 语句——“如果这个条件为真,就这样做。”使用@media 规则的媒体查询,很像它的表亲@supports 规则,就像 CSS 的 if 语句,特别适合显示页面的环境的功能。在这种特殊情况下,浏览器视窗至少需要 35 ems 宽。我们引入媒体查询的宽度通常被称为断点

请注意,我们放置断点的地方的测量与任何特定类型的设备(移动设备或其他设备)的测量无关。这只是一个我们可以更好、更有效地利用空间的地方。我们应该避免根据特定的器件宽度设置断点,因为新的器件总是在不断地产生。最终,我们将无法通过进一步划分来打破“移动网络”和“桌面网络”的人为划分。

在这一章的后面,我们将再看一看如何构造媒体查询和断点。现在,需要记住的重要事情是,媒体查询中的 CSS 仅在满足特定条件时应用。

查找更多断点

继续增加浏览器窗口的大小,我们发现更多的地方可以更有效地利用空间。在大约 800 像素(50 ems)的情况下,我们可以并排放置四个故事,让特色故事占据一半的宽度(见图 8-3 )。这开始类似于最初的“无响应”的例子,但是子类别标题仍然在故事的顶部。

A314884_3_En_8_Fig3_HTML.jpg

图 8-3。内容区现在有四个栏目,而专题文章占据了两倍的空间。尽管如此,标题仍然在顶部。
@media only screen and (min-width: 50em) {
  .row-quartet > * {
    width: 25%;
  }
  .subcategory-featured {
    width: 50%;
  }
}

最后,我们发现我们可以在大约 70 ems 或 1120 像素的位置将标题放入故事的侧面(见图 8-4 )。

A314884_3_En_8_Fig4_HTML.jpg

图 8-4。随着窗口变宽,我们可以添加另一个媒体查询来调整标题,使其作为侧边栏工作
@media only screen and (min-width: 70em) {
  .subcategory-header {
    width: 20%;
  }
  .subcategory-content {
    width: 80%;
  }
}

现在,我们已经重新创建了这个例子的响应版本,涵盖了四种不同的布局。我们还做了一些进一步的微小的样式调整,这里没有涉及。完整的示例代码(可以在本书附带的文件中找到)包括这些调整,以及使响应式布局在移动设备上工作的视口声明。(我们将在本章的后面深入研究视口的细节。)

前一个示例中相对较短的代码片段包含了许多有用的技术和原则。我们从一个简单的单栏布局开始,并使用媒体查询来创建设计变更的范围——这是响应性 web 设计的健壮方法的基础。在我们进一步探索响应式编码技术之前,我们先来看看响应式 web 设计的由来。

响应的根源

设计师兼开发人员伊桑·马科特(Ethan Marcotte)在 2010 年发表在《与众不同》(alistapart.com/article/responsive-web-design)上的一篇同名文章中创造了“响应式网页设计”这个术语(见图 8-5 )。在那篇文章中,他使用这个术语来描述流体网格、灵活的嵌入对象(如图像或视频)和媒体查询的组合使设计适应任何屏幕尺寸。这篇文章后来变成了同名的书,球在滚动。

A314884_3_En_8_Fig5_HTML.jpg

图 8-5。引发这一切的文章。有趣的事实:插图本身是有反应的——去找到文章并调整浏览器窗口的大小!

虽然响应式网页设计作为一种现象还是相对较新的,但将单一设计应用于多种设备的根源比它的名字还要古老。

在技术层面上,响应式网页设计的组成部分在这个术语被创造出来之前就已经存在了。如果没有一些人预见到需要适应浏览器的布局,媒体查询(及其前身,媒体类型)就不会被标准化。事实上,伊桑这篇文章的主要灵感之一是约翰·奥尔索普 2000 年的一篇名为“网页设计之道”(alistapart.com/article/dao)的文章。在那篇文章中,John 认为好的网页设计更多的是适应用户,而不是实现完美的控制。我们花了一段时间才到达那里,但事情正在发生变化。

到 2010 年,媒体询问获得了更广泛的支持。这也是在移动设备上浏览变得很普遍的一个时间点。通过将这些技术结合在一起,并创造了响应式网页设计这个术语,Ethan 为一个方向起了一个名字,这个方向是网页已经想发展很久了。

响应式网页设计正迅速成为设计网页的事实方式,并可能很快被视为“好的网页设计”在此之前,响应式网页设计是一个有用的术语,用来描述在多种设备和多种屏幕尺寸上进行设计的具体方法。

超越 CSS 的响应

今天,响应技术被用在大大小小的网站上。Ethan 的响应性的三个主要支柱仍然构成了响应性网页设计的基础,但是它们补充了更多的适应工具。最常见的一种方法是使用 JavaScript 来增加交互性或改变页面在不同设备上的呈现方式。

例如,你可能见过现在无处不在的“汉堡菜单”一种常见的模式是在较大的屏幕尺寸上展开全局导航菜单,但隐藏在较小视窗上的按钮下面(见图 8-6 )。通常,根据视口大小改变菜单会涉及到一些 JavaScript。需要指出的是,初始内容和标记仍然是相同的,与使用什么设备查看站点无关。这种“核心体验”可以用你喜欢的任何方式通过脚本进行转换。

A314884_3_En_8_Fig6_HTML.jpg

图 8-6。Shopify 是许多在小视窗上使用“汉堡菜单”的网站之一

这种模式应该是一种常见的模式:首先加载一组核心资源,然后在确定了设备的功能后再加载更多的资源。响应式网页设计的确是渐进式改进的另一个例子。

在这一章中,我们将试着把重点放在响应式网页设计中我们可以用 CSS 来影响的部分,并简短地探讨一下响应式图片。如果你想开始为响应式网站寻找更高级的模式,布拉德·弗罗斯特收集了大量的模式和代码示例,名为“这就是响应式”(bradfrost.github.io/this-is-responsive/)。

掌握响应式 CSS 的第一步是理解我们必须使用的画布——视口。

浏览器视口的工作方式

视窗是显示网页的矩形。它是影响我们布局的区域:我们在 CSS 方面有多少空间。为了让视口很好地与我们的响应式设计配合,我们需要理解它是如何工作的以及如何操作它。在桌面浏览器上,视口的概念非常简单。我们有许多 CSS 像素可以使用,并且我们尽可能地使用了视窗中的空间。

这里有一个重要的区别,CSS 像素和物理像素不是一回事。我们在 CSS 中衡量事物时谈论的像素与屏幕的物理像素有着非常不稳定的关系,这取决于硬件、操作系统、浏览器以及用户是否放大或缩小了页面等因素。

作为一个思想实验,我们可以想象将两个 div 元素直接放在页面的 body 元素中。如果我们将第一个 div 的宽度设置为 100%,第二个 div 的宽度设置为 px,那么在哪个 px 度量下它们的宽度相同?该度量是以 CSS 像素为单位的当前视区的宽度,而不管使用多少物理像素来显示它。

作为一个具体的例子,iPhone 5 的物理屏幕宽度为 640 像素,但就 CSS 而言,视口宽度为 320 像素。这里有一个缩放因素—这个特定设备上的每个 CSS 像素都使用 2×2 物理像素显示(见图 8-7 )。

A314884_3_En_8_Fig7_HTML.gif

图 8-7。CSS 像素和高分辨率设备上的设备像素之间的差异

“虚拟”CSS 像素和实际硬件像素之间的比率目前范围从 1(其中每个 CSS 像素= 1 个物理像素)到大约 4(其中每个 CSS 像素= 4×4 个硬件像素),具体取决于设备。

好消息是,由于我们只需要跟踪 CSS 像素,以实现响应性布局,像素比率在很大程度上是不相关的。坏消息是,我们需要更深入地挖掘视口的真实世界机制,以了解如何按照我们的意愿来弯曲它们。

视口定义的细微差别

触摸屏智能手机和其他移动设备稍微搅动了一下。他们更多地使用缩放来处理不适合在如此小的屏幕上浏览的网页。这使得设备制造商发明了影响视口的新概念。移动平台战略家彼得-保罗·科赫(quirksmode.org)发表了关于这些不同级别的视口如何工作的广泛研究,并试图给它们起一个有用的名字。

默认和理想视口

随着智能手机浏览器的出现,没有多少网站针对这种尺寸的屏幕进行了优化。因此,默认情况下,大多数手机(以及平板电脑)上的浏览器都是硬连接的,显示桌面大小的视窗,允许非优化的网站显示。通常,它们模拟大约 1000 个 CSS 像素的视口宽度,有效地缩小页面。我们称之为默认视窗。这就是为什么当我们希望响应式设计正确显示时,我们必须经历一些困难。

由于默认视口是仿真的视口尺寸,因此从逻辑上讲,存在更接近设备本身尺寸的视口定义。这就是我们所说的理想视口。理想的视口因设备、操作系统和浏览器而异,但通常手机的视口宽度为 300 至 500 CSS 像素,平板电脑的视口宽度为 800 至 1400 CSS 像素。在之前的 iPhone 5 例子中,这就是 320 像素宽度的来源。

在响应式设计中,这是我们设计的视口。图 8-8 显示了加载使用理想视窗的移动优化mobile.nytimes.com与加载相同站点的“桌面版”之间的比较,显示了使用默认视窗的缩小桌面布局。

A314884_3_En_8_Fig8_HTML.jpg

图 8-8。纽约时报网站的移动站点使用理想的视口进行布局(左)。如果您切换到“桌面”站点,您将获得缩小的默认视口外观(右),模拟 980 像素的宽度。

视觉和布局视口

通过区分手机和平板电脑上的默认视口和理想视口来设置场景,我们得到了一个关于视口实际工作方式的通用、更直观的定义。我们称其内部显示网页的基本矩形为可视视口。这意味着浏览器窗口,没有任何按钮,工具栏,滚动条等。(被称为“浏览器 chrome”)围绕在实际的 web 内容周围。

当我们放大一个页面时,布局的某些部分会在可视视窗之外结束,如图 8-9 所示。我们现在看到的矩形仍然是可视视口,但我们现在将约束整个页面布局的假想矩形称为布局视口。视觉视口和布局视口之间的这种分离在概念上在桌面浏览器上的工作方式与在手机和平板电脑上的工作方式相同。

A314884_3_En_8_Fig9_HTML.jpg

图 8-9。在手机上查看的放大站点上的可视视口和布局视口

正如您所看到的,对于视窗来说,不仅仅是看上去那么简单。底线是在响应式网页设计中,我们的目标是设计我们的页面来适应每个设备的理想视窗。桌面浏览器不需要任何特殊处理,因为理想视口等于默认视口。但是在手机和平板电脑上,我们需要排除默认视口的虚假测量,并使其等于理想视口。这是通过一小段被称为 meta viewport 标签的 HTML 完成的。

配置视口

通过在页面的 head 元素中添加一个小标签,我们可以让具有不同默认视口的设备使用理想视口。看起来是这样的:

<meta name="viewport" content="width=device-width, initial-scale=1">

这告诉浏览器我们希望使用设备的理想尺寸(设备宽度)作为视窗宽度的基础。这里还有另一个偏好设置:initial-scale=1 位。该部分设置缩放级别以匹配理想的视口,这也有助于防止 iOS 中一些奇怪的缩放行为。当设置缩放级别时,大多数设备将采用设备宽度,但为了在设备和操作系统之间完全兼容,两者都是必需的。

将“初始比例”设置为大于 1 的值意味着进一步缩放布局,这样做可以减小布局视口的大小,因为适合的像素更少。相反,将该值设置得越低,缩小的布局视口就 CSS 像素而言就越大。

其他值并结合初始比例

还可以将视口内的宽度设置为像素度量,而不是 device-width 关键字,从而有效地将布局视口锁定为您选择的值。如果你把它和一个初始比例值结合起来,移动浏览器会选择两者中较大的一个。

不要禁用缩放!

通过在 meta viewport 标签中设置最大比例和最小比例属性(为一个数值),可以将缩放锁定在特定级别。您也可以通过设置 user-scalable=no 来完全禁用缩放。这样的 meta viewport 标记并不少见:

<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">

这使得你的用户无法在移动设备上缩放页面,这意味着你的页面不容易被访问。

即使你小心翼翼地设计页面,使文本清晰可辨,页面上可操作的部分(如链接和按钮)有足够的尺寸,视力较低或有运动障碍的用户可能不这么认为。

一些开发人员倾向于锁定缩放因子,以使 web 应用程序的行为更像本地应用程序。这样做也解决了一些与旧平台上缩放和定位相关的错误和古怪行为,但随着移动平台的成熟,这些错误正在得到修复。

我们认为锁定缩放因子听起来就像把婴儿和洗澡水一起倒掉——毕竟,通用访问是网络建设的最大好处之一。

设备自适应和 CSS @viewport

在一个标签中声明 viewport 属性是目前最好的方法,但是这也是一种非标准的机制,正如您可能从上一节中看到的那样。苹果在第一代 iPhone 自带的 Safari 浏览器中引入了它作为专有开关,其他人也纷纷效仿。

因为这是页面呈现方式的一部分,所以视口属性应该是 CSS 的一部分是有意义的。为此提出了一个标准,称为 CSS 设备适配。它建议我们在页面的头部使用类似这样的东西来代替 meta viewport 标签:

<style>
@viewport {
  width: auto;
}
</style>

将 viewport 声明放在 HTML 的样式元素中,而不是放在实际的 CSS 文件中,这是一个很小但很重要的细节。浏览器不应该在知道视窗大小之前等待 CSS 文件被下载。将这些信息作为 HTML 文件的一部分可以防止浏览器在外部 CSS 文件到达时做额外的工作。

到目前为止,@viewport 声明还没有被广泛实现。在撰写本文时,它仅在 Windows 和 Windows Phone 8 上的 Internet Explorer 10+中得到部分支持。像 Chrome 和 Opera 这样的基于 Blink 的浏览器在一些平台上实现了隐藏设置。它也许是未来最有可能控制视口的候选者,但在撰写本书时并不是非常重要。

这项技术有几个小警告,正如实验技术所预期的那样:开发人员 Tim Kadlec 有一篇关于这些缺陷的好文章(timkad LEC . com/2013/01/windows-phone-8-and-device-width/)。

媒体类型和媒体查询

既然我们已经对视口有了透彻的了解,视口是我们约束布局的空间,那么是时候进入响应式设计的“如何”部分了:根据媒体查询调整您的设计。我们用一个简单的例子开始了这一章,但是这一次我们要更深入,从媒体查询的前身:媒体类型开始。

媒体类型

基于设备的能力来分离样式的能力始于媒体类型。这些是在 HTML 4.01 和 CSS 2.1 中定义的,创建它们是为了让你针对特定类型的环境:屏幕样式、打印样式、电视样式等等。

您可以通过向链接元素添加媒体属性来定位媒体类型,如下所示:

<link rel="stylesheet" href="main.css" **media="screen, print"**>

前面的代码片段意味着该样式表适用于两种屏幕(任何类型的屏幕)以及打印页面时。如果您不关心它用于哪种类型的媒体,您可以将 all 作为值,或者省略 media 属性。逗号分隔的有效类型列表意味着它们中的任何一个都可以匹配,如果没有一个匹配,则不应用样式表。

您也可以将媒体类型选择作为 CSS 文件的一部分。最常见的方法是使用@media 语法,如下所示:

**@media print {** 
  /* selectors and rule sets for print media go in here */
  .smallprint {
    font-size: 11pt;
  }
**}** 

还有几种媒体类型可供选择:其中包括手持设备和电视。这些听起来似乎应该对响应式设计有用,但遗憾的是它们没有。出于各种原因,浏览器制造商回避明确传输他们属于什么类型的设备,所以唯一有用的类型几乎是屏幕、打印和所有。

媒体查询

因为我们不仅要针对设备的类型,还要针对设备的功能,所以创建了 CSS 3 媒体查询规范。它定义了媒体类型基础的扩展。媒体查询被写成媒体类型和由括号内的媒体特征组成的媒体条件的组合。媒体选择语法中还有一些其他新的关键字,提供了一些额外的逻辑。

链接元素上的媒体查询可能如下所示:

<link rel="stylesheet" href="main.css" **media="screen and (min-width: 600px)"**>

这表明 main.css 文件应该用于任何与媒体条件相匹配的屏幕媒体,其中视窗的宽度至少为 600 个 css 像素。

注意

许多浏览器仍然下载 CSS 文件,即使媒体查询当前不匹配。这意味着您应该小心不要在媒体查询中过度使用 link 元素—您可能会不必要地为您的用户创建额外的请求,这是一个严重的性能问题。

与 CSS 文件中的@media 规则结合使用的相同语句如下所示:

@media screen and (min-width: 600px) {
  /* rules go here */
}

and 关键字就像是媒体类型和我们测试的任何条件之间的粘合剂,所以我们的查询可以有几个媒体条件:

@media screen and (min-width: 600px) and (max-width: 1000px) {}

多个媒体查询可以用一个逗号字符连接在一起,该字符用作“或”如果任何媒体查询为真,将应用块内的规则。如果所有的媒体查询都是假的,它将被跳过。

您可以完全省略媒体类型,但仍然使用语句的媒体条件部分:

@media (min-width: 30em) {/*...*/}
/* ...is the same as... */
@media all and (min-width: 30em) {/*...*/}

还可以用 not 关键字否定媒体查询。下面的意思是里面的规则对除了屏幕以外的任何媒体都有效:

@media not screen {
  /* non-screen styles go here. */
}

我们还有唯一的关键字,这是为了防止旧浏览器误解媒体查询而引入的。

当不支持媒体查询的浏览器看到屏幕和(最小宽度:...,它应该把整个东西作为一个糟糕的媒体类型丢弃,然后继续前进。然而,一些旧的浏览器似乎在看到屏幕的第一个字符串后停止,将其识别为有效的媒体类型,并将样式应用于所有屏幕。

因此,在媒体查询规范中引入了唯一的关键字。当老的浏览器在开始看到它时,他们放弃了整个@media 规则,因为不存在唯一的媒体类型。所有支持媒体查询的浏览器都被要求忽略唯一的部分,就好像它不存在一样。

为了真正防止较旧的浏览器应用错误的样式,您应该声明任何需要将范围限定为特定媒体类型的媒体查询,如下所示:

@media only screen and (min-width: 30em) {/*...*/}

如果您不关心具体的媒体类型,您可以将其简化为:

@media (min-width: 30em) {/*...*/}

维度查询

除了宽度和高度,宽度尺寸(及其最小和最大前缀)是响应式网页设计的真正主力。当 quirksmode.org 的 Peter-Paul Koch 在网站开发者和设计者中进行一项关于媒体查询的调查时,他发现与宽度相关的媒体查询是最受欢迎的,而且是压倒性的。

宽度如此重要的原因是,我们创建网页的默认方式是仅在填充视窗之前使用水平布局。在垂直方向上,我们可以让事物随心所欲地增长,让用户滚动。我们想知道何时用尽(或获得更多)布局的水平空间是有意义的。

远离设备测量

我们还可以向浏览器询问设备宽度和设备高度。这并不总是意味着与视窗尺寸相同的东西,而是整个屏幕的尺寸。

可悲的是,许多开发人员将设备宽度与普通宽度查询互换使用,导致移动浏览器制造商效仿,以确保网站在其浏览器上工作。在即将发布的媒体查询规范版本中,设备测量查询也已被弃用。总而言之,设备宽度和设备高度很容易混淆,所以除非出于某种原因被迫使用,否则请远离它们。

其他维度:分辨率、纵横比和方向

虽然对视口尺寸的查询可能构成媒体查询使用的绝大部分,但是应该注意,我们可以查询设备的其他方面。例如,我们可以仅在设备宽度小于设备高度时更改布局,这意味着它处于纵向方向:

@media **(orientation: portrait)** {
  /* portrait orientation styles here. */
}

类似地,我们可以仅在例如视口匹配某个最小纵横比时应用规则:

@media **(min-aspect-ratio: 16/9)** {
   /* only applied when the viewport aspect ratio is at least the widescreen 16:9 ratio. */
}

我们之前提到过,设备的像素比例在很大程度上是不相关的。说到布局确实如此。在本章的前面,我们将使用 min-resolutionmedia 查询来调整加载哪个图像,其中像素比率非常重要。

媒体查询可能会在未来得到扩展,以便能够检测用户设备和环境的其他方面。虽然有许多令人兴奋的进步即将出现(甚至在浏览器中已经有了一些实验性的支持),但我们将把重点放在本书中最有用的查询上,让您为今天的工作做准备。

对媒体查询的浏览器支持

基本的媒体查询几乎在任何地方都得到支持。可悲的是,与许多其他“CSS 3”功能一样,像 IE8 和更老版本的浏览器有点落伍了。

您可以使用各种策略来应对这种情况,要么为这些旧浏览器提供固定宽度的布局,要么使用 polyfill 一种假装支持缺失功能的脚本。

Scott Jehl(github.com/scottjehl/Respond)的 Respond.js 就是这样一个脚本。在不支持媒体查询的浏览器中,它会查看所有链接的 CSS 文件并搜索媒体查询语法。然后,它根据屏幕尺寸应用或删除这些部分,模拟本地媒体查询的工作方式。

使用 Respond.js 有一些缺点。例如,该脚本不能直接在页面的样式元素中处理媒体查询。还有其他边缘情况约束需要考虑,所以在使用 Respond.js 之前一定要参考网站上的说明。

如果使用 JavaScript 不能解决这个问题,你可以使用一个单独的样式表,将旧版本 IE 中的设计锁定在一个特定的“桌面”宽度,并使用条件注释来包含它。

条件注释是一个奇怪的特性,在 IE10 之前(但不包括 IE10)一直存在于 IE 中。它们使得将 HTML 片段包装在所有其他浏览器视为普通注释的东西中成为可能,但 IE 可以进入并获取隐藏在其中的 HTML。一个特殊的语法可以让你针对个别或分组版本的 IE 浏览器。

为桌面 IE 提供这些宽屏样式的条件评论需要考虑旧版本的 IE,同时仍然不针对旧版本 Windows Phone 中的 IE。它看起来像这样:

<!--[if (lt IE 9) & (!IEMobile)]>
<link rel="stylesheet" href="oldIE.css" media="all">
<![endif]-->

这种策略依赖于您将规则的其余部分放在一个样式表中,其中小屏幕样式是“默认”的,宽屏幕样式是媒体查询的范围。无论如何,这是一个好主意,我们将在下一节看到。

为响应式设计构建 CSS

在本章开始的初始示例中,我们从代码中去掉了宽度和布局规则,并将它们添加回最小宽度媒体查询的范围。这种方法不仅仅是一种高效的模式,因为您只需要编写很少的代码;这也是一项重要战略的一部分。

移动第一 CSS

你可能听说过“移动优先”这个术语这是一个围绕如何集中你的设计和开发努力的策略。移动设备的屏幕较小,更难打字,而且通常比台式机处理器更弱,内存更少。它们也是离大多数人最近的设备。

通过在设计和开发过程中首先关注这些设备,我们从一组强调数字产品核心的约束条件开始。当我们扩展网站或应用程序以在其他设备上工作时,我们可以利用扩展的容量。

如果我们反过来做,我们将需要把现有的特性塞进一个更加受限的平台——这是一个更加困难的壮举。

同样的思维模式也适用于 CSS,即使你正在重写一个最初被认为是“桌面”网站的项目。

CSS 文件中的第一条规则构成了最小屏幕和不理解媒体查询的浏览器的基本体验:

  • 排版基础:大小、颜色、行高、标题、段落、列表、链接等。

  • “框”的基础:任何特定的边框样式、填充项目、灵活的图像、背景颜色和一些有限的背景图像

  • 浏览和消费内容的基本组件:导航、表单、按钮

当您在各种类型和大小的移动设备和浏览器上测试这些样式时,您会发现它们在某个点开始出现问题。线的长度将变得太长,项目将变得太远,等等。当这种情况发生时,你应该考虑在那个时候添加一个媒体查询——这就是为什么它被称为断点的原因。重申一下,这可以是任何度量:代码适应站点的内容比适应任何特定设备的像素度量更重要。

/* start off with the baseline and small-screen styles. */
.myThing {
  font-size: 1em;
}
/* ...then adjust inside min-width media queries: */
@media only screen and (min-width: 23.75em) {
  .myThing {
    width: 50%;
    float: left;
  }
}
/* ...and further adjustments... */
@media only screen and (min-width: 38.75em) {
  .myThing {
    width: 33.333%;
  }
}

你会从我们在本章开始时如何重写新闻站点的例子中认出这个方法。这就是转化为代码的“移动优先”的思维模式。它也反映了移动优先、响应性网页设计和渐进式改进是如何齐头并进的。编写尽可能少的代码,同时满足尽可能多的设备的需求,这是你做得对的一个明确标志!

媒体查询和 Ems

正如我们在这里所做的那样,使用 ems 编写媒体查询是一种针对不断变化的环境进一步增强设计的方法。当你在桌面浏览器中缩放时,大多数浏览器将缩放基于像素的查询,但是用户也可以选择改变基本字体大小而不是缩放。

使用 ems 作为度量标准可以确保您的布局也能适应这种情况,因为它与文档的基本字体大小有关。

请注意,ems 中设置的媒体查询始终与浏览器首选项中的基本字体大小相关,而不是您可以在 CSS 中调整的 html 元素(1rem)的字体大小。

高效小屏幕样式的最大宽度查询

使用最小宽度查询作为我们的主要工具,我们可以对越来越宽的视口进行分层调整。但是最大宽度查询不可低估。有时,我们可能有一些样式在小屏幕上有意义,但在大屏幕上没有意义。这意味着,如果使用最小宽度,我们必须首先声明样式,然后否定它。使用最大宽度查询可以减少工作量。

作为一个浓缩的例子(没有双关语的意思!),您可能希望在较小的视窗上使用窄字体,以防止过度换行(见图 8-10 )。

A314884_3_En_8_Fig10_HTML.gif

图 8-10。响应式排版的一个例子是在较小的视窗上使用较窄的字体,以避免过多的换行

使用最小宽度查询和“移动优先”CSS 策略,这个场景可能如下所示:

body {
**font-family: 'Open Sans', 'Helvetica Neue', Arial, sans-serif;** 
}
h1,h2,h3 {
  font-family: 'Open Sans Condensed', 'Arial Narrow', Arial, sans-serif;
}
@media only screen and (min-width: 37.5em) {
  h1,h2,h3 {
**font-family: 'Open Sans', 'Helvetica Neue', Arial, sans-serif;** 
  }
}

突出显示的部分指出了基本字体系列声明需要如何重复,以否定精简标题的小屏幕样式。如果我们使用最大宽度查询,我们会得到一个稍微短一点的例子,没有重复,因此需要维护的代码更少:

body {
  font-family: 'Open Sans', 'Helvetica Neue', Arial, sans-serif;
}
@media only screen and (max-width: 37.5em) {
  h1,h2,h3 {
    font-family: 'Open Sans Condensed', 'Arial Narrow', Arial, sans-serif;
  }
}

当然,还有其他类型的媒体查询,你可以用来改变你的网站。正如许多事情一样,每个项目的具体细节都可以归结为一点“视情况而定”但是使用最小宽度查询作为您的主要工具与使用媒体查询作为一种渐进增强形式的想法非常一致。

在哪里放置您的媒体查询

首先是基本的“未分段”样式,然后是最小宽度查询,这个例子很适合作为包含媒体查询的样式表的基本结构的例子。

不过,媒体查询可以服务于稍微不同的目的:调整一个小细节或者重新安排整个布局。通常,这两类媒体查询也以略微不同的方式出现,因此区别对待它们是有意义的。

对于如何选择自己的结构没有硬性规定,但是我们发现对不同种类的媒体查询进行稍微不同的分组是有意义的:

  • 影响页面整体布局的媒体查询通常与描述站点主要组件的几个类名有关,并且跨越几个屏幕尺寸。将它们放在靠近这些布局规则的地方通常是有意义的。

  • 如果您有特定的媒体查询,只调整站点的一个特定组件,请将媒体查询代码放在描述该组件的规则旁边。

  • 最后,如果您发现对布局的大量更改以及对单个组件的一些较小的调整都出现在相同的断点,那么最好将它们都放在样式表的末尾。这样做,您就保持了从“未分类的”规则开始,然后通过覆盖样式变得更加具体的模式。

重要的是,在你的 CSS 中没有一个明确的地方可以放置所有的媒体查询。这也意味着,作为开发人员,创建适合您或您的团队的结构和约定是您的责任。

警告

媒体查询不会增加选择器的特殊性,所以您需要确保放置它们的结构和顺序不意味着它们会在源代码的其他地方被覆盖。将它们放在最后并不保证它们会覆盖任何东西:它们仍然遵循级联的正常规则。

更敏感的模式

“移动优先”的 CSS 编写方式是响应式设计基本模式的一个例子。还有很多其他的模式可以让你的设计更加灵活,反应更加灵敏,随着新技术的出现,我们会创造和改进更多的模式。这一节包含几个好的。

响应文本列

我们在第四章中遇到的 CSS 3 多列布局规范是 CSS 从一开始就内置了响应模式的第一部分之一,远远早于这个术语的出现。通过使用列宽而不是设定的列数,内容将流入容器中适合的尽可能多的列中(参见图 8-11 )。

A314884_3_En_8_Fig11_HTML.gif

图 8-11。在较窄的视口中,段落排列成一列,而在较宽的视口中,会自动出现多列
<div class="multicol">
  <p>Lorem ipsum [...]<p>
  <!-- ...etc -->
</div>

CSS 是一个单独的列声明行——不需要媒体查询!

.multicol {
**column-width: 16em;** 
}

值得重申的是,在网页上应该少用多栏文本。不过,它肯定有一个用例。只要文本不太长,即使在较宽的屏幕上也不会迫使用户上下滚动,这是一种回收水平空间的方法,而不需要使用宽得令人不舒服的尺寸。

无媒体查询的响应式 Flexbox

Flexbox 是 CSS 的另一部分,它内置了一定程度的响应能力。无需使用任何媒体查询,我们就可以创建使其布局适应可用空间的组件。

假设我们想要构建一个小部件,通过点击按钮来增加或减少购物篮中的部件数量,您可以在其中订购时间机器的备件(参见图 8-12 )。

A314884_3_En_8_Fig12_HTML.jpg

图 8-12。我们订购零件的小工具

部件列表是一个无序列表,其中每个项目具有以下结构:

<ul class="ordering-widget">
  <li class="item">
    <span class="item-name">Flux capacitor regulator</span>
    <span class="item-controls">
      <button class="item-control item-increase" aria-label="Increase">+</button>
      <button class="item-control item-decrease" aria-label="Decrease">-</button>
    </span>
  </li>
  <!-- ...and so on. -->
</ul>

通过使用灵活的大小来设计项目名称和按钮控件的样式,我们可以创建一个组件,当没有足够的空间将它们放在同一行时,它可以改变布局。

首先,列表的一些重置样式,以及基本的排版规则:

.ordering-widget {
  list-style: none;
  margin: 0;
  padding: 0;
  font-family: 'Avenir Next', Avenir, SegoeUI, sans-serif;
}

然后,我们将每个项目转换成一个包装 flex 行:

.item {
  color: #fff;
  background-color: #129490;
**display: flex;** 
**flex-wrap: wrap;** 
  font-size: 1.5em;
  padding: 0;
  margin-bottom: .25em;
}

每个项目的名称必须至少有 13 个 ems 宽,以适应最长的名称,否则应该扩展以填充可用空间:

.item-name {
  padding: .25em;
**flex: 1 0 13em;** 
}

接下来,包裹两个按钮的 span 元素也应该填充可用空间,并且至少 4em 宽。它还充当按钮的 flex 容器。

.item-controls {
  flex: 1 0 4em;
  display: flex;
}

每个按钮又是一个 flex 项目,占用相同的空间。其余的样式主要是中和按钮元素的默认样式(我们将在第九章回到表单控件的样式):

.item-control {
**flex: 1;** 
  text-align: center;
  padding: .25em;
  cursor: pointer;
  width: 100%;
  margin: 0;
  border: 0;
  color: #fff;
  font-size: inherit;
}

剩下的只是按钮本身的背景颜色:

.item-increase {
  background-color: #1E6F6D;
}
.item-decrease {
  background-color: #1C5453;
}

这就是响应式小部件的所有样式!有趣的部分来了:当按钮控制(的。item-controls 元素)空间不足,无法与固定宽度的元素放在同一行。item-name 元素,它们自然会换行到第二行。自从。item-controls 元素的 flex-grow 因子为 1,它将扩展到占据整个第二行(参见图 8-13 )。每个按钮将依次增长,占据其自身行的一半。

A314884_3_En_8_Fig13_HTML.jpg

图 8-13。当空间紧张时,按钮会出现在物品名称的下方

灵活的容器相关组件

在前面的例子中,我们创建了另一个响应组件,而没有求助于媒体查询,从而降低了 CSS 的复杂性。这种包装行为虽然简单,但对于浮动或内联块显示是不可能的。

还需要注意的是,这种类型的灵活组件并不响应视口的大小,而是响应组件中渲染它的实际可用空间。这往往是我们实际想要实现的。

虽然媒体查询非常适合基于视口调整布局,但是它们没有考虑到一个特定的组件可以出现在多个位置,以不同的宽度呈现。换句话说,如果一个组件出现在一个窄边栏中,我们希望它使用在窄上下文中有意义的样式来显示,而不管视窗大小如何。直到我们有了某种形式的“容器查询”(容器查询正在研究中——见github.com/ResponsiveImagesCG/cq-usecases),像 flexbox 这样的技术帮助我们实现了部分目标。

具有网格模板区域的响应式网格

网格布局属性允许您将大量布局工作从单个元素转移到网格容器中。当使用我们在第七章中看到的命名模板区域语法时,下一个模式极大地简化了使页面布局响应的过程。

注意

提醒一下,在撰写本文时,Grid Layout 仍然有非常不稳定的浏览器支持。然而,在未来几年,它很可能成为响应式布局的重要组成部分。

如果我们从第七章来看新闻网站例子的第二小节,我们可以用相对较少的改变把它改编成一个完全响应的布局。但是首先,回顾一下标记结构:

<section class="subcategory">
  <div class="grid-b">
    <header class="subcategory-header"></header>
    <article class="story"></article>
    <article class="story"></article>
    <div class="ad"></div>
    <div class="ad"></div>
  </div>
</section>

标记包含部分标题、两篇文章和两条广告。在不应用任何布局样式(网格布局或其他)的情况下,它们在页面上排列成全幅块。这在小视窗中非常有效(见图 8-14 )。

A314884_3_En_8_Fig14_HTML.jpg

图 8-14。左侧是未设置样式的单列布局。在右边,通过使用网格模板在故事之间插入了一个广告。

源顺序突出了页面内容中的重要内容,故事首先出现在标记中,然后是广告。但是,如果广告销售团队需要我们在移动视图上的故事之间插入广告,这样广告就不会在页面底部丢失,该怎么办?

我们可以使用网格声明来解决这个问题。首先,我们需要为标题和故事定义网格区域名称:

.grid-b .subcategory-header {
  grid-area: hd;
}
.grid-b .story:nth-of-type(1) {
  grid-area: st1;
}
.grid-b .story:nth-of-type(2) {
  grid-area: st2;
}

在不使用媒体查询的情况下,我们现在可以定义网格容器的基本内容,以及其中单个列的行顺序。网格模板现在可以控制单个内容列中项目的排序。广告现在自动流入故事之间的未命名区域(用点表示)。

.grid-b {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-areas: "hd" "st1" "." "st2" ".";
}

当有更多的空间时,我们可以通过在媒体查询中添加新模板来将故事部分更改为 2×2 的网格(参见图 8-15 )。

A314884_3_En_8_Fig15_HTML.jpg

图 8-15。对于稍微大一点的视窗,故事和广告现在是以方格形式出现的
**@media only screen and (min-width: 37.5em)** {
  .grid-b {
    grid-template-columns: **1fr 1fr**;
grid-template-areas: **"hd  hd "** 
**"st1 ..."** 
                         **"... st2"**;
  }
**}** 

请记住,我们可以使用任意数量的连续点来表示一个匿名网格区域,以便更整齐地排列模板字符串。

在稍微大一点的视窗中,标题仍然在内容的上面,但是故事和广告形成了我们在第七章的例子中看到的相同的三栏布局(见图 8-16 ):

@media only screen and (**min-width: 55em**) {
  .grid-b {
    grid-template-columns: **1fr 1fr 1fr**;
grid-template-areas: **"hd  hd hd "** 
**"st1 .. st2"** 
                         **"st1 .. st2"**;
  }
}

A314884_3_En_8_Fig16_HTML.jpg

图 8-16。标题横跨顶部,故事和广告形成三列/两行的布局

最后,我们切换到使用侧边栏标题和三列布局的布局(见图 8-17 )。

A314884_3_En_8_Fig17_HTML.jpg

图 8-17。侧边栏标题现在非常适合三个栏
@media only screen and (min-width: 70em) {
  .grid-b {
    grid-template-columns: **20% 1fr 1fr 1fr**;
grid-template-areas: **"hd st1 . st2"** 
                         **"hd st1 . st2"**;
  }
}

如您所见,网格布局属性允许我们在某些断点处重新定义整个网格,而无需触及各个组件。当然,对于响应式布局,您可以使用其他网格定位方法,但是网格模板区域功能特别适合于响应式工作。请记住,不支持的浏览器将退回到单列布局,所以这可能还需要一段时间才能成为响应网格的首选武器。

超越布局

到目前为止,我们已经熟悉了视窗和媒体查询如何工作的细节,以及响应性布局技术的示例。但是响应式网站需要处理的不仅仅是布局。在这一节中,我们将研究一些技术来确保我们网站的其他方面也能适应。我们将从媒体开始,首先作为背景图像,然后作为嵌入的页面内容。

响应背景图像

在 CSS 中使背景图像适应屏幕大小相当简单,因为我们可以访问媒体查询。

对于我们的例子,我们将重温第五章的页面标题例子(猫的社交网络,记得吗?).标记由单个元素组成,充当页面的标题——我们现在将省略标题的其余内容,只关注应用背景。

<header class="profile-box" role="banner"></header>

我们将使用两个不同的图像文件作为背景。较小的版本为 600 像素宽,裁剪为正方形,而较大的版本为 1200 像素宽,裁剪更宽松(见图 8-18 )。

A314884_3_En_8_Fig18_HTML.gif

图 8-18。我们的两只猫形象

对于最小的视窗,我们将使用紧密裁剪的较小版本:

.profile-box {
  height: 300px;
  background-size: cover;
  background-image: url(img/small-cat.jpg);
}

现在,当视口变得比背景图像大时,它会被放大(通过 background-size: cover 声明),并开始看起来模糊。此时,我们可以将它换成大图:

@media only screen and (min-width: 600px) {
  .profile-box {
    height: 600px;
    background-image: url(img/big-cat.jpg);
  }
}

这个简单的例子说明了两件事。首先,我们可以使用媒体查询来为视口提供最合适大小的图像。其次,我们不仅可以使用响应式背景来加载不同分辨率的图像源,还可以通过根据视口裁剪不同的背景图像来指导响应式设计。

使用分辨率查询切换图像

在前面的示例中,我们根据视口的尺寸更改了图像。但是,我们可能还希望根据设备的像素比率,为相同的视窗大小加载不同分辨率的图像。对于图像,图像的实际像素尺寸和 CSS 像素需要协同工作。即使在高分辨率屏幕上,内在尺寸为 400 x400 像素的图像也会显示为 400 x400CSS 像素。这意味着图像将被放大,在这个过程中失去清晰度。如果我们只想在高分辨率设备上加载更大、更清晰的图像,我们需要使用分辨率查询。

假设我们想为最小的视窗提供 medium-cat.jpg 文件,但前提是它的像素比率至少为 1.5。这个 medium-cat.jpg 文件是同样的正方形裁剪,但是大小为 800×800 像素。数字 1.5 有些武断,但它确保了大图像用于大多数高分辨率手机和平板电脑,其中 1.5 是最低的范围。您可以随时添加更多的媒体查询(和更详细的图像大小)以获得更高的分辨率,只需留意图像的文件大小即可!

为了根据像素比率切换出图像,要测试的标准化媒体功能称为分辨率,因此我们使用 dppx 单位(“设备像素/像素”)检查最小分辨率。然而,并不是所有的设备都支持这种标准化的查询,所以我们用检查-webkit-min-device-pixel-ratio 来补充它,这主要是由 Safari 使用的。后者的度量是一个无单位的数。

@media (-webkit-min-device-pixel-ratio: 1.5),
       (min-resolution: 1.5dppx) {
  .profile-box {
    background-image: url(medium-cat.jpg);
  }
}

将对尺寸的查询与对分辨率的查询结合起来,可以确保为每个设备类别加载最佳图像。

旧的解析查询语法

对于解析查询,您可能会遇到各种其他建议。有一些非标准的查询,比如非常奇怪的 min - moz-device-pixel-ratio,在非常老的 Firefox 浏览器中使用,还有在 dpi 单元中设置的最小分辨率查询。

在一些较旧的实现中,dpi 单元是唯一支持标准化最小分辨率查询的单元,最著名的是 Internet Explorer 9–11。可悲的是,IE 实现错误地获取了设备的 dpi 数字,这导致在某些情况下错误地加载了高分辨率图像。

仅将-webkit-min-device-pixel-ratio 查询与 min-resolution 查询(和 dppx 单元)结合使用,可能会覆盖大多数运行高分辨率设备的用户,并将代码复杂性保持在最低水平,这就是我们推荐它的原因,尽管缺乏 IE 支持。如需进一步阅读,请参见 W3C 的 Elika Etemad 的这篇博文:www . w3 . org/blog/CSS/2012/06/14/un fix-WebKit-device-pixel-ratio/

响应式嵌入式媒体

响应式 web 设计的一个更棘手的方面是获得内容图像、视频和其他嵌入对象的灵活性。有了 CSS 中的背景图片,我们可以让媒体查询做很多工作。随着东西嵌入到页面中,CSS 的逻辑并不总是帮助浏览器做出正确的决定。

其中一些在技术上超出了 CSS 的范围,但是掌握这些问题是很重要的,因为它们会以如此巨大的方式影响你构建的站点的性能。

响应式媒体基础

在第五章中,我们已经遇到了让图像、视频和其他物体以流畅的方式表现的最基本的技术之一。将最大宽度设置为 100%会使元素流动,但仍不会超出其固有尺寸:

img, object, video, embed {
  width: auto;
  max-width: 100%;
  height: auto;
}

虽然前面的规则有些天真,但它代表了一个很好的基线,可以防止固定宽度的元素潜入您的流体和响应设计中。但是,每种使用情况可能需要不同的调整方法。

第五章中的“长宽比感知容器”技巧对于创建灵活的视频容器特别有用。它还有助于解决许多 SVG 内容的大小调整问题;Sara Soueidan 写了一篇关于如何响应性地调整 SVG 大小的好文章(tym pus . net/codrops/2014/08/19/making-svgs-responsive-with-CSS/)。

响应图像和 srcset 属性

虽然调整图像大小相对简单,但它并没有解决加载右图像的更大问题。图像文件的大小是整个页面权重的头号因素,而 Web 正以惊人的速度变得越来越重。今天,平均网页超过 2MB,其中图片占 60%以上(见图 8-19 )。

A314884_3_En_8_Fig19_HTML.jpg

图 8-19。来自httparchive.org的截图:平均网页上不同内容之间的字节大小分布,2016 年 2 月

当响应式 web 设计被引入时,许多开发人员通过向每个设备提供相同的图像而加剧了文件大小的问题,而不管屏幕大小或功能如何。这意味着提供最大的图像,并针对较小的视窗缩小图像,以保持图像清晰。这不仅是因为页面的整体重量;缩放图像需要处理器时间和大量内存空间,这两者在手机等设备上都不充足。

浏览器会对 HTML 进行所谓的 pre- 解析,在浏览器完成在内存中构建完整页面或执行任何 JavaScript 之前,图像等资产就已经开始下载了。这使得单独使用脚本不可能以合理的方式解决响应图像。这就是为什么在过去的几年里,有很大的努力来标准化响应图像。由此带来的改进之一是 srcset 属性。

srcset 属性及其伴随的属性大小,是 img 元素最简单的扩展形式。它允许您指定图像的几个不同方面:

  • 这张图片的替代源文件是什么,它们的像素宽度是多少?

  • 就 CSS 而言,图像在各种断点处应该有多宽?

通过在标记中而不是在 CSS 中提供这些信息,预解析器可以尽快决定加载哪个图像。

srcset 语法的早期版本最初是几年前在基于 WebKit 的浏览器中引入的。它只处理目标分辨率,并允许您指定一个备选图像源列表,以及物理像素与 CSS 像素的最小比率,称为“x 描述符”对于新闻部分示例中的专题文章,我们可以将 600×300 的图像用于默认分辨率或不支持的浏览器,但当比例更高时,可以切换到两倍大的图像(见图 8-20 ):

<img src="img/600x300.png" **srcset="img/1200x600.png 1.5x"** alt="Dummy image">

A314884_3_En_8_Fig20_HTML.jpg

图 8-20。在高分辨率屏幕上查看新闻示例页面,使用 x-descriptor 语法加载更高分辨率的“专题文章”图像(最左边的文章)

分辨率切换不考虑图像将以何种尺寸显示。为此,您需要添加 sizes 属性,并描述图像的宽度,而不是预期的像素比率。

这就是 srcset 语法变得有点棘手的地方。如果我们有许多包含不同尺寸(从 300×150 到 1200×600)的相同图形的源图像,我们将它们与一系列成对的介质条件和宽度测量值结合,描述如何使用图像。我们可以按照自己的意愿精确地表达预期的大小,例如,使用相对于视口的单位和从 CSS 借用的 calc()函数符号:

<img src="img/xsmall.png"
     srcset="img/xsmall.png 300w,
             img/small.png 400w,
             img/medium.png 600w,
             img/large.png 800w,
             img/xlarge.png 1200w"
     sizes="(min-width: 70em) 12.6875em,
            (min-width: 50em) calc(25vw * 0.95 - 2.75em),
            (min-width: 35em) calc(95vw / 2 - 4.125em),
            calc(95vw - 1.375em)"
     alt="Dummy image" />

我们会一点一点地分解它。除了正常的 src 和 alt 属性,我们还有 srcset。它描述了一个图像 URL 列表,并为浏览器提供了一个线索,显示它们的实际像素宽度——而不是 CSS 像素。这个语法,在宽度后面有一个 w 字符,叫做宽度描述符

srcset="img/xsmall.png **300w**,
        img/small.png **400w**,
        ..."

接下来,我们需要向浏览器解释我们打算如何使用图像。我们通过提供一个宽度列表来做到这一点,每个宽度可以选择以一个媒体条件开始,就像在媒体查询中一样。需要注意的是,这些表达式不是 CSS,所以它们不遵循级联规则,即最后声明的匹配规则获胜。相反,第一个匹配规则缩短了评估并胜出,因此我们从最宽的媒体条件开始。最后一个尺寸不需要条件,因为它作为一个后备测量,匹配最小的屏幕。

sizes="(min-width: 70em) 12.6875em,
       (min-width: 50em) calc(25vw * 0.95 - 2.75em),
       (min-width: 35em) calc(95vw / 2 - 4.125em),
       calc(95vw - 1.375em)"

媒体条件之后的测量是基于当前响应布局,计算图像在各种断点处将显示的大致宽度。这是对响应图像的权衡:我们实际上需要将一些关于 CSS 的信息放入标记中。我们不能在这里使用百分比,因为这些是相对于 CSS 样式的计算,但是我们可以使用像 vw 和 ems 这样的视口单位。这里的 em 单位大小对应于浏览器的默认字体大小,就像媒体查询一样。

注意

vw 单位与视口宽度相关,其中 1 个单位是视口的 1%。我们将在本章稍后回到相对于视口的单位。

最后,浏览器决定当前视窗尺寸的最佳候选,并下载该图像。

要完全掌握 srcset 和 sizes 属性是如何结合在一起的,需要一段时间。最终结果是,通过向浏览器提供一个图像文件列表和 img 元素的预期宽度,浏览器将为您解决这个问题。

这可能导致基于高速缓存中已经存在的图像加载较大的文件,或者基于带宽限制、低电量等加载较小的文件。类似地,它会判断你是否在一个高密度屏幕的设备上,并加载更大的图像,而不需要在标记中指定。

图片元素:艺术指导、文件类型支持等等

除了在不同分辨率的源图像之间切换之外,响应式图像还有一些更重要的使用案例:

  • 由于渲染尺寸和观看距离的不同,我们可能希望在较小和较大的屏幕上以不同的方式裁剪图像,就像背景图像示例中一样。当我们只使用 srcset/sizes 时,浏览器可能会认为源文件都具有相同的纵横比,只是分辨率不同。

  • 我们可能希望基于浏览器支持的不同文件格式来加载图像。我们已经在第五章提到了 WebP 格式,但是也有其他格式,比如 JPEG2000 格式(Safari 支持)和 JPEG-XR 格式(IE 和 Edge 支持),等等。与跨浏览器支持的格式相比,这些格式中的许多都节省了大量文件大小。

这些问题的标准解决方案是图片元素。它充当了一个标签的包装器,并在 srcset 和 sizes 属性之上添加了更多的功能。

我们可以在支持的地方加载 WebP 格式的图像来补充 responsive news 站点示例中的 srcset 标记。标记现在看起来像这样:

**<picture>** 
**<source type="image/webp"** 
**srcset="img/xsmall.webp 300w,** 
**img/small.webp 400w,** 
**img/medium.webp 600w,** 
**img/large.webp 800w,** 
**img/xlarge.webp 1200w"** 
**sizes="(min-width: 70em) 28em,** 
**(min-width: 50em) calc(50vw * 0.95 - 2.75em),** 
**calc(95vw - 1.375em)" />** 
  <img src="img/xsmall.png"
       srcset="img/xsmall.png 300w,
               img/small.png 400w,
               img/medium.png 600w,
               img/large.png 800w,
               img/xlarge.png 1200w"
       sizes="(min-width: 70em) 28em,
              (min-width: 50em) calc(50vw * 0.95 - 2.75em),
              calc(95vw - 1.375em)"
       alt="Dummy image" />
**</picture>** 

虽然这非常冗长,但逻辑只是稍微复杂一点。标签及其所有内容都是相同的。新的是包装器,以及其中的标签,重复了中的许多模式。

首先,img 仍然需要在图片元素中——图片和源元素的功能是选择哪个图像文件成为 img 的最终源。此外,它还可以作为不支持图片的浏览器的后备。

我们在 img 上仍然有 srcset 和 sizes 属性,但是让我们暂时保留它们。当浏览器遇到一个包含 img 的图片元素时,它将开始遍历所有的源元素,试图找到与 img 元素相匹配的内容。在我们的例子中,只有一个源元素,但也可能有几个:

<source **type="image/webp"** ...>

我们的 source 元素有一个设置为 image/webp 的 type 属性,所以只有在浏览器知道该文件类型的情况下,它才会继续被评估为潜在的匹配。

接下来,source 元素具有与 img 元素相同的 srcset 和 sizes 属性,但是 srcset 属性中列出的源文件都是 WebP 文件:

<source type="image/webp"
**srcset="img/xsmall.webp 300w,** 
**img/small.webp 400w, ..."** 
**sizes="(min-width: 70em) 28em,** 
               **(min-width: 50em) calc(50vw * 0.95 - 2.75em)..."**>

如果浏览器设法匹配其中一个,那么该文件将作为 img 元素的源进行加载。如果没有匹配的源元素,它最终会转到 img 元素本身,并检查那里的任何属性。如果没有匹配项(或者图片语法不受支持),则使用 src 属性。

此时,该示例为我们协商了分辨率图像文件类型支持。如果我们将这个例子在 Firefox 的高分辨率屏幕上和 Chrome 的标准分辨率屏幕上进行比较,每个浏览器都会为我们选择最合适的图像。在图 8-21 中,我们看到 Chrome 加载了一个较小的 WebP 文件,而 Firefox 选择了高分辨率的 PNG 文件。

A314884_3_En_8_Fig21_HTML.gif

图 8-21。标准分辨率屏幕上的 Chrome(左)加载了一个较小的 WebP 文件。高分辨率屏幕上的 Firefox(右)加载高分辨率 PNG 文件。

在前面的示例中,我们检查了 sizes 属性中的媒体条件,以匹配带有断点的显示宽度。如果我们想更好地控制何时使用哪个源,我们还可以在源元素本身上使用 media 属性,在其中包含完整的媒体查询:

<picture>
  <source **media="(min-width: 70em) and (min-resolution: 3dppx)"** srcset="..." />
  <img src="..." alt="..." />
</picture>

通过这样的组合,您可以很好地控制何时加载哪些文件。与 srcset 的不同之处在于,浏览器将而不是在选择使用哪个源元素方面为您做出判断。srcset 属性中的选择仍然由浏览器决定,但是它必须使用与 media 或 type 属性匹配的第一个 source 元素。

这意味着作为开发人员,您将获得更多的控制权。例如,当您想要艺术导向的图像时,对于不同的视口使用不同的裁剪是有意义的。但这也带来了更大的小心谨慎的责任。毕竟,目标是减少不必要的大量下载。

对于大多数情况,srcset 和 sizes 就足够了,但是对于 picture,您需要使用大工具箱。

浏览器支持和图片填充

在撰写本文时,几乎所有浏览器的最新版本都完全支持 srcset 和 sizes 语法。一些浏览器(最明显的是稍旧版本的 Safari)提供部分支持,使用带有 x 描述符的语法。Internet Explorer 11(及更早版本)完全被排除在外。

对图片元素的支持稍弱,但正在赶上。Chrome、Opera、Firefox 和 Microsoft Edge 已经提供了支持。在本文撰写之时,Safari 正准备在 OS X 和 iOS 上提供支持,首先是桌面版 Safari 9.1 和 iOS 9.3。

好的一面是 srcset 和 picture 解决方案被设计成有一个后备,如果没有支持,它们都依赖于现有的 img 元素。这意味着您仍然可以立即使用这些技术实现一个解决方案,并具有合理的后备映像大小。由于响应性图像会对性能产生巨大影响,如果使用聚合填充,可能会有更大的影响。

这些标准有一个官方的基于 JavaScript 的 polyfill,叫做 picture fill(scottjehl.github.io/picturefill/)。您可能还记得,从本节开始,JavaScript 不足以成为 responsive images 解决方案的合适解决方案。这仍然是事实,polyfill 附带一些警告:

  • 您需要在 img 元素上有一个“假”src 属性,以避免在不支持的浏览器中重复下载。这意味着不加载 JavaScript polyfill 和不支持图片的浏览器将根本看不到任何图像。

  • IE 10 之前的版本将忽略 HTML 中的源元素,除非它们是视频元素的子元素(源元素在加载视频时起着类似的作用),因此,如果您需要使用针对 IE 的源文件类型的图片,您将需要摆弄条件注释,以将“假”视频元素添加到您的标记中。详细信息在 Picturefill 的文档中。

注意

对于本书附带的图片元素示例,您会看到我们没有为 IE 使用 hack 这是因为 IE 不理解 WebP 格式,所以没有必要多填充该特定功能。

响应式印刷

很容易理解为什么布局对响应式设计很重要。在为各种不同类型的设备设计时,字体可能同样重要。不仅屏幕的大小不同,而且我们与不同类型的设备的交互方式也不同。在这一节中,我们将介绍在各种外形中调整版式的最重要的考虑因素。

不同的设备,不同的措施

当我们在大屏幕上阅读时,我们通常习惯于每行 45 到 70 个字符。对于手机等较小的屏幕,超过 70 个字符意味着字体太小,让人不舒服。这意味着我们需要在较小的屏幕上调整大小,使平均行长度更接近 35 到 45 个字符。

当每行的字符数变少时,我们通常可以稍微降低行高。如果你为桌面大小的字体设置 1.5 左右的行高,那么最小的屏幕也可以设置 1.3。

当决定尺寸时,你正在影响测量——这两者总是联系在一起的。那么,如何决定网站正文的合适大小呢?

一个简单的方法是坐在你的屏幕前,保持一个舒适的距离,然后在你正常阅读的距离举起一本实体书籍或杂志。将书中的文本大小与您屏幕上的文本大小进行比较(参见图 8-22 )。屏幕上的文字是变小了还是变大了?通常,你最终会得到一个 20 像素左右的文本大小来匹配这本书。

A314884_3_En_8_Fig22_HTML.gif

图 8-22。在舒适的阅读距离拿着一本书作为指南,你可以找到其他设备的正确尺寸和尺寸

网页上的文本设置得比这小很多是很常见的。这可能更多是出于设计者和开发者的习惯,而不是文本的实际可读性。然而,传统正在改变,以阅读体验为中心的网站正在引领潮流。例如,medium.com使用 22 像素的字体在桌面浏览器上显示正文。

在手机上重复同样的实验,你可能会发现你拿手机的距离比拿书的距离更近一点。尝试这种方法可能会让你的字体大小达到 16-18 像素。

要判断这一指标是否在特定屏幕尺寸和字体组合的可接受范围内,你可以使用设计师特伦特·沃尔顿(Trent Walton)的技巧。他只是在可接受的范围开始和结束的字符位置添加了一个特殊字符,并在设备上将其测试为段落文本(trentwalton.com/2012/06/19/fluid-type/):

<p>Lorem ipsum dolor sit amet, consectetur adip *isicing elit, sed do eius mod* tempor incidid.</p>

该段落中的星号位于字符编号 45 和 70 处。这意味着当它们都在第一行时,这个小节就太长了。在移动设备上测试时,段落的第一个换行符应该靠近(或在)第一个星号之前。

当你为最小和最大的屏幕找到了一个好的字体大小和尺寸,你就有了一个好的基础来为你网站的其他响应排版做准备。下一步是实现它,正如许多其他事情一样,有许多方法可以实现它。其中一些方式比其他方式更灵活。

使用灵活的字体大小

在讨论排版时,我们经常会谈到像素大小,但是正如我们在第四章中看到的,还有其他描述大小和距离的方式。

使用相对长度(如 em、rem 和视口单位(vw、vh、vmin 和 vmax ))来调整字体大小是在不同屏幕尺寸上调整文本大小的一种非常有效的方法。这些单元为我们提供了一种方式,通过在更高的级别更新字体大小,然后让所有元素向下级联,对各种形状因子进行小的调整。毕竟,这些是级联样式表。

设置基本字体大小

几乎每个浏览器在其用户代理样式表中都将基本字体大小设置为 16 像素。我们可以通过改变 html 元素的字体大小来改变它。基于 em 单元的媒体查询总是基于由浏览器设置的基本大小,因此为了响应站点的 CSS 的一致性,您可能希望在 body 元素上设置新的基本大小。

使用适合最小屏幕的基本字体大小是有意义的,因为它允许我们将“移动优先”的策略应用到我们的排版中。我们的下一个任务是设置不同于基本大小的字体大小:标题、列表、菜单和其他内容。我们提到了在响应式设计中使用灵活字体大小的重要性,现在是时候深入研究这项技术了。随着我们的目标是越来越大的屏幕,我们将需要扩大我们的整个排版。

如果您的字体大小是以像素为单位设置的,您可能会得到如下所示的样式表:

p { font-size: 16px; }
h1 { font-size: 36px; }
h2 { font-size: 30px; }
h3 { font-size: 26px; }
/* ...etc */
@media only screen and (min-width: 32.5em) {
  p { font-size: 18px; }
  h1 { font-size: 40px; }
  /* ...and on and on... */
}
@media only screen and (min-width: 52em) {
  p { font-size: 20px; }
  h1 { font-size: 44px; }
  /* oh no, there’s more... */
}

你可以看到这是怎么回事:基于像素的字体大小使得重新缩放布局变得非常乏味。使用相对大小使重缩放更容易:

p { font-size: 1em; }
h1 { font-size: 2.25em; }
h2 { font-size: 1.875em; }
h3 { font-size: 1.625em; }
/* ...etc */
@media only screen and (min-width: 32.5em) {
  body { font-size: 1.125em; /* done! */ }
}
@media only screen and (min-width: 52em) {
  body { font-size: 1.25em; } /* done! */
}

这种技术效率很高,但还远远不够。制作一个排版系统的比例尺并不仅仅是在几个断点处改变基本字体大小那么简单。例如,您可能希望在超大屏幕上使用相对于正文而言较大的标题,但在手机上使用稍微适中的大小差异。如果你想要一个良好的基础,贾森·帕曼瑟尔的综合文章《一个更现代的网页排版尺度》(http://typecast . com/blog/A-More-Modern-Scale-for-Web-排版)为你提供了建议和样板代码(见图 8-23 )。

A314884_3_En_8_Fig23_HTML.jpg

图 8-23。Jason Pamenthal 关于响应式排版的文章是一个方便的资源

使用灵活的度量允许您在媒体查询中上下缩放字体大小(以及其他相对度量,如边距、行高和填充),然后花时间调整缩放不一致的地方。这比每个断点都重新声明要有效得多。

排版的视口相关单位

em 和 rem 单位是灵活的,因为它们不代表任何特定的像素度量。灵活调整大小的下一步是使用相对于视口的单位将字体的大小与视口的大小联系起来。使用这些单位,值 1 等于视口宽度或视口高度的 1%:

  • vw 表示视口宽度。

  • vh 表示视口高度。

  • vmin 代表宽度或高度中最小的一个。

  • vmax 代表最大的宽度或高度。

开始时,了解视口单位是如何工作的可能有些棘手。让我们试一个例子。

p {
  font-size: 5vw;
}

5vw 是什么意思?好吧,假设你在一个 400 像素宽的视窗(像在一些移动浏览器中)上查看一个具有该大小的段落集,它将是视窗宽度的 1%的五倍,1%是 4 个像素。数学变成 5 × 4 = 20 像素。

在某种程度上,这给了我们最终的响应排版,因为类型将改变没有任何媒体查询。这也是与视口相关的单位的风险:您可能会错过一些不应该缩放得太远的东西,在极端情况下会变得太大或太小。在上一段的例子中,我们对典型的移动屏幕尺寸做得很好,但是对于桌面大小的浏览器来说,该有多大呢?假设视窗宽度为 1400 像素(举例来说),那么大小就是 14 × 5 = 70 像素。那有点太大了!

这意味着我们仍然需要以某种方式限制范围。开发人员 Mike Riethmuller 在他的文章“对响应式排版的精确控制”(madebymike . com . au/writing/Precise-control-responsive-typography)中记录了一个非常有创意的方法,那就是用 CSS calc()函数变得非常有创意。由于 calc()有自己的怪癖和缺陷,我们经常不得不求助于设置断点来重新定义字体大小,即使是在相对于视口的单位中设置。不管是哪种方法,好处是任何设置了视窗相对大小的东西都可以很好地缩放,甚至是断点之间的 ??。

浏览器对视口单位的支持相对较好。从版本 9 开始,IE 以及所有最新版本的 Chrome、Firefox 和 Safari 都支持它们。在 4.4 版之前的 Android 浏览器和 Opera Mini 中缺少支持。不过也有一些怪癖:

  • IE9 将 vmin 单元实现为 vm。

  • IE9、IE10 和 Safari 6–7 缺少 vmax 单元。

  • iOS 上的 safari 6–7 也有一些严重的错误,其中视口单元完全混乱(见github.com/scottjehl/Device-Bugs/issues/36)。

调整和测试

响应式印刷是一种全新的看待字体的方式。与响应式布局一样,我们的任务是试图找到将它作为一个系统来思考的方法,我们需要这个系统转化为任何形式的因素和设备。同时,底线是非常相同的。有基本的样式、尺寸和度量规则可以遵循,但是对于每种情况,您都需要测试、调整和再测试。选择构建 CSS 的方法是很重要的,但是最终的目标应该是让使用你构建的东西的人有很好的体验,不管是什么设备。

摘要

在这一章中,我们已经了解了如何构建响应式 CSS——用于布局、图像和排版。我们深入探究了响应式设计的技术基础,解读了各种视口,以及在使用 CSS 时如何让它们按照你想要的方式运行。

我们还看了一系列例子,说明如何使用 flexbox 和 Grid Layout 等新规范来调整响应性布局,无论有无媒体查询。我们已经看到了如何使用 CSS 的新增内容以及响应内容图像的新标准来实现响应图像。最后,我们研究了不同形式的字体设计的一些注意事项。

在下一章,我们将关注样式的另一个重要领域:表单和表格。我们将会看到那里也有相应的挑战。

九、设计表单和数据表的样式

表单是现代 web 应用程序中极其重要的一部分。它们允许用户与系统交互,使他们能够做任何事情,从留下评论到预订复杂的旅行路线。表单可以像电子邮件地址和消息字段一样简单,也可以非常复杂,跨越多个页面。

除了需要捕获用户数据,web 应用程序越来越需要以一种易于理解的格式显示这些数据。表格可能是显示复杂数据的最佳方式,但需要仔细设计以避免过于庞大。组成表格的元素集合是 HTML 中比较复杂的部分之一,很容易出错。

表单和数据表的设计相对来说被忽略了,而倾向于更高层次的设计。然而,好的信息和交互设计可以成就或毁灭一个现代的网络应用。

在本章中,您将了解

  • 创建有吸引力和可访问的数据表

  • 让表格为响应式布局服务

  • 创建简单和复杂的表单布局

  • 设计各种表单元素的样式,包括复选框和选择菜单的定制样式

  • 提供可访问的表单反馈

样式数据表

表格数据是可以按列和行排列的信息。一个月的日历视图是可以标记为表格的一个很好的例子。

即使是相对简单的数据表,如果包含多行和多列,也很难阅读。如果数据单元格之间没有分隔,信息会模糊在一起,导致布局混乱(见图 9-1 )。

A314884_3_En_9_Fig1_HTML.jpg

图 9-1。乍一看,紧凑的数据表可能非常令人困惑

相反,有大量空白的表格也很难阅读,因为列和单元格开始失去彼此之间的视觉关联。当你试图跟踪具有很大列间距的表格上的信息行时,这尤其成问题,如图 9-2 所示。如果不小心,在列之间移动时很容易不小心误入错误的行。这在桌子中间最明显,桌子顶部和底部的硬边提供了较少的视觉锚。

A314884_3_En_9_Fig2_HTML.jpg

图 9-2。间距很大的表格也很难立即理解

幸运的是,通过应用一些基本的设计技术,可以大大提高数据表的可读性。图 9-3 中的日期被赋予了一点行高和默认宽度的喘息空间。表头用不同的文字样式和一个边框清晰区分,涉及当前日期的各种状态和周末是哪几天都有清晰的标注。结果是一个易于使用的日历小部件。

A314884_3_En_9_Fig3_HTML.jpg

图 9-3。样式数据表

特定于表的元素

如果表格对视力正常的用户来说很困难,那么想象一下,对于使用辅助技术(如屏幕阅读器)的人来说,表格是多么复杂和令人沮丧。在最基本的层次上,表格是从 table 元素创建的,由 tr 元素(表格行)和 td 元素(表格单元格)组成。幸运的是,HTML 规范包含了更多的元素和属性,旨在提高数据表的可访问性。

表格标题

表格标题元素充当表格的标题。尽管不是必需的元素,但尽可能使用标题总是一个好主意。在本例中,我们使用标题向用户显示他们正在查看的月份:

<table class="cal">
  <caption><strong>January</strong> 2015</caption>
</table>

thead、tbody 和 tfoot

使用 thead、tfoot 和 tbody 可以将表分成逻辑部分。例如,您可以将所有的列标题放在 thead 元素中,这为您提供了一种单独设置特定区域样式的方法。如果选择使用 thead 或 tfoot 元素,则必须至少使用一个 tbody 元素。在一个表中只能使用一个 thead 和 tfoot 元素,但是可以使用多个 tbody 元素来帮助将复杂的表分成更易于管理的块。

行和列的标题应该标记为 th 而不是 td。可以给表格标题一个范围属性值 row 或 col,以定义它们是行标题还是列标题。如果 scope 属性与多行或多列相关,也可以为它们赋予 rowgroup 或 colgroup 值。一周中的日期标记列,因此它们应该将 scope 属性设置为 col。

<thead>
  <tr>
    <th scope="col">Mon</th>
    <!-- ...and so on -->
    <th scope="col">Sun</th>
  </tr>
</thead>

列和列组

tr 元素提供了对整行进行样式化的目标。但是列呢?我们可以使用:n-child 来选择表格单元格,这可能会变得混乱。col 和 colgroup 元素就是为此而存在的。colgroup 用于定义一个或多个列的组,由 col 元素表示。col 元素本身没有任何内容,而是在实际表格的一个特定列中代表表格单元格。

<colgroup>
   <col class="cal-mon">
   <col class="cal-tue">
   <col class="cal-wed">
   <col class="cal-thu">
   <col class="cal-fri">
   <col class="cal-sat cal-weekend">
   <col class="cal-sun cal-weekend">
</colgroup>

colgroup 需要放在 table 元素中,在任何标题之后,任何 thead、tfoot 或 tbody 元素之前。

然后将样式应用于 col(或 colgroup)元素,而不是特定列中的所有表格单元格,例如日历中的所有星期六和星期天。可以为列设置样式的属性非常有限。您可以设置背景属性、边框属性以及宽度和可见性的样式,但仅此而已。

最重要的是,列的可见性只能有 visible 或 collapse 值,即使这样,浏览器也不太支持。值折叠应该不仅仅是隐藏,而且是折叠表格部分的维度,这在某些情况下会很方便,但这只是一些浏览器开发者似乎跳过的事情之一。

完成的表格标记

将所有这些 HTML 元素和属性放在一起,您可以创建如图 9-1 所示的日历表格的基本轮廓。

<table class="cal">
  <caption><strong>January</strong> 2015</caption>
  <colgroup>
    <col class="cal-mon">
    <!-- ...and so on -->
    <col class="cal-sat cal-weekend">
    <col class="cal-sun cal-weekend">
  </colgroup>
  <thead>
    <tr>
      <th scope="col">Mon</th>
      <!-- ...and so on one per day.-->
      <th scope="col">Sun</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td class="cal-inactive">29</td>
      <td class="cal-inactive">30</td>
      <td class="cal-inactive">31</td>
      <td><a href="#">1</a></td>
      <td><a href="#">2</a></td>
      <td><a href="#">3</a></td>
      <td><a href="#">4</a></td>
    </tr>
    <!-- ...and so on, one row per week... -->
    <tr>
      <td><a href="#">26</a></td>
      <td class="cal-current"><a href="#">27</a></td>
      <!-- ...and so on -->
      <td><a href="#">31</a></td>
      <td class="cal-inactive">1</td>
    </tr>
  </tbody>
</table>

我们已经用一个占位符锚元素包装了所有的日子(假设当您单击一个日期时,日历组件会将您带到某个地方或做一些事情)。我们还添加了几个类名来表示当天。cal-current)和当月之外的天数(。校准-无效)。

设置表格元素的样式

CSS 规范有两种表格边框模型:分离的和折叠的。在单独的模型中,边框放置在单个单元格周围,而在折叠模型中,单元格共享边框。我们希望单元格共享单个 1 像素的边框,因此我们将表格的边框折叠属性设置为折叠。

表格也有一个调整单元格大小的算法,我们可以通过 table-layout 属性来控制。默认情况下,使用 auto 值,这基本上是让浏览器根据单元格的内容来决定单元格的宽度。通过将其更改为 fixed,任何单元格宽度都将根据表格第一行或任何 col 或 colgroup 元素中的单元格宽度来确定。这通过 CSS 给了我们更多的控制。

接下来,我们设置字体堆栈,将表格中的所有文本居中。最后,我们将添加一个宽度和一个最大宽度来创建一个流体组件,它尽可能多地占用空间,而不会宽得令人难以接受。

.cal {
  border-collapse: collapse;
  table-layout: fixed;
  width: 100%;
  max-width: 25em;
  font-family: "Lucida Grande", Verdana, Arial, sans-serif;
  text-align: center;
}

设置表格内容的样式

基础工作已经做好了,现在是开始添加视觉样式的时候了。为了让表格标题看起来更像普通标题,我们将增加字体大小和行高。我们还会将它向左对齐,并给它一个边框,将它与表头分开。

.cal caption {
  text-align: left;
  border-bottom: 1px solid #ddd;
  line-height: 2;
  font-size: 1.5em;
}

接下来,我们将使用 col 元素为周末设置粉色背景。请记住,背景属性是您可以对整列进行更改的少数几项内容之一。我们将使用一种高度透明的颜色,这样它可以和任何背景融为一体,但是在此之前提供一个纯色的后备声明,以适应旧的浏览器。

.cal-weekend {
  background-color: #fef0f0;
  background-color: rgba(255, 0, 0, 0.05);
}

接下来,我们将对单个单元格进行样式化。所有单元格都需要多一点行高,我们将为它们提供一个宽度。默认情况下,表格有一个根据单元格内容分配空间的布局算法。由于工作日表标题的大小不同,这导致列略有不同。我们可以指定一个等于表格宽度七分之一的宽度(14.285%)来纠正这个问题。事实上,宽度只需要至少为表格宽度的七分之一—如果单元格相加超过 100%(当使用固定表格布局模型时),它们将各自按比例缩小,直到适合为止。如果我们想让单元格等宽,不管有多少个,我们可以将它们的宽度设置为 100%。虽然这是一个很方便的技巧,但是为了清楚起见,在本例中我们将宽度保留为总宽度的七分之一。你可以在这篇由克里斯·科伊尔撰写的 CSS-Tricks 文章中阅读更多关于表格布局的古怪之处:【https://css-tricks.com/fixing-tables-long-strings/

在一些浏览器中,表格单元格也有默认的填充,我们想要移除它。我们还将为表格单元格添加一个模糊的边框,但不是标题单元格。

.cal th,
.cal td {
  line-height: 3;
  padding: 0;
  width: 14.285%;
}
.cal td {
  border: 1px solid #eee;
}

为了将表格标题与表格数据(实际日期)分开,我们将添加一个更粗的边框。这应该和在 thead 元素上设置边框一样简单。

.cal thead {
  border-bottom: 3px solid #666;
}

这在大多数浏览器(Chrome、Firefox、Safari、Opera 等)中都能正常工作。)但遗憾的是在 Internet Explorer 或 Edge 中没有。表格中的边框,不管它们是在表格单元格、一行还是一组行上(例如 thead 或 tbody),都在我们选择的折叠表格模型中融合在一起。幸运的是,大多数浏览器在设置一整行的边框时都会覆盖垂直边框。IE 和 Edge 会尝试将左右两边的边框和 thead 元素上的边框连接起来,造成难看的缝隙(见图 9-4 )。

A314884_3_En_9_Fig4_HTML.jpg

图 9-4。IE 和 Edge 都让垂直边框撞上了表头的水平边框,产生了空隙

种方法可以解决这个问题。我们可以退出折叠的边框模型,为单个表格单元格的边框添加更多的规则,但是在这种情况下,我们将保持原样。如果你遇到这个问题,并需要它在 IE 中看起来和在其他浏览器中完全一样,你可能不得不求助于单独的边框模型或使用类似背景图像的东西来代替。

接下来,我们将处理日历小部件中代表可点击日期的锚链接。我们将删除下划线,给他们一个深紫色,并设置他们显示为块。这将导致它们扩展以填充整个表格单元格,从而创建一个更大的可点击区域。最后,我们将为悬停和聚焦状态添加规则,其中我们显示半透明的背景色(使用与前面相同的后退技术到纯色)。

.cal a {
   display: block;
   text-decoration: none;
   color: #2f273c;
}

.cal a:hover,
.cal a:focus {
  background-color: #cde7ca;
  background-color: rgba(167, 240, 210, 0.3);
}

最后,我们将为日历日期的其他状态添加样式。我们有不在当前月份的日期,所以我们会给它们一个褪色的颜色,并清楚地表明它们不能通过使用不同的指针来选择。

对于当前日期,我们将把背景颜色改为另一种稍微半透明的色调。各种状态的半透明颜色混合在一起,因此我们将根据我们是否有当前日期、当前日期被悬停、当前日期被悬停在“周末”内等,自动得到不同的结果颜色。,都没有任何额外的规则(见图 9-5 )。

A314884_3_En_9_Fig5_HTML.jpg

图 9-5。我们为悬停、当前和非活动日期添加了各种微妙的状态
.cal-inactive {
  background-color: #efefef;
  color: #aaa;
  cursor: not-allowed;
}
.cal-current {
  background-color: #7d5977;
  background-color: rgba(71, 14, 62, 0.6);
  color: #fff;
}
.cal-current a {
  color: #fff;
}

现在你有了一个漂亮的日历,如图 9-3 所示。

响应式表格

桌子本身就需要空间。它们内置了两个轴的概念,并且随着列数的增加需要更多的宽度。结果是,复杂的桌子往往需要相当大的空间,这与能够在所有屏幕上舒适地显示事物的响应目标相冲突,无论屏幕大小。

我们之前提到过,在 CSS 中,表格(以及表格的每个组件)都有自己的显示模式。我们可以利用这一点,让不是表格的东西借用表格的“网格特性”,以达到布局的目的。但是我们也可以使用相反的策略,让表格不显示为表格!我们将采用这种方法使表格数据适合较小的屏幕。

线性化表格

当我们有一个包含大量列的表格时,我们可以翻转它,使每一行都表示为一个由表格标题文本和该行的值组成的块。让我们创建一个示例表来直观显示这一点,其中包含一组汽车模型的数据。最终结果将在大屏幕上看起来如图 9-6 所示。

A314884_3_En_9_Fig6_HTML.jpg

图 9-6。汽车模型的数据表,带有一些简单的样式

对于较小的屏幕,每行都有自己的块。表格标题行是隐藏的,列标签打印在每条数据之前。它看起来有点像图 9-7。

A314884_3_En_9_Fig7_HTML.jpg

图 9-7。小屏幕线性化的表格
<table class="cars">
  <caption>Tesla car models</caption>
  <thead>
    <tr>
      <th scope="col">Model</th>
      <th scope="col">Top speed</th>
      <th scope="col">Range</th>
      <th scope="col">Length</th>
      <th scope="col">Width</th>
      <th scope="col">Weight</th>
      <th scope="col">Starting price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Model S</td>
      <td>201 km/h</td>
      <td>426 km</td>
      <td>4 976 mm</td>
      <td>1 963 mm</td>
      <td>2 108 kg</td>
      <td>$69 900</td>
    </tr>
    <tr>
      <td>Roadster</td>
      <td>201 km/h</td>
      <td>393 km</td>
      <td>3 946 mm</td>
      <td>1 873 mm</td>
      <td>1 235 kg</td>
      <td>$109000</td>
    </tr>
  </tbody>
</table>

该表格的样式由一些简单的边框、字体规则和一种“斑马条纹”技术组成,其中表格的每一偶数行都有不同的背景颜色:

.cars {
  font-family: "Lucida Sans", Verdana, Arial, sans-serif;
  width: 100%;
  border-collapse: collapse;
}

.cars caption {
  text-align: left;
  font-style: italic;
  border-bottom: 1px solid #ccc;
}

.cars tr:nth-child(even) {
  background-color: #eee;
}
.cars caption,
.cars th,
.cars td {
  text-align: left;
  padding: 0 .5em;
  line-height: 2;
}
.cars thead {
  border-bottom: 2px solid;
}

如果我们调整屏幕大小,我们发现在大约 760 像素宽时,这个表格开始变得非常拥挤,难以阅读(见图 9-8 )。这就是我们需要放置断点并开始改变的地方。

A314884_3_En_9_Fig8_HTML.jpg

图 9-8。我们的桌子在大约 760 像素宽时开始变得拥挤

表格有很多默认样式和显示模式。如果我们走“移动优先”的路线,改变默认样式,然后使用最小宽度条件为更大的屏幕重置默认值,我们可能要做很多工作。这就是为什么我们将使用最大宽度条件,以在较小的屏幕上特别针对这种特殊情况:

@media only screen and (max-width: 760px) {
  .cars {
    display: block;
  }
  .cars thead {
    display: none;
  }
  .cars tr {
    border-bottom: 1px solid;
  }
  .cars td, .cars th {
    display: block;
    float: left;
    width: 100%;
    box-sizing: border-box;
  }
  .cars th {
    font-weight: 600;
    border-bottom: 2px solid;
    padding-top: 10px;
  }
  .cars td:before {
    width: 40%;
    display: inline-block;
    font-style: italic;
    content: attr(data-label);
  }
}

表格单元格现在设置为以块的形式显示,并占据 100%的宽度,在行内将它们堆叠在一起。表格标题完全隐藏。为了保持列标签和 td 元素中的单个值之间的关联,我们在标记中的每个表格单元格上插入了每个列的标签作为数据标签属性:

<th scope="row">Model S</th>
<td data-label="Top speed">201 km/h</td>
<td data-label="Range">426 km</td>
<td data-label="Length">4 976 mm</td>
<!-- ...and so on -->

我们现在可以使用:before 伪元素在每一行单元格内容之前插入这些标签。我们可以通过使用 attr()函数符号和 content 属性来获取元素属性的内容——这是一个揭示隐藏在 HTML 中的额外数据的简便技巧。为了避免在 CSS 中硬编码标签的值,标签在标记中的重复是一个很小但必要的代价。

除了一些进一步的样式更改以保持表格的可读性之外,上一个示例中还有一些其他重要的代码部分。

首先,我们将表格本身设置为显示:block。这对于演示来说不是必需的,但是有助于提高可访问性。切换表格的显示模式不应该改变屏幕阅读器对它的解释,但它确实改变了。这意味着,当标记中有一个表格,但表格单元格设置为显示为其中的常规块(通过 CSS)时,一些屏幕阅读器会感到困惑。将表格本身设置为显示为一个块似乎会触发这些屏幕阅读器将表格作为文本流来读取,这保持了内容的可访问性,尽管失去了表格的性质。accessibleculture.org 的 Jason Kiss 在 http://accessible culture . org/articles/2011/08/responsive-data-tables-and-screen-reader-accessibility/上发表了一篇很有帮助的文章,解释了各种屏幕阅读器之间的差异。

为了使这个解决方案有效,我们做的第二件事是向表格单元格添加一个 float 声明。这只是为了应对 IE9 中的一个错误,IE9 支持媒体查询,但似乎不接受将表格单元格的显示模式更改为@media 规则内部的块。但是,它确实应用了浮动,这实际上是把单元格变成了块,这是一个副作用。将单元格的宽度设置为 100%可以抵消浮动的收缩包装效果,并保证它们像块一样垂直排列。

高级响应表

为小视口线性化表格只是创建响应表格的解决方案之一。有几种方法可以解决同一个问题,老实说,这仍然是一个相当新的问题,因为这本书正在编写。没有“一刀切”的解决方案,但有一些策略可供选择。大多数依赖 JavaScript 在需要时结合 CSS 来操作标记。各种策略都是一些基本机制的变体:

  • 当屏幕太小时,为表格的列引入某种滚动机制。例如,第一列可以固定在适当的位置,并作为一个锚点,帮助您知道您正在查看哪一行,其余的列可以滚动。

  • 当屏幕变小时隐藏列,以便只显示最重要的内容。

  • 在一个单独的窗口中链接到一个更大版本的表格,用户必须依赖缩放。

  • 使得用切换机制显示和隐藏列成为可能。

如果您需要支持复杂的响应式表格场景,您可能会发现 tool Tablesaw 非常适合(www.filamentgroup.com/lab/tablesaw.html),如果没有什么可以作为设计模式的灵感的话。它是 jQuery 插件的集合,可以帮助您实现前面列表中提到的一些策略。

样式表单

表单是 web 页面的访问者实际上做其他事情而不是消费内容的地方。它可以是填写一个联系表格,写一篇要发表的文章,输入付款信息,或者最后点击“立即购买”按钮。很明显,这些都是非常有价值的活动,但是尽管非常重要,表单的设计和编码通常很糟糕。

也许其中一个原因是表单编码总是有点麻烦。它们有很多活动部件,传统上很难设计。这是因为许多表单控件被实现为用替换内容,这意味着像 select 元素中下拉菜单中的箭头这样的控件实际上并不由任何 HTML 元素表示。它更像是一个黑盒,每当你在标记中声明一个<选择>标签时,浏览器就扔在那里。这主要是为了确保与用户当前操作系统的默认 UI 控件保持一致。

但是,我们至少可以设计表单控件外观的某些部分。对于我们不能设计样式的部分,我们可以用一些创造性的编码来伪装自定义控件的外观。

表单也不仅仅是表单控件本身,所以这一节将介绍如何标记和样式化组件,使之成为一个有吸引力的 HTML 表单。应该注意的是,它绝不是 HTML 表单各个方面的全面指南——元素和属性太多了,我们无法在这里一一介绍。

一个简单的表单示例

当表单标签垂直显示在相关表单元素上方时,简短且相对简单的表单最容易填写。用户只需一步一步地向下移动表单,阅读每个标签并完成下面的表单元素。这种方法适用于收集相对简单且可预测的信息(如联系信息)的短表单(见图 9-9 ),但对于在较小的视窗(如移动浏览器)上查看表单,这也是一个非常好的基准。

A314884_3_En_9_Fig9_HTML.jpg

图 9-9。简单表单布局

字段集和图例

HTML 提供了许多有用的元素,可以帮助表单增加结构和意义。第一个是 fieldset 元素。字段集用于对相关的信息块进行分组。在图 9-9 中,使用了三个字段集:一个用于联系详情,一个用于评论,一个用于“记住我”偏好设置。

要确定每个字段集的用途,可以使用 legend 元素。图例的作用有点像字段集的标题,通常垂直居中显示在字段集的顶部边框上,并稍微向右缩进。默认情况下,字段集通常呈现为双边框。不同的浏览器以不同的方式实现了这种略有不同的外观。这似乎是浏览器渲染引擎中的一个特例,用普通的 CSS 属性来撤销奇怪的定位很少会有你期望的效果。当我们设计表单的时候,我们将回头来讨论这个问题。

标签

label 元素是一个非常重要的元素,因为它可以帮助添加结构,提高表单的可用性和可访问性。顾名思义,这个元素用于为每个表单元素添加一个有意义的描述性标签。在许多浏览器中,单击 label 元素会使关联的 form 元素获得焦点。

使用标签的真正好处是为使用辅助设备的人增加表单的可用性。如果表单使用标签,屏幕阅读器会正确地将表单元素与其标签相关联。屏幕阅读器用户还可以调出表单中所有标签的列表,允许他们以听觉方式浏览表单,就像视力正常的用户以视觉方式浏览表单一样。

可以通过以下两种方式之一将 label 元素与 form 控件关联起来:通过在 label 元素中嵌套 form 控件来隐式关联:

<label>Email <input name="comment-email" type="email"/><label>

或者通过将标签的 for 属性设置为与相关表单元素的 id 属性值相等来显式设置:

<label **for="comment-email"**>Email<label>
<input name="comment-email" **id="comment-email"** type="email"/>

你会注意到这个输入和本章中的大多数表单控件都包含了一个 name 属性和一个 id 属性,因为我们通常不会将输入嵌套在标签中。id 属性是创建表单输入和标签之间的关联所必需的,而 name 属性是必需的,这样表单数据就可以发送回服务器。id 和名称不必相同,但是为了保持一致性,尽可能保持它们相同是一个方便的约定。

与使用 for 属性的表单控件相关联的标签不需要靠近源代码中的那些控件;它们可能在文档中完全不同的部分。从结构的角度来看,将表单控件与其标签分开是不明智的,应该尽可能避免。

输入字段和文本区域

在这个简单的例子中,我们有两种类型的表单控件元素:input 和 textarea。文本区域用于键入多行文本,就像在注释字段中一样。cols 和 rows 属性可用于设置文本区域的默认大小,主要用于指示预期内容的大致长度。稍后我们将自由地用 CSS 进一步样式化文本区域。

<textarea name="comment-text" id="comment-text" cols="20" rows="10"></textarea>

input 元素是一个更加通用的表单控件。默认情况下,它呈现为单行文本输入,但是 type 属性可以将其更改为各种不同的窗体控件。设置 type="password "会创建一个值模糊的输入,而 type="checkbox "会创建一个复选框。type 属性有许多不同的值,其中许多是在 HTML5 中添加的。有些主要是文本输入的变体,但在幕后有特殊的行为—例如,电子邮件、url 和搜索。一些类型创建了非常不同的界面控件,如复选框、单选、颜色、范围和文件。除了类型之外,还有一大堆用于输入的属性来声明预期的格式。

不同类型的表单输入及其属性对于表单的自动验证非常有用。我们将在这一章的后面简要介绍一下,但是现在,我们将讨论另一个大的好处。在带有屏幕键盘的设备上,更改类型会触发软件键盘更改其布局。如果我们添加正确类型的电子邮件字段和 URL 字段,智能手机和平板电脑上的键盘将自动调整,以便在我们聚焦每个字段时更容易键入正确的值(见图 9-10 )。

A314884_3_En_9_Fig10_HTML.jpg

图 9-10。当输入具有 type="email "属性时,软件键盘会显示更适合键入电子邮件地址的布局

因为 type 属性的缺省值是 text,所以不支持 HTML5 的旧浏览器将忽略这些新类型,并退回到普通输入。这使得选择新的输入类型对我们来说是一个非常有用的改进。

将字段集放在一起

使用到目前为止我们已经看到的结构元素,我们可以通过标记第一个字段集的内容来开始布局表单。未样式化的字段集如图 9-11 所示。

A314884_3_En_9_Fig11_HTML.jpg

图 9-11。无样式字段集

在表单内部,我们用一个 div 包装了 fieldset 元素,原因稍后将会清楚。标签和输入的每个组合也用 p 元素包装。过去,输入元素不允许作为表单元素的直接子元素。HTML5 中不再是这种情况,但是标准仍然建议您用像 p 这样的块元素包装标签和表单控件,因为它们在语义上表示表单中不同的内容“短语”。

我们还为每个段落添加了一个 field 的类名,以便在以后想要将它们与表单中的其他类型的段落分开时有一个特定的样式挂钩。此外,我们通过给包含文本输入组件的字段起一个类名称 field-text 来分离它们。

<form id="comments_form" action="/comments/" method="post">
  <div class="fieldset-wrapper">
    <fieldset>
      <legend>Your Contact Details</legend>
      <p class="field field-text">
        <label for="comment-author">Name:</label>
        <input name="comment-author" id="comment-author" type="text" />
      </p>
      <p class="field field-text">
        <label for="comment-email">Email Address:</label>
        <input name="comment-email" id="comment-email" type="email" />
      </p>
      <p class="field field-text">
        <label for="comment-url">Web Address:</label>
        <input name="comment-url" id="comment-url" type="url" />
      </p>
    </fieldset>
  </div>
</form>

如果您希望更改字段集和图例元素的默认外观,最好的办法是不要设计实际字段集元素本身的样式,而是尽可能多地删除默认样式,然后在字段集周围添加一个包装元素。您的样式将被添加到包装元素中。

要取消字段集的样式,我们将为其指定以下规则:

fieldset {
  border: 0;
  padding: 0.01px 0 0 0;
  margin: 0;
  min-width: 0;
  display: table-cell;
}

我们删除了默认的边界和空白。我们还将填充设置为 0——顶部填充除外,它被设置为一个很小的值(0.01 像素)。这是为了应对一些基于 WebKit 的浏览器中的怪异行为,在这些浏览器中,图例之后元素上的任何边距都被转移到 fieldset 元素的顶部。给字段集一点点填充顶部可以阻止这个错误。

下一个奇怪之处是:一些浏览器(基于 WebKit 和 Blink 的)对字段集元素有一个默认的最小宽度,我们会覆盖它——如果没有,字段集有时会以最小的尺寸突出视口,产生水平滚动条。Firefox 也有字段集元素的最小宽度,但这是硬编码的,覆盖最小宽度没有帮助。解决方法是将显示模式改为表格单元。这与 IE 混淆了,所以我们需要使用特定于 Mozilla 的非标准规则块,只针对基于 Mozilla 的浏览器:

@-moz-document url-prefix() {
  fieldset {
    display: table-cell;
  }
}

@-moz-document 规则允许基于 Mozilla 浏览器的用户在其用户样式表中覆盖特定站点的样式,但它也适用于作者样式。通常,您会在 url-prefix()函数中放入一个特定的 url,但是让它为空意味着不管 URL 是什么,它都会工作。不可否认,这是一个丑陋的黑客,但它代表了移除我们的字段集的默认样式的最后一块拼图。现在我们可以专注于包装器的样式。

我们将给包装一个背景,一些空白和填充,和一个微妙的阴影。不支持 box-shadow 属性的旧浏览器会得到一个边框,然后使用:root 伪类作为选择器的前缀来删除它。这只是指 HTML 元素(作为文档的根元素),但 IE8 和其他旧浏览器不理解这个选择器,所以它们将获得边框。

.fieldset-wrapper {
  padding: 1em;
  margin-bottom: 1em;
  border: 1px solid #eee;
  background-color: #fff;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.25);
}
:root .fieldset-wrapper {
  border: 0;
}

至于图例,我们将删除默认填充,并在底部添加一些额外的内容,以增加它和表单字段之间的空间。遗憾的是,边距对图例元素的影响不一致,因此我们将避免这些影响。最后,我们将把它的显示模式改为表格。这种方法允许它在必要时在 IE 中包装成多行,否则这是不可能的。

legend {
  padding: 0 0 .5em 0;
  font-weight: bold;
  color: #777;
  display: table;
}

此时,字段集本身看起来不错,如图 9-12 所示,我们可以将注意力转向字段。

A314884_3_En_9_Fig12_HTML.jpg

图 9-12。字段集现在失去了图例的双边框和奇怪的定位,并获得了背景和阴影

设置文本输入字段的样式

接下来,我们将添加一个规则,使表单控件从文档的其余部分继承字体属性。这将覆盖浏览器的默认设置:例如,输入字段中的字体大小被设置为小于文档中正常文本的大小。

input,
textarea {
  font: inherit;
}

定位标签使其垂直出现在表单元素的上方非常简单。默认情况下,标签是内联元素。将它的 display 属性设置为 block 将导致它生成自己的块框,将输入元素强制放到下面的行上。

文本输入框的默认宽度因浏览器而异,但是我们可以用 CSS 来控制它。为了创建一个灵活的输入字段,我们将在默认情况下以百分比来设置宽度,但是在 ems 中设置字段包装器的最大宽度,这样它就不会变得太宽。这将适用于大多数屏幕尺寸。在计算 100%的含义时,我们还需要更改 box-sizing 属性以考虑边框和填充。

.field-text {
  max-width: 20em;
}
.field-text label {
  cursor: pointer;
}
.field-text label,
.field-text input {
  width: 100%;
  box-sizing: border-box;
}

标签的 cursor 属性设置为 pointer,使基于鼠标的用户更清楚这是一个可点击的元素。标签也包含在上面设置宽度的规则中,因此它们与输入具有相同的宽度。

最后,我们将稍微调整文本输入的样式。我们将为它们设置微妙的圆角,设置边框颜色,并添加一些填充:

.field-text input {
  padding: .375em .3125em .3125em;
  border: 1px solid #ccc;
  border-radius: .25em;
  -webkit-appearance: none;
}

设置 border 属性通常会删除呈现文本输入时可能显示的任何特定于操作系统的边框外观和嵌入阴影。一些基于 WebKit 的浏览器(如 iOS 上的 Safari)仍然显示嵌入阴影,因此为了消除这种情况,我们将 proprietary -webkit-appearance 属性设置为 none。

注意

没有标准化的外观属性,但是-webkit-appearance(基于 webkit 和 Blink 的浏览器)和-moz-appearance (Firefox)都允许您覆盖特定于操作系统的控件的一些呈现细节。通常,你最好避开这些,但是它们对于移除特定于浏览器的输入元素样式很有用。

处理焦点状态

更改输入元素的边框后,我们还需要注意元素的焦点状态。当输入元素被聚焦时,大多数浏览器会在输入元素周围显示某种形式的轮廓或光晕。此标记帮助用户区分哪个字段是焦点,并且可以通过重写 outline 属性或 border 属性来移除,具体取决于浏览器。一旦我们影响了这些属性中的,我们需要确保我们没有无意中让键盘用户无法访问表单。

这意味着为了跨浏览器的兼容性,我们必须自己关注焦点状态。我们将添加一个不同的边框颜色:焦点,以及一个微妙的蓝色发光使用框阴影(见图 9-13 )。这样,我们还可以在聚焦时将 outline 属性设置为 0,以避免在某些浏览器中出现聚焦状态的双重标记。

A314884_3_En_9_Fig13_HTML.jpg

图 9-13。一个聚焦的文本输入使用框阴影得到不同的边框颜色和一点光晕
.field-text input:focus {
  box-shadow: 0 0 .5em rgba(93, 162, 248, 0.5);
  border-color: #5da2f8;
  outline: 0;
}

在我们到目前为止创建的规则中,我们已经明确地将我们在这个表单中使用的基于文本的输入类型作为目标。字段文本选择器。这是为了避免为其他类型的输入部件(如复选框)设置不必要的规则。我们本来可以用一个属性选择器列表来代替,但是因为 type 属性有许多可能的值,所以父元素上的一个实用程序类名称会使代码更加简洁。

添加剩余的字段集

到目前为止,我们创建的规则同样适用于其他表单元素,如 textareas:

<div class="fieldset-wrapper">
  <fieldset>
    <legend>Comments</legend>
    <p class="field field-text">
      <label for="comment-text">Message:</label>
      <textarea name="comment-text" id="comment-text" cols="20" rows="10"></textarea>
    </p>
  </fieldset>
</div>

要调整 textarea 元素的外观,我们可以简单地将它添加到任何规则的选择器列表中,在这里我们为文本输入和标签设置属性,并获得相同的行为:

.field-text label,
.field-text input,
**.field-text textarea {** 
**/*...*/** 
**}** 

Textareas 将获得基于 rows 属性的默认高度,但是我们当然可以用 height 来覆盖它。当用户输入比可见空间更长的文本时,文本区将溢出并接收滚动条。

许多浏览器还会让用户调整文本区域的大小,这样他们就可以看到输入的整个文本。有些浏览器允许文本区域调整宽度和高度,有些只允许调整高度。我们实际上可以在 CSS 中使用 resize 属性来明确这一点。它可以设定为任何关键字“垂直”、“水平”、“无”或两者兼有,但此处显示为设定为“垂直”:

textarea {
  height: 10em;
  resize: vertical;
}

添加单选按钮

对于表单的最后一部分,我们添加了一个单选按钮控件,使用户只能从两个选项中选择一个。这些由类型设置为 radio 的输入元素表示。这些元素的标签通常在右边,而不是在上面(见图 9-14 )。

A314884_3_En_9_Fig14_HTML.jpg

图 9-14。我们的单选按钮,标签文本在右边而不是上面

只能选择其中一个输入的效果是通过使这两个输入的 name 属性相等来实现的(尽管 id 属性仍然可以不同):

<div class="fieldset-wrapper">
  <fieldset>
    <legend>Remember Me</legend>
    <p class="field">
      <label><input **name="comment-remember"** type="radio" value="yes" />Yes</label>
    </p>
    <p class="field">
      <label><input **name="comment-remember"** type="radio" value="no" checked="checked" />No</label>
    </p>
  </fieldset>
</div>

注意,在这种情况下,我们选择将输入嵌套在标签中,而不是将其与输入元素上的 for 属性和 id 相关联。这意味着标签上的 display: block 声明不会将它放在与单选按钮不同的行上。

我们要做的最后一件事是给单选按钮添加一点右边距,以在标签之间提供一些间距:

input[type="radio"] {
  margin-right: .75em;
}

小跟班

在我们的表格完成之前,我们还有一件事要添加。用户需要一个按钮来提交表单,这样服务器就可以处理它。

用 HTML 创建按钮有两种方法。首先,输入元素的类型设置为 button、reset 或 submit:

<input type="submit" value="Post comment" />

然后是按钮元素,它可以具有相同的类型属性值:

<button type="submit">Post comment</button>

当在表单外使用时,按钮类型可用于 JavaScript 启动的操作,而不是实际将表单提交给服务器。重置类型(目前不常用)将表单重置为初始值。最后,如果按钮在表单元素中,submit 值将表单数据发送到表单的 action 属性中指定的 URL。当缺少 type 属性时,它是默认值。

按钮的这两个元素工作方式相同,最初看起来也一样。我们建议您为按钮使用 button 元素,因为您可以在其中放置其他元素(如 spans 或 images)来帮助设计样式。

按钮在每个平台上都有特定的默认外观(见图 9-15 ),复选框、单选按钮和其他表单控件也是如此。因为按钮是用户界面中无处不在的一部分,所以你很有可能想要根据你正在构建的站点的特定外观来设计它们。幸运的是,它们是用 CSS 样式化的更容易的表单组件之一。

A314884_3_En_9_Fig15_HTML.jpg

图 9-15。来自 OS X 的 Chrome、Windows 7 的 IE10、Windows 7 的 Firefox 和 Windows 10 的 Microsoft Edge 的无样式按钮元素

我们将使用渐变和方框阴影为我们的按钮添加一个非常微妙的 3D 外观的边缘(见图 9-16 )。与 input 元素一样,修改 border 属性会关闭特定于操作系统的样式。

A314884_3_En_9_Fig16_HTML.jpg

图 9-16。我们的样式按钮
button {
  cursor: pointer;
  border: 0;
  padding: .5em 1em;
  color: #fff;
  border-radius: .25em;
  font-size: 1em;
  background-color: #173b6d;
  background-image: linear-gradient(to bottom, #1a4a8e, #173b6d);
  box-shadow: 0 .25em 0 rgba(23, 59, 109, 0.3), inset 0 1px 0 rgba(0, 0, 0, 0.3);
}

按钮上的伪 3D 边缘是用框阴影而不是边框属性创建的。这允许我们保持按钮的尺寸不变,因为阴影不会影响盒子模型。阴影也会自动跟随按钮的圆角。注意,我们使用了两个阴影。一个是创建边缘的外部阴影,一个是插入,在按钮顶部添加一个微妙的 1 像素的颜色偏移。

我们还为按钮的焦点状态添加了一条规则(见图 9-17 ),这里的背景变浅了一点,并添加了第三个阴影,产生了与文本输入相同的轻微“发光轮廓”效果。(在第十章的中,当我们探索变换和转换的时候,我们将会重温按钮被按下的状态。)

A314884_3_En_9_Fig17_HTML.jpg

图 9-17。按钮的正常状态(左)和聚焦状态(右)
button:focus {
  background-color: #2158a9;
  background-image: linear-gradient(to bottom, #2063c0, #1d4d90);
  box-shadow: 0 .25em 0 rgba(23, 59, 109, 0.3),
              inset 0 1px 0 rgba(0, 0, 0, 0.3);
}

不支持圆角、渐变和方框阴影的浏览器在这两种状态下都会有一个看起来很平的按钮,但是仍然可以很好地使用。

清除表单反馈和帮助文本

糟糕的反馈和错误信息一直被认为是网络上最糟糕和最常见的设计问题。当设计表单时,确保你不只是让表单控件看起来漂亮,还要注意帮助和错误信息的样式。

您可以使用占位符属性将预期输入的示例放入输入字段(参见图 9-18 )。浏览器将显示该文本,直到您聚焦该字段或开始书写。

A314884_3_En_9_Fig18_HTML.jpg

图 9-18。使用占位符属性的输入示例
<input **placeholder="http://example.com"** name="comment-url" id="comment-url" type="url" />

我们可以对占位符属性进行一些有限的样式设置,例如,将其设置为斜体。占位符没有标准的选择器,但是不同的浏览器提供了不同的前缀伪元素,您可以对它们进行样式化。因为每一个都只能被各自的浏览器引擎识别,所以它们不能合并成一个规则。当浏览器看到一个无法识别的选择器时,它会将规则作为一个整体丢弃,所以我们需要重复一下:

::-webkit-input-placeholder {
  font-style: italic;
}
:-ms-input-placeholder {  
  font-style: italic;
}
::-moz-placeholder {
  font-style: italic;  
}

占位符是用来举例输入的,所以不能用作标签。毕竟,占位符会随着用户与表单的交互而消失,所以如果用户失去焦点(没有双关语!)在一段时间内,他们需要能够让任何指令仍然存在。

如果标签不够,可以在表单控件旁边添加帮助文本。因为我们想节省空间并使表单更整洁,所以我们将使用同级选择器仅在输入字段被聚焦时显示额外的帮助(见图 9-19 )。

A314884_3_En_9_Fig19_HTML.jpg

图 9-19。当字段被聚焦时显示额外的帮助文本

我们希望在视觉上隐藏文本,但不一定是为了屏幕阅读器,即使字段没有被聚焦。使用 clip 属性、绝对定位和 overflow: hidden 的组合可以达到这个目的。

这种特定的属性组合是为了避免旧浏览器中的各种错误。在 Jonathan Snook 的博客(Snook . ca/archives/html _ and _ CSS/hiding-content-for-accessibility)中可以找到关于这种技术的更深入的讨论。然后,当帮助文本位于焦点输入旁边时,使用同级选择器覆盖这些属性:

.form-help {
  display: block;
  /* hide the help by default,
     without hiding it from screen readers */
**position: absolute;** 
**overflow: hidden;** 
**width: 1px;** 
**height: 1px;** 
**clip: rect(0 0 0 0);** 
}

input:focus + .form-help {
  padding: .5em;
  margin-top: .5em;
  border: 1px solid #2a80fa;
  border-radius: .25em;
  font-style: italic;
  color: #737373;
  background-color: #fff;
  /* override the "hiding" properties: */
**position: static;** 
**width: auto;** 
**height: auto;** 
**crop: none;** 
}
易接近的隐藏技术

帮助文本示例中的技术使用 CSS 来隐藏可视内容,同时保持屏幕阅读器可以访问它。使用其他技术,如 display: none 或 visibility: hidden,可以让屏幕阅读器完全跳过文本。

在设计表单时,视觉设计中省略一个标签或者多个字段使用一个标签是很常见的模式。例如,您可能将日期部分分成三个字段,分别表示年、月和日,但只有一个标签显示“出生日期”

在这种情况下,使用可访问的隐藏技术允许您为任何字段添加标签,而无需在页面上实际显示它。当然,这种技术可以用于任何有助于页面语义结构的元素,但对于视觉用户来说可能是不必要的。

这是“助手类”的理想选择,只要出现这种情况,就可以应用到标记中。HTML5 样板项目(html5boilerplate.com/)对类名使用了这种技术。例如,视觉隐藏。

我们的帮助文本的标记非常简单,但是增加了一些语义丰富性,以确保帮助是可访问的:

<input placeholder="http://example.com" name="comment-url" **aria-described-by="comment-url-help"** id="comment-url" type="url"  />
<span **id="comment-url-help" role="tooltip"** class="form-help">Fill in your URL if you have one. Make sure to include the "http://"-part.</span>

input 元素上的 aria-describedby 属性应该指向帮助文本的 id。这使得屏幕阅读器将帮助文本与字段相关联,并且当字段被聚焦时,他们中的许多人除了阅读标签之外还会阅读帮助文本。设置为 tooltip 的角色属性进一步向屏幕阅读器阐明了这是在用户与表单域交互时显示的文本。

如果您在服务器上或者在浏览器中使用 JavaScript 进行表单验证,HTML 中的任何错误消息都可以用类似的方式标记,使用 aria-describedby 将消息与表单控件相关联。

支持 HTML5 的现代浏览器也有内置的表单验证,以及一系列帮助客户端验证的 CSS 伪类。

HTML5 表单验证和 CSS

一旦您使用 HTML5 表单的新属性,浏览器将尝试帮助您验证表单字段的值。例如,当您使用类型设置为电子邮件的输入,并填写无效的内容并尝试提交表单时,浏览器将显示一条错误消息(参见图 9-20 )。

A314884_3_En_9_Fig20_HTML.jpg

图 9-20。Mozilla Firefox 中的验证消息

支持 HTML5 验证的浏览器也为我们提供了许多对应于表单字段中各种状态的伪类。例如,我们可以使用下面的代码用红色边框和红光突出显示无效的文本输入字段:

.field-text :invalid {
  border-color: #e72633;
  box-shadow: 0 0 .5em rgba(229, 43, 37, 0.5);
}

我们已经在第二章中看到了:required,:optional,:valid 和:invalid 伪类。它们还有几个,对应于数字输入、滑块等的各种状态。基于这些伪类设计输入字段的样式没有问题,但是实际的错误消息呢?

可悲的是,这些是 CSS 无法触及的界面元素的另一个例子。基于 WebKit 的浏览器提供了一些有限的可能性来使用特定于浏览器的伪元素来样式化错误消息,例如::-webkit-validation-bubble,但是除此之外,还没有其他方法来样式化它们的外观。

如果您需要对这些错误消息进行更多的控制,有许多 JavaScript 插件可以与浏览器触发的表单事件挂钩。它们覆盖内置的验证,通常为您提供为错误消息生成元素(和设置文本)的方法,并为旧浏览器提供验证支持。例如,参见 Webshim 项目的表单插件(afarkas.github.io/webshim/demos/)。

高级表单样式

到目前为止,我们将表单样式保持在非常合理的最低限度。理由很充分:表单很少是实验的地方。想要注册个人资料或为产品付费的人可能会喜欢清晰,而不是为了清晰而与众不同。这并不意味着更有创造性的 CSS 技术在表单设计中没有一席之地。在这一节中,我们将展示一些技巧来解决细节问题。

用于表单布局的现代 CSS

默认情况下,大多数表单元素显示为内嵌块,因此沿文本方向排列。我们在前面的示例中使用了块显示模式,使标签和输入字段在页面上堆叠显示。

当我们想要更高级的表单布局时,一些新的布局机制真的大放异彩。Flexbox 是专门针对按钮的行或列或其他界面元素而创建的,它们之间的空间需要以一种巧妙的方式进行划分。这是表单的常见情况,所以让我们看一个使用 flexbox 的例子。

基于我们在前一个简单示例中看到的样式,让我们创建一个稍微复杂一点的表单,在这里我们收集一些关于求职者的信息。我们的目标是收集申请人的姓名、电子邮件地址和 Twitter 账号,以及申请人已经掌握的编码语言列表(见图 9-21 )。

A314884_3_En_9_Fig21_HTML.jpg

图 9-21。我们的申请表

对于较大的视口,表单的顶部从堆叠版本(标签堆叠在字段上)切换到标签与字段显示在同一行的版本。Twitter handle 字段前面还有一些指导文本,表明只需要填写“@”后面的部分。让我们从内联字段开始。

我们将使用 flexbox 来控制字段布局。为了检测它,我们将使用在第六章中看到的 Modernizr 库。简单回顾一下,Modernizr 可以通过 JavaScript 检测 CSS 特性,并将每个受支持特性的类名添加到页面的 html 元素中。您可以创建一个自定义脚本,只包含您在 https://modernizr.com需要的检测。在这种情况下,flexbox 检测将 flexbox 类添加到 html 元素中。

我们现在可以开始使用。flexbox 类作为选择器前缀,并且确信只有支持它的浏览器才能看到它。

首先,我们只想在视窗大到足以处理它时服务于内联布局。大约 560 像素看起来差不多,也就是 35em:

@media only screen and (min-width: 35em) {
  /* the rest of the code snippets go here */
}

接下来,我们的文本输入字段需要在较大的视口中变成一个 flex 容器,其中的项目水平排列(这是默认设置)。它们还需要具有更大的最大宽度。

.flexbox .field-text {
  display: flex;
  max-width: 28em;
}

我们希望标签都具有相同的宽度(大约 8em 似乎就可以了),既不收缩也不增长,也就是说,flex-grow 和 flex-shrink 设置为 0,flex-basis 为 8em:

.flexbox .field-text label {
  flex: 0 0 8em;
}

至于标签文本,我们希望它垂直居中。我们可以用行高来做这件事,但是我们会把它和输入元素的高度联系起来。Flexbox 实际上也可以帮助我们做到这一点,不需要具体的测量。

为了达到这种效果,我们需要将标签本身声明为内容居中的 flex 容器。因为 label 元素没有可以居中的子元素,所以我们依赖于这样一个事实,即 flex 容器中的任何文本内容都成为一个匿名的 flex 项。然后,我们可以告诉容器将其所有项目垂直居中。

.flexbox .field-text label {
  flex: 0 0 8em;
**display: flex;** 
**align-items: center;** 
}

这为我们提供了更大视窗的最终视场布局,如图 9-22 所示。

A314884_3_En_9_Fig22_HTML.jpg

图 9-22。用于较大视口的内嵌标签/字段放置

至于输入元素的宽度,flexbox 会自动算出来。它们已经设置为宽度:之前的 100%,由于默认的伸缩值为 0 1 auto,因此将会缩小以便为固定宽度的标签腾出空间。读出该值意味着“根据 width 属性(auto)确定宽度,不要超过该值,但可以随意缩小以腾出空间。”

带 Flexbox 前缀的输入字段

当谈到前置文本时,这是 flexbox 真正擅长的情况。我们有一些限制,使用任何其他布局技术都很难灵活解决:

  • 输入和前置文本组件需要具有相同的高度。

  • 前置元素的宽度需要保持灵活,这取决于里面的文本。

  • 然后需要调整输入的宽度,以便前置文本和输入字段的组合与其他文本字段的宽度相同。

为了定位这些组件,我们将把整个东西包装在一个 span 中,并应用一些通用的类名。我们还将添加相关属性,以使前置文本的目的易于理解。以下是该字段的完整标记:

<p class="field field-text">
  <label for="applicant-twitter">Twitter handle:</label>
**<span class="field-prefixed">** 
**<span class="field-prefix" id="applicant-twitter-prefix" aria-label="You can omit the @">@</span>** 
**<input aria-describedby="applicant-twitter-prefix" name="applicant-twitter" id="applicant-twitter" type="text" />** 
**</span>** 
</p>

aria-label 属性在这里为 prefix 元素提供了一个屏幕阅读器可访问的名称,解释了带前缀的文本的用途。

对于样式,我们将首先为不支持 flexbox 的浏览器创建一个后备样式。我们将保持简单,只提供一个包含前置文本的内嵌块框,并使它旁边的输入足够短,不会在小屏幕上出现在不同的行上(见图 9-23 )。

A314884_3_En_9_Fig23_HTML.jpg

图 9-23。我们的基线只是一个样式化的内联块框中的前置文本
.field-prefix {
  display: inline-block;
  /* border and color etc omitted for brevity. */
  border-radius: .25em;
}
.field-prefixed input {
  max-width: 12em;
}

我们还必须补充设置输入宽度的规则,方法是将我们预先添加的字段类名添加到选择器中:

.field-text label,
.field-text input,
**.field-prefixed,** 
.field-text textarea {
  /* ... */
}

最后,我们将应用 flexbox 的魔力,使用。flexbox 类名来限定我们的规则。我们会赶上的。字段前缀包装到 flex 容器中,并将前缀元素的内容垂直居中。就像前面内联字段示例中的标签一样,我们创建一个嵌套的 flex 容器,并在里面垂直对齐匿名项。我们还调整了边框,使其只在物品的外角变圆。

.flexbox .field-prefixed {
  display: flex;
}
.flexbox .field-prefix {
  border-right: 0;
  border-radius: .25em 0 0 .25em;
  display: flex;
  align-items: center;
}

输入需要重新应用它的最大宽度。除此之外,它会自动填满剩余的空间。

.flexbox .field-prefixed input {
  max-width: 100%;
  border-radius: 0 .25em .25em 0;
}

图 9-24 显示了结果。这些样式为将文本(和其他控件)添加到文本输入字段提供了一个灵活且可重用的基础,同时使它们与其他输入保持相同的宽度。它可以很容易地扩展为在字段后追加

A314884_3_En_9_Fig24_HTML.jpg

图 9-24。完成的前缀字段

对复选框集合使用多列布局

与内联字段放置可以节省垂直空间一样,我们可以通过将复选框集合放置到列中来节省空间。多列布局模块是一个很好的选择,在不支持的情况下,它会回到单列布局。

我们的标记非常简单:一个带有复选框类名的无序列表,在每个列表项中包含单个复选框及其相关标签。

<ul class="checkboxes">
  <li>
    <input type="checkbox" name="lang-as" id="lang-as">
    <label for="lang-as">ActionScript</label>
  </li>
  <!-- ...and so on-->
</ul>

我们可以像处理单选按钮一样将复选框嵌套在标签中,但是在下一个示例中,这里的顺序实际上将用于样式目的。

要让复选框按列排列,我们可以简单地告诉浏览器每列的最小宽度。考虑到较长的标签,10 个左右的 ems 似乎是合理的。除此之外,我们将删除列表样式,并调整边距和填充。图 9-25 显示了复选框列。

A314884_3_En_9_Fig25_HTML.jpg

图 9-25。中等大小的视窗中自动生成的四列中的复选框
.checkboxes {
  list-style: none;
  padding: 0;
  column-width: 10em;
}
.checkboxes li {
  margin-bottom: .5em;
}

设置不稳定的样式:仿自定义复选框

我们已经看到,按钮和文本输入可以通过移除默认样式(如边框)来驯服。但是按钮大多只是一个平面,里面有一些文本,其他表单组件更复杂。例如,复选框由小方框和其中可能的勾号组成。例如,对复选框应用填充是什么意思?它是在盒子的图形里面还是外面?我们对复选框应用的任何大小调整会影响复选标记吗?

避开这些问题,我们可以选择用图形完全替换复选框。这是通过巧妙使用 label 元素和表单状态的伪类实现的。

由于标记的顺序,结合兄弟选择器和:checked 伪类,我们可以根据复选框的状态为复选框和标签的外观生成规则。

我们还需要在浏览器支持方面划清界限。不理解选择器的旧浏览器,比如:checked 需要退回到非样式化的本地复选框。为了实现这一点,我们将重用:root 选择器技巧,它会导致 IE8 等旧浏览器跳过整个规则:

:root input[type="checkbox"] + label {
  /* unchecked checkbox label */
}
:root input[type="checkbox"]:checked + label {
  /* checked checkbox label */
}

接下来,我们需要使复选框本身不可见,但仍然可以访问和聚焦。我们将使用我们自己创建的复选框的图像作为标签的背景图像。图 9-26 说明了思路。

A314884_3_En_9_Fig26_HTML.gif

图 9-26。使用 CSS 隐藏复选框本身,label 元素有一个显示假复选框的背景图像

使用鼠标或触摸屏的人可以点击标签,这将触发复选框改变状态,从而更新样式。键盘用户仍然可以聚焦复选框并与之交互,状态同样会反映在标签的样式中。

这项技术有两个关键部分。首先,标签需要紧跟在标记中的 input 元素之后,并具有关联两者的适当的 for 属性。第二,标签需要隐藏但仍可访问。最后一个问题是为它提供与前面示例中隐藏的帮助消息相同的样式集合:

:root input[type="checkbox"] {
  position: absolute;
  overflow: hidden;
  width: 1px;
  height: 1px;
  clip: rect(0 0 0 0);
}

现在我们需要为复选框的各种状态的图像提供规则,包括键盘访问的焦点状态。我们总共需要四个图像:未选中的、选中的、带焦点的未选中的和带焦点的选中的。图 9-27 显示了最终结果。

A314884_3_En_9_Fig27_HTML.jpg

图 9-27。我们的复选框样式的结果:我们的复选框现在在所有现代浏览器中遵循页面的整体颜色主题

我们使用 Modernizr 来检测对 SVG 的支持,因此我们的规则中添加了一个 svgasimg 类:

注意

Modernizr 测试实际上检测了对 SVG 中的元素的支持,但是它与对背景图像的 SVG 支持重叠得很好,否则是检测不到的,还有用于 img 元素源的 SVG 文件(因此有了类名)。

:root.svgasimg input[type="checkbox"] + label {
  background: url(img/checkbox-unchecked.svg) .125em 50% no-repeat;
}
:root.svgasimg input[type="checkbox"]:checked + label {
  background-image: url(img/checkbox-checked.svg);
}
:root.svgasimg input[type="checkbox"]:focus + label {
  background-image: url(img/checkbox-unchecked-focus.svg);
}
:root.svgasimg input[type="checkbox"]:focus:checked + label {
  background-image: url(img/checkbox-checked-focus.svg);
}

在最后的示例文件中,我们还包含了一些标签填充和文本样式的规则。我们还对(微小的)SVG 文件进行了 URL 编码,并将它们作为数据 URIs 内嵌在 CSS 文件中,这有助于减少请求数量。

遗憾的是,有些浏览器支持我们用过的所有选择器,但不支持 SVG。为了解决这个问题,我们需要使用一个后备解决方案,如果 JavaScript 无法运行或者浏览器不支持 SVG 作为背景图像,我们就退回到 PNG 图像。我们在 CSS 中的 SVG 解决方案之前添加了基于 PNG 的解决方案:

:root input[type="checkbox"] + label {
  background-image: url(img/checkbox-unchecked.png);
}
:root input[type="checkbox"]:checked + label {
  background-image: url(img/checkbox-checked.png);
}
/* ...and so on. */

好处是我们现在在任何支持表单伪类的浏览器中都完全支持我们的自定义复选框。IE8(及更老版本)退回到普通的本地复选框。完全相同的技术也可以用于单选按钮,在样式方面,您可以随心所欲。

你也可以为复选框图形使用其他技术,包括动画显示选中和取消选中的动作,就像马诺埃拉·伊里奇(tympanus.net/Development/AnimatedCheckboxes/)的演示一样,如图 9-28 所示。

A314884_3_En_9_Fig28_HTML.jpg

图 9-28。一个使用动画 SVG 图形来“铅笔”单选按钮选项的演示

缺点是我们引入了一个对 JavaScript 的小依赖,以增强我们的 checkbox 组件的全部潜力,但这是一个非常小的依赖,并且有一个非常好的后备。

关于自定义表单小部件的一句话

到目前为止,我们已经看到,我们可以成功地用 CSS 样式化输入字段和按钮。我们还可以使用 CSS 和图像替换技术来设计复选框和单选按钮的样式。select 元素是一个稍微复杂一点的表单控件,由下拉菜单本身、箭头指示器和选项列表组成。还有像 input 元素的文件上传和颜色选择器版本这样的东西,它们有更复杂的小部件来表示。

传统上,这些类型的小部件实际上不可能进行样式化,导致大量 JavaScript 驱动的解决方案使用常规的 div 和 spans 来伪造文件选择器或 select 元素的外观。虽然这些解决方案解决了能够对小部件进行样式化的问题,但它们通常会带来更难解决的新挑战。

这些挑战包括在移动设备上不中断,使用与原生版本相同的键盘控制,以及跨不同设备和浏览器运行。例如,试图在一个选择元素中伪造选项,然后对其进行样式化是一件非常危险的事情,因为控件在移动设备上看起来完全不同(见图 9-29 )。

A314884_3_En_9_Fig29_HTML.jpg

图 9-29。iOS 上的选择元素根本不显示选择下方的选项,而是在屏幕底部触发一个旋转器类型的小部件

当决定一个设计时,你可能要再三考虑自定义这些类型的控件,以及让它们匹配页面的主题是否值得潜在的麻烦。

也就是说,也有很多开发人员努力尝试使用 JavaScript 以深思熟虑的方式解决问题,所以使用第三方库的选项是存在的。这些库大多依赖于 jQuery 这样的通用 DOM 操作库,因为页面中元素的创建和处理对于这类小部件来说很快就变得不简单了。

您可能想要签出以下任何一个库:

  • Filament Group 为 jQuery 发布了一个简单的选择菜单插件,它的工作方式类似于前面描述的复选框技术,但是增加了一些 JS 技巧。它提供了一种快速的方式来设计选择元素本身的样式,但不是选项列表:github.com/filamentgroup/select。Filament Group 也有一个小插件,使用类似的方法进行文件输入。

  • chosen(harvesthq.github.io/chosen/)和 Select 2(select2.github.io/)是两个比较流行的 jQuery 插件,用于 select 元素的高级增强。选项包括设置占位符和选项的样式、搜索或过滤,以及更好的多选 UI。这两个库在最近的版本中都在可访问性方面做了改进,但是你应该知道它们可能仍然有问题。

开发人员 Todd Parker 做出了巨大的努力,为最简单的选择元素下拉按钮的样式找到了这样一个纯粹的 CSS 解决方案。你可以在 http://filamentgroup.github.io/select-css/demo/看到他的解决方案。在撰写本文时,它更多的是一种概念验证,而不是一种已完成的技术,但是它设法在大多数浏览器中对 select 元素(没有选项)进行样式化,不使用 JavaScript,只使用一个包装器元素进行样式化。旧的浏览器被聪明的黑客过滤掉了,所以它们得到了无样式的默认选择。

对于表单,无论您使用自定义样式还是高级小部件,都要确保在现场使用时,它们能像本地元素一样工作。

摘要

在这一章中,我们已经学习了设计表单和表格的样式。它们是一些更复杂的 HTML 元素的集合,但是对于帮助用户与网页交互和理解复杂的数据来说通常是至关重要的。

我们已经了解了如何设计数据表的样式,以及一种简单的方法来使它们具有响应性。

我们创建了一个简单的表单,并学习了如何设计字段集、标签、文本输入和按钮的样式。我们还了解了如何使用现代 CSS 布局技术来更有效地利用表单中的空间,以及如何解决复选框和单选按钮等表单组件的样式问题。

在下一章中,我们将把交互性提升到另一个层次,并向您展示如何使用变换、过渡和动画使您的网页变得生动。

十、让它动起来:变换、过渡和动画

这一章是关于移动物体的——或者通过空间,变换,或者通过时间,使用动画和过渡。通常,这两个系列的属性一起工作。

变换与移动具有定位或其他布局属性的事物是不同的概念。事实上,变换一个对象根本不会影响页面的布局。您可以旋转,倾斜,平移和缩放元素,甚至在三维!

动画元素可以用 CSS 动画属性来完成。过渡是动画的一种简化形式。当你只有一个开关状态时(比如悬停在一个元素上),转换是用来自动完成这个过程的。

综上所述,这些属性为您的页面注入了活力。作为额外的奖励,他们也有非常好的表现。

在本章中,我们将讨论以下内容:

  • 二维变换:平移、缩放、旋转和倾斜

  • 简单和高级过渡效果

  • 你能做什么,不能做什么

  • 关键帧动画和动画属性

  • 三维转换和透视

这一切是如何结合在一起的

CSS 变换允许我们在空间中移动事物,而 CSS 过渡和 CSS 关键帧动画控制元素如何随时间变化。

即使这两个方面有些不相关,变换、过渡和关键帧动画通常在概念上被混为一谈。这是因为它们经常被用来相互补充。当制作动画时,你每秒钟要改变它的外观 60 次。转换允许您以浏览器可以非常有效地计算的方式描述外观的某些类型的变化。

转场和关键帧动画允许您以一种智能的方式制作这些变化的动画。因此,这些功能是相辅相成的。最终结果让我们有能力做一些事情,就像谷歌创造的动画 3D 弹出书(见图 10-1 )展示其产品的创造性使用(【http://creativeguidebook.appspot.com/】??)。

A314884_3_En_10_Fig1_HTML.jpg

图 10-1。谷歌制作了一个动画 3D 立体书来展示其产品的创造性使用

因为这一章中的例子确实有很多移动的部分,所以很难在一本书的页面上描述它们。我们强烈建议您在阅读时在浏览器中尝试这些示例,以了解发生了什么。很多时候,JavaScript 用于交互性——我们不会深入研究脚本如何工作的细节,但是示例也包括 JS 文件供您探索。

关于浏览器支持的说明

变换、过渡和关键帧动画的规范仍在制定中。尽管如此,大多数这些功能在常用的浏览器上都得到了很好的支持,明显的例外是 Internet Explorer 8 和 Opera Mini。IE9 仅支持使用-ms 前缀的 2D 变换子集,不支持关键帧动画或过渡。变换、过渡和关键帧动画都需要-webkit-前缀才能在各种版本的基于 webkit 和 Blink 的浏览器中工作。只有当您需要覆盖旧版本的 Firefox 时,才需要-moz-前缀。

2D 变换

CSS 转换允许您通过平移、旋转、倾斜或缩放来改变页面上元素的呈现方式。此外,您可以在组合中添加第三个维度!在这一节,我们将从 2D 变换开始,稍后再看 3D。图 10-2 给出了 2D 变换的概述。

A314884_3_En_10_Fig2_HTML.gif

图 10-2。不同类型的 2D 变换举例说明

用技术术语来说,当元素出现在页面上时,转换会改变元素的坐标系。看待变换的一种方式是将它们视为“扭曲场”任何属于已转换元素的渲染的像素都被捕捉到失真场中,并被传送到页面上的新位置或新大小。元素仍然保持在页面上原来的位置,但是元素的结果图像被转换。

假设页面上有一个 100×100 像素的元素,类名为 box。元素的位置会受到页边距、位置、页面流中其他元素的大小等因素的影响。不管它是如何结束的,我们都可以通过使用视口内的坐标来描述框的位置,例如,距离页面顶部 200 像素,距离页面左侧 200 像素。这是视口坐标系

在页面中,为元素保留了一个 100×100 像素的空间,就像它正常呈现的那样。现在,让我们设想将元素旋转 45 度进行变换:

.box {
**transform: rotate(45deg);** 
}

对一个元素应用变换会为该元素最初放置的空间创建一个所谓的局部坐标系。局部坐标系是变形场,它取代了元素的像素。

因为元素在页面上被表示为矩形,所以可能最容易想到的是在盒子的四个角上的四个点会发生什么。Firefox 开发人员工具在检查元素时有一个很好的可视化效果。在检查器的“规则”面板中,将鼠标悬停在变换规则上以查看结果变换(参见图 10-3 )。

A314884_3_En_10_Fig3_HTML.jpg

图 10-3。在 Firefox 开发工具中可视化 45 度转变。原始框和变换后的框与箭头一起显示,箭头显示角的改变位置。

页面仍然保留了它的 100×100 像素的间隙,该间隙曾经是框的位置,但是属于框的任何点现在都被扭曲场所转换。

除了影响元素在页面上的位置的其他属性之外,对元素应用转换时,理解这一技术背景非常重要。当我们对转换后的 div 应用 margin-top: 20px 时会发生什么?指向上方的角现在是否在距离原始位置顶部 20 个像素处结束?否:旋转属于盒子的任何东西的整个局部坐标系,包括边距,如图 10-4 所示。

A314884_3_En_10_Fig4_HTML.jpg

图 10-4。旋转一个框包括旋转它的整个坐标系,因此上边距也会旋转

同样重要的是要注意,旋转后的外观不会干扰页面其余部分的布局,就像没有转换时一样。如果我们将盒子旋转整整 90 度,使上边距在视觉上向右突出,它不会推动可能位于盒子右侧的任何元素。

注意

页面上实际上有一样东西会受到变换元素的影响,那就是溢出。如果转换后的元素最终在任何具有导致滚动条的溢出属性的框的文本方向上突出,则转换后的元素可能会影响可滚动区。在从左到右的语言中,这意味着你可以使用向上或向左的平移来隐藏屏幕外的内容,但不能向下或向右。

变换原点

默认情况下,任何变换都基于元素边框的中心进行计算。负责控制的属性是 transform-origin。例如,我们可以围绕一个点旋转盒子,这个点距离盒子顶部 10 个像素,距离盒子左侧 10 个像素。

我们可以为 transform-origin 提供一到三个值,给出 x、y 和 z 轴的坐标。(z 轴用于 3D 变换;我们将在本章的后面回到这一点。)如果你只提供一个值,第二个被假定为关键字中心,就像你设置背景位置一样。第三个值不影响二维转换,所以我们现在可以放心地忽略它。

.box {
**transform-origin: 10px 10px;** 
  transform: rotate(45deg);
}

这给了我们旋转盒子时完全不同的结果,如图 10-5 所示。

A314884_3_En_10_Fig5_HTML.jpg

图 10-5。围绕距离上边缘和左边缘 10 个像素的点旋转盒子
注意

如果将转换应用于 SVG 元素,那么转换的工作方式会有所不同。一个例子是 transform-origin 属性的默认值:它默认为左上角,而不是元素的中心。

翻译

翻译一个元素仅仅意味着把它移动到一个新的位置。您可以选择使用 translateX()和 translateY()函数沿单个轴平移,或者使用 translate()同时设置两者。

translate()函数的工作方式是向它提供一对表示 x 和 y 轴上平移量的位置。此数量可以是任何长度,如像素、ems 或百分比。值得注意的是,上下文中的百分比指的是元素本身的尺寸,不是包含的块。这允许你在不知道元素有多宽的情况下,将元素平移到其原始位置的右侧(见图 10-6 )。

A314884_3_En_10_Fig6_HTML.jpg

图 10-6。我们的盒子 100%向右平移
.box {
  /* equivalent to transform: translateX(100%); */
  transform: translate(100%, 0);
}

多重转换

一次应用多个变换是可能的。转换以空格分隔值列表的形式提供给 transform 属性,并按声明顺序应用。让我们看一个例子,我们既做平移又做旋转。

在这个例子中,我们将使用一个有序的规则列表来表示“搏击俱乐部”我们将对编号规则进行一些格式化,并将它们旋转到列表中每一项的下方(见图 10-7 )。我们希望列表编号从下到上读取,但定位在列表项的顶部。

A314884_3_En_10_Fig7_HTML.jpg

图 10-7。规则列表,带有向下旋转的编号项目

首先,我们的标记中需要一个有序列表。我们将从第三条规则开始,因为前两条规则神秘地丢失了:

<ol class="rules" start="3">
  <li>If someone says "stop", goes limp or taps out, the fight is over.</li>
  <li>Only two guys to a fight.</li>
  <li>One fight at a time.</li>
  <li>No shirts, no shoes.</li>
  <li>Fights will go on as long as they have to.</li>
  <li>If this is your first night at FIGHT CLUB, you HAVE to fight.</li>
</ol>

默认情况下,我们无法真正影响有序列表中数字的呈现。CSS 列表和计数器模块 3 级规范描述了一个::marker 伪元素来控制列表标记样式,但是在编写本文时还没有浏览器支持它。我们将发挥创造性,使用 CSS 中支持良好的计数器属性结合伪元素来解决这个问题。计数器允许您通过计算某些元素来生成数字,然后您可以将这些数字插入到页面中。

首先,我们删除了默认的列表样式(删除了数字),并添加了一个计数器重置规则。它告诉浏览器这个元素重置了一个名为 rulecount 的计数器的编号。这个名称是我们自己选择的任意标识符。名字后面的数字告诉计数器它应该有哪个初始值。

.rules {
  list-style: none;
  counter-reset: rulecount 2;
}

接下来,我们将告诉计数器在每次遇到列表中的列表项时增加 rulecount 值。这意味着第一个项目将被编号为 3 ,依此类推。

.rules li {
  counter-increment: rulecount;
}

最后,我们使用 content 属性将 rulecount 计数器中的数字作为伪元素注入到每个列表项的文本之前。我们将在数字前插入一个分段符号()。这给了我们在图 10-8 中看到的渲染内容。

A314884_3_En_10_Fig8_HTML.jpg

图 10-8。带有注入部分符号和编号的列表
.rules li:before {
  content: '§ ' counter(rulecount);
}

它看起来还不是很好,但是我们现在有一些东西可以抓取和设计,而不是默认的数字。接下来,我们将尝试沿嵌线的边垂直放置节编号,而不是与文本内联。

我们不希望列表数字在页面中占据任何空间,所以我们需要绝对地定位它们;这样做会自动将章节号放在项目的左上角。为了实现数字被旋转但固定在列表项顶部的效果,我们需要考虑如何平移和旋转它。我们将变换原点设置在右下方(100% 100%),将其向左平移 100%,向上平移 100%(记住百分比指的是变换元素的尺寸),然后逆时针旋转 90 度。图 10-9 显示了一步一步的转变。

A314884_3_En_10_Fig9_HTML.gif

图 10-9。转换计数器的步骤,使其在列表项文本旁边从上到下运行
.rules li {
  counter-increment: rulecount;
**position: relative;** 
}
.rules li:before {
  content: '§ ' counter(rulecount);
**position: absolute;** 
**transform-origin: 100% 100%;** 
**transform: translate(-100%, -100%) rotate(-90deg);** 
}

转换函数的顺序在这里非常重要。如果我们从旋转伪元素开始,平移会相对于旋转的坐标发生,x 和 y 轴上的偏移会指向错误的方向 90 度!转换列表越来越多,因此您必须提前计划它们。

更改转换列表

当您声明一个转换列表时,您不能在事实之后添加它,而只能替换整个列表。例如,如果您有一个带平移的变换元素,并且您还想旋转它:hover,下面的将不会像预期的那样工作:

   .thing {
     transform: translate(0, 100px);
   }
   .thing:hover {
     /* CAUTION: this will remove the translation! */
     transform: rotate(45deg);
   }

相反,您必须重新声明整个列表,但是要附加循环:

   .thing:hover {
     /* preserves the initial translation, and then rotates. */
     transform: translate(0, 100px) rotate(45deg);
   }

在完成的示例中,我们已经向列表项添加了一个灰色的左边界,区域编号位于它的顶部。在这里使用边框的好处是,如果规则超过几行,它会自动延伸到列表项的高度。我们还添加了一些排版规则,并稍微调整了一下填充。例如,生成的部分编号在顶部有一点填充。但是请记住,这实际上意味着填充-右,因为它现在是旋转的!(参见图 10-10 。)

A314884_3_En_10_Fig10_HTML.jpg

图 10-10。使用边框可以让我们在换行的情况下绘制侧边栏背景。我们添加到节号的右填充现在位于旋转后的元素的顶部。

缩放和倾斜

到目前为止,我们已经研究了 translate()和 rotate()转换函数。在剩下的 2D 变换中,只剩下缩放()和倾斜()。这两者都具有在单个轴上缩放和倾斜的相应功能。就像 translate 一样,我们有 scaleX()、scaleY()、skewX()和 skewY()

使用 scale()函数非常简单。scale()函数中使用无单位数字。它接受一个或两个数字。如果只使用一个元素,则该元素在 x 轴和 y 轴上的缩放比例相等。例如,两个轴的比例都为 2,意味着元素的宽度和高度都是原来的两倍。比例测量值为 1 表示元素未改变。

.doubled {
  transform: scale(2);
  /* ...is equivalent to transform: scale(2, 2);
  /* ...and also equivalent to transform: scaleX(2) scaleY(2); */
}

仅缩放一个轴意味着元素挤压(见图 10-11 )或伸展。

A314884_3_En_10_Fig11_HTML.jpg

图 10-11。缩放到 x 轴上小于 1 的尺寸的文本会被挤压
.squashed-text {
  transform: scaleX(0.5);
  /* ...equivalent to transform: scale(0.5, 1);
}

倾斜意味着元件的水平或垂直平行边缘相对于彼此移动一定的度数。很容易把 x 轴和 y 轴搞混——在 x 轴上歪斜意味着水平线还是水平的,而垂直线是倾斜的。关键是考虑你希望边相对于彼此移动到哪个轴。

回到搏击俱乐部的例子,我们可以使用倾斜来创建一个流行的“2.5D”效果,也许是受复古视频游戏的启发(它的花哨名称似乎是轴测投影)。

如果我们给列表项交替的背景和边框颜色以及交替的倾斜变换,我们会得到一个“手风琴”式表面的外观(见图 10-12 ):

A314884_3_En_10_Fig12_HTML.jpg

图 10-12。使用倾斜变换创建“2.5D”外观
/* some properties omitted for brevity */
.rules li {
  transform: skewX(15deg);
}
.rules li:nth-child(even) {
  transform: skewX(-15deg);
}

2D 矩阵变换

正如我们在本节开始时所讨论的,变换导致变换元素表面上的每个点都经过一次计算,该计算确定了它在局部坐标系中的最终位置。

当我们写 CSS 的时候,我们会用“围绕它的中心旋转这个元素,并向左上方移动它”这样的术语来思考。对于浏览器来说,我们应用的所有这些转换都被融合到一个数学结构中,称为转换矩阵。您可以使用底层的 matrix()函数,使用六个不同数值的组合来直接操作它的值。

现在,不要担心:这不是你通常会用手做的事情,因为任何超出单个缩放或平移操作的事情都需要相当的数学技能。

下面是 matrix()函数的一个应用程序,它等于将一个元素旋转 45 度,然后将其放大两倍,然后在旋转后的 x 轴上平移 100 个像素,最后在 x 轴上倾斜 10 度。乍看之下,得到的数字(为了节省空间,已经进行了一定程度的四舍五入)与单个变换的原始值几乎没有相似之处。

.box {
  width: 100px;
  height: 100px;
  transform: matrix(1.41, 1.41, -1.16, 1.66, 70.7, 70.7);
  /* equivalent to:
     transform: rotate(45deg) translate(100px, 0) scale(2) skewX(10deg); */
}

不太容易解释,是吗?

对于所有的意图和目的,转换矩阵是一个“黑盒”,需要输入各种数字来表示最终的转换,这可能是几个步骤的组合。我们可以预先计算这些值(如果我们知道数学的话),然后将它们输入 matrix()函数,但是我们无法查看 matrix()函数的值并知道其中包含了哪些单独的转换。

关键点在于,单个矩阵可以简洁地表示任意数量的变换的组合。matrix()函数的主要用例不是为了节省空间和炫耀数学技能——但是当与 JavaScript 结合使用时,它真的会大放异彩。事实上,当您在一个元素上设置一个转换并在 JavaScript 文件中请求转换的计算样式时,您会得到一个矩阵表示。

由于矩阵可以由脚本非常有效地操纵,然后插回到 matrix()函数中,许多基于 JavaScript 的动画库大量使用它。如果你正在手工编写 CSS,那就简单多了(可读性也更好!)坚持使用正常的转换函数。

如果你想了解更多关于如何操作 CSS 变换矩阵背后的数学知识,你可以在www . useragentman . com/blog/2011/01/07/css3-matrix-transform-for-the-the-mathematical-challenged/找到 Zoltan·霍利卢克的精彩介绍。

转换和性能

当浏览器计算 CSS 如何影响页面上的元素时,有些东西在性能方面比其他东西更昂贵。例如,如果更改文本大小,生成的行框可能会随着文本换行而变得不同,然后元素可能会变高。变高会推低页面上的其他元素,这反过来又会迫使浏览器做进一步的重新计算。

当您使用 CSS 变换时,这些计算只影响应用它的元素的坐标系,既不改变元素内部的布局,也不改变元素外部的布局。此外,这种计算几乎可以独立于页面中的所有其他事情(如运行脚本或布置其他元素)来完成,因为转换不太可能干扰这些。大多数浏览器也试图让图形处理器来处理这些事情(如果有的话),因为它是专门为这种数学而构建的。

这意味着从性能的角度来看,转换是非常好的。任何时候,当你想创造一个可以通过变换复制的效果时,它很有可能会有更好的表现。快速连续地进行多次变换会增加收益,例如在制作元素动画或变换元素时。

一些最终转换“抓住了”

转换具有很好的性能,并且非常容易使用。也就是说,使用转换有一些意想不到的副作用:

  • 有些浏览器会为转换后的元素切换抗锯齿方法。这意味着当动态应用转换时,像文本呈现这样的事情看起来会突然不同。为了解决这个问题,您可以尝试应用一个仅使用初始值的转换,在页面加载时将元素保留在原位。这样,渲染甚至会在应用最终变换之前切换。

  • 应用于元素的任何变换都会创建新的堆叠上下文。这意味着在组合 z-index 和 transform 时需要小心,因为转换后的元素会创建自己的堆栈——即使在转换后的元素内将子元素的 z-index 设置得很高,它们也不会位于元素外的元素之上。

  • 变换后的元素还为固定定位建立了新的包含块。如果已转换的元素中包含 position: fixed 元素,它会将已转换的元素视为其视口。

过渡

转换是从一种状态到另一种状态的自动动画,例如当按钮从正常状态变为按下状态时。通常情况下,这种变化会立即发生,或者至少以浏览器最快的速度发生。当你点击一个按钮时,浏览器会计算出页面的新外观,并在几毫秒内绘制出来。当你应用一个过渡时,你告诉浏览器这个变化需要多长时间,浏览器会计算在这段时间内屏幕应该是什么样子。

过渡会自动在两个方向上运行,因此一旦状态反转(就像释放按钮时),动画就会反向运行。

让我们从表单章节(第九章)中的按钮来说明这一点,并为它创建一个平滑的按下状态动画。我们的目标是通过将按钮在页面上向下移动几个像素来创建按钮被按下的外观,并减少阴影的偏移来进一步增强按钮消失在页面后面的错觉(见图 10-13 ):

A314884_3_En_10_Fig13_HTML.jpg

图 10-13。正常和:按钮的活动状态
<button>Press me!</button>

以下是第九章中按钮代码的基础(为简洁起见,省略了一些属性)。这一次,我们向规则添加了转换属性。

button {
  border: 0;
  padding: .5em 1em;
  color: #fff;
  border-radius: .25em;
  background-color: #173b6d;
  box-shadow: 0 .25em 0 rgba(23, 59, 109, 0.3), inset 0 1px 0 rgba(0, 0, 0, 0.3);
**transition: all 150ms;** 
}
button:active {
**box-shadow: 0 0 0 rgba(23, 59, 109, 0.3), inset 0 1px 0 rgba(0, 0, 0, 0.3);** 
t**ransform: translateY(.25em);** 
}

当按钮被激活时,我们将其向下平移与 y 轴阴影偏移相同的距离。同时,我们减少了阴影偏移。通过使用 transform 属性来移动按钮,我们避免了强制页面回流。

前面的代码还告诉按钮使用一个转换来更改所有受影响的属性,并且更改应该在 150 毫秒或 0.15 秒内发生。使用动画向我们介绍了新的与时间相关的单位:ms 代表毫秒,s 代表秒。用户界面组件中的大多数过渡应该低于 0.3 秒,否则会感觉迟钝。其他视觉效果可能需要更长时间。

transition 属性是一种允许我们一次设置几个属性的简写方式。设置转换的持续时间并告诉浏览器转换所有在状态之间改变的属性也可以通过以下方式完成:

button {
  transition-property: all;
  transition-duration: .15s;
}

如果我们只想专门转换 transformand 和 box-shadow 属性,而其他更改(例如,不同的背景颜色)应该立即发生,我们将不得不指定单个属性,而不是所有属性。

我们不能为单个转换指定一个以上的属性名,但是我们可以指定几个转换,用逗号分隔。这意味着我们可以重复相同的值,但是对于不同的属性关键字:

button {
  transition: box-shadow .15s, transform .15s;
}

请注意,我们现在必须在两个转场中重复持续时间。这种重复是以后事情不同步的原因。不要重复自己(简称 DRY)是写好代码的一个基本准则。当过渡更复杂时,最好单独设置 transition-property 以避免重复:

button {
  /* First, specify a list of properties using transition-property */
  transition-property: transform, box-shadow;
  /* Then, set values that go for those properties. */
  transition-duration: .15s;
}

当您在转换声明中使用多个逗号分隔的值时,它们的工作方式类似于多个背景属性。transition-property 中的列表决定了要应用的过渡数量,如果其他列表较短,它们会重复。

在前面的示例中,过渡持续时间只有一个值,但定义了两个过渡属性,因此持续时间值适用于这两个属性。

注意

当转换带前缀的属性时,必须将带前缀的属性名作为转换属性。例如,transition: transform .25s 通常需要用-webkit-transition:-WebKit-transform. 25s 来补充,在基于 WebKit 的旧浏览器中,transition 和 transform 都有前缀。

过渡时序功能

默认情况下,转场的变化速率在帧与帧之间并不完全相同,它开始时稍慢,然后快速加速,最后逐渐减速,直到达到最终值。

这种移动速度在动画术语中被称为放松,它通常会让变化看起来更自然、更平稳。有一些数学函数负责创建这种变量变化,并使用转换时间函数属性来控制它们。

有一些关键字代表不同样式的计时功能。前面描述的缺省值称为 ease。其他的是线性、渐进、渐出和渐出。

“放松”意味着开始时缓慢,然后加速。“放松”的意思正好相反:开始时快速,然后在结束时慢下来。最后,双管齐下会让我们在开始和结束时缓慢变化,但在中间会加速变化。

将结果可视化在书页上很难,但是图 10-14 中的插图应该会给你一个想法。它代表一个矩形,我们在一秒钟内将背景颜色从黑色变为白色。

A314884_3_En_10_Fig14_HTML.gif

图 10-14。在 1 秒钟的动画中,采样停止点之间相隔 100 毫秒

如果我们想改变按钮动画来使用渐强计时功能,我们可以这样做:

button {
  transition: all .25 ease-in;
  /* ...or we could set transition-timing-function: ease-in; */
}

三次贝塞尔函数和“弹性”过渡

在幕后,处理变化率的数学函数建立在一种叫做三次贝塞尔函数的东西上。每个关键字都是使用这些带有特定参数的函数的快捷方式。通常情况下,使用这些函数的时间变化被视为一条曲线,从初始时间和初始值(左下角)到持续时间结束时的最终值(右上角),如图 10-15 所示。

A314884_3_En_10_Fig15_HTML.gif

图 10-15。显示渐出转场定时功能的曲线

一个三次贝塞尔函数需要四个参数来计算随时间的变化,我们可以使用三次贝塞尔()函数作为 CSS 转换中的缓动值。这意味着您可以通过计算和填充这四个值来创建自己的计时函数。这四个值代表形成该曲线的两个控制点的两对 x 和 y 坐标。

就像矩阵变换一样,这不是你通常手工做的事情,因为它需要高级数学技能。幸运的是,还有其他人使用这些技能为我们其他人创造工具!Lea Verou 专门为 CSS 编写了一个这样的工具,可在cubic-bezier.com获得(见图 10-16 )。

A314884_3_En_10_Fig16_HTML.jpg

图 10-16。在cubic-bezier.com,你可以玩不同赛季的预设值,并创建自己的预设值

使用自定义计时功能的一个更有趣的结果是,您可以在转换时将值更改到起始值和结束值之外(如图 10-16 所示)。例如,在实践中,这意味着当你移动某物时,在最终停止之前超过你的目标。这使您有可能创建有弹性的过渡,其中元素看起来有弹性或咬合到位。试着在 http://cubic-bezier.com上玩这个例子看看效果!

阶跃函数

除了使用预设关键字和立方贝塞尔()函数指定缓动之外,您还可以创建逐步发生的过渡。这对于创建定格动画非常有用。假设有一个元素,它的背景图像由七个不同的图像组成,都在同一个文件中。定位图像,以便仅显示其中一个(参见图 10-17 )。

A314884_3_En_10_Fig17_HTML.jpg

图 10-17。使用背景位置的七帧定格动画

当我们将鼠标悬停在该元素上时,我们希望通过移动 background-position 属性来动画显示背景图像。如果我们使用线性或缓和的过渡来做这件事,背景图像将会滑过,破坏这种错觉。相反,我们需要它以六个不连续的步骤进行过渡:

.hello-box {
  width: 200px;
  height: 200px;
  transition: background-position 1s **steps(6, start)**;
  background: url(steps-animation.png) no-repeat 0 -1200px;
}
.hello-box:hover {
  background-position: 0 0;
}

transition-timing-function 现在设置为 steps(6,start),意思是“将转换持续时间分成六个步骤,并在每个新步骤开始时更改属性。”总而言之,我们得到七个不同的帧,包括起始状态。

默认情况下,steps(6)会在每一步的 end 处更改属性,但是如果您将 start 或 end 作为第二个参数传入,您可以明确地说明这一点。因为我们想在用户悬停在元素上时直接看到变化,所以我们选择在每一步开始时开始转换。

现在,转换的 steps()函数有一个问题。当您在过渡完成之前反转状态时(例如,通过快速移开鼠标指针),过渡将向后播放。这是意料之中的,但出乎意料的是,反向转换仍然有六个步骤。这些步骤现在不再映射到仔细考虑过的背景位置,使我们的动画看起来很糟糕。

可悲的是,这在当前版本的规范中是未定义的行为,所有浏览器似乎都以这种可以说是糟糕的方式对待 step 函数。为了应对这种糟糕的体验,我们可以使用一些有用的过渡技巧,这将在下一节中介绍。

正向和反向的不同转换

有时,我们希望某些东西在一个方向上快速过渡,而在另一个方向上缓慢过渡,反之亦然。在前面的步进示例中,当悬停状态在转换完成之前中止时,我们不能优雅地后退。我们可以通过立即恢复过渡来解决这个问题。

为此,我们需要定义不同的转换属性集:一个用于未悬停状态,一个用于悬停状态。诀窍是把正确的放在正确的地方。

我们给初始转换一个 0 的持续时间,然后设置“真正的”转换在元素悬停时发生。现在悬停状态触发动画,当悬停被取消时,图像会迅速恢复。

.hello {
  transition: background-position **0s** steps(6);
}
.hello:hover {
  transition-duration: **.6s**;
}

“粘性”过渡

转换的另一个技巧是让转换完全不反转,这与我们之前的例子相反。为了进行“粘性”转换,我们可以使用一个长得离谱的转换持续时间。从技术上来说,当我们取消悬停时,它仍然会向后运行,但非常非常慢——你必须让浏览器标签打开多年才能看到任何变化!

.hello {
  transition: background-position **9999999999s** steps(6);
}
.hello:hover {
  transition-duration: **0.6s**;
}

延迟转换

通常,状态一改变,元素就开始转换——例如,当一个类名被 JavaScript 改变或者一个按钮被按下时。我们可以选择使用 transition-delay 属性来延迟这个转换。例如,我们可能只想在用户将指针悬停在停止动画上超过一秒钟时运行停止动画。

就值的顺序而言,转换的简写相当宽松,但是延迟必须是第二次出现的时间值,第一次出现的时间值将被解释为持续时间。

.hello {
  transition: background-position 0s **1s** steps(6);
  /* equivalent to adding transition-delay: 1s; */
}

你也可以使用负延迟。遗憾的是,这并不能实现时间旅行,但它可以让你从一开始就直接跳到过渡的中途。如果在 10 秒的过渡上设置过渡延迟:-5s,则当触发过渡时,它将立即跳到中间标记。

你能做什么,不能做什么

到目前为止,我们已经转换了变换、框阴影值和背景位置。但是并不是每个 CSS 属性都可以被动画化,也不是每个值都可以。大多数情况下,使用长度或颜色的就可以了:边框、宽度、高度、背景颜色、字体大小等等。关键是你能否计算出它们之间的一个值。您可以在 100px200px 之间以及红色蓝色之间找到中间值(因为颜色也是幕后的数值),但不能在例如显示属性的 block 和 none 之间找到中间值。这条规则也有一些例外。

插值

尽管没有明确的中间值,但有些属性是可以动画化的。例如,当使用 z-index 时,值不能是 1.5,但 1 或 999 就可以了。对于许多属性,比如只接受整数值的 z-index 或 column-count,浏览器会将它们插值成整数,有点像前面的 steps()函数。

一些可以插值的值有点令人惊讶。例如,您可以转换 visibility 属性的值,但是一旦转换通过两个结束状态之间的中点,浏览器就会将该值“捕捉”到两个结束状态中的任一个。

设计师奥利·斯图德霍尔姆有一个很方便的属性列表,既有 CSS 规范中的属性,也有 SVG 中的属性,可以通过 CSS 制作动画:oli.jp/2010/css-animatable-properties/

过渡到内容高度

转换的最后一个缺陷是,可以转换的属性,比如高度,只能在数值之间转换。这意味着其他关键字,如 auto,不能表示为要转换到的状态之一。

一种常见的模式是拥有一个折叠的元素,当用户与它交互时,您可以将它转换为完整的高度,就像手风琴组件一样。浏览器不知道如何在像 0 这样的长度和关键字 auto 之间转换,甚至不知道如何在像 max-content 这样的固有测量关键字之间转换。

在图 10-18 中,我们有一个餐馆菜单组件,最初显示了前三个菜单选项。当我们切换列表的其余部分时,它应该向下滑动并淡入。

A314884_3_En_10_Fig18_HTML.jpg

图 10-18。扩展菜单列表组件

在这种情况下,我们知道列表的大致高度,因为它总共有 10 个条目——它仍然会有一些变化,因为可能会有带换行符的长名称。我们现在可以用最大高度来代替它。使用这种技术,我们从最初设置的尺寸到一定比元素扩展高度高的长度。在这种情况下,我们决定将其余的“顶部菜单选项”限制为另外七个项目。

组件的标记基于两个有序列表,其中第二个列表从数字 4 开始:

<div class="expando">
  <h2 class="expando-title">Top menu choices</h2>
  <ol>
    <li>Capricciosa</li>
    <li>Margherita</li>
    <li>Vesuvio</li>
  </ol>
  <ol class="expando-list" start="4" aria-label="Top menu choices, continued.">
    <li>Calzone</li>
    <!-- …and so on… -->
    <li>Fungi</li>
  </ol>
</div>

该标记在第二个列表中包含 aria-label 属性,以便让屏幕阅读器的用户清楚这两个列表的用途。

为了切换状态,我们使用一小段 JavaScript 来设置场景。在运行的示例中,您可以找到这个脚本,它为我们创建了一个按钮,将它附加到标题,并在单击按钮时切换容器元素上 is-expanded 的类名。

它还向 html 元素添加了一个类名 js。然后,我们可以将我们的样式基于这些类名的存在,因此如果 JavaScript 不运行,用户将从一开始就看到完整的扩展列表。

.js .expando-list {
  overflow: hidden;
  transition: all .25s ease-in-out;
**max-height: 0;** 
**opacity: 0;** 
}
.js .is-expanded .expando-list {
**max-height: 24em;** 
**opacity: 1;** 
}

扩展的 max-height 被设置为一个比实际列表的预期最大高度大很多的值。这是为了有一个安全余量:例如,如果在小屏幕上的菜单项中有几个意外的换行符,我们不希望列表被最大高度截断。

小缺点是 max-height 转换仍然会运行,就好像元素正好是 24 ems 高一样,这使得缓和点和停止点超出了列表的整个高度。如果您使用该示例,这在塌陷动画中作为一个小延迟是最明显的。在一个更健壮的例子中,脚本最初可以过渡到一个非常高的 max-height,然后在过渡之后测量元素,以基于内容动态更新 max-height。

CSS 关键帧动画

CSS 转场是隐式动画。我们为浏览器提供了两种不同的状态,当一个元素从一种状态转换到另一种状态时,包含在转换中的任何属性都将被激活。有时,我们需要做的不仅仅是在两种状态之间动画化,或者明确地动画化某些可能一开始就不存在的属性。

CSS 动画规范允许我们使用关键帧的概念来定义这些种类的动画。此外,它们允许我们控制动画运行的其他几个方面。

让生活的幻觉栩栩如生

使用动画的好处之一是通过展示而不是讲述来传达信息。我们可以用它来引导注意力(就像移动的箭头告诉你“看这里!这很重要!”),解释刚刚发生的事情(例如,当使用淡入动画来显示添加了列表项时),或者只是为了使我们的网页看起来更生动一些,以建立情感联系。

华特·迪士尼工作室教授通过动画表达性格和个性的 12 条原则。这些后来被收集在一本名为《生活的幻觉》的书中。动画师文森佐·洛迪贾尼用一个小立方体作为主角,创作了一部动画短片来说明这些原则(【https://vimeo.com/93206523】)。去看看吧!

受此启发,我们将创建一个动画方形标志,展示一些关键帧动画可以做什么。徽标的静态渲染由单词“Boxmodel”旁边的一个正方形组成(见图 10-19 ),这将是我们虚构的公司名称。

A314884_3_En_10_Fig19_HTML.jpg

图 10-19。静态徽标

标记非常简单:一个 heading 元素和一些额外的 span 元素来包装单词,两个嵌套的 span 元素来表示小方块。使用额外的空元素来表示并不理想,但是为了达到我们想要的效果,这是必要的,原因将变得很清楚。

<h1 class="logo">
  <!-- This is the box we are animating -->
  <span class="box-outer"><span class="box-inner"></span></span>
  <span class="logo-box">Box</span><span class="logo-model">model</span>
</h1>

对于基本的样式,我们给页面一个背景颜色,给 logo 一些字体属性,并设置正方形的尺寸和颜色。我们通过将表示正方形的两个 span 元素的显示模式设置为 inline-block 来准备动画,因为不可能转换内联文本元素。

body {
  background-color: #663399;
  margin: 2em;
}
.logo {
  color: #fff;
  font-family: Helvetica Neue, Arial, sans-serif;
  font-size: 2em;
  margin: 1em 0;
}
.box-outer {
  display: inline-block;
}
.box-inner {
  display: inline-block;
  width: .74em;
  height: .74em;
  background-color: #fff;
}

创建动画关键帧块

接下来,我们需要创建实际的动画。我们想模仿“生命的幻觉”电影的开场序列,小方块挣扎着滚过屏幕。

CSS 动画在语法和结构上有点奇怪。使用@keyframes 规则定义和命名动画序列,然后使用 animation-*属性将该序列连接到 CSS 中的一个或多个规则集。

下面是第一个关键帧块的外观:

@keyframes roll {
  from {
    transform: translateX(-100%);
    animation-timing-function: ease-in-out;
  }
  20% {
    transform: translateX(-100%) skewX(15deg);
  }
  28% {
    transform: translateX(-100%) skewX(0deg);
    animation-timing-function: ease-out;
  }
  45% {
    transform: translateX(-100%) skewX(-5deg) rotate(20deg) scaleY(1.1);
    animation-timing-function: ease-in-out;
  }
  50% {
    transform: translateX(-100%) rotate(45deg) scaleY(1.1);
    animation-timing-function: ease-in;
  }
  60% {
    transform: translateX(-100%) rotate(90deg);
  }
  65% {
    transform: translateX(-100%) rotate(90deg) skewY(10deg);
  }
  70% {
    transform: translateX(-100%) rotate(90deg) skewY(0deg);
  }
  to {
    transform: translateX(-100%) rotate(90deg);
  }
}

它确实很长,但是有很多重复的地方。首先,我们将关键帧序列命名为 roll——这可以是任何有效的标识符,只要它不与 CSS 中任何预定义的名称冲突。我们还没有决定这个动画需要多长时间,所以使用关键帧选择器选择块内的时间点,以百分比的形式写在时间轴上。

我们还可以使用特殊的关键字 from 和 to,它们分别是 0%和 100%的别名。如果缺少 from(或 0%)或 to(或 100%),它们将根据元素现有属性的初始状态自动构建。您可以拥有的关键帧选择器的数量从一个到您需要的任何数量,由您决定。

第一个关键帧(0%)设置动画计时功能属性。它的工作原理与过渡类似:使用预置关键字进行缓动,或使用三次贝塞尔()函数。在关键帧选择器中设置计时功能可以控制这个关键帧和下一个关键帧之间的过渡时间。

我们还使用 translateX(-100%)将正方形的起始位置移动到其自身左侧的 100%。

接下来,我们设置应用各种变换的整个范围的关键帧,以及单独的定时功能。图 10-20 显示了元素在每个关键帧中的样子。请注意,有些关键帧是相同的,例如在结尾:这是为了控制动画的速度。

A314884_3_En_10_Fig20_HTML.jpg

图 10-20。我们动画的各种关键帧

该元素将首先倾斜一点,好像是为了聚集动量,然后旋转和拉伸,几乎在 45 度角处停止,最终完成 90 度旋转,并在旋转轴线上倾斜一点,进行弹性停止。那是我们的第一部动画。

将关键帧块连接到元素

现在我们已经定义了一个动画关键帧序列,我们需要将它连接到徽标中的方块。正如转场属性一样,也有控制持续时间、延迟和计时功能的动画属性,但还有一些附加控件:

.box-inner {
  animation-name: roll;
  animation-duration: 1.5s;
  animation-delay: 1s;
  animation-iteration-count: 3;
  animation-timing-function: linear;
  transform-origin: bottom right;
}

我们将 animation-name 应用于该元素,以使用滚动动画。使用动画持续时间,我们设置每次迭代的时间。animation-delay 属性告诉浏览器在运行动画之前等待 1 秒钟。我们希望盒子在停止前翻转三次,所以我们将动画-迭代-计数设置为 3。

我们可以在关键帧选择器和动画元素上设置动画计时功能。这里,定时功能在整个序列中设置为线性,但我们已经看到如何在单个关键帧之间覆盖它。

注意

您可以使用与过渡相同的逗号分隔语法将多个动画应用到同一元素。如果两个动画试图同时制作同一属性的动画,则最后声明的动画获胜。

最后,我们将 transform-origin 属性设置为 bottom right,因为我们希望正方形以其右下角为轴心。

使用速记动画属性,我们可以将前面的所有细节浓缩成一行,就像过渡一样:

.box-inner {
**animation: roll 1.5s 1s 3 linear;** 
  transform-origin: bottom right;
}

但是我们还没完。到目前为止,我们有一个广场,重复滚动动画到位。我们需要它从视窗外移动到最终目的地。这在使用单个动画时是可能的,但是使用大量的关键帧。相反,我们可以在外部跨度元素上应用另一个动画和另一组变换。这一次,动画简单多了。我们希望它从左边移动,移动的距离是盒子宽度的三倍:

@keyframes shift {
  from {
    transform: translateX(-300%);
  }
}

因为我们想要从某个东西到初始状态的动画,我们可以省略关键帧,只留下从状态。

关于关键帧块和前缀的注释

我们在这一章中保持了各种属性的标准化的、无前缀的版本。代码示例具有完整的带前缀的代码。

在带有动画属性前缀的浏览器中,关键帧规则也带有前缀。这意味着您必须为每个前缀编写一套关键帧规则!幸运的是,现在大多数浏览器都接受无前缀的版本,所以通常只需要添加-WebKit-前缀。

现在,我们可以使用步进时序函数将移位序列应用于外部跨度。共有三个步骤,因此每次滚动动画结束并将正方形恢复到初始位置时,步进函数都会将其向前移动相同的量。这就是产生正方形在屏幕上滚动的错觉的原因;这很难说明,但是可以尝试一下代码示例,看看它是如何工作的。

.box-outer {
  display: inline-block;
**animation: shift 4.5s 1s steps(3, start) backwards;** 
}

最后一个关键字 backwards 设置动画序列的动画填充模式属性。填充模式告诉浏览器如何在动画运行之前或之后处理动画。默认情况下,在动画运行之前,不会设置第一个关键帧中的属性。如果我们提供 backward 关键字,这些值会在时间上向后填充,因此即使动画最初被延迟或暂停,第一个关键帧属性也会立即设置。向前填充使序列中的最后一个值在时间上保持向前,并且向前和向后填充。

在这种情况下,我们希望动画立即离开屏幕,但保留最终值(因为它与盒子的初始位置相同),所以我们向后填充。

至此,我们的第一个关键帧动画完成了。当你加载这个例子时,这个小方块高兴地在屏幕上挣扎。

沿着曲线制作动画

根据定义,对两点之间的元素位置设置动画会使其沿直线移动。您可以通过创建大量关键帧来创建曲线运动的外观,每次都略微改变方向。更好的方法是通过以特定顺序组合旋转和平移来移动对象,就像 Lea Verou 的这个示例:Lea . Verou . me/2012/02/moving-an-element-along-a-circle/

在示例文件中,我们包含了一个加载动画的示例,它使用这种技术来演示文件上传到服务器的过程。这些文件沿着一个半圆形的路径从计算机“跳到”服务器图标上,同时缩小一点以适应服务器图标的后面(见图 10-21 )。

A314884_3_En_10_Fig21_HTML.gif

图 10-21。文件图标沿着弯曲的路径移动到服务器图标

这是该动画的关键帧块:

@keyframes jump {
  from {
    transform: rotate(0) translateX(-170px) rotate(0) scale(1);
  }
  70%, 100% {
    transform: rotate(175deg) translateX(-170px) rotate(-175deg) scale(.5);
  }
}

初始关键帧将文件元素向左平移 170 个像素(以便在计算机图标上重新开始)。第二个关键帧选择器将元素旋转 175 度,仍然平移相同的量,然后以相反的方向将其旋转 175 度。由于这是在平移位置完成的,它用于保持元素直立,因此在旋转时不会上下颠倒。最后,我们将元素缩小到一半大小。

图 10-22 展示了这种特殊的变换组合是如何沿着弧线运动的。

A314884_3_En_10_Fig22_HTML.gif

图 10-22。由于旋转是在平移之前应用的,因此图标沿弧线移动。这是它在动画中大约四分之一处旋转 45 度后的样子。

然后,我们将这个动画连接到 file-icon 元素,并设置持续时间和放松功能。由于是加载动画,所以我们设置为无限重复(我们都经历过!),通过添加关键字 infinite 作为 animation-iteration-count 值。

.file-icon {
**animation: jump 2s ease-in-out infinite;** 
}

您可能已经注意到,最终的关键帧选择器同时针对动画的 70% 标记和 100% 标记。这是因为我们希望动画在重新开始之前暂停一会儿。

没有特定的属性来控制这种延迟,所以我们希望 70%和 100%的状态保持不变,我们可以以这种方式组合具有完全相同属性的关键帧,就像我们组合普通的逗号分隔选择器一样。

动画事件、播放状态和方向

在某个时候,文件传输应该完成了,希望如此。在完整的代码示例中,我们添加了按钮来模拟动画的结束、重新开始和暂停。它们唯一的功能是给文件图标添加两个类名中的一个。这些类将属性 animation-play-state 添加到文件图标,并将其设置为 paused。这个属性有两个值:默认情况下,它被设置为 running。

停止动作不同于暂停,因为它与动画开始、停止或启动新的迭代时触发的 JavaScript 事件挂钩。动画完成当前迭代后,文件图标会消失,服务器图标旁边会出现一个复选标记。您可以研究这个示例的源代码,看看它是如何工作的,或者在 MDN 上阅读更多关于 JavaScript 动画事件的内容(developer . Mozilla . org/en-US/docs/Web/API/animation event)。

最后,您还可以使用 animation-direction 属性来控制动画的方向。默认情况下,它被设置为正常,但是您可以使用 reverse 关键字反向运行动画,并且您将有一个免费的“下载”动画!

还有 alternate 和 alternate-reverse 关键字,它们在动画迭代之间改变方向。它们之间的区别是交替开始于正常方向,而交替-反向开始于反向。

一些动画“Gotchas”

当使用 CSS 关键帧动画时,有相当多的陷阱和不一致。以下是一些值得了解的信息:

  • 一些动画在页面加载时就开始运行,尽管会有一点延迟。这可能很棘手,因为一些浏览器在一开始就运行流畅时会出现错误行为;如果您在一些不同的浏览器中查看滚动正方形示例,您有时会注意到这一点。当一切就绪时,使用 JavaScript 触发动画通常更好。

  • 关键帧中的属性没有任何特异性。它们只是改变它们所应用到的元素的属性。尽管如此,一些浏览器(但不是全部)允许您用!正常规则中的重要标志,来自动画内部,这可能会令人困惑。

  • 相反,在关键帧块中设置的属性不允许用!重要的旗帜。关键帧块中设置了该标志的任何声明都将被忽略。

  • Android 操作系统的版本 2 和 3 支持 CSS 动画,但是一次只能支持一个属性!如果您尝试制作两个或更多属性的动画,该元素将完全消失。为了解决这个问题,您可以将动画分割成单独的关键帧块。

3D 转换

现在我们已经使用了常规的 2D 变换、过渡和动画,是时候看看 CSS 工具包中可能最令人印象深刻的工具:3D 变换了。

我们已经学习了 2D 空间中变换和坐标系的基础知识。当我们转向 3D 时,我们正在处理完全相同的概念,但这一次,我们还必须考虑 z 维度。3D 变换允许我们将坐标系旋转、倾斜、缩放或移动远离我们。为了达到这种效果,有必要引入透视的概念。

获得一些观点

当处理 3D 时,我们需要在三个轴上表示变换。x 和 y 轴仍然代表同样的东西,但是 z 代表一条穿过屏幕并朝向我们观众的线,可以这么说(见图 10-23 )。屏幕本身的表面通常被称为 z 平面,这是 z 轴上的默认位置。

A314884_3_En_10_Fig23_HTML.gif

图 10-23。3D 坐标系中的 z 轴

这意味着,当我们远离它们(z 轴的负方向)时,它们需要显得更小,当我们靠近它们时,它们需要显得更大。在 x 或 y 轴上旋转某物会使它的一部分变大,另一部分变小,等等。

让我们开始尝试一个例子。我们将使用来自 2D 部分的可靠的 100×100 像素的盒子,并围绕 y 轴旋转它:

.box {
  margin: auto;
  border: 2px solid;
  width: 100px;
  height: 100px;
  transform: **rotateY(60deg)**;
}

仅仅这样并不能让你走得更远:盒子会看起来更窄(这是在 y 轴上旋转时所期望的),但会完全缺乏任何 3D 感(参见图 10-24 的最左边部分)。

A314884_3_En_10_Fig24_HTML.jpg

图 10-24。我们旋转的 100×100 像素的盒子,没有透视(左),透视:140 像素(中),透视:800 像素(右)

原因是我们还没有定义一个视角:我们必须选择我们应该出现在离盒子多远的地方。你离一个物体越近,变化就越明显,离得越远,变化就越小。默认的视角基本上是无限远,所以我们没有得到非常明显的效果。

我们通过在要转换的元素的父元素上设置 perspective 属性来解决这个问题:

body {
  perspective: 800px;
}

这个测量值表示视点应该位于离屏幕多远的地方。您必须根据具体情况进行实验以找到正确的值,但是大约 600 到 1000 像素是一个很好的起点。

透视原点

默认情况下,假定查看者的视角以应用了该视角的元素为中心。从技术上讲,这意味着消失点在中心。您可以使用透视原点属性对此进行控制。它的工作方式类似于 transform-origin 属性:您为它提供一对带有关键字(上、右、下、左)、百分比或长度的 x 和 y 坐标值。

图 10-25 展示了三维物体在物体元素上的透视。所有的盒子都在 x 轴上旋转了 90 度(所以它们都是朝上的),但是左边和右边的图像有不同的透视原点。

A314884_3_En_10_Fig25_HTML.jpg

图 10-25。左侧的浏览器窗口有一个默认的透视原点(50% 50%),右侧的透视原点设置为左上角

Perspective()转换函数

在父元素上设置 perspective 属性会使其内部元素上的所有 3D 转换共享同一个透视图。这通常是你想要的,因为它能产生更真实的效果。

perspective()函数允许您在每个变换的元素上设置单独的透视图。通过下面的方法可以得到与前面的例子相似的结果,但是透视图不能在元素之间共享:

.box {
  transform: perspective(800px) rotateY(60deg);
}

创建 3D 微件

现在我们有了移动东西并以 3D 视角显示它们的方法,我们可以创建一些更有用的东西。除了使用动作来增加一点趣味或解释正在发生的事情,我们还可以结合动作和 3D 来节省空间,同时整理设计。

我们的目标是使用 CSS 和 JavaScript 构建一个 3D 小部件,其中用户界面的一些部分隐藏在元素的背面。我们将重用前面的菜单组件,并添加过滤选项,而不是展开所有项目。通过点击“显示过滤器”按钮,元件将翻转 180 度并显示背板(参见图 10-26 )。点击“给我看看披萨!”再次翻转它,在现实世界的例子中,比萨饼列表现在将根据复选框的选择进行过滤。

A314884_3_En_10_Fig26_HTML.gif

图 10-26。我们的“可翻转”部件

首先,对于不支持 3D 转换的浏览器,或者当 JavaScript 不能正常运行时,我们需要一些可靠的标记和默认情况。当浏览器不支持 3D 变换时,我们可以在页面上一个接一个地显示正面和背面,如图 10-27 所示。理论上,点击“给我看看披萨!”按钮将简单地重新加载应用了新过滤器的页面。

A314884_3_En_10_Fig27_HTML.jpg

图 10-27。基本的“2D”版本在页面中一个接一个地显示了小部件的两面

标记类似于本章前面的菜单,但是我们添加了一些新的类名和一个包装器来保存整个结构。

<div class="flip-wrapper menu-wrapper">
  <div class="flip-a menu">
    <h1 class="menu-heading">Top menu choices</h1>
    <ol class="menu-list">
      <li>Capricciosa</li>
      <!-- ...and so on, all 10 choices -->
    </ol>
  </div>
  <div class="flip-b menu-settings">
    <!-- the form on the back of the widget goes here. -->
  </div>
</div>

我们将使用 Modernizr 来检测对 3D 转换的支持,因此当支持 CSS 3D 转换时,增强的小部件的规则将以添加到 html 元素中的类名作为“前缀”。

首先,我们将在 body 元素上设置透视图,并使 wrapper 元素成为其后代的定位上下文。然后我们将添加转换,目标是包装器的 transform 属性。

.csstransforms3d body {
  perspective: 1000px;
}
.csstransforms3d .flip-wrapper {
  position: relative;
  transition: transform .25s ease-in-out;
}

现在,我们将使小部件背面的内容绝对定位,使其覆盖与正面相同的空间,并在 y 轴上将它翻转 180 度。我们也希望双方都是不可见的,当他们以错误的方式翻转时,这样一方就不会遮住另一方。我们将使用背面可见性属性来控制这一点;它默认为可见,但将其设置为隐藏会使元素在从后面看时不可见。

.csstransforms3d .flip-b {
  position: absolute;
  top: 0;  left: 0;  right: 0;  bottom: 0;
  margin: 0;
  transform: rotateY(-180deg);
}
.csstransforms3d .flip-b,
.csstransforms3d .flip-a {
  backface-visibility: hidden;
}

当我们旋转小部件时,我们希望整个东西都被旋转,包括已经翻转的背面。默认情况下,应用于父元素的任何 3D 变换都会使子元素上的 3D 变换无效,从而使它们变平。我们需要创建一个 3D 上下文,其中子对象的变换发生在与父对象相同的 3D 空间中。我们通过将包装元素上的 transform-style 属性设置为 preserve-3d 值来实现这一点:

.csstransforms3d .flip-wrapper {
  position: relative;
  transition: all .25s ease-in-out;
  **transform-style: preserve-3d;** /* default is flat */
}

现在拼图的最后一块是让 JavaScript 在点击前后按钮时切换包装元素上的类名。添加的 is-flipped 类名会触发整个小部件在 y 轴上旋转 180 度:

.csstransforms3d .flip-wrapper.is-flipped {
  transform: rotateY(180deg);
}

就这样,造型到位了。但是,可悲的是,现实世界中有一些约束,迫使我们重新审视这个小部件,使其能够跨浏览器兼容和访问。

IE 和 preserve-3d 的缺失

Internet Explorer 10 和 11 不支持 preserve-3d 关键字。这意味着任何元素都不能共享父元素的 3D 空间,这也意味着我们不能翻转整个小部件并让侧面跟随。我们必须单独转换每一面以使 IE 工作。

此外,IE 在父元素与多个转换元素的组合上有一些严重的错误,这意味着我们必须求助于转换列表中的 perspective()函数。

更新后的代码在小部件的前面设置一个 0 度的初始变换,在后面设置一个-180 度的变换,然后在切换包装器元素上的类名时翻转这两个变换。此外,perspective()函数需要在每一个转换链中首先引入。

.csstransforms3d .flip-b,
.csstransforms3d .flip-a {
  transition: transform .25s ease-in-out;
}
.csstransforms3d .flip-a {
  transform: perspective(1000px) rotateY(0);
}
.csstransforms3d .flip-b {
  transform: perspective(1000px) rotateY(-180deg);
}
.csstransforms3d .flip-wrapper.is-flipped .flip-a {
  transform: perspective(1000px) rotateY(180deg);
}
.csstransforms3d .flip-wrapper.is-flipped .flip-b {
  transform: perspective(1000px) rotateY(0deg);
}

iOS 8 上的 Safari 有一个相反的错误,应用了 perspective()变换的元素有时会在开始过渡时消失。一种解决方法是将多余的透视属性重新应用到 body 元素上:

.csstransforms3d .flip-wrapper {
  perspective: 1000px;
}

负责任的代码:处理键盘控制和可访问性

当开发隐藏东西的组件时,我们在前面的章节中已经看到如何隐藏它们是很重要的。简单地将某些内容旋转出视图并不会将其从例如文档的 tab 键顺序中移除。在 3D 小部件的最终代码(以及随之而来的 JavaScript 代码)中,我们加入了几个其他的修正以使小部件更加健壮:

  • 除了使用 Modernizr 来检测对 3D 转换的支持,我们还检测对 classList JavaScript API 的支持。这用于在小部件状态改变时有效地切换类名。这意味着最终代码中的所有 CSS 规则都以. csstransforms3d.classlist 为前缀。

    对 3D 转换的支持和对 classList API 的支持几乎重叠,但是我们不希望有任何边缘情况留下坏的小部件。如果浏览器不支持这两个特性,小部件将不会运行,并且“2D”样式也不会改变。

  • 当小部件的一侧被隐藏时,它会自动添加类名 is-disabled,并将 aria-hidden 属性设置为 true。is-disabled 类将 CSS 中的 visibility 属性设置为 hidden。

    这可以防止键盘用户意外跳转到他们看不到的表单控件,并防止屏幕阅读器读取内容。(aria-hidden 属性仅用于屏幕阅读器,因此不依赖于 CSS 隐藏技术。)隐藏首先发生在翻转完成之后,因此它取决于 transitionend 事件。

  • 相反,在使用类名 is-enabled 显示之前,另一侧是显式可访问的。

  • 当向后翻转小部件时,键盘焦点移回到“显示过滤器”按钮。

3D 转换的高级功能

本节介绍了 3D transforms 规范中在日常编码中可能较少使用的部分,但是提供了一些额外的功能。

Rotate3d()函数

除了单独的旋转函数——rotateX()、rotateY()和 rotateZ()(及其 2D 等价的 rotate())—还有一个名为 rotate3d()的函数。此函数允许您围绕穿过 3D 空间的任意线旋转元素,而不是在每个轴上旋转指定的量。下面是使用该函数的情况:

.box {
  transform: rotate3d(1, 1, 1, 45deg);
}

rotate3d()函数有四个参数:代表 x、y 和 z 的三个数字向量坐标和一个角度。坐标定义了空间中的一条线,旋转围绕该线发生。例如,如果向量坐标是 1,1,1,旋转将围绕一条假想的线,该线从变换原点出发,经过位于相对于原点的 x 轴(右)1 个单位、y 轴(下)1 个单位和 z 轴(朝向观察者)1 个单位处的点。

我们不需要在这里指定是哪个单位,因为这些点都是相对于彼此的——如果我们使用 100,100,100,我们会得到相同的结果,因为穿过元素的线是相同的。

实际上,3D 旋转相当于在每个轴上的一些旋转(0 度或更大),但是在计算在每个轴上的旋转量时涉及到一些非常复杂的数学运算。更容易把这个函数看作是围绕你选择的线旋转某个角度的一种方式。如果您需要同时在几个轴上旋转特定的角度,那么坚持单轴旋转的组合要容易得多。

3D 矩阵变换

正如 CSS 变换的 2D 子集一样,matrix3d()函数允许您在三个轴中的每个轴上组合任意数量的平移、缩放、倾斜和旋转。

我们不会在这里深入讨论 3D 矩阵如何工作的细节,但是函数本身需要 16(!)最终操纵坐标系的各个方面的论据。可以说,它获得了“有史以来最复杂的 CSS 属性”的奖项

就像 2D 版本一样,3D 矩阵不是你通常手写的东西,但它们可以帮助你创建高性能的交互体验,如使用 CSS 和 JavaScript 组合的游戏。例如,本章开头的数字创意指南示例(如图 10-1 所示)大量使用 matrix3d()来计算动画书中所有角色的变换。

摘要

在这一章中,我们开始操纵空间和时间中的元素。我们研究了 2D 或 3D 中的变换如何改变元素的呈现,但不影响页面上的其他元素。我们先睹为快,了解了 matrix()函数和 rotate3d()等高级转换。

将这些与动画放在一起,使用 CSS 过渡或 CSS 关键帧动画,我们可以创建像动画标志一样生动的“喜悦者”,或者像翻转披萨菜单一样更实用的 3D 小部件。

纵观全文,我们已经看到了相应地应用这些效果的技术,尽一切努力不破坏浏览器不支持它们的用户以及使用屏幕阅读器浏览或仅使用键盘导航的用户的体验。

十一、前沿视觉效果

单独使用 CSS 对创造性的设计进行编码总是很困难。直到最近,这种语言本身在你可以使用的视觉效果方面还相当有限。从像 Photoshop 这样的图形编辑包中重新创建视觉效果是困难的,如果不是不可能的话,而且经常需要令人讨厌的黑客。

我们总是能够通过牺牲简单性(纯粹用于表示目的的额外元素)或性能(页面上图像太多,JavaScript 做不出视觉效果)来解决这些限制。

在这一章中,我们将看看如何使用各种 CSS 特性来实现这些效果。其中一些是非常新的,目前只支持有限的浏览器,而另一些已经存在了几年。许多已经在 SVG 中存在了很长时间,但只是现在才渗透到 CSS 中——我们将在本章的后面看到一些这种协调的例子。

所有这些技术都可以让你的设计更上一层楼:它们是将原材质提升到新高度的调味品。因此,应该谨慎使用它们,并在应用时考虑到逐步增强。您还应该意识到,这些技术中的许多都有相关的缺陷。因此,即使在支持它们的浏览器中,它们也经常是一项正在进行的工作。

在本章中,我们将探讨以下主题:

  • CSS 形状

  • 通过 CSS 和 SVG 使用剪辑路径和蒙版

  • CSS 混合模式

  • 使用 CSS 和通过 SVG 过滤

在图 11-1 中,我们拼凑了一个充满视觉效果的页面(描述一些天体)。就在几年前,这些效果在 CSS 中是不可能重现的。页面布局,如果曾经尝试过的话,将会用大量的图片和额外的元素重新创建。

A314884_3_En_11_Fig1_HTML.jpg

图 11-1。使用一系列视觉效果的页面

今天,这些类型的效果实际上可以在许多浏览器中用 CSS 来实现,如果它们失败了,可以让它们优雅地失败。在页面上同时使用大量的视觉效果仍然需要小心,因为它们会带来性能损失;有些比其他人多。尽管如此,将这些效果作为 CSS 的一部分还是有很大的好处。它们变得不那么依赖于粗糙的标记,并且更容易维护。此外,一旦功能在竞争浏览器中标准化,性能往往会随着时间的推移而变得更好。

在这一章的剩余部分,我们将会浏览在“观星”页面例子中使用的所有技术,以及更多。

打破常规:CSS 形状

正如我们之前说过的,网页布局都是关于矩形的,所以有一个内在的盒子。在前面的章节中,我们已经看到了如何通过使用图像和渐变来引入更有机的感觉,以及通过使用圆角来创建更柔和的形状甚至圆形来解决这个问题的例子。

CSS 形状是一个新的标准,允许在网页设计中使用更广泛的形状。形状元素影响页面中内容的实际流动,而不仅仅是表面外观。

内部和外部形状

CSS Shapes 由两组新属性组成:一组设置影响框内内容的形状,另一组设置影响形状元素周围内容流的外部形状。在图 11-2 中,一个元素被设置为圆形。左边的示例显示了外部形状如何影响围绕圆形流动的内容,而右边的示例显示了内部形状如何影响圆形内的内容。

A314884_3_En_11_Fig2_HTML.gif

图 11-2。外部形状与内部形状

这两种形状方法是在 CSS 形状规范的不同层次中定义的。shape-outside 属性(在 CSS Shapes Level 1 中定义)是唯一一个已经达到合理成熟度的属性,并且已经开始进入浏览器。我们将 shape-inside 排除在这一部分之外,因为它还没有在任何浏览器中实现,但它很可能很快就会出现。

shape-outside 属性只对浮动元素有效。它的工作原理是雕刻出一个影响元素外部内容流动的形状,但它不会改变元素本身的外观。

在我们示例的“月亮”部分,文本通过 shape-outside 围绕月亮图像的形状流动(见图 11-3 ),如下所示:

A314884_3_En_11_Fig3_HTML.jpg

图 11-3。文字在月亮图像外呈圆形流动
.fig-moon {
  float: right;
  max-width: 40%;
  shape-outside: circle();
}

在我们深入了解形状如何工作之前,值得注意的是形状外部如何影响布局。图像文件本身具有黑色背景。如果我们改变这部分页面的背景颜色,形状的效果会显示得更清楚(见图 11-4 )。图像本身仍然是一个正方形,但是文本在图像上流动,围绕着图像内部的一个圆形。在不支持 CSS 形状的浏览器中,文本将正常围绕矩形流动。

A314884_3_En_11_Fig4_HTML.jpg

图 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 )。

A314884_3_En_11_Fig5_HTML.jpg

图 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 所示,我们可以看到图像中的透明度数据被拾取,生成了形状。

A314884_3_En_11_Fig6_HTML.jpg

图 11-6。图像透明部分的轮廓用于创建形状
小费

如果您尝试在浏览器中直接打开 HTML 文件,即使浏览器支持 CSS 形状,也不会成功。您需要通过 web 服务器获取页面,以便引用的图像具有适当的 HTTP 头,将它描述为来自与 CSS 相同的源服务器。这是一些较新的浏览器中存在的安全权衡,以防止引用的文件对您的计算机做不安全的事情。

默认情况下,形状轮廓将从图像完全透明的轮廓生成,但是我们可以使用 shape-image-threshold 属性更改该值。默认值为 0.0(完全透明),而较高的值(接近 1.0)意味着在创建形状边缘之前可以接受较高的不透明度值。例如,如果我们将土星图像更改为使用 0.9 的图像阈值,半透明环将不会包含在形状轮廓中,文本将会与它们重叠(参见图 11-7 ):

A314884_3_En_11_Fig7_HTML.jpg

图 11-7。使用形状-图像-阈值,图像的半透明部分现在在生成形状时会被忽略
.fig-planet {
  float: right;
  max-width: 65%;
  shape-outside: url(img/saturn.png);
  shape-image-threshold: 0.9;
}

异形框和页边距

除了使用形状函数或图像,我们还可以使用元素的参考框来生成形状。这一开始听起来可能很奇怪,因为这是我们想要摆脱的四方形,但形状也将遵循圆角。

例如,如果我们回到月亮的例子,我们可能希望改变该部分的背景颜色,但同时去掉图像周围的黑色方框,如图 11-8 所示。我们可以在图像上使用边界半径来创建圆形:

A314884_3_En_11_Fig8_HTML.jpg

图 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 )。

A314884_3_En_11_Fig9_HTML.jpg

图 11-9。使用边界框作为形状参考,边界距离跟随圆角

如果我们想给土星图像更复杂的形状添加一个边距,有一个新的属性叫做 shape-margin 来设置整个形状周围的边距距离,而不管创建它的方法如何(见图 11-10 ):

A314884_3_En_11_Fig10_HTML.jpg

图 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 )。

A314884_3_En_11_Fig11_HTML.jpg

图 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 的绝佳机会。由于这些新的视觉效果只在最近的浏览器中实现,我们可以安全地用这种方式来定义它们。

A314884_3_En_11_Fig12_HTML.jpg

图 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 所示的页面导航中的形状所做的。

A314884_3_En_11_Fig13_HTML.jpg

图 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 文件,这个过程在不同的图形编辑软件之间有所不同;我们将省略这方面的细节,并将重点放在一般的工作流程上。

A314884_3_En_11_Fig14_HTML.jpg

图 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 )。这种逐渐透明是通过掩蔽实现的。

A314884_3_En_11_Fig15_HTML.jpg

图 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 展示了渐变遮罩在文本上的大小和位置。

A314884_3_En_11_Fig16_HTML.jpg

图 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 中,我们已经移除了文本,并稍微减轻了渐变,这样结果更加清晰可见。

A314884_3_En_11_Fig17_HTML.jpg

图 11-17。有地球照片的页眉

通常使用 PNG 图像来获得具有烘焙透明度的照片图像。PNG 的缺点是文件太大——作为 PNG,地球图像大约有 190 KB。在这项技术中,我们将使用 SVG 的强大功能,通过遮罩将 alpha 透明度应用于 JPEG 文件。结果文件大约为 24 KB。

SVG 中的图像

我们需要做的第一件事是创建一个普通的 JPEG 图像,背景仍然在那里,如图 11-18 所示。

A314884_3_En_11_Fig18_HTML.jpg

图 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 中的位图图像上画了一个巨大的半透明圆圈,以便快速获得一些测量值。我们也可以绘制一条路径来创建遮罩形状,但是径向渐变给了我们一个更平滑边缘的可能性。这只是一份一次性文件,用来提供正确的数字。

A314884_3_En_11_Fig19_HTML.jpg

图 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 相同的混合模式。在合成和混合标准中,这一天终于到来了。

A314884_3_En_11_Fig20_HTML.jpg

图 11-20。在 Adobe Photoshop 中混合图层

合成是将图像图层合并在一起的技术术语。混合模式可能是最常见的合成方式。如果你以前没有使用过混合模式,或者没有考虑过它们是什么,它们都代表了将一个图像(称为)的颜色值组合到另一个图像(称为目的地)之上的不同数学方法。

最简单的例子可能是“乘法”混合模式,其中源像素的每个颜色通道值与其后面像素的值相乘,从而产生更暗的图像。这有助于从灰度的角度来考虑这个例子,颜色从 0(黑色)到 1(白色)。如果源为 0.8,目标为 0.5,则结果像素的颜色值将为 0.8 × 0.5 = 0.4。

给背景图像着色

另一个例子是“光度”混合模式。它从光源获取亮度级别,并将它们应用到目标的色调和饱和度。我们的示例页面中的“银河系”部分有一个背景图像,带有一些相当鲜明的蓝色调。我们通过应用略带紫色的背景颜色,然后应用背景混合模式:亮度,对其进行了微调(见图 11-21 )。这将为图像着色,并为其提供更均匀的颜色范围。

A314884_3_En_11_Fig21_HTML.jpg

图 11-21。用亮度混合模式给背景图像着色
.section-milkyway {
  background-image: url(img/milkyway.jpg);
**background-color: #202D53;** 
**background-blend-mode: luminosity;** 
}
注意

如果你正在用黑白打印或单色电子阅读器阅读这篇文章:对不起!在这种情况下,很难展示色彩效果。您可以在书中的示例文件中找到工作代码。

如果不了解颜色数学,很难解释全部 16 种混合模式的作用。它们中的大多数只在某些情况下有用,比如光度允许你通过混合纯色层给图像着色。在图 11-22 中,我们将相对深蓝色的背景图像与浅粉色的背景颜色混合,向您展示当两个图层存在夸大的差异时每个模式的效果。

A314884_3_En_11_Fig22_HTML.gif

图 11-22。16 种混合模式

如果你想更深入地了解每种模式是如何工作的,我们推荐派尔·吉尔萨(www.slrlounge.com/school/photoshop-blend-modes/)的文章和视频《理解混合模式的终极视觉指南》。

正如我们在第五章中所讨论的,每个元素可以有多个背景图像,并且背景按照它们被声明的相反顺序堆叠在一起。背景色位于这一叠背景层的底部。如果您有多个图像层,您可以声明一个逗号分隔的背景混合模式列表,依次应用于每个层及其下面的层。

请注意,无论背景透明度如何,背景图层都不会与元素本身后面的内容混合在一起。你不能为一个单独的背景颜色层设置背景混合模式——混合元素是通过一个单独的属性来实现的,我们将在下一步解决这个问题。

混合元素

就像你可以混合背景层一样,你也可以混合元素和它们的背景。这意味着一个静态定位的子元素与其父元素混合,或者类似于一个绝对定位的元素覆盖了页面的另一部分。需要注意的是,不同堆栈环境中的元素不会相互融合;我们稍后将进一步研究这种影响。

语法与其背景对应物完全相同,但该属性被称为 mix-blend-mode。Saturn 图像使用屏幕混合模式来更好地适应页面部分的背景颜色(参见图 11-23 )。

A314884_3_En_11_Fig23_HTML.jpg

图 11-23。土星图像使用屏幕混合模式来更好地适应背景
.fig-planet {
  mix-blend-mode: screen;
}

屏幕混合模式是另一种更直接有用的模式。它得名于在同一个屏幕上投影两个图像到另一个之上,从而产生一个整体更亮的图像。在一个图像“发光”较少(即较暗)的情况下,来自第二个图像的任何光都会透过,反之亦然,从而生成整体较亮的图像。

这意味着白色源部分将完全不透明,但黑色源部分变得透明,这使它成为一种有用的遮罩技术。我们可以用它来制作一些有趣的“挖空文本”效果。

带有挖空文本的排版锁定

这种透视效果正是我们在示例页面的“可观察宇宙”部分标题中所做的,如图 11-24 所示。

A314884_3_En_11_Fig24_HTML.jpg

图 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 规则中,我们设置了混合模式,但也将文本的填充颜色更改为黑色,使其完全透明。

A314884_3_En_11_Fig25_HTML.jpg

图 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,这将强制一个新的堆叠上下文并隔离混合。

A314884_3_En_11_Fig26_HTML.jpg

图 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 ):

A314884_3_En_11_Fig27_HTML.jpg

图 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 显示了没有应用任何滤镜的图像。

A314884_3_En_11_Fig28_HTML.jpg

图 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 中的结果:

A314884_3_En_11_Fig29_HTML.jpg

图 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 参数在这里丢失了。

A314884_3_En_11_Fig30_HTML.jpg

图 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 的模糊半径:

A314884_3_En_11_Fig31_HTML.jpg

图 11-31。模糊了土星的图像
.fig-planet {
  filter: blur(10px);
}

至少在当前的实现中,blur()过滤器往往是一个性能猪,所以要明智地使用它。这有点遗憾,因为模糊可以产生一些有趣的动画效果。当在界面中引导注意力,或者淡化背景中的某些东西时,模糊和聚焦是一种有效的工具。

背景滤镜

说到背景,还有一些地方过滤器已经潜入了 CSS。在示例页面的“银河”部分,我们使用了 2 级滤镜效果规范中的一个实验属性:背景滤镜。

它的工作方式与 filter 属性完全一样,但是它将滤镜应用于元素背景与其后面的页面的合成中。这使我们能够做一些漂亮的“毛玻璃”效果,例如(见图 11-32 )。

A314884_3_En_11_Fig32_HTML.jpg

图 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 ):

A314884_3_En_11_Fig33_HTML.jpg

图 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 )。

A314884_3_En_11_Fig34_HTML.jpg

图 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”过滤器。

A314884_3_En_11_Fig35_HTML.jpg

图 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 中的开发工具来找出使用了哪个字体文件,而不仅仅是字体堆栈声明是什么。

A314884_3_En_12_Fig1_HTML.jpg

图 12-1。使用 Firefox 开发工具来找出哪个字体文件被用在了www.microsoft.com的特定元素上

进一步深入开发工具,您会发现面板和按钮,让您检查其他质量。这使您不仅可以看到应用了什么,还可以看到如何应用以及何时应用。为了理解这些工具,了解一点浏览器如何解析 CSS 是有帮助的。

浏览器如何解释 CSS

接下来是从 CSS 文件到“屏幕上的像素”的旋风之旅,以便更好地理解我们编写的 CSS 的影响。以下部分描述的步骤代表了每次加载新页面时发生的情况的简化模型,但是当页面被交互时,一些(或所有)步骤也可能发生。

解析文件和构造对象模型

当你加载一个站点时,浏览器首先会收到一个 HTML 响应。这种响应被解释为彼此有关系的对象(节点)。例如,body 节点是 html 节点的后代,p 和 h1 节点可能存在于 body 节点中。这就是 DOM:文档对象模型(见图 12-2 )。

A314884_3_En_12_Fig2_HTML.gif

图 12-2。文档对象模型是浏览器内部理解 HTML 的方式

当在 HTML 文档中遇到指向 CSS 文件的链接元素时,浏览器将获取并解析该文件。类似于如何将 HTML 转换成 DOM 树,CSS 文件被解析成称为 CSSOM 的东西:CSS 对象模型。不仅是外部文件,样式元素或内联样式属性中的任何 CSS 都将被解析并添加到 CSSOM 中。就像 DOM 一样,它是一个树状结构,包含页面样式的组合层次结构(见图 12-3 )。

A314884_3_En_12_Fig3_HTML.gif

图 12-3。CSSOM 树表示样式表中样式的层次结构

每个 DOM 节点都与相关的 CSS 选择器相匹配,并进行最终的样式计算(基于级联、继承和特定性之类的东西)。

DOM 和 CSSOM 都是标准化的,并且应该跨浏览器工作。在这一步之后,如何从现在拥有的数据到屏幕上显示的内容取决于浏览器,但所有浏览器都遵循类似的步骤来实现这一点。

渲染树

呈现页面的下一步是构建另一个树结构,通常称为呈现树。在这里,每个对象都代表要在屏幕上呈现的内容。这个结构看起来有点像 DOM 的树,但是它们并不相同。例如,可视化隐藏的 DOM 节点将不会出现在渲染树中,而像::before 这样的伪元素可能会有一个不在 DOM 中的渲染对象。浏览器还需要表现页面视觉表现的其他方面,比如滚动块和视窗(见图 12-4 )。

A314884_3_En_12_Fig4_HTML.gif

图 12-4。一个简化的假想渲染树。head、title 和 meta 等元素没有自己的渲染对象。对于带有显示的元素也是如此:例如,none

构建渲染树时,渲染树中的每个对象都知道它应该是什么颜色,任何文本是什么字体,它是否有明确的宽度,等等。

布局

在下一阶段,计算每个渲染对象的几何属性。这被称为布局回流阶段。浏览器将遍历渲染树,并试图找出每个项目在页面上的位置。

由于很多网页布局都是为了保持页面的流动,其中元素“推”上其他元素,这可能会变得相当复杂。图 12-5 来自程序员 Satoshi Ueyama 的一个有趣的视频(【https://www.youtube.com/watch?v=dndeRnzkJDU】)他破解了 Gecko 引擎,展示了 Firefox 在布局网站时的实际回流操作。

A314884_3_En_12_Fig5_HTML.jpg

图 12-5。一段视频截图,显示了火狐浏览器中www.wikipedia.org的缓慢回流

有时,在这个阶段需要构建具有自己的呈现属性的附加呈现对象。例如,一段具有特定字体大小的文本可能会生成一个换行符,将它拆分成两个匿名的行框。这反过来会影响父元素的最终高度,以及它后面的其他元素。

最终,每个渲染对象的位置都会被计算出来,是时候把它们放到屏幕上了。

绘画、合成和绘图

在一个非常简化的模型中,浏览器现在从渲染树中获取它可以学习到的一切,并将可视化表示放在屏幕上。实际上,事情要复杂一些。

当每个渲染对象的位置和属性被确定后,浏览器可以计算出屏幕上要显示的实际像素,这个过程被称为绘制。但除此之外,浏览器可能还需要做一些进一步的工作。

当浏览器知道最终图形表示的某个部分不能影响页面其余部分的显示时,它可能会决定将绘制工作分成不同的任务,每个任务负责页面的一个特定部分,称为

有些东西,如 3D 变换,甚至可以通过使用专用的图形芯片进行硬件加速。其他图层可能应用了滤镜或混合模式,这将决定它们如何与其他图层混合。这种将渲染分割成层,然后将它们重新组合成最终结果的任务称为合成。如果页面是用描图纸做的,这就相当于在不同的纸上画画,然后把它们粘在一起。

最后,页面准备好在屏幕上显示(或绘制)。唷!

优化渲染性能

如果页面中有任何变化,浏览器将需要再次执行前面的一些步骤。为了保持页面在屏幕上平滑显示,最好在 16 毫秒内完成,这是每次屏幕更新之间的时间,假设它有标准的 60hz 刷新率。

从性能角度来看,有些东西通常非常便宜,比如滚动:整个最终渲染只是在不同的位置重新绘制。当某个因素导致页面样式改变时,性能会有所下降。

如果我们在 JavaScript 中更改元素的宽度或高度属性,浏览器将需要进行布局、合成和绘制。仅改变文本的颜色不会影响布局,因此会触发绘画和合成。最后,我们能做的最便宜的操作是完全通过合成来完成的。

网站csstriggers.com是哪个属性映射到哪个渲染操作的便利参考(见图 12-6 )。该网站(由 Paul Lewis 创建)目前跟踪谷歌 Chrome 的渲染操作,但它们很可能在大多数浏览器中类似地工作。

A314884_3_En_12_Fig6_HTML.jpg

图 12-6。CSS 属性和它们在浏览器中触发的工作量,来自csstriggers.com

我们可以使用开发人员工具来查看这些不同的步骤何时执行,以及最终的性能如何。通过进入 Chrome DevTools 中的时间轴面板,我们可以记录我们与页面的交互,并跟踪交互是否触发了特定的呈现步骤。其他浏览器也有时间线记录功能,但 Chrome DevTools 一直是功能最丰富的。在图 12-7 中,我们记录了一个 1.5 秒的时间线,我们滚动了一个示例页面,在滚动的同时,一个固定的标题被动画显示在视图中。

A314884_3_En_12_Fig7_HTML.jpg

图 12-7。Chrome DevTools 中的时间线记录。单击右侧面板中的圆形图标可以开始和停止录制

我们可以放大到单个帧的级别,并确定浏览器内部正在进行何种操作。每个条代表一个渲染帧,每个条的彩色位代表一个渲染操作。在这种情况下,绿色代表绘画操作。在时间线下方,列出了每项操作,我们可以单击每一行以获得更多详细信息。

正如时间轴所示,某些东西导致了每一帧中的绘画操作。这并不可怕,但它可能会阻止在较慢的机器上平滑滚动,并且不应该在滚动时发生。为了弄清楚发生了什么,我们可以在渲染选项卡中打开一个叫做“绘画闪烁”的东西。然后,当我们与页面交互时,浏览器会在任何重新绘制的区域周围绘制一个颜色高亮(见图 12-8 )。

A314884_3_En_12_Fig8_HTML.jpg

图 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 )。

A314884_3_En_12_Fig9_HTML.jpg

图 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 中的警告消息框看起来完全一样,但实现方式不同。当我们查看源代码时,我们希望关于内部代码质量的一些理论性的谈论会变得更加清晰。

A314884_3_En_12_Fig10_HTML.jpg

图 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 ):

A314884_3_En_12_Fig11_HTML.jpg

图 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 中删除大量重复内容。

A314884_3_En_12_Fig12_HTML.jpg

图 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,就此而言),网页的基本表示也应该是有意义的。

A314884_3_En_12_Fig13_HTML.jpg

图 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 语义和前端架构”(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 允许我们定义变量,然后我们可以在多个地方重用它们。例如,primarycolorsecondary-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(【http://csslint.net】)和 style Lint(【http://stylelint.io/】)。这些工具检查你的选择器和声明中可能不需要的语法错误和模式(见图 12-14 )。两者都是可配置的,你甚至可以编写自己的规则。

A314884_3_En_12_Fig14_HTML.jpg

图 12-14。在崇高文本编辑器中使用 CSS Lint。左边空白处的圆点表示潜在的问题,解释信息打印在底部栏上

构建工具

除了林挺之外,在开发一个网站时,我们可能需要在编辑器之外反复做很多工作:

  • 预处理 CSS

  • 如果在多个较短的文件中工作,将 CSS 文件连接在一起

  • 缩小 CSS,删除注释等等来节省空间

  • 优化 CSS 中引用的图像

  • 运行开发服务器

  • 重新加载一个或多个浏览器来检查我们的更改

幸运的是,有许多工具可以帮助您在项目中自动完成这些任务。它们包括从命令行运行的更高级的工具,以及可以在图形用户界面中管理的更简单的设置,如考拉(见图 12-15 )。

A314884_3_En_12_Fig15_HTML.jpg

图 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 的文件,这个文件记录了运行项目所需的内容。这些依赖关系存储在 https://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 所示,有几个部分。

A314884_3_En_12_Fig16_HTML.gif

图 12-16。调用我们的元素的假设结果

在幕后,这个定制元素可以根据链接的 URL 获取数据,并用自己隐藏的 DOM 片段(称为影子 DOM)替换其内容。标题、评论分数和图片在这个片段中都有自己的元素。在某种意义上,这有点像 iframe——内容被屏蔽在页面的常规 DOM 树之外,它们使用单独的上下文来编写脚本和样式。

在 Shadow DOM 片段中,样式元素的范围自动限制在 web 组件的根元素,就像我们使用 scoped 属性一样。父文档中的样式也不会渗透到元素中,因此组件样式被完全封装。诀窍在于定制属性可以级联到组件中——因此组件作者可以准确地指定允许您覆盖哪些属性。有了这种机制,可以安全地更改排版和配色方案等内容,以匹配使用组件的站点。

到目前为止,许多提议的 Web 组件标准还不受支持,但是封装到影子 DOM 中已经在具有类似的元素的浏览器中发生了。即使标记中的元素是空的,它也隐藏了视频控件之类的元素——例如,你可以激活 Chrome DevTools 中的“显示用户代理阴影 DOM”选项来查看它的运行情况(参见图 12-17 )。

A314884_3_En_12_Fig17_HTML.jpg

图 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 的真正途径。**

posted @   绝不原创的飞龙  阅读(59)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· Trae初体验
点击右上角即可分享
微信分享提示