响应式设计

本章概要

  • 基于多种设备及屏幕尺寸构建页面
  • 用媒体查询,根据视口大小来变更设计
  • 采用移动端优先的方式(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