前言

本篇文章修改、整理自我以前写的一篇文章

在阅读这篇文章之前,你需要了解设备像素、逻辑像素(设备独立像素)和CSS像素的区别,见我的前一篇文章理解设备像素、设备独立像素和css像素

在经典文章A tale of two viewports中,作者定义了两种视口:

  1. layout viewport 包含了页面中的所有内容,浏览器已经计算好了layout viewport中的所有样式。
  2. visual viewport 用户看到的的浏览窗口(在CSS标准中被称为viewport)。如果页面内容溢出了visual viewport,用户需要移动visual viewport(滚动)才能看完页面中的所有内容。visual viewport只是一个屏幕上的一个“窗口”,用户通过这个窗口来观察页面。

在讨论layout viewport、visual viewport的尺寸的时候,我们应该使用CSS像素为单位,而不是设备独立像素。因为我们关心的是它们能容纳多大的元素、多少个元素,这些元素的大小都是通过CSS来定义的。

在这篇文章,我们从CSS2.1标准(主要是8、9、10、11章)出发,更加规范地讨论这些内容。

initial containing block(layout viewport)与 visual viewport

首先需要先了解一下containing block。containing block影响着其中元素的尺寸和定位。比如我们都知道position:absolute的元素是相对于【最近已定位祖先】来定位的,其背后的原因是:这个元素的盒子(box)的containing block由【最近已定位祖先的padding edge】产生。详见MDNLayout and the containing block

在CSS标准中,<html>元素的containing block称为initial containing block。其他文章所说的layout viewport其实就是initial containing block。后面我将混用这两个词。

initial containing block的尺寸

initial containing block的尺寸有什么用?它可以决定<html>元素的尺寸。当<html>的宽度、高度、padding、margin使用百分数的值时,这个百分数的基准就是initial containing block的尺寸。

那么initial containing block的尺寸是怎么确定的呢?

桌面浏览器

在桌面浏览器中,initial containing block的尺寸等于visual viewport的尺寸

以下例子验证了,initial containing block的尺寸是等于浏览窗口的。并且我们可以利用它,来元素的width、height、padding(margin同理):

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
  <title>test</title>
  <style>
    * {
      padding: 0;
      margin: 0;
      box-sizing: border-box;
    }

    html,
    body {
      /* 使html, body的尺寸始终与visual viewport相同(即使你缩放、调整浏览器窗口的大小)
      对于默认为block的元素可以省略width: 100%; */
      width: 100%;
      height: 100%;
    }

    html {
      /* 相对于initial containing block计算百分比 */
      padding-left: 50%;
    }


    #box {
      /* 填满body元素,方便看出body的大小 */
      width: 100%;
      height: 100%;
      /* 为什么不直接通过在body上应用background-color来看它的大小?
      因为body上使用background会有一个诡异的现象:background会超出body覆盖整个页面。
      https://css-tricks.com/just-one-of-those-weird-things-about-css-background-on-body/ */
      background-color: aqua;
    }
  </style>
</head>

<body>
  <div id="box">
  </div>
</body>

</html>

移动端浏览器

在移动端浏览器上,layout viewport的尺寸有一些不同:现在大部分的移动端浏览器都有2种模式:“查看桌面版网站”和“查看移动版网站”:

  • 在“查看桌面版网站”模式下,浏览器会将layout viewport的设置为一个预定义尺寸,宽度一般是980或1024个CSS像素,高度一般是1500以上,不管visual viewport的尺寸是多少。
  • 在“查看移动版网站”模式下(默认处于这个模式),浏览器浏览器会根据viewport meta tag的信息来决定layout viewport的尺寸。如果没有viewport meta tag,则浏览器会认为这个网站没有针对小屏设备进行优化,因此表现与“查看桌面版网站”模式相同。

常用的viewport meta tag是<meta name="viewport" content="width=device-width, initial-scale=1.0">。它告诉“查看移动版网站”模式下的浏览器,将layout viewport的宽度(CSS像素)设为设备的宽度(设备独立像素,一般是360px左右)。这样,在缩放为100%的情况下(CSS像素大小=设备独立像素大小),屏幕恰好能装下layout viewport,从而不会出现横向滚动条。

可以看出,在移动端浏览器,不管处于哪种模式,不管有没有viewport meta tag,layout viewport的尺寸在加载以后就固定了。

内容可以溢出 initial containing block(layout viewport)

不要觉得"initial containing block"名字听起来很厉害,就肯定会将所有内容包含在其区域内。就像其他普通的containing block,页面中的内容完全可以溢出它。比如绝对定位、overflow:visible。
例子:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>test viewport</title>
    <style>
        * {
            padding: 0;
            margin: 0;
            box-sizing: border-box;
        }

        .box {
            width: 100%;
            height: 200px;
            background-color: greenyellow;
        }

        .out {
            position: absolute;
            right: -30px;
            background-color: rosybrown;
        }
    </style>
</head>

<body>
    <div class="box">box</div>
    <div class="out">out</div>
</body>

</html>

其中div.out就溢出了initial containint block的区域。
由于有内容溢出了visual viewport,因此在visual viewport上出现了横向滚动条。visual viewport上的滚动条在css溢出机制探究中讨论。

