响应式设计

本章概要

  • 基于多种设备及屏幕尺寸构建页面
  • 用媒体查询,根据视口大小来变更设计
  • 采用移动端优先的方式(mobile-first approach)来设计页面样式
  • 使用响应式图片(responsive images)

现代社会中,网络(Web)无处不在:上班用台式机上网,回家躺床上用平板电脑上网,甚至连客厅里的智能电视都能联网,更别说随身携带的智能手机了。这个由 HTML、CSS 和 JavaScript 搭建的 Web 平台,俨然成为了一个前所未有的独特生态系统。

这也给 Web 开发人员带来一个颇有挑战的问题:网站究竟该如何设计,才能让用户在可能用到的各种设备上访问页面时,做到既好用又好看呢?起初,不少开发人员的解决方案是同时搭建两个网站:桌面端(译注:即 PC 端)和移动端。服务器会将来自移动端设备的网络请求,从原来的 http://www.wombatcoffee.com 重定向到 http://m.wombatcoffee.com。该移动端站点往往会针对尺寸较小的屏幕提供更简约的用户体验,设计上也更精简。

随着越来越多的上网设备在市面上不断涌现,这种解决方案的好日子也基本到头了:平板设备该用移动端还是桌面端呢?主打一个大屏的手机又该如何是好?换成 iPad Mini 情况又如何?要是移动端用户偏要用桌面端的某些功能怎么办?最终,这种将 PC 端和移动端强行剥离的方案所带来的麻烦远比它能解决的问题多得多。除此之外,还需要同时维护多个站点。

一种更为理想的方案,是给所有用户提供同一套 HTML 和 CSS:通过应用几个关键技术,网页内容就能根据用户实际的浏览器视口尺寸(或者屏幕分辨率)渲染出不同的页面效果。这样就无需维护两个不同的站点了,创建一次,就能同时在智能手机、平板电脑、或者其他任意智能设备上流畅运行。这种页面设计方案由 Web 设计师 Ethan Marcotte 发扬光大,并称之为 响应式设计(responsive design)。

浏览网页时,不妨留意一下您遇到的响应式设计,看看那个网站是如何响应浏览器的不同宽度的。新闻类的网站特别有意思,它们往往要将很多内容塞进同一个页面。撰写本书时,波士顿环球报的官网 就是个绝佳案例。该网站能够根据浏览器窗口宽度的不同分别提供单栏、双栏或三栏布局。通常只要缩放浏览器窗口的宽度,就可以直接查看页面最终响应的布局效果。这便是响应式设计的工作方式。

响应式设计的三大原则如下:

之前在介绍第二章相关内容时提到过上述部分设计原则。本章将针对这三个核心原则进行更深入地探讨。先从一个响应式页面的构建开始,然后逐步展开介绍这三个原则。因为图片在响应式网站中的处理比较特殊,最后还会专门介绍一些响应式图片相关的知识。

7.1 移动端优先设计原则

Mobile first

