后面。这意味着它会流动到头部下面的空间。为了实现设计效果,这里需要进行一项不常用的操作:使用绝对定位将菜单切换按钮拉到上面,让它出现在头部元素里面。将代码清单3的菜单样式代码添加到样式表。
/*给两个绝对定位的子元素创建包含块*/
.menu {
position: relative;
}
.menu-toggle {
position: absolute;
/*负的top值将按钮拉到了包含块的上面*/
top: -1.2em;
right: 0.1em;
border: 0;
background-color: transparent;
font-size: 3em;
width: 1em;
height: 1em;
line-height: 0.4;
text-indent: 5em;
white-space: nowrap;
overflow: hidden;
}
.menu-toggle::after {
position: absolute;
top: 0.2em;
left: 0.2em;
display: block;
/*用一个表示汉堡包图标的Unicode符号将按钮覆盖*/
content: "\2261";
text-indent: 0;
}
.menu-dropdown {
display: none;
position: absolute;
right: 0;
left: 0;
margin: 0;
}
/*当给菜单加上类 is-open 的时候,显示下拉菜单*/
.menu.is-open .menu-dropdown {
display: block;
}
在按钮上用一些替换的“小把戏”:限制它的宽度,加上较大的文字缩进,并且将overflow 设置成 hidden,从而隐藏了按钮本身的文字(“toggle menu”)。然后给按钮的::after伪元素加上一个 Unicode 字符( \2261)作为内容。这个字符是一个数学符号,由三条横线组成,即汉堡包菜单。如果想要自定义按钮的图标,可以给伪元素使用背景图片。
使用类 is-open 是另一个“小把戏”。使用这个类时,最后的选择器( .menu.is-open.menudropdown)就会选中下拉菜单。没有这个类时,就不会选中下拉菜单。这样就实现了菜单下拉的功能。如下图所示,还未添加其他样式的下拉菜单(注意左侧主图上面的四个链接)。
代码清单4的 JavaScript 会在按下切换按钮的时候添加和删除类 is-open。将以下代码放到标签之前。
(function() {
var button = document.getElementById('toggle-menu');
button.addEventListener('click', function(event) {//监听器点击事件(也包括触屏设备的轻触事件)
event.preventDefault();
var menu = document.getElementById('main-menu');
menu.classList.toggle('is-open');//在菜单上切换类is-open
})
})();
当点击汉堡包图标的时候,会打开下拉菜单,可以看到菜单的文字出现在网页内容前面。再次点击汉堡包图标就会关闭菜单。这种方式下, CSS 会负责显示和隐藏指定元素, JavaScript 只需要负责改变一个类。
现在下拉菜单可以工作了。还需要给 nav-menu 添加一些样式。将代码清单5添加到样式表。
.nav-menu {
margin: 0;
padding-left: 0;
border: 1px solid #ccc;
list-style: none;
background-color: #000;
color: #fff;
}
.nav > li + li {
border-top: 1px solid #ccc;
}
.nav-menu > li > a {
display: block;
padding: 0.8em 1em;
color: #fff;
font-weight: normal;
}
相邻的兄弟选择器会选中除了第一个元素之外的所有菜单项,给每个菜单项之间加上边框。
有个地方值得注意:菜单项链接周围的内边距。因为是给移动设备设计,通常是触屏设备,所以关键的点击区域应该足够大,并很容易用一个手指点击。
1.2. 给视口添加meta标签
现在移动版设计已经完成,但是还差一个重要细节:视口的 meta 标签。这个 HTML 标签告诉移动设备,你已经特意将网页适配了小屏设备。如果不加这个标签,移动浏览器会假定网页不是响应式的,并且会尝试模拟桌面浏览器,那之前的移动端设计就白做了。为了避免这种情况,按照代码清单6更新 HTML 里的,将 meta 标签包含进去。
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wombat Coffee Roasters</title>
</head>
meta 标签的 content 属性里包含两个选项。首先,它告诉浏览器当解析 CSS 时将设备的宽度作为假定宽度,而不是一个全屏的桌面浏览器的宽度。其次当页面加载时,它使用initial-scale 将缩放比设置为 100%。
这些选项还可以设置为其他值,但是以上配置应该最能满足实际需求。例如,你可以明确设置 width=320 让浏览器假定视口宽度为 320px,但是通常不建议这样,因为移动设备的尺寸范围很广。通过使用 device-width,可以用最合适的尺寸渲染内容。此外 content 属性还有第三个选项 user-scalable=no,阻止用户在移动设备上用两个手指缩放。通常这个设置在实践中并不友好,不推荐使用。当链接太小不好点击,或者用户想要把某个图片看得更清楚时,这个设置会阻止他们缩放页面。
2. 媒体查询
响应式设计的第二个原则是使用媒体查询。 媒体查询( media queries)允许某些样式只在页面满足特定条件时才生效。这样就可以根据屏幕大小定制样式。可以针对小屏设备定义一套样式,针对中等屏幕设备定义另一套样式,针对大屏设备再定义一套样式,这样就可以让页面的内容拥有多种布局。
媒体查询使用@media 规则选择满足特定条件的设备。一条简单的媒体查询如下代码所示。
@media (min-width: 560px) {
.title > h1 {
font-size: 2.25rem;
}
}
在最外层的大括号内可以定义任意的样式规则。 @media 规则会进行条件检查,只有满足所有的条件时,才会将这些样式应用到页面上。本例中浏览器会检查 min-width: 560px。只有当设备的视口宽度大于等于 560px 的时候,才会给标题设置 2.25rem 的字号。如果视口宽度小于560px,那么里面的所有规则都会被忽略。
媒体查询里面的规则仍然遵循常规的层叠顺序。它们可以覆盖媒体查询外部的样式规则(根据选择器的优先级或者源码顺序,同理,也可能被其他样式覆盖。媒体查询本身不会影响到它里面选择器的优先级。
警告: 在媒体查询断点中推荐使用 em 单位。在各大主流浏览器中,当用户缩放页面或者改变默认的字号时,只有 em 单位表现一致。以 px 或者 rem 单位为断点在 Safari 浏览器里不太可靠。同时当用户默认字号改变的时候, em 还能相应地缩放,因此它更适合当断点。
将媒体查询加入代码清单7中,让网页头部拥有响应式行为。
.title > h1 {
color: #333;
text-transform: uppercase;
font-size: 1.5rem;
margin: 0.2em 0;
}
/*命中 35em 以上的断点*/
@media (min-width: 35em) {
.title > h1 {
/*用更大的字体覆盖移动端字体(1.5rem)*/
font-size: 2.25rem;
}
}
现在根据视口大小,网页标题有两种不同的字号。当视口小于 35em 的时候是 1.5rem,大于35em 的时候是 2.25rem。
通过缩放浏览器窗口就能测试标题样式。当窗口很窄的时候,标题是适应移动端的小字号。慢慢放大浏览器窗口,字号会平滑地改变,因为网页被设置了响应式( calc())字号。只要网页宽度达到 35em(或者 560px),标题的字号马上就会变成 2.25rem。
在这里, 560px 这个临界值被称为断点。大多数情况下,整个样式表里的媒体查询只会复用少数几个断点。
2.1. 媒体查询的类型
还可以进一步将两个条件用 and 关键字联合起来组成一个媒体查询,如下代码所示。
@media (min-width: 20em) and (max-width: 35em) { ... }
这种联合媒体查询只在设备同时满足这两个条件时才生效。如果设备只需要满足多个条件之一,可以用逗号分隔,如下代码所示。
@media (max-width: 20em), (min-width: 35em) { ... }
这句媒体查询匹配小于等于 20em 的视口,以及大于等于 35em 的视口
1. min-width、 max-width 等
在前面代码里, min-width 匹配视口大于特定宽度的设备, max-width 匹配视口小于特定宽度的设备。它们被统称为媒体特征( media feature)。
min-width 和 max-width 是目前用得最广泛的媒体特征,但还有一些别的媒体特征,如下所示。
- (min-height: 20em)——匹配高度大于等于 20em 的视口。
- (max-height: 20em)——匹配高度小于等于 20em 的视口。
- (orientation: landscape)——匹配宽度大于高度的视口。
- (orientation: portrait)——匹配高度大于宽度的视口。
- (min-resolution: 2dppx)——匹配屏幕分辨率大于等于 2dppx( dppx 指每个 CSS 像素里包含的物理像素点数)的设备,比如视网膜屏幕。
- (max-resolution: 2dppx)——匹配屏幕分辨率小于等于 2dppx 的设备。
基于分辨率的媒体查询比较棘手,因为该特征比较新,浏览器支持得不太好。一些浏览器支持有限或者要求用前缀语法。比如 IE9~11 和 Opera Mini 不支持 dppx 单位,因此需要使用 dpi(每英寸的像素点数)单位代替(比如用 192dpi 代替 2dpx)。 Safari 和 iOS 的 Safari 支持前缀版的媒体特征-webkit-min-device-pixel-ratio。总之,最好的方式是用两种方式结合起来匹配高分辨率(视网膜屏)的显示器。
@media (-webkit-min-device-pixel-ratio: 2),
(min-resolution: 192dpi) { ... }
这种方式兼容了所有现代浏览器。当你想在高分辨率的屏幕上提供更高清的图片或者图标时,可以用这种方法。这样低分辨率的屏幕就不会浪费带宽去加载大图,因为在这些屏幕上看不出区别。
提示 媒体查询还可以放在标签中。在网页里加入,只有当 min-width媒体查询条件满足的时候才会将 large-screen.css 文件的样式应用到页面。然而不管视口宽度如何,样式表都会被下载。这种方式只是为了更好地组织代码,并不会节省网
络流量。
2. 媒体类型
最后一个媒体查询的选项是媒体类型( media type)。常见的两种媒体类型是 screen 和print。使用 print 媒体查询可以控制打印时的网页布局,这样就能在打印时去掉背景图(节省墨水),隐藏不必要的导航栏。当用户打印网页时,他们通常只想打印主体内容。
针对打印样式,使用@media print 查询语句。不需要像 min-width 或者其他媒体特征那样加小括号。同理,针对屏幕样式,使用@media screen。
考虑打印样式开发 CSS 的时候,通常在事后才会处理打印样式,而且只在需要的时候才会去考虑,但还是有必要思考用户是否想要打印网页的。为了帮助用户打印网页,需要采取一些通用步骤。大多数情况下,需要将基础打印样式放在@media print {…}媒体查询内。
使用 display: none 隐藏不重要的内容,比如导航菜单和页脚。当用户打印网页时,他们绝大多数情况下只关心网页的主体内容。
还可以将整体的字体颜色设置成黑色,去掉文字后面的背景图片和背景色。大多数情况下,用通用选择器就能实现。下面的代码使用了!important,这样就不必担心被后面的代码覆盖。
@media print {
* {
color: black !important;
background: none !important;
}
}
花一点时间实现打印样式就能给用户提供很棒的服务。如果你的网站有很多打印需求(比如食谱网站),那就应该花更多时间确保所有的打印样式正常。
2.2. 给网页添加断点
通常来说,移动优先的开发方式意味着最常用的媒体查询类型应该是 min-width。在任何媒体查询之前,最先写的是移动端样式,然后设置越来越大的断点。整体结构如代码清单8所示。
/*移动端样式,对所有的断点都生效*/
.title {
...
}
/*中等屏幕的断点:覆盖对应的移动端样式*/
@media (min-width: 35em) {
.title {
...
}
}
/*大屏幕断点:覆盖对应的小屏幕和中等屏幕断点的样式*/
@media (min-width: 50em) {
.title {
...
}
}
最优先的是移动端样式,因为它们不在媒体查询里,所以这些样式对所有断点都有效。然后是针对中等屏幕的媒体查询,其中的规则基于移动端样式构建并且会覆盖移动端样式。最后是针对大屏幕的媒体查询,在这里添加网页最后的布局。
有的设计可能只需要一个断点,有的设计可能需要多个断点。对网页上有很多元素来讲,无须给每个断点都添加样式,因为在小屏幕或者中等屏幕的断点下添加的样式规则在大屏幕的断点下也完全有效。
有时候移动端的样式可能很复杂,在较大的断点里面需要花费较大篇幅去覆盖样式。此时需要将这些样式放在 max-width 媒体查询中,这样就只对较小的断点生效,但是用太多的max-width 媒体查询也很有可能是没有遵循移动优先原则所致。 max-width 是用来排除某些规则的方式,而不是一个常规手段。
接下来给中等屏幕断点添加样式。在较大的屏幕上,可用空间较多,布局可以较宽松一些。在代码清单9中,给头部和主元素添加更大的内边距,然后单独给主图加大内边距,使它更加明显,同时给页面增加了更多的视觉趣味。导航菜单不必隐藏了,要隐藏汉堡包图标,并让菜单项一直显示(参见代码清单10)。最终可以将主内容变成三列布局(参见代码清单11),页面将如下图所示。
有些改变显而易见,比如适当增加了内边距和字号。通常,最好每次按照相关选择器的规则立即对应修改。简单起见,我在代码清单9中将代码合并了。将以下代码加入到你的样式表。
.title > h1 {
color: #333;
text-transform: uppercase;
font-size: 1.5rem;
margin: 0.2em 0;
}
/*命中 35em 以上的断点*/
@media (min-width: 35em) {
.title > h1 {
/*用更大的字体覆盖移动端字体(1.5rem)*/
font-size: 2.25rem;
}
}
.hero {
padding: 2em 1em;
text-align: center;
background-image: url(./image/caffee.jpg);
background-size: 100%;
color: #fff;
/*深色的文字投影确保浅色文字在复杂背景中可读*/
text-shadow: 0.1em 0.1em 0.3em #000;
}
@media (min-width: 35em) {
.hero {
padding: 5em 3em;
font-size: 1.2rem;
}
}
main {
padding: 1em;
}
@media (min-width: 35em) {
main {
padding: 2em 1em;
}
}
总是确保每个媒体查询都位于它要覆盖的样式之后,这样媒体查询内的样式就会有更高的优先级。将浏览器从窄变宽,看看网页宽度大于 35em 的时候发生的变化。
接下来处理菜单样式。菜单将涉及两处变化:首先,要将下拉菜单的打开和关闭行为去掉,这样才能始终保持可视;其次,要将菜单从垂直排列改为水平排列布局。这两处改变将一起实现。将代码清单10中的媒体查询代码块添加到之前写的.menu 和.nav-menu 样式之后(后面写的样式会覆盖前面写的样式)。
@media (min-width: 35em) {
/*将菜单的切换按钮隐藏,让下拉菜单的内容显示出来*/
.menu-toggle {
display: none;
}
.menu-dropdown {
display: block;
/*覆盖绝对定位*/
position: static;
}
}
@media (min-width: 35em) {
/*将菜单改为弹性容器,让菜单子元素扩展,填满屏幕宽度*/
.nav-menu {
display: flex;
border: 0;
padding: 0 1em;
}
.nav-menu>li {
flex: 1;
}
.nav-menu>li+li {
border: 0;
}
.nav-menu>li>a {
padding: 0.3em;
text-align: center;
}
}
虽然前面为了适配移动端布局,给菜单添加了很多复杂样式,但我们能够轻松地覆盖样式让布局恢复到静态的块级元素。不需要覆盖移动样式里的 top、 left、 right 属性,因为它们对静态定位的元素不起作用。
用 Flexbox 处理列表项是一个很棒的方法,它能够让列表项增长到填满可用空间。菜单元素的内边距也像其他元素一样所调整,不过这次是减小了内边距。在中等屏幕断点下,可以确定用户不是在小手机上访问,因此不需要将点击区域设置得那么大。
2.3. 添加响应式的列
最后一步是要为中等屏幕断点引入多列布局。
写标记的时候,给想要加上三列布局的地方加上 row 和 column 类。接下来定义相关样式。将代码清单11添加到样式表。
@media (min-width: 35em) {
.row {
display: flex;
/*使用负的外边距将行容器扩大,补偿列的外边距*/
margin-left: -.75em;
margin-right: -.75em;
}
.column {
flex: 1;
/*添加列间距*/
margin-right: 0.75em;
margin-left: 0.75em;
}
}
现在缩放浏览器,到达断点时三列布局就会出现。在小于该断点时,这些元素没有任何样式,因此它们会按照自然文档流的顺序自上而下排列。当大于断点时,这些元素就会变成弹性容器加弹性元素。
许多响应式设计遵循这种方法:当设计要求元素并排摆放时,只在大屏上将它们摆放在一行。在小屏下,允许每个元素单独一行,填满屏幕宽度。这种方法适用于列、媒体对象,以及任意在小屏下容易拥挤的元素。你可能会好奇为什么在代码清单7中要将断点设置为 35em,因为在这个宽度时,三列布局就开始显得拥挤了。本例中小于35em 时,每列就太窄了。
有时候,甚至不需要媒体查询,自然地折行就能实现响应式的列。可以通过在 Flexbox 布局中使用 flex-wrap: wrap 并设置合适的 flex-basis 来实现。还可以在网格布局中使用auto-fit 或者 auto-fill 的网格列,在折行之前就可以决定一行放几个元素。用 inline-block的元素也行,只不过它们无法扩展到填满容器。
3. 流式布局
响应式设计的第三个也是最后一个原则是流式布局( fluid layout)。流式布局,有时被称作液体布局( liquid layout),指的是使用的容器随视口宽度而变化。它跟固定布局相反,固定布局的列都是用 px 或者 em 单位定义。固定容器(比如,设定了 width: 800px 的元素)在小屏上会超出视口范围,导致需要水平滚动条,而流式容器会自动缩小以适应视口。
在流式布局中,主页面容器通常不会有明确宽度,也不会给百分比宽度,但可能会设置左右内边距,或者设置左右外边距为 auto,让其与视口边缘之间产生留白。也就是说容器可能比视口略窄,但永远不会比视口宽。
在主容器中,任何列都用百分比来定义宽度(比如,主列宽 70%,侧边栏宽 30%)。这样无论屏幕宽度是多少都能放得下主容器。用 Flexbox 布局也可以,设置弹性元素的 flex-grow 和flex-shrink(更重要),让元素能够始终填满屏幕。要习惯将容器宽度设置为百分比,而不是任何固定的值。
网页默认就是响应式的。没添加 CSS 的时候,块级元素不会比视口宽,行内元素会折行,从而避免出现水平滚动条。加上 CSS 样式后,就需要你来维护网页的响应式特性了。
3.1. 给大视口添加样式
接下来要为下一个屏幕断点加上媒体查询。一边实现一边观察的过程中,就会发现在每个断点处都没有给容器固定宽度。容器可以自然地伸展到 100%(减去一些内边距和/或外边距)。在三列布局的时候使用了 Flexbox,让每一列占据视口宽度的三分之一。
大视口下的网页布局最终效果如下图所示。类似于中等屏幕视口,只是可用空间更多了。现在可以随意使用内边距,这正是接下来要做的事情。
左右内边距从 1em 加到了 4em。主图上的文字周围的内边距也增加了,这样图片可以更大。新增样式如代码清单12所示。
将所有的( min-width: 50em)媒体查询代码放到样式表中。请再次确认将相同的规则放到较小的断点后面(如.page-header、 .hero、 main),这样媒体查询内的这些样式才能覆盖前面断点内的样式。
@media (min-width: 50em) {
.page-header {
padding: 1em 4em;
}
.her {
padding: 7em 6em;
}
main {
padding: 2em 4em;
}
.nav-menu {
padding: 0 4em;
}
}
还需要最后一点调整。现在根元素的响应式字号为 font-size: calc(1vm + 0.6em),在大屏上就显得太大了,可以在最大的断点处给字号加一个上限。如代码清单13所示更新样式表。
:root {
box-sizing: border-box;
font-size: calc(1vw + 0.6em);
}
@media (min-width: 50em) {
:root {
font-size: 1.125em;
}
}
3.2. 处理表格
在移动设备的流式布局里,表格的问题特别多。如果表格的列太多,很容易超过屏幕宽度(如下图所示)。
如果可以的话,建议在移动设备上用别的方式组织数据。比如将每一行数据单独用一块区域展示,让每块区域顺序叠放,或者用更适合小屏的可视化图形或者图表展示。但是,有时候就是需要用表格。
有一个办法是将表格强制显示为一个普通的块级元素,如下图所示。
这个布局由
、 、 元素组成,但是我们对它们使用了 display: block 声明,覆盖了正常的 table、 table-row、 table-cell 的显示值。可以用 max-width 媒体查询限制在小屏下才改变表格元素的显示。 CSS 代码如代码清单14所示。(可以将代码应用到任意标签查看效果。)@media (max-width: 30em) { table, thead, tbody, tr, th, td { /*让表格的所有元素都显示为块级*/ display: block; } thead tr { /*将表头移到屏幕外,将其隐藏*/ position: absolute; top: -9999px; left: -9999px; } tr { /*在表格数据的每一个集合之间加上间隔*/ margin-bottom: 1em; } }
以上样式让每个单元格从上到下排列,并且在每个 之间添加了外边距,但是这样会让行不再跟下面的每一列对齐,因此要用绝对定位将头部移出视口。出于可访问性的缘故,我们没有用 display: none,这样屏幕阅读器能够读到表头。虽然不是完美的解决办法,但是当其他方式失效的时候,这就是最好的方式。
4. 响应式图片在响应式设计中,图片需要特别关注。不仅要让图片适应屏幕,还要考虑移动端用户的带宽限制。图片通常是网页上最大的资源。首先要保证图片充分压缩。在图片编辑器中选择“Save for Web”选项能够极大地减小图片体积,或者用别的图片压缩工具压缩图片。 还要避免不必要的高分辨率图片,而是否必要则取决于视口大小。也没有必要为小屏幕提供大图,因为大图最终会被缩小。
4.1. 不同视口大小使用不同的图片响应式图片的最佳实践是为一个图片创建不同分辨率的副本。如果用媒体查询能够知道屏幕的大小,就不必发送过大的图片,不然浏览器为了适配图片也会将其缩小。 使用响应式技术给不同屏幕尺寸提供最合适的图片。比如,处理网页主图的 CSS,如代码清单15所示。将这段代码添加到样式表。 /*给移动设备提供最小的图*/ .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: 35em) { .hero { padding: 5em 3em; font-size: 1.2rem; background-image: url(coffee-beans-medium.jpg); } }
/*给大屏幕提供完整分辨率的图*/ @media (min-width: 50em) { .hero { padding: 7em 6em; background-image: url(coffee-beans.jpg); } }
4.2. 使用srcset提供对应的图片媒体查询能够解决用 CSS 加载图片的问题,但是 HTML 里的标签怎么办呢?对于这种行内图片,有另一个重要的解决方法: srcset 属性(“source set”的缩写)。 这个属性是 HTML 的一个较新的特性。它可以为一个标签指定不同的图片 URL,并指定相应的分辨率。浏览器会根据自身需要决定加载哪一个图片(如代码清单16所示)。 <img alt="A white coffee mug on a bed of coffee beans" src="coffee-beans-small.jpg" srcset="coffee-beans-small.jpg 560w, coffee-beans-medium.jpg 800w, coffee-beans.jpg 1280w" />
提示 图片作为流式布局的一部分,请始终确保它不会超过容器的宽度。为了避免这种情况发生,一劳永逸的办法是在样式表加入规则 img { max-width: 100%; }。
5. 总结
- 优先实现移动端设计。
- 使用媒体查询,按照视口从小到大的顺序渐进增强网页。
- 使用流式布局适应任意浏览器尺寸。
- 使用响应式图片适应移动设备的带宽限制。
- 不要忘记给视口添加 meta 标签。
|