第一次写移动端自适应布局?那就对了

changli

一、前言

随着移动互联网的发展,做出来的网页可能显示在不同屏幕大小的设备上,为了针对不同大小的屏幕都可以有良好的样式体验特别是一套代码适配多端的情况,自然有不同布局方案设计出来,我主要把他们分为三类

  • 基于宽度百分比和浮动配合的栅格布局或者叫流式布局,flex 弹性布局, grid 多维网格布局等。由于 flex 在这类布局中是主流,为了方便就统一称为弹性布局。
  • 基于 @media 的媒体查询,可以根据查询条件选择执行特定的样式代码,从而实现不同的布局,由于主要是针对设备特性进行条件查询,所以可以称为媒体布局。(媒体查询条件一览 这里提一点经常用的 device-aspect-ratio 这个查询设备像素比的条件已经被废弃了由 resolution 来取代)
  • 基于 rem 或者是 vm 等单位实现的一套布局,这类布局的特点是严格按照设计图的比例来呈现页面,可以称为等比例布局或者叫自适应布局。

关于布局的名称,其实是没有统一的约定,主要是方便下文的叙述,这三类布局在实际使用时也是可以混合使用的,主要由实际场景决定。弹性布局和媒体布局在了解了基本的 css 知识后都比较容易理解(很久前写的一个flex布局测试页面,方便学习用的),而对于自适应布局,可能有点不好理解,当然我会以最少的概念来说,一开始太多的抽象概念反而阻碍对实质的理解。

由上面的自适应布局的特点,即严格按照设计图比例来呈现网页,重点是按照比例,不是尺寸,这个很重要。那是什么比例呢,对于网页设计来说一般可以简单地概括为设计元素的尺寸比上设计图的宽度尺寸,这个有点像地图的比例尺,只要按照比例尺的比例画图就可以把真实的地形画在一张有限尺寸的纸上。对于我们网站开发来说,其实也是一样,我们只需要找来一张图纸,然后按照比例把图重新画在图纸上就是了,其他可以不用管,只要保证重新画上的元素的尺寸比上图纸宽度等于设计元素的尺寸比上设计图的宽度就可以了,而这里的图纸宽度在浏览器上就叫做布局宽度,可以通过 document.documentElement.clientWidth 来获取。到目前为止只需要记住这一个概念就可以了,下面我会以一个素人的角度来展开,通过素人的经历,相信你会对自适应布局有更好的理解

二、viewport 初识

小明是一个街口职业技术学院的毕业生,毕业后听说前端妹子漂亮,然后就去应聘了一个叫前端的职位,还应聘上了,但是上岗后发现前端没有妹子,迫于就业压力,也只好迎难而上了。很快小明就熟悉了公司的业务,而且了解到和他对接的 UI 是一个妹子,名字叫小红,他就变得更加好学了,心想一定能完美实现小红的设计效果。很快就有一项任务交给了小明,而且小红也给了设计图,其中 PM 啊强嘱咐小明要在不同大小的手机屏幕都要按照设计图比例来显示,小明表示这完全没有问题 。

小红的设计图

小明仔细研究了一番设计图,发现就只有两个方块和一条分割线,要比例一致立即就想到了宽高百分比,但很快就发现了问题,高度的百分比不是相对宽度的,而且要让高度百分比起作用,上面的 body html 都要设置高度,问题是设计图居然没有标记高度,看来这种方法不行。否定这个方法后,小明没有止步不前,立即就写下了下面的实现方案,说不定浏览器本身就自已自动适配呢

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>浏览器请自动适配</title>
  <style>
    body {
      margin: 0
    }
    .top {
      width: 750px;
      height: 100px;
      background-color: green;
      border-bottom: 1px solid black;
    }
    .left {
      width: 200px;
      height: 500px;
      background-color: red;
    }
  </style>
</head>
<body>
  <div class="top"></div>    
  <div class="left"></div>    
</body>
</html>

