【响应式布局】initial containing block、viewport以及相关尺寸

前言

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

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

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

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

    溢出、滚动条的原理,我总结在了另一篇文章中:css溢出机制探究

在讨论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的尺寸。

paddingmargin使用百分数值的时候都是相对于containing block的width计算的,包括xxx-topxxx-bottom
<html>元素是一个block element,与其他的block element一样,它的宽度默认为containing block的100%(对于<html>就是initial containing block的100%),它的高度默认由子元素<body>撑开(除非明确设置了高度)。

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

桌面浏览器

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

为了避免混淆,在这篇文章都使用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的尺寸都不会改变。
因此,不管你在移动端浏览器如何缩放,页面布局都不会改变。

“layout viewport的尺寸在页面加载以后就固定了”,这个归纳有一个例外:用户可以在加载好页面以后切换横屏、竖屏模式,从而meta viewport tag中的device-width发生改变,从而layout viewport宽度改变。

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

media query

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

MDN 文档也指出了这一点:... if the virtual viewport(也就是这里所说的layout viewport) is 980px for example, media queries that kick in at 640px or 480px or less will never be used, limiting the effectiveness of such responsive design techniques.

例子:

<!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各代的逻辑分辨率和物理分辨率

在上图中列出了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像素变大了),当用户缩小的时候这个数值增大。缩放改变浏览器窗口都会改变这个属性的值

与之对应的,window.outerWidth/outerHeight给出整个浏览器窗口的大小(包括各种栏),但是单位是设备独立像素

3. document.documentElement.clientWidth/Height

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

document.documentElement指的是html元素,通常Element.clientWidth应该给出元素的内容区域的大小,但是document.documentElement.clientWidth/Height并不衡量html元素的大小,这是一个特例。各个浏览器都遵循着这个约定。并且,这个约定正在被标准化

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
当用户进行缩放的时候,浏览器会尽量保证:原先在内容区顶部的元素,在缩放以后依然在内容区顶部,看以下例子:
放大前:
放大前
放大后:
放大后
原本数字3在顶部,放大后3依然在顶部。window.pageYOffset大致相同。大致相同的原因是CSS像素数量不随着缩放而变化,原本在上方的内容高度有多少个CSS像素,放缩以后依然是多少个CSS像素。至于为什么不是完全相同,是因为"原先在内容区顶部的元素,在缩放以后依然在内容区顶部"这一机制无法完美地做到。

参考资料

相关规范的进展

一些比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文档。

csRyan的学习专栏
分享对于计算机科学的学习和思考,只发布有价值的文章: 对于那些网上已经有完整资料,且相关资料已经整...

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart...

1.1k 声望
181 粉丝
0 条评论
推荐阅读
手写一个Parser - 代码简单而功能强大的Pratt Parsing
在编译的流程中,一个很重要的步骤是语法分析(又称解析,Parsing)。解析器(Parser)负责将Token流转化为抽象语法树(AST)。这篇文章介绍一种Parser的实现算法:Pratt Parsing,又称Top Down Operator Precede...

csRyan阅读 2.7k

手把手教你写一份优质的前端技术简历
不知不觉一年一度的秋招又来了,你收获了哪些大厂的面试邀约,又拿了多少offer呢?你身边是不是有挺多人技术比你差,但是却拿到了很多大厂的offer呢?其实,要想面试拿offer,首先要过得了简历那一关。如果一份简...

tonychen152阅读 17.8k评论 5

封面图
正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青56阅读 8.5k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy48阅读 7.1k评论 12

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木75阅读 7.1k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs42阅读 6.8k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木45阅读 8.6k评论 6

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart...

1.1k 声望
181 粉丝
宣传栏