移动端适配知识你到底知多少

15,710 阅读11分钟

本文从设备像素,CSS像素这些基本概念出发,先向大家介绍移动端适配必须要掌握的一些基本像素知识及viewport理论,最后在向大家介绍移动端适配解决方案:flexible.js和vw/vh

设备像素 & CSS像素 & 设备独立像素

设备像素:device pixel,dp,物理像素,不可变,下图标红的部分指的就是设备像素

CSS像素:web编程用到的,我们在JS和CSS中使用的10px就是CSS像素,是可变的。CSS像素受屏幕缩放和设备像素比(dpr)的影响。如我们网页的中的字体在网页放大之后会变大,还有在移动端看起来会比PC端小一些

设备独立像素:device independent pixel,dip,与设备无关的像素

再来聊下它们之间的关系:PC端和移动端

PC

100%缩放

1设备独立像素 = 1设备像素

200%缩放

1设备独立像素 = 2设备像素

移动端

因为不同设备的PPI不同,在标准屏幕下(160PPI)

1设备独立像素 = 1设备像素

至于CSS像素和他们之间的关系,等下面讲到了设备像素比再说

设备像素比

device pixel ratio,dpr

DPR = 设备像素 / 设备独立像素

下图是一些手机的设备像素比数据

在JS中我们可以通过window.devicePixelRatio来得到当前设备的dpr
在CSS中我们可以通过-webkit-device-pixel-ratio来进行媒体查询

注意,当我们放大或者缩小屏幕的时候,window.devicePixelRatio是可变的

有了dpr再来说说,CSS像素和设备像素之间的关系

当dpr为1,1个CSS像素对应1个设备像素
当dpr为2,1个CSS像素对应4个设备像素
当dpr为3,1个CSS像素对应9个设备像素
......

针对有些同学后面提出的疑问,我详细解释这块,当dpr为1,此时1个CSS像素对应1个设备像素,这个还是很好理解的,当dpr为2,此时在水平方向上的设备像素是dpr为1的两倍,竖直方向上的设备像素也是dpr为1的两倍,所以此时的1个CSS像素对应2^2个设备像素,这个就相当于我们把一个矩形的长宽放大为之前的两倍,此时的矩形面积为之前的四倍,当dpr为3时,此时的1个CSS像素对应3^2个设备像素,所以1个CSS像素对应dpr^2个设备像素

下图生动的表示了他们之间的关系

这里跟大家说一个小技巧,就是在移动端的时候可以根据dpr的值,使用不同分辨率的图片,如2X还是3X,这样可以保证与在普通屏幕上看到的图片效果一致,不至于失真

DPI & PPI

DPI(Dots Per Inch)源于印刷行业,表示每英寸打印机喷的墨汁点数
PPI(Pixels Per Inch)计算机借鉴了DPI,创造了PPI,表示每英寸的像素数量,即像素密度

PPI的计算公式如下:

现在二者都可用于描述计算机显示设备的像素密度,意思一样

下面一张图描述了iphone三个机型的参数

其中可以发现iphoneX的像素密度达到了458ppi,完爆其它两个

Retina

Retina屏幕即视网膜屏幕,是苹果发布iphone 4提出的。之所以称作是视网膜屏幕,是因为ppi太高,人类无法分辨出屏幕上的像素点,目前很多智能手机都采用Retina屏幕

iphone3G/S 和 iphone4的屏幕尺寸都是3.5寸,但是iphone4在水平和竖直方向的物理像素都是iphone3G/S的一倍

PC端几个尺寸

PC端有几个尺寸我们需要弄懂下,它们是:

  • screen.width
  • window.innerWidth
  • document.documentElement.clientWidth
  • document.documentElement.offsetWidth
  • ...

screen.width

screen.width指的是我们显示器的水平方向的像素时,不随着我们浏览器窗口的变化而变化,是用设备像素衡量的

window.innerWidth

window.innerWidth指的是浏览器窗口的宽度,是可以变化的,所以使用的是CSS像素

下面是100%缩放,window.innerWidth的截图

可以发现在100%缩放情况下,window.innerWidth的值为1192,window.innerHeight的值为455,接着我们尝试将放大到200%,再来看看效果

可以看到当放大2倍之后,window.innerWidth和window.innerHeight都变成了放大之前的1/2,但是此时window.devicePixelRatio变成了放大之前的2倍,为什么是这样子呢?

其实这个也不难理解?因为window.innerWidth是用CSS像素衡量的,放大两倍之后,浏览器窗口只能看到之前一半的内容,所以window.innerWidth是之前的一半,而dpr = 设备像素 / 设备独立像素,这里的设备独立像素就是我们的window.innerWidth,所以dpr变为原来的2倍,如果看的有点晕,不如尝试缩放自己的浏览器看下效果就知道了

document.documentElement.clientWidth

document.documentElement.clientWidth指的是viewport的宽度,与window.innerWidth的区别就只差了一个滚动条