缩放、调整浏览器窗口大小的影响

缩放、调整浏览器窗口大小的时候,会改变visual viewport的尺寸(用可容纳的CSS像素数量来衡量):

  • 在调整缩放比例的时候,浏览器窗口可容纳的设备独立像素数量不变,而CSS像素的大小改变了,因此visual viewport可容纳的CSS像素数量也改变;
  • 在调整浏览器窗口大小的时候,CSS像素的大小不变,而浏览器窗口可容纳的设备独立像素数量改变了,因此visual viewport可容纳的CSS像素数量也改变。

桌面浏览器

在桌面浏览器中,layout viewport(initial containing block)始终保持与visual viewport尺寸相同(这是为了防止出现横向滚动条,见我上一篇文章对page zoom的解释),因此当你通过缩放、调整浏览器窗口大小来改变visual viewport的大小时,layout viewport(initial containing block)也会随之改变。
比如,你在桌面端增大缩放比例,visual viewport会缩小,initial containing block随之缩小,这就是为什么我们在桌面端缩放可能会造成布局错乱。(顺便提一下,这个问题的简单解决方案是在HTML元素上设置min-width,防止HTML元素跟着initial containing block一起变小,不过会出现横向滚动条。复杂解决方案:移动端适配)

例子+注释:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>test</title>
  <style>
    * {
      padding: 0;
      margin: 0;
      box-sizing: border-box;
    }

    html,
    body,
    main {
      /* 对于block元素其实可以省略width: 100%。
      放在这里只是为了强调一下,通过级联的width:100%,main的宽度始终等于visual viewport的宽度。
      如果你缩小浏览器窗口的宽度,main的宽度(以CSS像素或设备独立像素为单位)也会(响应式地)减小,从而会增加更多的换行以便容纳内部的div.ilbk。
      如果你增加缩放比例(通过Ctrl+鼠标滚轮),main的宽度(以CSS像素为单位)也会(响应式地)减小,从而会增加更多的换行以便容纳内部的div.ilbk。 */
      width: 100%;
    }

    .ilbk {
      display: inline-block;
      width: 200px;
      height: 50px;
      background-color: aquamarine;
    }
  </style>
</head>

<body>
  <main>
    <div class="ilbk"></div>
    <div class="ilbk"></div>
    <div class="ilbk"></div>
    <div class="ilbk"></div>
    <div class="ilbk"></div>
    <div class="ilbk"></div>
    <div class="ilbk"></div>
    <div class="ilbk"></div>
    <div class="ilbk"></div>
    <div class="ilbk"></div>
    <div class="ilbk"></div>
  </main>
</body>

</html>

以上例子中,通过级联的百分数宽度做到了响应式宽度,即,元素的宽度由客户端的宽度动态决定(在这个例子中是<main>元素),而不是写死在CSS中。
用桌面浏览器打开以上例子,随便改变浏览器窗口大小、改变缩放比例,你会发现<main>的宽度(以CSS像素为单位)会随之改变:

移动端浏览器

在移动端浏览器,不管处于哪种模式,不管有没有viewport meta tag,layout viewport的尺寸(以CSS像素为单位)在页面加载以后就固定了。无论用户如何缩放、调整浏览器窗口大小(这在手机上似乎做不到),layout viewport的尺寸都不会改变。
因此,不管你在移动端浏览器如何缩放,页面布局都不会改变。

造成以上不同的原因是,在桌面端的缩放和在移动端的缩放有不同的性质。见我在上一篇文章的讨论

media query

使用media query查询width、height的时候(比如@media screen and (max-width: 500px) {...}),查到的是layout viewport的尺寸,并且px指的是CSS像素。在桌面端和移动端都是如此。

例子:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>test1</title>
  <style>
    * {
      padding: 0;
      margin: 0;
      box-sizing: border-box;
    }

    html,
    body,
    main {
      /* 对于block元素其实可以省略width: 100%。
      放在这里只是为了强调一下,通过级联的width:100%,main的宽度始终等于visual viewport的宽度。
      如果你缩小浏览器窗口的宽度,main的宽度(以CSS像素或设备独立像素为单位)也会(响应式地)减小,从而会增加更多的换行以便容纳内部的div.ilbk。
      如果你增加缩放比例(通过Ctrl+鼠标滚轮),main的宽度(以CSS像素为单位)也会(响应式地)减小,从而会增加更多的换行以便容纳内部的div.ilbk。 */
      width: 100%;
      height: 100%;
      background-color: aquamarine;
    }

    @media screen and (max-width: 500px) {
      main {
        background-color: purple;
      }
    }
  </style>
</head>

<body>
  <main>
  </main>
</body>

</html>

这个例子中,在桌面浏览器,通过改变浏览器窗口大小或者改变缩放比例,都能造成媒体查询结果的改变。前面已经解释过了,这两个操作都会造成layout viewport尺寸的改变。

例子