但很明显小明想多了,而且他很快发现了一个奇怪的问题,他打开 chrome 浏览器的控制台的移动端模式,把尺寸设置为 750px,还是这样,他想那肯定是 dpr 问题,如是他把 dpr 调节为 1 2 3 还是一样,需然这个时候还不知道什么是 dpr(在线查看该例子)

宽度 750px DPR 1 2 3 下的样子

小明被头顶那块绿震惊了,为什么缩水了?小明通过审查元素,发现其宽高还是设置的宽高,作为一个学过 js 的人,小明觉得不能被 css 所迷惑,然后在控制台输入了下面的代码

    document.querySelector('.top').style.width

很快浏览器就输出了 "",很明显宽度样式是设置在 css style 标签上的,不是设置元素的 style 内联属性上,自然会返回空字符串,小明很快就看穿了这个问题,接着写下了

  getComputedStyle(document.querySelector('.top')).width

浏览器返回了 "750px",看来控制台的 css 没有欺骗我啊,小明感慨道,但是小明还是不死心

  document.querySelector('.top').getBoundingClientRect().width

浏览器依然输出 750
这就奇怪了,调试的尺寸为 750px,元素的宽度也是 750px,怎么就不能占满呢?是不是浏览器有 bug
小明是个聪明人,他很快就打开搜索引擎输入了 第一次写移动端布局遇到了 bug 怎么办 不出意外他找到了这篇文章,并很快看到了前言,很明显了他记住了布局宽度和 document.documentElement.clientWidth,他也是一个务实不会摸鱼的人并没有浪费时间在继续阅读这篇文章上,他显然不知道这篇文章主角是他。

事情很快就给了小明一个答案

  document.documentElement.clientWidth

浏览器输出了 980,对就是 980,这个就是浏览器的默认布局宽度,我写的元素就是放在这个大小上的,小明胸有成竹地说道。小明也是一个富有想象力的人,他先把自已化身为一个浏览器,然后用浏览器的身份把自已化身为一个镜头,现在镜头出现了一个物体,物体的大小超过了镜头的视野,那么镜头应该怎么可以记录下整个物体呢?
其实有两个方法,第一个方法是让镜头移动或者是让物体移动,通过镜头和物体的相对移动就可以记录到物体整个大小。
第二个方法是,把镜头和物体的距离拉远,视野开阔了,自然就可以看到整个物体,相对来说物体就看似变小了。
这两种方法中,镜头和物体的大小其实一直都没有变,变的只是距离和相对移动的坐标。而这里的镜头就是浏览器的可视区域,很多时候就是屏幕区域,而物体显然就是布局区域,那么浏览器是采用何种方法来显示布局区域呢,如果是采用第一种方法,那么显然 980 的布局宽度是大于屏幕宽度的,如果要浏览整个网页,自然就需要向左滑动,类似于相对移动,很明显事实不是这样,整个页面都能看到,那肯定是浏览器通过拉大距离来把页面塞进去,得到的效果就像是把整个布局区域都缩小塞进了屏幕区域内,很显然这种缩放也是等比例缩放的。
小明毕竟是一个务实的人,他很快从工程部借来一把游标卡尺,对着屏幕量起了元素的尺寸来(px 和实际距离的换算是基于特定设备的,有兴趣可以看这里),并查询了设备的 dpi(每英寸有多少像素) 值,最后计算出顶部的绿色块的大小应该是 575px,这个大小就是缩放后元素的尺寸值,后面只需要计算对应比例是否一致就可以得出浏览器是否采用了自动缩放来实现这个效果

  575 / 750 = 0.7666666666666667
  750 / 980 = 0.7653061224489796

毕竟是使用游标卡尺测量的,这点误差其实是可以接受的,小明继续测量了其他的尺寸,也得到类似答案,最后小明总结了一条要使网页符合要求的公式,

  这个是要求: 最后缩放后的实际大小 / 屏幕的宽度 = 设计图的尺寸 / 设计图的宽度
  因为: 最后缩放后的实际大小 / 屏幕的宽度 = css 尺寸的大小 / 布局宽度
  所以保证: css 尺寸的大小 / 布局宽度 = 设计图的尺寸 / 设计图的宽度
  就可以实现需要的要求