document.documentElement.offsetWidth则是取得html标签的宽度

看到没document.documentElement.offsetHeight此时为0,我打开调试定位了下,发现此时html高度确实是为0,而document.documentElement.clientHeight此时为455,是viewport的高度,只不过此时viewport的高度和window.innerHeight相等

小结

对于pc端,总之记住以下几点:

  • window.innerWidth指的是浏览器窗口的宽度(包含滚动条),用CSS像素衡量
  • document.documentElement.clientWidth指的是viewport的宽度,等于浏览器窗口的宽度(不包含滚动条)
  • document.documentElement.offsetWidth指的是html的宽度,默认为浏览器窗口的宽度
  • document.documentElement.offsetHeight指的是html的高度,没有显示给html指定高度的话,为0

移动端的三个viewport理论

移动端的话和PC端截然不同,我们必须先要掌握三个viewport:

  • layout viewport
  • visual viewport
  • ideal viewport

layout viewport

布局layout,和PC端的viewport很像,PC端的viewport的宽由浏览器窗口的宽决定的,用户可以通过拖动窗口或者缩放改变viewport的大小,但是在移动端则不同,在IOS中 layout viewport默认大小980px,在android中layout viewport为800px,很明显这两个值都大于我们浏览器的可视区域宽度。我们可以通过document.documentElement.clientWidth来获取layout viewport的宽度

visual viewport

有了layout viewport,我们还需要一个viewport来表示我们浏览器可视区域的大小,这个就是visual viewport。visual viewport的宽度可以通过window.innerWidth获取

移动端浏览器为了不让用户通过缩放和滑动就能看到整个网页的内容,默认情况下会将visual viewport进行缩放到layout viewport一样大小,这也就解释了为什么PC端设计的网页在手机上浏览会缩小,其实这是跟移动浏览器默认的行为有关系

ideal viewport

设备理想viewport,有以下几个要求:

  • 用户不需要缩放和滚动条就能查看所有内容
  • 文字大小合适,不会因为在高分辨率手机下就显示过小而看不清,图片也一样

这个viewport就叫做ideal viewport。但是不同的设备的ideal viewport不一样,有320px,有360px的,还有384px的......

总之在移动端布局中我们需要的是ideal viewport。它等于我们移动设备的屏幕宽度,这样针对ideal viewport设计的网站,在不同分辨率的屏幕下,不需要缩放,也不需要用户滚动,就可以完美呈现

meta标签

我们可以通过meta标签设置我们viewport,下面这段代码你应该见过不止一次了

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0">

这段代码的作用就是设置当前的layout viewport的宽度为设备的宽度,初始化缩放为1.0,同时不允许缩放,这应该是我们大家都想要的效果

关于meta viewport标签最初是由Apple公司在Safari浏览器中引入的,目的就是为了解决移动设备的viewport的问题,后来其余公司纷纷效仿,它有以下几个属性:

属性 说明
width 设置layout viewport的宽度,数字或者device-width
height 设置layout viewport的高度,数字或者device-height
initial-scale 页面初始缩放值
maximum-scale 用户最大缩放值
minimum-scale 用户最小缩放值
user-scalable 允许用户缩放,yes或no

width=device-width

设置当前的layout viewport的宽度为设备的屏幕宽度,这样我们的网站就是针对设备的屏幕宽度进行排版的,而这个不正是上面所说的ideal viewport,所以通过这样我们可以让我们layout viewport的宽度等于ideal viewport的宽度

但是在iphone和ipad上,无论是横屏还是竖屏,device-width都是竖屏的屏幕宽度

下面是我设置了width=device-width之后

可以看到设置了width=device-width之后,document.documentElement.clientWidth和window.innerWidth都变成了375,即设备的屏幕宽度

initial-scale=1.0

通过这种方式我们也可以让我们的layout viewport的宽度等于ideal viewport的宽度,原因是initial-scale是针对ideal viewport进行缩放的,当我们设置为1.0也就是缩放100%,就可以让我们的layout viewport和ideal viewport一样大

下面是我设置了initdial-scale=1.0之后

效果同设置了width=device-width一样

但这次我们发现在winphone上,无论横屏还是竖屏,都将layout viewport的宽度设置为竖屏的屏幕宽度

所以为了兼容,建议我们同时写上这两个属性,即

width=device-width,initial-scale=1.0

initial-scale=其它值

当我们设置init-scale为其他值又是个什么情况呢?

下图是我设置了initial-scale=0.5的效果

可以看到document.documentElement.clientWidth和window.innerWidth都变成了750,为initial-scale=1的两倍,由此我们可以有一个假设:

layout viewport宽度 = ideal viewport宽度 / initial-scale

我们继续设置initial-scale=3,按照上述的结论,此时document.documentElement.clientWidth和window.innerWidth应该为125

事实证明我们上述的假设是正确的

flexible.js源码分析