为了让读者明白meta viewport、媒体查询出现的原因,这里举一个例子:
有很多网站没有针对移动端进行优化。对于这些网站,如果在移动端上将layout viewport的尺寸设置为visual viewport的尺寸(宽度为360CSS像素左右),那么排版可能会完全乱掉(意料之外的换行、溢出)。为了能正确显示这种网站的排版,如果没有meta viewport的指示,移动端浏览器将layout viewport的尺寸设为与电脑浏览器一样,比如980px(单位:CSS像素)。由于手机的屏幕逻辑像素宽度一般只有300~400逻辑像素,因此需要将多个css像素由1个逻辑像素显示(也就是缩小,不要忘记缩放比例=css像素边长/逻辑像素边长),通过缩小css像素让手机屏幕显示的css像素与网页的css像素一样多。

但是这会引发一个问题:字体小得难以阅读。用户阅读的时候又不得不用手指将缩放比例调整到100%左右(一个设备独立像素显示一个css像素,对于我的手机来说,水平方向只有360个设备独立像素),这个时候visual viewport只显示layout viewport的一部分了。阅读的时候需要横向、纵向滚动。

虽然能够阅读网站内容,但这依然是一种非常差的用户体验。

适配移动端的时候,先使用<meta name="viewport" content="width=device-width, initial-scale=1.0">来定义layout viewport的宽度,然后通过媒体查询来为不同的layout viewport定义不同的CSS排版。以下是浏览的效果(使用“查看移动版网站”模式):

现在的字体大小合适了,网页的排版变化了,没有元素横向溢出,没有横向滚动条,在移动端上的阅读体验更好。


相关属性

1. screen.width/height

上一篇文章说过的screen.width/height:整个屏幕的宽度和高度。这两个数值的单位是设备独立像素。这两个数值不随页面缩放、浏览器窗口大小而改变,在前端开发的过程中可以认为是固定不变的(除非你通过操作系统改变屏幕的分辨率)。这两个数值是操作系统决定的,由于设备独立像素:设备像素经常不等于1:1,实际屏幕物理像素的分辨率不一定是screen.width×screen.height。

在上图中列出了iphone各代的设备分辨率(物理分辨率)逻辑分辨率,我们只需要看这两行。

设备分辨率就是屏幕上的物理像素的数量,当手机厂商宣传自己的屏幕有多么清晰锐利的时候,相互攀比的就是这个数值。

逻辑分辨率就是screen.width/height。为什么iphone3GS以后的iphone都要把这个值设为实际屏幕分辨率的1/2或1/3呢?因为随着屏幕上塞进越来越多的物理像素,屏幕大小的变化却不那么明显,因此像素密度也越来越高。如果还让逻辑分辨率:真实屏幕分辨率=1:1,那么12px的字体就会越来越小,影响阅读体验。因此,后续的iphone用4个物理像素(甚至9个像素)组合成一个“逻辑像素”。这样,即使物理像素越来越小,每一个“逻辑像素”的大小变化不大。浏览器可以放心地使用逻辑像素来衡量大小,而不用担心真实大小在不同的显示器上出现严重偏差。

2. window.innerWidth/Height

visual viewport的大小,也就是浏览器内容窗口的大小,不包括菜单栏、地址栏、状态栏等,但是包括滚动条单位是CSS像素。通过这个属性你可以知道,当前的浏览器窗口可以容纳多少个css像素。当用户放大的时候这个数值会减少(因为css像素变大了),当用户缩小的时候这个数值增大。缩放改变浏览器窗口都会改变这个属性的值

3. document.documentElement.clientWidth/Height

Layout Viewport(initial containing block)的尺寸。注意,Layout Viewport没有滚动条(根据css溢出机制探究中的讨论,只有元素或者visual viewport才能拥有滚动条)。单位是CSS像素

4. document.documentElement.offsetWidth/Height

<html>元素的尺寸。前面已经讨论过<html>元素的尺寸是如何计算的了,默认情况下<html>的宽度始终与Layout Viewport宽度相同。单位是CSS像素。<html>元素的高度由内容撑开。

5. window.pageXOffset/pageYOffset

滚动距离,描述visual viewport已经向右、向下滚动了多少个像素。也可以理解为visual viewport相对于layout viewport的偏移值。单位是CSS像素

它们分别有1个别名(前者的兼容性更好些):

window.pageXOffset == window.scrollX; // always true
window.pageYOffset == window.scrollY; // always true

此外,由于Element上就有获取内容滚动的scrollLeftscrollTop属性(所有Element都可以使用),因此还有:

window.pageXOffset === document.documentElement.scrollLeft; // always true
window.pageYOffset === document.documentElement.scrollTop; // always true

参考资料

相关规范的进展

一些比css2.1更新的文档(但是还没有正式作为Recommondation规范):

  1. CSS Snapshot CSS3开始,CSS不再由一份大而全的文档来定义,而是分成多个模块、由多个文档来定义,方便各个技术的独立演化。这份文档收集了当前隶属于CSS的、相对稳定的文档。
  2. CSS Box Model Module Level 3 盒模型文档。该文档的内容与CSS2.1相比没有变化
  3. CSS Positioned Layout Module Level 3 布局、层叠文档。
  4. CSS Display Module Level 3 CSS formatting box tree文档。
03-05 15:14