设计图和设计图的宽度还有布局宽度都是已知的,然后通过换算就可以计算每一个元素的尺寸大小了,很快小明就写了下面的版本

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>浏览器布局980</title>
  <style>
    body {
      margin: 0;
      background-color: blue;
    }
    .top {
      width: 980px;
      height: 130.67px;
      background-color: green;
      border-bottom: 1.37px solid black;
    }
    .left {
      width: 261.33px;
      height: 653.33px;
      background-color: red;
    }
  </style>
</head>
<body>
  <div class="top"></div>    
  <div class="left"></div>    
</body>
</html>

小明打开了控制台并调节了不同的宽度和 dpr 效果都很好,当然从上面的公式来看其实和 dpr 也是无关的。(在线查看该例子)

宽度 474x DPR 3 下的样子

但是很快小明就找到问题,如果调节的宽度超过 980,布局宽度就不再是 980,而是变为调节的宽度,这个时候尺寸的比值就不再被维持,而当尺寸的宽度是少于 250 时,浏览器不再等比缩放了,而是采取上面的说到的第一种方法,这个时候上面公式也不再被维持。当然小明内心是毫无波澜的,毕竟谁的手机是这种尺寸的,如果是那就不是手机了

不过这明显是有问题的,那就是肯定不是全部的手机浏览器的默认布局是 980,如果遇到不是 980,那肯定就不行了,那有什么办法能够设置浏览器布局宽度呢?小明毕竟是一个聪明人,既然可以从 document.documentElement.clientWidth 从获取值,那应该也能设置值吧,

document.documentElement.clientWidth = 500
500
document.documentElement.clientWidth
980

很明显小明想太多了,不能使用这个方法来设置布局宽度。前面提到小明只看到前言,但是 viewport 初识 这个标题,他应该是看到的,很快他就找到了设置浏览器布局宽度的方法

  <meta name="viewport" content="width=">

那么 width 应该设置多少呢?小明翻了翻公式

css 尺寸的大小 / 布局宽度 = 设计图的尺寸 / 设计图的宽度

如果把布局宽度设置为设计图宽度,那 css 尺寸的大小不就可以设置为设计图的尺寸,而且还不用执行额外的计算,那实在是太好了,小明明显为了自已发现而兴奋不已,如是写下了下面的版本

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>浏览器布局设计图尺寸</title>
  <meta name="viewport" content="width=750">
  <style>
    body {
      margin: 0;
      background-color: blue;
    }
    .top {
      width: 750px;
      height: 100px;
      background-color: green;
      border-bottom: 1px solid black;
    }
    .left {
      width: 200px;
      height: 500px;
      background-color: red;
    }
  </style>
</head>
<body>
  <div class="top"></div>    
  <div class="left"></div>
</body>
</html>

同样在不同的宽度和不同的 dpr 有很好的效果(在线查看该例子)

宽度 520px DPR 2 下的样子

而且调节屏幕宽度大小,当超过布局宽度时,也不会改变变布局宽度,但是依然在少于 250 时遇到上面的情况,不过这显然也不会有影响,于是小明就愉快地提交了任务。

三、rem hack

提交了任务不久,小明就接收到小红的信息,你的 1px 太粗了,我希望细一点的,小明疑惑了,我的 1px 不是按照你的 1px 设置的吗?怎么太粗了呢。接着小红发过来了一张截图,一条黑线在红绿之间,并看到黑线是横跨了两个像素,小红重复道,我希望这个是 1px 的。
难度是上面的公式有问题,怎么会出现 1px 变粗的问题?很快小明就找到问题的原因了,是 dpr 导致的,原来设计图上标注的 1px 和设计图其他地方标注的 px 单位是不同的,或者说 ui 期望的是真实的 1px,而不是逻辑上的 1px。例如如果一部手机的分辨率是 1920 * 1080,如果设置 css 640px 就可以覆盖这个宽度,那么该设备的 dpr = 1920 / 640 = 3,dpr 是一个设备的特性,不同的设备的 dpr 都有不同,表示设备像素比,上面的布局公式不是错误的,只是误认为设计图的 1px 就是逻辑像素 1px。如果上面的方案放在一部 dpr 是 3,屏幕宽度是 750px 的手机上显示时,得到的结果就是分隔线将横跨 3 个设备像素。