flexible.js是阿里无线前端团队开源的用于移动端适配的库。虽然现在官方都承认可以放弃这个解决方案了,但是了解其中的思想还是很重要的

由于viewport单位得到众多浏览器的兼容,lib-flexible这个过渡方案已经可以放弃使用,不管是现在的版本还是以前的版本,都存有一定的问题。建议大家开始使用viewport来替代此方案。vw的兼容方案可以参阅《如何在Vue项目中使用vw实现移动端适配》一文。

废话不多说,直接上代码

(function flexible (window, document) {
  var docEl = document.documentElement
  var dpr = window.devicePixelRatio || 1

  // adjust body font size
  function setBodyFontSize () {
    if (document.body) {
      document.body.style.fontSize = (12 * dpr) + 'px'
    }
    else {
      document.addEventListener('DOMContentLoaded', setBodyFontSize)
    }
  }
  setBodyFontSize();

  // set 1rem = viewWidth / 10
  function setRemUnit () {
    var rem = docEl.clientWidth / 10
    docEl.style.fontSize = rem + 'px'
  }

  setRemUnit()

  // reset rem unit on page resize
  window.addEventListener('resize', setRemUnit)
  window.addEventListener('pageshow', function (e) {
    if (e.persisted) {
      setRemUnit()
    }
  })

  // detect 0.5px supports
  if (dpr >= 2) {
    var fakeBody = document.createElement('body')
    var testElement = document.createElement('div')
    testElement.style.border = '.5px solid transparent'
    fakeBody.appendChild(testElement)
    docEl.appendChild(fakeBody)
    if (testElement.offsetHeight === 1) {
      docEl.classList.add('hairlines')
    }
    docEl.removeChild(fakeBody)
  }
}(window, document))

由于版本不同,可能大家拿到的代码局部地方有所不同,但是整体思路还是不变的。

var docEl = document.documentElement
var dpr = window.devicePixelRatio || 1

// adjust body font size
function setBodyFontSize () {
    if (document.body) {
      document.body.style.fontSize = (12 * dpr) + 'px'
    }
    else {
      document.addEventListener('DOMContentLoaded', setBodyFontSize)
    }
}
setBodyFontSize();

setBodyFontSize这个函数的作用就是设置body标签的fontSize,fontSize的值dpr * 12,这个函数的作用是为了覆盖html的fontSize

// set 1rem = viewWidth / 10
function setRemUnit () {
    var rem = docEl.clientWidth / 10
    docEl.style.fontSize = rem + 'px'
}

setRemUnit()

首选获取document.documentElement.clientWidth的值,这个值表示当前设备layout viewport的宽度(你可以理解html标签的宽度),在iphone6 7 8下这个值是750,然后将整个视口分成10份,这样每一份的宽度为clientWidth / 10,即1rem = clientWidth / 10,之所以分成10份,完全是方便计算,你也可以随意切分,但是最小值不要小于12,因为在谷歌浏览器中有最小fontSize的限制

// reset rem unit on page resize
window.addEventListener('resize', setRemUnit)
window.addEventListener('pageshow', function (e) {
    if (e.persisted) {
      setRemUnit()
    }
})

这段代码是为了在window触发了resize和pageShow事件之后自动调整html的fontSize值

// detect 0.5px supports
if (dpr >= 2) {
    var fakeBody = document.createElement('body')
    var testElement = document.createElement('div')
    testElement.style.border = '.5px solid transparent'
    fakeBody.appendChild(testElement)
    docEl.appendChild(fakeBody)
    if (testElement.offsetHeight === 1) {
      docEl.classList.add('hairlines')
    }
    docEl.removeChild(fakeBody)
}

这句话的代码是检测0.5px的支持,但是我自己还没弄懂,有哪位同学如果弄明白了,可以在下面发个评论,大家互相学习

还有一点差点忘记了,设置viewport

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">

查看demo

看了下flexible的实现,我自己也简单的实现了下,基本思想同flexible.js一样,只不过我添加了一个自动计算scale的功能

var docuEl = document.documentElement
var metaEl = document.createElement('meta')
var dpr = window.devicePixelRatio
scale = 1 / dpr
metaEl.setAttribute('name', 'viewport')
metaEl.setAttribute('content', `initial-scale=${scale},maximum-scale=${scale},minimum-scale=${scale},user-scalable=no`)
docuEl.setAttribute('data-dpr', dpr)
document.head.appendChild(metaEl)

function resizeFontSize() {
    docuEl.style.fontSize = docuEl.clientWidth / 10 + 'px'
}
resizeFontSize()

window.onresize = resizeFontSize
window.onpageshow = function(e) {
    if (e.persisted) {
      resizeFontSize()
    }
}

vw & vh

如今flexible.js已经成了过去式,我们实现移动端自动适配,还要在head中添加js代码,对于任何一个追求完美的人确实不能忍,还好我们有vw和vh,而且它们在如今大部分手机中都得到了支持

查看我能否用vw

未完待续