响应式设计的第一原则就是 移动端优先(mobile first,顾名思义,就是移动端布局的构建要先于桌面端布局。这是确保两个版本都能生效的最佳方案。

开发移动端页面就像戴着脚镣跳舞:除了屏幕大小受限、网速偏慢外,页面交互所使用的控件(controls)也和 PC 端不太一样:虽然可以打字,但总感觉不太顺手,更没法将鼠标悬停在元素上触发一些特定效果。倘若一开始就设计一个功能全面的网站,然后企图根据移动端的诸多限制削减某些功能,这么做往往都会以失败告终。

而选用移动端优先的方式,则会让您在网站设计之初就开始考虑这些制约因素。一旦解决了移动端的用户体验问题(至少做了相关规划),后续就可以通过“渐进式增强(progressive enhancement)”技术去改善大尺寸屏幕用户的交互体验。

本章最终要实现的页面效果如图 7.1 所示。没错,这就是一版移动端的页面设计。

图 7.1 待实现的移动端页面设计效果图
图 7.1 待实现的移动端页面设计效果图

该页面有三个主要部件:标题栏(header)、带了些文字内容的页面主图(hero image)、以及主内容区(main content)。要是轻触或单击页面右上角那个图标,还能弹出一个隐藏菜单(如图 7.2 所示)。这个由三条横线组成的图标因为形似汉堡包中的面包和肉饼,通常也被称作 汉堡(hamburger 图标。

图 7.2 点击或轻触移动端页面的“汉堡”图标后打开的菜单效果
图 7.2 点击或轻触移动端页面的“汉堡”图标后打开的菜单效果

移动端布局一般是很朴素的设计。除了这个带交互效果的菜单,移动端更侧重于内容的展示。相对于大屏有大块的空间来布局标题栏、页面主图和菜单区,移动端用户往往浏览网页的目的性更强。他们很可能与友人在户外玩耍,只想快速查到商店营业时间或者像价格、地址这样的具体信息。

因此移动端的设计就是围绕内容展开的。试想有这么一个 PC 端页面,一边设计为文章内容,另一边则是包含链接的侧边栏,里面还有些不太重要的内容。要是换到移动端来,肯定是希望先看到文章内容。换句话说,我们希望最重要的内容先出现在 HTML 里。这一点恰好与页面可访问性关注的焦点不谋而合:一款读屏工具会优先读到“重要的内容(good stuff)”;或者让用户通过键盘操作,率先获取到这篇文章中的链接,其次才是侧边栏里的。

话虽如此,上述原则也并非放之四海而皆准。比如上面谈到的示例页,尽管页面主图没有下方的内容重要,但它不失为整个页面最抢眼的部分,因此考虑将其留在页面顶部的位置也是合理的。另外,它还带有少量文字内容,浏览起来也不费工夫。

重点

做响应式设计时,一定要确保 HTML 里涵盖了各种屏幕尺寸所需的全部内容。每个屏幕尺寸固然可以有各自的 CSS 样式,但它们必须共享同一份 HTML。

再来看看稍大一些的视口(viewport)该如何设计。屏幕较小的移动端布局固然要先行,但在一头扎进移动端样式之前,大屏需要的整体设计也得做到心里有数,以便在代码结构方面合理决策。

移动端样式一旦就绪,就需要在页面上分别设置一中一大两个 断点(breakpoint。这可以借助 媒体查询(media queries) 叠加额外的样式来实现。额外引入的这些样式仅对尺寸更大的屏幕生效。

断点的定义

断点(breakpoint 是一个特殊的临界点。它对应于浏览器的某个宽度或高度。页面样式会在屏幕尺寸跨过该点时发生改变,旨在为当前的屏幕尺寸提供最佳的布局效果。

本章后续还将对这些断点的设置细节做深入考察,现阶段只要知道页面会添加这些断点就行了;此外,还需要考虑在更大尺寸的屏幕下,页面布局一般都会涉及哪些样式调整。图 7.3 显示的是中等屏幕下的页面布局效果:

图 7.3 中等屏幕视口下的页面效果
图 7.3 中等屏幕视口下的页面效果

这时的视口尺寸相比移动端稍微多了一些可供发挥的余地。标题栏和页面主图可以设置更大的内边距;各菜单项由于刚好可以在一行铺开,因此也无需隐藏了;汉堡图标因为不用控制菜单的开合,也随即去掉了;而主内容区则可以设计三个等宽列,并让大部分元素填充在距离视口边缘 1em 的范围内。

而尺寸更大的视口则与上面一样,但也可以适当增加页面的外边距,或者让页面主图再大些,如图 7.4 所示:

图 7.4 大尺寸屏幕视口下的页面效果
图 7.4 大尺寸屏幕视口下的页面效果

由于要先实现移动端设计,所以才更有必要了解清楚页面在大尺寸屏幕视口下的渲染效果,以便在一开始就确定出合理的 HTML 结构。我们先创建一个新页面和一个新样式表,然后将代码清单 7.1 中的 HTML 标记添加到新页面中。

这些代码看起来很像非响应式设计下的版本,但我针对移动端设计融入了好几处调整,稍后再详述。

代码清单 7.1 响应式设计下的页面 HTML 标记

<!doctype html>
<html lang=”en-US”>
<head>
  <meta charset="UTF-8">
  <title>Wombat Coffee Roasters</title>
  <link href="styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
<header id="header" class="page-header">
  <div class="title">
    <h1>Wombat Coffee Roasters</h1>
    <div class="slogan">We love coffee</div>
  </div>
</header>

<nav class="menu" id="main-menu">
  <button class="menu-toggle" id="toggle-menu"> <!-- 定义移动端菜单的“汉堡”状按钮 -->
    toggle menu
  </button>
  <div class="menu-dropdown"> <!-- 在移动端设备上默认隐藏的主菜单 -->
    <ul class="nav-menu">
      <li><a href="/about.html">About</a></li>
      <li><a href="/shop.html">Shop</a></li>
      <li><a href="/menu.html">Menu</a></li>
      <li><a href="/brew.html">Brew</a></li>
    </ul>
  </div>
</nav>
<aside id="hero" class="hero">
  Welcome to Wombat Coffee Roasters! We are
  passionate about our craft, striving to bring you
  the best hand-crafted coffee in the city.
</aside>
<main class="main">
  <section class="column"><!-- 用于中等尺寸和大尺寸视口的三列布局元素 -->
    <h2 class="subtitle">Single-origin</h2>
    <p>We have built partnerships with small farms
      around the world to hand-select beans at the
      peak of season. We then carefully roast in
      <a href="/batch-size.html">small batches</a>
      to maximize their potential.</p>
  </section>
  <section class="column"><!-- 用于中等尺寸和大尺寸视口的三列布局元素 -->
    <h2 class="subtitle">Blends</h2>
    <p>Our tasters have put together a selection of
      carefully balanced blends. Our famous
      <a href="/house-blend.html">house blend</a>
      is available year round.</p>
  </section>
  <section class="column"><!-- 用于中等尺寸和大尺寸视口的三列布局元素 -->
    <h2 class="subtitle">Brewing Equipment</h2>
    <p>We offer our favorite kettles, French
      presses, and pour-over cones. Come to one of
      our <a href="/classes.html">brewing
      classes</a> to learn how to brew the perfect
      pour-over cup.</p>
  </section>
</main>
</body>
</html>

上述代码中,切换移动端菜单的按钮位于 nav 元素内。nav-menu 元素放置的位置也恰好可以同时满足移动端和桌面端的设计需求。样式类 maincolumn 则用于桌面端的布局设计(构建新页面时可能一开始还摸不清这些元素的作用,不过不要紧,后面会演示)。

接下来给页面添加样式。先处理比较简单的元素样式,如页面字体、标题、字体颜色等,如图 7.5 所示。因为当前关注的是移动端样式,所以要将浏览器的宽度调小来模拟一个移动设备的尺寸。这样就能看到小屏幕上的页面是什么样的了。

图 7.5 加上简单样式后的移动端页面效果
图 7.5 加上简单样式后的移动端页面效果

该页面对应的样式如代码清单 7.2 所示。将它们更新到本地样式表,以建立边框盒模型(border box sizing),并让代码设置的字体和链接颜色生效。该代码用到了第 2 章(第 2.4.1 节)中介绍过的基于视口的响应式字号,并且定义了页面标题即主内容区的相关样式。

代码清单 7.2 给页面设置初始样式

*,
*::before,
*::after {
  box-sizing: border-box;
}

:root {
  font-size: clamp(0.9rem, 0.5svw + 0.6em, 1.125rem); /* 基础字号会根据视口大小适当缩放(详见第2章内容) */
}

body {
  margin: 0;
  font-family: Helvetica, Arial, sans-serif;
}

a:link {
  color: #1476b8;
  font-weight: bold;
  text-decoration: none;
}
a:visited {
  color: #1430b8;
}
a:hover {
  text-decoration: underline;
}
a:active {
  color: #b81414;
}

/* 页面标题栏样式 */
.page-header {
  padding: 0.4em 1em;
  background-color: #fff;
}

/* 主标题样式 */
.title > h1 {
  color: #333;
  text-transform: uppercase;
  font-size: 1.5rem;
  margin-block: 0.2em;
}

/* 副标题样式 */
.slogan {
  color: #888;
  font-size: 0.875em;
  margin: 0;
}

.hero {
  padding: 2em 1em;
  text-align: center;
  background-image: url(coffee-beans.jpg); /* 给页面加上主图 */
  background-size: 100%;
  color: #fff;
  text-shadow: 0.1em 0.1em 0.3em #000; /* 深色的文字阴影效果确保浅色文字在复杂背景中清晰可辨 */
}

/* 主内容区样式 */
main {
  padding: 1em;
}

.subtitle {
  margin-block: 1.5em;
  font-size: 0.875rem;
  text-transform: uppercase;
}

上面的样式代码大都比较简单。它将页面标题和正文中的副标题都转换为全大写(all caps),还给页面各组件加上了内外边距,并调整了字号。

主图样式中的 text-shadow 属性可能比较陌生。该属性由若干个属性值构成。由这些值共同定义的文字阴影效果,最终将渲染到目标文字的后面。这些值的前两个,分别代表直角坐标系中的坐标位置,表征该阴影相对于文字位置的偏移量;而 0.1em 0.1em 则表明该阴影将相对于文字稍微往右下方偏移;第三个值(0.3em)为模糊半径,代表该阴影区域的模糊程度。最后的 #000 则指明了阴影的颜色。

7.1.1 创建移动端菜单

Creating a mobile menu

至此,页面中要实现的最复杂的部分就只剩菜单了。完成后的页面效果将如图 7.6 所示:

图 7.6 在移动端设备打开的导航菜单效果图
图 7.6 在移动端设备打开的导航菜单效果图

译注

由于翻译是分开发表的,不像原文可以在同一页面上下滚动来回看提到的 HTML 片段;即便用 PDF 格式的电子书进行阅读,定位某个内容也比看单独发表的博文容易得多,因此书里并没有给出汉堡按钮相关的 HTML 片段,后面的讨论只能全靠大家想象。这里特地补上一段,减轻大家的跳转负担:

<header id="header" class="page-header">
  <div class="title">
    <h1>Wombat Coffee Roasters</h1>
    <div class="slogan">We love coffee</div>
  </div>
</header>

<nav class="menu" id="main-menu">
  <button class="menu-toggle" id="toggle-menu"> <!-- 定义移动端菜单的“汉堡”状按钮 -->
    toggle menu
  </button>
  <div class="menu-dropdown"> <!-- 在移动端设备上默认隐藏的主菜单 -->
    <ul class="nav-menu">
      <li><a href="/about.html">About</a></li>
      <li><a href="/shop.html">Shop</a></li>
      <li><a href="/menu.html">Menu</a></li>
      <li><a href="/brew.html">Brew</a></li>
    </ul>
  </div>
</nav>

不管用什么语言写代码都有个迭代过程,CSS 也不例外。在本页中,菜单的设计就经过了一番深思熟虑。<nav> 原本是放在 <header> 中的,因为希望汉堡图标出现在 <header> 元素内;后来写 CSS 的时候发觉不对,应该将这两个元素设计成并列的兄弟节点,这样才能在桌面端中呈自然的上下排列。HTML 里的某些内容有时候也需要像这样反复调试,才能达到最佳的预期效果。

从功能上看,该菜单很像上一章(代码清单 6.9)里的下拉菜单:先隐藏 menu-dropdown 元素,再用 JavaScript 添加一些交互功能;用户一点击(或轻触)menu-dropdown 元素,就会出现下拉菜单;再点一次,菜单就隐藏。

提示

读屏工具会将某些 HTML5 元素,比如 <form><main><nav> 以及 <aside> 视为 导航标识(landmarks,以帮助视力欠佳的用户群体快速浏览网页。因此,最好将控制菜单切换的汉堡按钮设计在 <nav> 元素的里面,以便用户浏览到这里时能快速发现它;不然等浏览到 <nav> 时会误以为里面是空的(因为读屏工具会忽略掉样式为 display: none 的下拉菜单)。

汉堡菜单的弊端

汉堡菜单已成为近年来一种流行的移动端设计方案。它解决了在小尺寸屏幕下显示更多内容的问题,但也存在弊端。事实证明,隐藏页面关键元素(如主导航菜单)会降低用户与之交互的可能性。

这些因素需要您和您的团队或设计师综合考量:有时启用汉堡菜单会很合适,有时则未必。无论如何,掌握汉堡菜单的构建方法还是有必要的,也很重要。

根据代码清单 7.1 的 HTML 标记内容,<nav> 作为同级元素出现在了 <header> 之后,这意味着它将随文档流来到标题区的下方位置显示。为了达到既定的设计要求,只能采用一种不太常用的做法,将 menu-toggle 按钮元素绝对定位到上面的标题栏区域内。根据代码清单 7.3 更新菜单样式:

代码清单 7.3 移动端菜单样式代码

.menu {
  position: relative; /* 给绝对定位的两个子元素创建包含块 */
}

.menu-toggle {
  position: absolute;
  top: -1.2em; /* 用负的 top 值将按钮拉到包含块的上方 */
  right: 0.1em;

  border: 0; /* 覆盖浏览器的默认按钮样式 */
  background-color: transparent;

  font-size: 3em;
  width: 1em;
  height: 1em;
  line-height: 0.4;
  text-indent: 5em; /* 隐藏按钮文字内容,字号设为 1em */
  white-space: nowrap;
  overflow: hidden;
}
.menu-toggle::after {
  position: absolute;
  top: 0.2em;
  left: 0.2em;
  display: block;
  content: "\2261"; /* 用一个代表汉堡图标的 Unicode 字符盖在按钮上面 */
  text-indent: 0;
}

.menu-dropdown {
  display: none;
  position: absolute;
  right: 0;
  left: 0;
  margin: 0;
}

.menu.is-open .menu-dropdown {
  display: block; /* 在菜单加上 is-open 类时显示下拉菜单 */
}

以上代码实现了很多效果,但大部分都是讲过的内容。相对定位的菜单容器为其内部的两个子元素(切换按钮与下拉菜单)建立了包含块;切换按钮通过负的 top 值往上走,right 属性则将其定位到屏幕右侧,最终位于页面标题区的右侧。

然后在按钮上设置一些文字替换的“小把戏”(replacement trick):限定宽度、加大 text-indent(文字缩进量)、并隐藏溢出部分,以实现按钮文字的隐藏;然后给按钮的 ::after 伪元素设置一个 Unicode 字符(\2261)作为内容。该字符是一个数学符号(译注:即恒等号、全等号 ),由三条横线组成。要是想进一步定制图标,还可以在伪元素上设置背景图片。

如果拿不准每个样式的作用是什么,可以先把它们注释掉,看看页面的实际效果。该页面在大尺寸屏幕下看着有点滑稽。把浏览器窗口调小些,看着就更像移动端里的效果了。

而样式类 is-open 则在打开菜单时通过 JavaScript 添加。下拉菜单仅在按钮存在该样式类时可见。下拉菜单隐藏前的页面效果,如图 7.7 所示(注意页面左侧位于主图上方的那四个菜单链接)。

图 7.7 样式生效后的汉堡按钮效果图
图 7.7 样式生效后的汉堡按钮效果图

按下切换按钮,以下 JavaScript 代码会添加或删除样式类 is-open。将这些代码添加到 <head> 标签内:

代码清单 7.4 实现下拉功能的 JavaScript 代码

<script type="module">
  var button = document.getElementById('toggle-menu');
  button.addEventListener('click', function(event) { // 点击事件的监听器(亦即触屏设备的轻触事件监听器)
    event.preventDefault();
    var menu = document.getElementById('main-menu');
    menu.classList.toggle('is-open');  // 在 menu 元素上切换 is-open 样式类
  });
</script>

此时点击汉堡图标,就会打开下拉菜单;同时可以看到,菜单文字出现在了网页内容的前方;再次点击按钮,则菜单关闭。这样就通过 CSS 实现了对应元素的显示与隐藏,而 JavaScript 只负责切换一个样式类即可。

现在下拉菜单可以正常工作了,但 nav-menu 元素还需加些样式。请根据代码清单 7.5 更新样式表:

代码清单 7.5 导航菜单的样式设置

.nav-menu {
  margin: 0;
  padding-left: 0;
  border: 1px solid #ccc;
  list-style: none;
  background-color: #000;
  color: #fff;
}

.nav-menu > li + li { /* 给每个菜单元素设置上边框 */
  border-top: 1px solid #ccc;
}

.nav-menu > li > a {
  display: block;
  padding: 0.8em 1em; /* 设置适当的内边距以确保点击区域足够大 */
  color: #fff;
  font-weight: normal;
}

上面的样式都没啥新东西,由于菜单是一个列表元素(<ul>),需要覆盖浏览器的默认左内边距并去掉列表项的默认原点图标。相邻兄弟组合选择器则会选中除了第一个菜单项外的所有元素,并给它们指定一个上边框。

有个地方需要特别注意一下:菜单链接元素的内边距设置。因为样式是针对移动端设计的,通常是在触屏设备上显示。因此点击的核心区域应该设计得足够大,方便一根手指进行操作。

提示

在应对移动端触屏设备的样式设计时,要确保所有的关键动作元素(key action items)都足够大,以便用一根手指轻松点击。切忌让用户为了准确点中某个小小的按钮或者链接而不得不放大页面。

7.1.2 给视口添加 meta 标签

Adding the viewport meta tag

至此,移动端设计业已完成,但还漏了一个关键细节:视口的 meta 标签(meta tag。该 HTML 标签会告诉移动端设备,该页面已经特地为小尺寸屏做了适配。如果不加的话,移动端的浏览器就会认定该页面并非响应式设计,并试图模拟桌面端浏览器进行渲染,之前所做的那些移动端设计就都白费了。这种事可干不得。

因此需要根据代码清单 7.6 更新 HTML 中的 <head> 元素,将 meta 标签包含进去。这也是给浏览器以明示,您已经专门考虑了网站的响应式行为。

代码清单 7.6 为移动端的响应式设计添加视口 meta 标签

<head>
  <meta charset="UTF-8">
  <!-- 视口 meta 标签: -->
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Wombat Coffee Roasters</title>
  <link href="styles.css" rel="stylesheet" type="text/css" />
</head>

meta 标签的 content 属性包含两个选项。首先,它告诉浏览器在解析 CSS 时要将设备的宽度作为假定宽度,而不是一个全屏的桌面版浏览器的宽度;其次,在页面加载时,要用 initial-scale 将缩放比例设置为 100%

提示

现代浏览器的开发者工具(DevTools)提供了模拟移动端浏览器的功能,包括较小的视口尺寸和视口 meta 标签的行为。这些实用工具能帮助我们测试响应式设计。更多信息,请参阅 https://mng.bz/ppa5Chrome 浏览器)或者 https://mng.bz/OZnKFirefox 浏览器)

视口 meta 标签还提供了其他配置选项,但以上配置应该最能满足实际需求。例如,可以明确设置 width=320 让浏览器假定视口宽度为 320px,但通常并不建议这样设置,因为移动端的设备尺寸范围很广。借助 device-width 就能让内容以最合适的尺寸进行渲染。

此外,开发人员偶尔还会在 content 属性中添加第三个配置项 user-scalable=no,用于阻止用户在移动端设备上通过两个手指进行缩放。通常该设置在实践中并不友好,不推荐使用。当链接太小无法点击,或者用户想把某个图片看得更清楚时,该设置会阻止他们缩放页面。有关视口 meta 标签的更多信息,请参阅 MDN 文档:https://mng.bz/Y7po

7.2 媒体查询

Media queries

响应式设计的第二个原则是使用 媒体查询媒体查询(media queries 允许某些样式仅在页面满足特定条件时才生效。这样就可以根据屏幕尺寸定制样式。可以先针对小屏设备定义一套样式,再针对中等屏幕设备定义另一套样式,最后针对大屏设备再定义一套样式,这样就能让页面内容呈现多种布局。

媒体查询使用写作 @media@规则 选中符合特定条件的设备。一条简单的媒体查询写法如以下代码所示:

@media (min-width: 560px) {
  .title > h1 {
    font-size: 2.25rem;
  }
}

在最外层的大括号内可以定义任意的样式规则。@media 规则会进行条件检查,只有满足所有条件时,才会将这些样式应用到页面上。本例中浏览器会检查 min-width: 560px;,即仅当设备的视口宽度大于等于 560px 时,样式类 title 下的一级标题 h1 元素才能设为 2.25rem 的大号字;若视口小于 560px,则内部规则集都会被忽略。

媒体查询中的规则仍然遵循常规的层叠原理。它们既可以覆盖媒体查询外部的样式规则(根据选择器的优先级或源码顺序等),也可能被这些样式规则覆盖掉。而媒体查询本身不会影响到它里面的选择器优先级。

关于在媒体查询中使用 px 与 em

虽然也可以使用很多其他单位来定义媒体查询,但 pxem 无疑是最常用的。例如,除了写成上面的 @media (min-width: 560px),还可以写成 @media (min-width: 35em)。而用 rem 则通常与用 em 差不多。

在媒体查询中,em 的值基于浏览器的默认字号,也就是关键字 medium(译注:媒介、媒体)通常对应的 16px。但如果用户在浏览器设置中更改了默认字号,em 的含义也会随之改变。这可能对设计而言是个好事,随着默认字号的增加,页面断点的尺寸也会同步增大。但要注意一点:该行为在所有浏览器中并不一致(Safari 就是个突出的反例)。

在过去,由于浏览器存在不一致性,开发人员(包括我本人在内)都会推荐在媒体查询中使用相对单位 em;然而在最近版本的浏览器中,像素单位 px 的表现更加一致。因此,在很多情况下我反倒更推荐使用 px;而当我希望断点处的尺寸可以随默认字号同步缩放时,再换回相对单位 em 也不迟。

接下来演示几个给页面设置媒体查询的例子。找到样式表中的 .title 样式,并按照代码清单 7.7 中的样式添加媒体查询,让页面标题拥有响应式行为。

代码清单 7.7 给页面标题样式添加断点

.title > h1 {
  color: #333;
  text-transform: uppercase;
  font-size: 1.5rem;
  margin-block: 0.2em;
}

@media (min-width: 560px) {  /* 匹配宽度在 560px 及以上的视口 */
  .title > h1 {
    font-size: 2.25rem;  /* 用更大的字号覆盖移动端的小号字(1.5rem) */
  }
}

此时,根据视口大小的不同,页面标题就有了两种不同的字号:视口宽度不足 560px 时字号为 1.5rem;超过 560px 则为 2.25rem(译注:刚好等于 560px 时也是 2.25rem)。

通过缩放浏览器窗口就能测出标题的样式:窗口很窄的时候,看到的是适配移动端的小字号;慢慢扩大浏览器窗口,字号也会平滑过渡,因为页面通过 clamp()(详见代码清单 7.2)事先设置了响应式的字号;而当页面宽度达到 560px 时,标题的字号则会立马变为 2.25rem

窗口宽 560px 的这个临界点,就被称为一个 断点(breakpoint。大多数情况下,整个样式表里的媒体查询只会复用几个为数不多的断点。本章稍后会介绍如何挑选合适的断点。

7.2.1 深入理解媒体查询的类型

Understanding types of media queries

还可以进一步将两个条件用 and 关键字联合起来,组合为 一个 媒体查询:

@media (min-width: 320px) and (max-width: 560px) { ... }

这样的组合式媒体查询仅在设备同时满足这两个条件时才会生效。如果设备只需要满足其中一个条件,可以用逗号分隔,写作:

@media (max-width: 320px), (min-width: 560px) { ... }

上述媒体查询将匹配宽度小于等于 320px、或者大于等于 560px 的视口。另外,关键字 or 已收入 W 3 C 媒体查询第四级规范(W 3 C Media Queries Level 4 specification),与这里的逗号等效。该写法相对较新,仅在最近版本的 ChromeFirefoxSafari 浏览器中有效。

1 最小宽度、最大宽度及其他

min-width, max-width, and beyond

刚才那段代码中,min-width 用于匹配视口大于特定宽度的设备;max-width 则相反,用于匹配视口小于特定宽度的设备。它们被统称为 媒体特征(media feature

min-widthmax-width 是目前使用最为广泛的媒体特征,但还有一些别的媒体特征可供选用。下面列出了其中的部分写法:

查看完整的媒体特征列表,详见 MDN 官方文档:https://developer.mozilla.org/en-US/docs/Web/CSS/@media

基于分辨率的媒体查询对于能够渲染更高分辨率的图像或图标的设备屏幕非常有用。这样一来,屏幕分辨率较低的用户在加载较大的图像时就不会浪费带宽,因为他们看不出差异。本章稍后还将进一步介绍响应式图像的相关知识。

提示

媒体查询还可以放在 <link> 标签中。在页面加入 <link rel="stylesheet" media="(min-width: 45em)" href="large-screen.css" />,只有在 min-width 处的媒体查询条件被满足时,文件 large-screen.css 中的样式才会在页面生效。但要注意,此时无论视口的宽度如何,该样式文件都会被下载。因此该方案旨在更好地组织代码,并不会节省网络流量。

此外,还有个关键点值得注意:在媒体查询中无法访问自定义属性。也就是说,像 @media (min-width: var(--breakpoint)) 这样的写法是无效的,因为 var() 的使用场景仅限于在给某个属性(property)的样式声明赋上某个值时使用。

2 媒体查询中的范围表示法

Range syntax in media queries

媒体查询还有一种新的语法,可以使用小于和大于符号。例如,之前的 @media (min-width: 800px) 可以直接写成 @media (width >= 800px)。此外,还可以为查询提供一组上下边界(upper and lower bound),例如写作 @media (800px < width < 1200px),表示仅对宽度介于 800px1200px 之间的视口生效。

该语法较之传统写法有两个明显优势:首先,使用了该语法的媒体查询读起来更加直观,尤其是在定义上下边界时,像 (800px <= width <= 1200px) 的写法比之前冗长的 (min-width: 800px) and (max-width: 1200px) 更容易理解。

而该语法的第二个好处,则是提供了小于等于(<=)和大于等于(>=)符号的配置选项。使用传统写法的媒体查询时,假设某个应用场景中同时存在像 max-width: 30emmin-width: 30em 这样的两个媒体查询。如果视口宽度恰巧为 30em,则二者都将生效,从而可能带来一些不符合预期的效果。而使用新的范围表示方法,就能通过 是否使用等号 来准确描述目标范围在对应边界上的包含情况。再来看几个类似的用法:

@media (480px <= width < 1200px) { ... }
@media (800px < height) { ... }
@media (width = 1600px) { ... }

该语法尽管仍然相对较新,但所有主流浏览器的最新版本都对其提供了支持。获取该语法在各版本浏览器中的最新兼容情况,详见:https://caniuse.com/css-media-range-syntax

3 浅色主题与深色主题

Light and dark themes

此外,您还可以使用媒体查询来检测用户的操作系统是否设置成了深色模式(dark mode)并据此来为页面提供浅色或深色背景,以匹配用户的偏好:

@media (prefers-color-scheme: dark) {
  :root {
    --theme-font-color: white;
    --theme-background: #222;
  }
}

或者匹配浅色主题来进行设置:

@media (prefers-color-scheme: light) {
  :root {
    --theme-font-color: #222;
    --theme-background: #eee;
  }
}

我发现,像上述代码这样充分利用自定义属性,不失为更改整个页面样式的一种相对简单的做法,只要能确保在样式表中始终如一地使用自定义属性就行。

实际运用过程中,完成这样的任务肯定不止这两个自定义变量。后续章节将进一步深入探讨颜色变量的管理。

4 媒体类型

Media types

最后一个要介绍的媒体查询配置选项,为 媒体类型(media type。常见的两种类型为 screenprint。使用 print 媒体查询可以控制打印时的页面布局,这样就能在打印时去掉背景图(为了省墨),或者隐藏不必要的导航栏。毕竟用户打印网页时,通常只想打印主体内容。

要编写仅在打印时生效的样式(即打印样式),需使用查询语句 @media print。与 min-width 及其他媒体特征一样,该语句不用加小括号。而针对屏幕样式,则使用 @media screen

开发 CSS 的时候,通常在事后才会处理打印样式(an afterthought),而且只在确实需要打印时才会考虑;但还是有必要思考用户是否想要打印该网页。为了帮助用户打印出网页,需要采取一些通用步骤。大多数情况下,都需要将基础打印样式放在 @media print {...} 的媒体查询内。

要使用 display: none 来隐藏像导航菜单、页脚这样的次要内容。用户打印网页时,他们绝大多数情况下只关心网页的主体内容。

还可以将整体的字体颜色设置为黑色,并去掉文字后面的背景图片或背景色。大多数情况下,使用通用选择器就能搞定这一切。以下代码使用了 !important 标记,这样就不用担心被后面的样式代码覆盖掉。

@media print {
  * {
    color: black !important;
    background: none !important;
  }
}

花费一些时间来优化打印样式,对用户来说是一项很不错的服务。如果要开发的网站预计会有很多打印需求(例如食谱类网站),那就应该花更多时间确保所有的样式都能正常打印。

7.2.2 页面断点的添加

Adding breakpoints to the page

通常,采用移动端优先的设计方案也意味着最常用的媒体查询类型应该是 min-width。在任何媒体查询之前,最先写的应该是移动端样式,接着再逐步过渡到更大的断点样式上。整体结构如代码清单 7.8 所示(先别急着将下面的代码加入页面)。

代码清单 7.8 响应式 CSS 的通用整体结构

.title { /* 移动端样式,对所有断点生效 */
  ...
}

@media (min-width: 35em) { /* 中等屏断点,覆盖对应的移动端样式 */
  .title {
    ...
  }
}

@media (min-width: 50em) { /* 大屏断点,覆盖对应的小屏及中等屏断点的样式 */
  .title {
    ...
  }
}

最先生效的样式是移动端样式,因为它们不在媒体查询里,故而对所有断点都生效;其次生效的是对应中等屏幕的媒体查询,其样式规则基于移动端样式构建,并且会覆盖相应的移动端样式;最后才是针对大尺寸屏的媒体查询,在这里完成最后的样式添加。

有的设计可能只需要一个断点,有的则需要多个。对于页面上的许多元素而言,也无需给每个断点都设计样式,因为对中小屏适用的断点样式在大尺寸屏也同样会生效。

有时候移动端的样式可能会很复杂,在大一些的断点里可能需要花很大篇幅去覆盖现有样式,此时就要将这些样式放到以 max-width 为过滤条件的媒体查询中,这样就只对尺寸较小的断点生效。但是 max-width 媒体查询用多了也可能是没有严格遵循移动端优先设计的结果。max-width 媒体查询通常只是用于排除某些例外情况,而非惯用手法。

接下来给中等屏幕断点添加样式。在较大的屏幕上,可用空间较多,布局可以适当宽松一些。在代码清单 7.9 中,先是给标题区和主内容区设置了更大的内边距,然后单独给主图区加大了内边距,使其更加吸睛,同时也给页面带来了更多视觉上的趣味性。导航菜单不必隐藏了,汉堡图标也不需要了,菜单项就让它们一直显示出来(详见代码清单 7.10)。最终可以将主内容区设计为三列布局(详见代码清单 7.11)。如此一来,页面效果将如图 7.8 所示:

图 7.8 中等屏幕断点下的页面效果

【图 7.8 中等屏幕断点下的页面效果】

有些改动显而易见,比如适当增大了内边距和字号。通常,最好每次按照相关选择器的规则立即修改对应的样式。简单起见,这些改动都合并到了代码清单 7.9 中。将下列代码加入您的样式表。

代码清单 7.9 中等屏幕断点下的内边距及字体样式调整

.page-header {
  padding: 0.4em 1em;
  background-color: #fff;
}

@media (min-width: 560px) {
  .page-header {
    padding: 1em; /* 增加标题的内边距 */
  }
}

.hero {
  padding: 2em 1em;
  text-align: center;
  background-image: url(coffee-beans.jpg);
  background-size: 100%;
  color: #fff;
  text-shadow: 0.1em 0.1em 0.3em #000;
}

@media (min-width: 560px) {
  .hero {
    /* 增加主图的内边距及字号 */
    padding: 5em 3em; 
    font-size: 1.2rem;
  }
}

main {
  padding: 1em;
}

@media (min-width: 560px) {
  main {
    padding: 2em 1em; /* 增加主元素的内边距 */
  }
}

务必确保每个媒体查询都位于其所要覆盖的样式之后,这样其内部的样式才会有更高的优先级。将浏览器从窄变宽,看看视口宽度达到 560px 时上述变更是否生效。

注意

务必将媒体查询放在需要覆盖的任何样式之后,以确保其样式能覆盖具有相同选择器优先级的其余样式。

接下来处理菜单样式。菜单将涉及两处变更:首先,要将下拉菜单的打开和关闭行为移除掉,这样才能始终可见;其次,要将菜单从之前的垂直排列改为水平排列布局。将代码清单 7.10 中的媒体查询代码添加到之前写的 .menu.nav-menu 样式后面。最后,再次确保这些媒体查询样式的位置要放在对应的移动端样式后面,以便根据实际需要覆盖掉它们。

代码清单 7.10 为中等屏幕断点重构导航菜单样式

@media (min-width: 560px) {
  .menu-toggle {
    display: none; /* 隐藏切换按钮 */
  }

  .menu-dropdown {
    display: block; /* 显示下拉菜单项 */
    position: static; /* 覆盖绝对定位 */
  }
}

@media (min-width: 560px) {
  .nav-menu { 
    /* 将菜单改为弹性容器,并让菜单项延展至填满屏幕宽度 */
    display: flex;
    border: 0;
    padding-inline: 1em;
  }

  .nav-menu > li {
    flex: 1; /* 将菜单改为弹性容器,并让菜单项延展至填满屏幕宽度 */
  }

  .nav-menu > li + li {
    border: 0;
  }

  .nav-menu > li > a {
    padding: 0.3em;
    text-align: center;
  }
}

虽然前面为了适配移动端布局设置了很多复杂的样式规则,但覆盖这些样式让布局恢复到静态块级元素(static, block-level element)却没那么复杂。例如移动端样式中的 topleftright 值就无需调整,因为它们对静态定位(static positioning)元素不生效。

用 Flexbox 布局处理列表项是个很不错的解决方案,它可以让各列表项延展到填满可用空间。菜单元素的内边距也同其他元素一样来进行调整,只不过这里变成了缩小内边距。在中等屏幕断点下,可以假定用户没在小尺寸屏的手机上访问页面,故而也无需设置当时那么大的可点击区域。

7.2.3 响应式列的添加

Adding responsive columns

最后一步是要为中等屏幕断点引入多列布局。与前面几章构建多列布局的方式一样,只是需要将这些样式放到一个媒体查询里,以免影响到小于这个断点尺寸的屏幕设备。

之前写示例页面的 HTML 标记时,我们在需要作三列布局的元素上设置了一个样式类 main。现在是启用它的时候了,将如下代码更新到本地样式表:

代码清单 7.11 媒体查询中的三列布局

@media (min-width: 650px) {
  .main {
    /* 定义一个包含三列的网格布局 */
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    
    gap: 1.5em;
    
    /* 限制网格宽度并水平居中 */
    max-inline-size: 1400px;
    margin-inline: auto;
  }
}

然后尝试缩放浏览器的窗口大小,经过断点时会看到各列快速重新定位:宽度小于该断点时,这些元素没有特定样式,因此会按照常规文档流的行为特征自上而下进行排列;而当宽度超过该断点后,这些元素就会变成相应的弹性容器和弹性元素。

不少响应式设计最终都会遵循这样的思路:当设计要求元素并排放置时,多行并排只对大尺寸屏幕生效;在小尺寸屏中,则让各元素独占一行,填满整个屏幕宽度。这种方法适用于多列、文字旁的多张图片、或者在小尺寸屏下布局显得拥挤(cramped)的其他元素。

注意,代码清单 7.11 中的断点尺寸比之前设置的略宽,这里用的是 650px,因为三列布局在达到这个宽度时才开始显得拥挤。本例中,宽度不足 650px 时,每列就太窄了。设计时没必要在所有位置都恪守完全相同的断点尺寸。

Web 设计师 Brad Frost 整理了一份响应式页面布局的列表,可以访问 https://bradfrost.github.io/this-is-responsive/patterns.html 进行查看。响应式设计中的列非常灵活多变,比如设计成一宽一窄的组合、或者等宽列布局、双栏布局、三栏布局等等,不一而足。最终,这些列的布局都会使用与本章类似的方式实现,在列的具体组合或者列宽设置上略有不同。

1 尽量精简媒体查询

Minimizing media queries

样式表如果包含了很多大型媒体查询的样式块,可能会变得极其复杂。如果再来点 min-widthmax-width 这样的组合查询,情况只会更糟。当不同屏幕尺寸都对应了多个不同的样式规则时,要一次性记住所有这些内容估计有点困难。

因此,要尽可能地让这些媒体查询块保持轻量与简短。其中一种做法是,在媒体查询外部使用自定义属性,并在媒体查询内因地制宜地进行修改。例如,可以考虑利用自定义属性来定义一个普通的间距尺寸,然后在更大的断点中进行放大:

:root {
  --gap-size: 1rem;
}

@media (min-width: 560px) {
  :root {
    --gap-size: 1.5rem;
  }
}

上述代码中的自定义属性可以在整个页面中使用,而这个轻量的媒体查询块将根据它生效的位置作相应的调整。还有一种方法也能让媒体查询块保持简短,就是在媒体查询块的外围设置网格布局,然后根据屏幕尺寸在媒体查询中重新定义网格的行或者列。

在很多情况下,可能根本不需要媒体查询,自然地换行就能实现响应式效果。通过在 Flexbox 布局中使用 flex-wrap: wrap 并设置合适的 flex-basis 来实现响应式就是个绝佳的解决方案。同理,在网格布局中使用 auto-fit 或者 auto-fill 的网格列,以便在换行前设置好一行放入几个元素,也有异曲同工之妙。

2 断点的选择

Breakpoint selection

本章开头介绍了如何实现简单的响应式元素,以便熟悉媒体查询的相关用法。大多数时候,您可能会先用断点来处理多列布局的设计。建议多试几次,直到找出适合页面布局的断点尺寸。只要确保各列在所选的断点位置不要过窄就行了。

有时候会忍不住想要根据设备来选择断点:这款 iPhone 14 宽多少像素、那个平板设备又是多宽多大……不要总盯着设备不放。市面上有成百上千种设备和屏幕分辨率,根本无法逐一测试。相反,应该选择适合设计的断点,这样不管在什么设备上渲染,都能有很好的表现。

多年来,媒体查询一直是响应式设计的基础,但还有另一种与之相关的工具,人们称它为 容器查询(container query。该工具可以根据容器元素的尺寸大小而非整个页面的大小来更改元素样式。我们将在第 10 章重点考察容器查询。

7.3 流式布局

Fluid layouts

响应式设计的第三个也是最后一个原则,即 流式布局(fluid layout。流式布局,有时也称 液体布局(liquid layout,是指让所用容器的尺寸随视口宽度的变化而变化。它跟固定布局相反,固定布局的列都是用 px 像素或 em 为单位进行定义。固定布局下的容器(例如设置了 width: 800px 的元素)在小尺寸屏幕上可能会超出视口的宽度,导致页面出现水平滚动条;而流式布局下的容器则会自动缩小尺寸以适应视口。

在流式布局中,主页面的容器通常没有明确的宽度,也不会设置百分比作为宽度,但可能会设置左右内边距,或者属性值为 auto 的左右外边距,从而在容器与视口边缘间留出一定空间。也就是说容器可能比视口略窄,但始终不会比视口更宽。

而在主容器中,任何列都用百分比来定义宽度(如正文栏宽 70%,侧边栏宽 30%)。这样无论屏幕宽度是多少都能容得下主容器。用 Flexbox 布局也行,只要设置弹性元素的 flex-grow 属性,更重要的是指定好 flex-shrink 属性,使得元素能填满屏幕宽度。要习惯将容器宽度设置为百分比的做法,而不是给定任何固定大小。

说明

流失布局容器在并非仅限本章示例页面,其实全书也几乎只用了流式布局。现在您应该已经或多或少熟悉这种布局方式了。

一个网格默认情况下其实就是响应式的。在没加 CSS 时,块级元素的宽度不会超过视口,行内元素为避免水平溢出也会换行(wrap);加上 CSS 后,页面的响应式特性就交由您来维护了。这个道理知易行难,但意识到每次都是从一个良好的默认状态开始,有助于我们更好地实现响应式布局。

7.3.1 为大视口添加样式

Adding styles for a large viewport

接下来要为下一个屏幕断电添加媒体查询样式。在实现过程中,您会发现我们从未给容器设置过固定大小的宽度。容器可以自然伸展到 100%(当然要减去一些内外边距)。在三列布局时我们使用了 Flexbox 技术,让每一列占据视口宽度的三分之一。

示例页面最终的大尺寸视口布局效果如图 7.9 所示。实现方法与中等尺寸屏幕视口类似,只是这里的空间更多了。您可以随意使用内边距,这正是接下来要完成的工作。

图 7.9 大视口下的页面布局效果

【图 7.9 大视口下的页面布局效果】

可以看到,左右两侧的内边距已由 1em 增加到 4em。主图中文字周围的内边距也相应增加了,这样图片可以变得更大。新增样式如代码清单 7.12 所示。

将媒体查询块 (min-width: 800px) 内的所有样式添加到示例页面的样式表中。请再次确认将相同的样式规则放到尺寸较小的断点后面(即 .page-header.hero 以及 main),这样媒体查询中的样式才能覆盖前面断点指定好的样式。

代码清单 7.12 大屏断点处增大内边距的样式代码

@media (min-width: 800px) {
  .page-header {
    padding: 1em 4em; /* 将页面的左右内边距增大至4em */
  }
}

@media (min-width: 800px) {
  .hero {
    /* 增加主图内边距,从而得到更大的主图 */
    padding: 7em 6em;
  }
}

@media (min-width: 800px) {
  main {
    padding: 2em 4em; /* 将页面的左右内边距增大至4em */
  }
}

@media (min-width: 800px) {
  .nav-menu {
    padding-inline: 4em; /* 将页面的左右内边距增大至4em */
  }
}

至此,一个具有三处页面断点的响应式页面就大功告成了。可以继续在上面试验,变更断点的宽度,看看浏览体验上会有什么变化。

7.3.2 表格的处理

Dealing with tables

在移动端设备的流式布局中,表格的问题特别多。如果表格列过多,很容易超过屏幕宽度(如图 7.10 所示)。

图 7.10 在移动端设备上表格的右端因为超宽被裁掉了

【图 7.10 在移动端设备上表格的右端因为超宽被裁掉了】

如果可以的话,建议在移动端设备上改用别的方式组织数据。比如将每一行数据单独用一块区域展示,并让每块区域顺次堆叠;或者使用更适合小尺寸屏的可视化图形或图表来展示。但有时就得用表格进行显示。

此时有一种方案是将表格强制显示为一个普通的块级元素,如图 7.11 所示:

图 7.11 同样是表格型数据,但各行与各单元格都设置了 display: block 的样式效果

【图 7.11 同样是表格型数据,但各行与各单元格都设置了 display: block 的样式效果】

该布局由 <table><tr><td> 元素组成,但它们分别设置了 display: block 样式,覆盖了此前默认的属性值(即 tabletable-rowtable-cell 等)。通过使用 max-width 媒体查询可将这些样式变动限制在小尺寸屏幕下。相应的 CSS 代码如代码清单 7.13 所示。(可将代码应用到任意 <table> 标签对比效果)

代码清单 7.13 在移动端设备上强制实现表格响应式布局的样式代码

table {
  inline-size: 100%;
}

@media (max-width: 480px) {
  table, thead, tbody, tr, th, td {
    display: block; /* 让表格的所有元素都呈块级显示 */
  }

  thead tr {
    /* 将表头移出屏幕外,令其隐藏 */
    position: absolute;
    top: -9999px;
    left: -9999px;
  }

  tr {
    margin-block-end: 1em; /* 在每组表格数据间添加一些间隙 */
  }
}

以上样式会让表格各单元格从从上到下排列,并在每个 <tr> 之间留出一定的外边距;但该方案也会让 <thead> 行不再跟下面的每一列对齐,因此要用绝对定位将表头那行移出视口。出于页面可访问性的考虑,这里并没有设置 display: none,否则读屏工具将无法识别表头。这断然不是最完美的解决方案,但在其他方案都不尽人意的时候,也不失为当下最好的效果了。

7.4 响应式图片

Responsive images

响应式设计中,图片需要特别关注:不仅要让图片适应屏幕,还要考虑移动端用户的带宽限制。图片往往是页面上用得最多的一个资源。首先要保证图片充分压缩。在图片编辑器中选择 “Save for Web”(保存以适应 Web)选项能够极大地缩小图片体积,或者使用别的图片压缩工具来压缩图片,比如 tinypng 网站:https://tinypng.com/

此外,还要避免不必要的高分辨率图片。不过,是否 “必要” 还得看视口的尺寸大小。也没有必要为小尺寸屏提供大图,因为它们最终都会被压缩处理。

7.4.1 不同的视口尺寸使用不同的图片

Using multiple images for different viewport sizes

响应式图片的最佳实践是为一个图片创建不同分辨率的副本。如果根据媒体查询得知屏幕的尺寸偏小,那么发送超大图片就完全没必要;最终浏览器还不得不降低图片分辨率,以适配小尺寸屏幕。

利用响应式技术,我们可以为不同屏幕尺寸提供大小最合适的图片。比如页面上的主图,其 CSS 样式如代码清单 7.14 所示,将它们添加到本地样式表。

代码清单 7.14 添加响应式背景图的 CSS 样式

.hero {
  padding: 2em 1em;
  text-align: center;
  background-image: url(coffee-beans-small.jpg); /* 在移动端设备上使用最小的图片 */
  background-size: 100%;
  color: #fff;
  text-shadow: 0.1em 0.1em 0.3em #000;
}
@media (min-width: 560px) {
  .hero {
    padding: 5em 3em;
    font-size: 1.2rem;
    background-image: url(coffee-beans-medium.jpg); /* 在中等屏幕上提供稍大的图片 */
  }
}

@media (min-width: 800px) {
  .hero {
    padding: 7em 6em;
    background-image: url(coffee-beans.jpg); /* 在大屏幕上提供完整分辨率的图片 */
  }
}

在不同屏幕尺寸的浏览器上加载上述页面,根本看不出有什么区别。这就是关键所在。在小断点下,屏幕尺寸不够宽,无论如何都显示不下完整分辨率的图片,但实际加载的图片所省下的流量少说也有几十到上百 Kb。在图片较多的页面上,这些累计节省出的流量就能显著提高页面的加载速度。

7.4.2 使用 srcset 提供对应的图片

Using srcset to serve the correct image

媒体查询固然可以解决通过 CSS 加载图片的问题,但是 HTML 里的 <img> 标签怎么办?对于这种行内图片(inline images),有另一个重要的解决方案:srcset 属性(即 “source set” 的缩写)。

该属性可以给一个 <img> 标签指定不同的图片 URL,并限定相应的分辨率。浏览器会根据自身需要决定加载哪一个图片(如代码清单 7.15 所示)。

代码清单 7.15 响应式 srcset 图片样式

<img alt="A white coffee mug on a bed of coffee beans"
     src="coffee-beans-small.jpg"  <!-- 给不支持srcset的浏览器(如IE、OperaMini等)提供常规src属性 -->
     srcset="coffee-beans-small.jpg 560w,   <!-- 每张图片的URL和它对应的宽度 -->
             coffee-beans-medium.jpg 800w,  <!-- 每张图片的URL和它对应的宽度 -->
             coffee-beans.jpg 1280w"        <!-- 每张图片的URL和它对应的宽度 -->
/>

所有现代浏览器目前均支持 srcset 属性;不支持的旧版浏览器则会根据 src 属性加载相应的 URL。这样一来,就实现了针对多种屏幕尺寸来优化图片。更棒的是,浏览器会针对高分辨率屏自动作出相应调整。如果设备屏幕的像素密度为 2x(即 2 倍),那么浏览器就会相应地加载更高分辨率的图片。

有关响应式图片的更多内容,详见 jakearchibald 网站刊登的文章 The Anatomy of Responsive Images。这篇文章还介绍了其他有用的配置项,例如根据加载的图片调整显示的大小。

提示

图片作为流式布局的一部分,请务必确保它们不会超过容器的宽度。为避免出现这样的情况,一劳永逸的办法是在样式表中加入规则:img { max-inline-size: 100%; }

页面响应式设计的结构实现方式千变万化。最终这些方式都会归结为应用这三大原则:移动端优先(mobile first)媒体查询(media queries)流式布局(fluid layout)。掌握这些设计原则有助于在构建页面时牢记响应式设计的各种需求。

这种方法在相当长的一段时间内被业内证明都是有效的。不过,还有一个重要工具需要补充:容器查询(container queries)。这是 CSS 的最新特性,同时也是众多开发人员多年来翘首以盼的一项强大功能。本书将在接下来的章节中介绍一些必要的基础知识,并在第十章再次探讨响应式设计的相关话题,重点介绍这一功能。

7.5 本章小结 Summary