了解了上面的原因后,正当小明着手解决时,啊强也找到了小明,啊强说你的方案挺好的,就是当手机横着看时,红色和绿色都感觉太粗了,这样吧你给限制个最大宽度 500px,如果手机的屏幕宽度大于 500px 时就让整个网页居中,两边留白。小明也觉得这个需求合理,如果对于宽度较大的屏幕,还这样等比例缩放,自然看到的东西就变少了,人们换一个大屏幕可不想看到的东西反而变少了。

啊强的需求看似更容易实现,应该比较容易解决,如是小明在前面的方案中增加了下面的代码

   html {
      background-color: white;
    }
    body {
      margin: 0 auto;
      background-color: blue;
      max-width: 500px;
    }


宽度 586px DPR 3 下的样子

在线查看例子

小明的意图很明显,通过最大宽度尺寸限制 body 的大小,然后通过 margin auto 让 body 居中。但是小明忽略了前提,就是为什么设置 px 单位也能够做到自适应,或者是上述的公式能够成立原因是因为浏览器能够自动等比例缩放布局区域到可视区域内部,这个有点像把一张图片放在 img 元素一样,会对整体进行缩放,body 虽然设置了 max-width: 500px,这个 500px 是放在布局宽度 750px 区域上,并随整体进行缩放,因此就看到这种效果。要想这个 500px 不受缩放影响,那么就需要要求这个尺寸不受等比例缩放影响,但这个方案把布局宽度定死了,如果要适配不同的屏幕宽度,自然就需要进行等比例缩放。
而且小明在捣鼓的过程中,还发现了这个方案的一个问题,就是媒体布局的视口宽度查询条件都失效了如 min-width 和 max-width 等条件不会按预期工作,原因是这些查询都是针对视口宽度的,现在的视口宽度被定死为设计图大小,自然就无法按预期工作。
因为等比例缩放布局区域到可视区域内部是浏览器的行为,外部是无法控制的,基于这个原因要使最大宽度设置起作用自然就希望浏览器不要等比例缩放,要实现这个其实也很简单

<meta name="viewport" content="width=device-width">

布局的宽度设置为屏幕宽度,这个设置还有一个好处,就是媒体查询会以期望的方式执行。

现在的问题是,现在自适应布局不再依赖浏览器的等比例缩放了,但是下面的公式依然是起作用的,因为不再等比例缩放,也可看做是缩放比例是 1,公式上的推导都是一样的

css 尺寸的大小 / 布局宽度 = 设计图的尺寸 / 设计图的宽度

右边设计图的尺寸和设计图的宽度是固定的,而左边的布局宽度变为不再是固定了,因为我们要适配不同的屏幕尺寸,那么怎么可以算出 css 尺寸的大小呢。我们需要一个单位,一个特殊的单位,我们就叫它布局单位,这个单位应该相对布局宽度,这样在计算时就会约去上下不定的布局宽度,这样才可能适配不同的尺寸。

小明犯愁了,在他的认识中 css 可没有布局单位啊,不过小明是一个聪明人,没有可以自已写一个啊,布局单位的特点不就是相对布局宽度嘛,在看了一众的单位后,小明选取了 rem,这个单位本来是一个相对单位,任何设置了该单位的尺寸都会相对根元素的字体大小尺寸,只要把该尺寸设置为布局宽度不就变为布局单位了。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>浏览器布局rem</title>
  <style>
    html {
      background-color: white;
    }
    body {
      margin: 0 auto;
      background-color: blue;
      max-width: 500px;
    }
    .top {
      width: 7.5rem;
      height: 1rem;
      background-color: green;
      border-bottom: 0.01rem solid black;
    }
    .left {
      width: 2rem;
      height: 5rem;
      background-color: red;
    }
  </style>
  <script>
    (function () {
    function resize() {
      var clientWidth = document.documentElement.clientWidth;
      clientWidth = clientWidth > 500 ? 500 : clientWidth
      document.documentElement.style.fontSize = (clientWidth / 7.5) +'px';
    }
    resize();
    window.onresize = resize;
  })()
  </script>
</head>
<body>
  <div class="top"></div>    
  <div class="left"></div>
</body>
</html>


宽度 426px DPR 3 下的样子


宽度 826px DPR 3 下的样子

在线查看例子

在屏幕宽度少于 500px 时,设计的要求是

设计要求: 元素实际大小 / 屏幕宽度 = 设计图的尺寸 / 设计图的宽度
等比缩放比例是 1 因此: 元素实际大小 / 屏幕宽度 = css 尺寸大小 / 布局宽度
所以: 设计图的尺寸 / 设计图的宽度 = css 尺寸大小 / 布局宽度

通过查看程序,我们知道 7.5rem 其实就是布局宽度,在按照比例带入

设计图的尺寸 / 750px = xrem / 7.5rem

这里故意设置为 7.5 就是方便计算,可以看到右边的 rem 被上下约去

x = 设计图的尺寸 / 100

在屏幕宽度大于 500px 时,设计的要求是

设计要求: 元素实际大小 / 500 = 设计图的尺寸 / 设计图的宽度
等比缩放比例是 1 因此: 元素实际大小 / 屏幕宽度 = css 尺寸大小 / 布局宽度
所以: 设计图的尺寸 / 设计图的宽度 * 500 / 屏幕宽度 = css 尺寸大小 / 布局宽度
布局宽度是等于屏幕宽度的: 设计图的尺寸 / 设计图的宽度 = css 尺寸大小 / 500

可以看到当屏幕宽度大于 500px 时,如果希望 x 的 css 尺寸设置也能应用在这种情况,我们需要 500 应该等于 7.5rem,这个也是程序中三目运算符的逻辑,这个时候 7.5rem 总是等于 500。

这种方案实现了阿强的需求,当然也有一点问题就是通过 rem 换算后出现了像素误差,而且也没有解决 1px 问题,下面小明就要解决 1px 像素误差了

经过一番搜索小明了解到曾经有一个 flexible 的方案,通过布局宽度的缩放来解决了 1px 的问题,其原理融合了等比例缩放和 rem 布局单位的设置。viewport 标签中有一个 initial-scale 属性,可以设置布局宽度相对屏幕宽度的倍数,现在把这个属性的属性值设置为 initial-scale = 1 / dpr,这样布局宽度就等于屏幕宽度 dpr 倍,但是依然会需要符合下面的公式

设计图的尺寸 / 设计图的宽度 = css 尺寸大小 / 布局宽度

因此对于布局单位 rem 来说,1rem 的实际尺寸变为了 1rem * dpr 的尺寸,而这个时候布局宽度是屏幕宽度的 dpr 倍,因此浏览器会缩放该布局宽度,刚好缩小 dpr 倍,因此 1rem 的实际尺寸和之前没有设置 initial-scale = 1 / dpr 是一样的。
但是对于非布局单位,比如 px, 如果设置 1px 在布局宽度增大 dpr 倍时,依然是 1px,但是在浏览器缩放该布局宽度时,会整体进行缩放,1px 大小的尺寸会被缩小 dpr 倍,而之前举的例子中,如果 1px 的分隔线横跨了 3 个像素,刚好经历了这番折腾就可以渲染为 1 个设备像素。
虽然解决了 1px 的问题,但也会带来另一个问题,就是任何的非布局单位,都会根据 dpr 的倍数被缩小,如果不是 1px 像素,那就需要进行额外的设置,而且在查阅中小明发现该方案已经弃用,应该使用 viewport 单位,哈哈原来布局单位很早就已经存在了。

三、vw 启程

rem 毕竟只是一个相对单位,不是布局单位,既然有了专门的布局单位,那就尽量使用布局单位吧.100vw 就是布局的宽度,有了这个信息,再加上以上的折腾,小明很快就写出了 vw 的版本

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>浏览器布局vw</title>
  <script>
  </script>
  <style>
    html {
      background-color: white;
    }
    body {
      margin: 0 auto;
      background-color: blue;
    }
    .top {
      width: 100vw;
      height: 13.33vw;
      background-color: green;
      border-bottom: 0.13vw solid black;
    }
    .left {
      width: 26.67vw;
      height: 66.67vw;
      background-color: red;
    }
  </style>
</head>
<body>
  <div class="top"></div>    
  <div class="left"></div>
</body>
</html>

不过小明很快就陷入了迷茫,就是不知道怎么实现啊强的需求,使用 rem 的话可以在达到最大值时把 7.5rem 设置为一个常数,但是如何把 100vw 设置为一个常数,好像只能是把布局宽度设置为一个常数,也就是动态修改 viewport 的值,但是把布局宽度设置为一个常数后,浏览器就会自动缩放该布局宽度,自然也就达不到需求。或者可以引入一个中继的单位让这个单位相对 vw 单位,在最大尺寸到达后把这个使用 vw 的单位设置为常数,但这个和 rem 的方案那有什么区别?看来先使用 rem 方案应付啊强了
最后就是解决 1px 了,根据 flexible 方案的启发,通过放大后缩小的方式就可以解决这个问题了
下面这个可以解决 dpr 2 的问题

.top::after {
      position: absolute;
      box-sizing: border-box;
      content: ' ';
      pointer-events: none;
      top: -50%;
      right: -50%;
      bottom: -50%;
      left: -50%;
      border: 0 solid #ebedf0;
      -webkit-transform: scale(0.5);
      transform: scale(0.5);
      border-bottom-width: 0.13vw;
    }

在线查看例子

最后

本文从布局宽度开始逐步介绍了自适应布局的不同方案,由浏览器的默认行为出发,并由基础公式的推导说明了直接设置 px 单位为何也能适配不同的屏幕尺寸。但也指出了该方案的缺点,固定死布局宽度会导致我们失去对网页的更多控制,为规避这些缺点,布局宽度变成可变的,相对屏幕宽度的,也通过公式的说明,要适配不同的屏幕宽度,引入布局单位是必须的,这样才可以约去可变的部分,让一套代码适配不同的屏幕宽度。在布局单位上,也介绍了 rem vw 单位,并说明了这些单位的工作原理,rem 作为 hack 手法,被改造为布局单位,而 vw 是正儿八经的布局单位,也指出了使用这些单位时可能遇到的部分问题。同样也指出了 1px 问题,设计图的 1px 和 css 设置的 1px 其实是两个概念,介绍了 flexible 为何能解决 1px 的问题,从这个方案的启发下,利用 transform 来解决 1px 问题。当然文中列出问题只是在实践中遇到的问题中很少的部分,而我们只需要记住的就是浏览器默认的缩放行为所涉及到的尺寸变化,然后基于设计的需求,利用公式来推导。
自适应布局只是我前言提到的三种布局中的一种,可以让我们回归到写一种传统 pc 页面的感觉,并严格实现了设计图的比例,很明显这种设计是 ui 主导的,前端变得毫无灵魂。而很多时候,这种设计的初始是静态的,自然很难去考虑多屏幕下的动态变化,这个时候是不是更应该让前端去发挥更多的主动性,而 ui 设计图只需要列出关键的设计尺寸,而作为前端,可以搭配弹性布局和媒体布局作出更好的设计,让工具人变得没那么工具好不

参考资料

阅读 2.3k

changli
个人技术分享
497 声望
4 粉丝
0 条评论
497 声望
4 粉丝
文章目录
宣传栏