一文看懂移动端适配

Published on
发布于·预估阅读21分钟
Authors
  • Name
    willson-wang
    Twitter

在我们的移动端开发中,常用的方式还是直接使用手淘的Flexible方案,引入手淘的flexible.js,然后通过postcss等插件将px转换成rem,不需要缩放的字体使用px,图片根据不同的dpr选择二倍、三倍图,或者要求不高的直接使用二倍图,通过各种方法解决1px边框问题

那么我们想过Flexible方案能够实现移动端适配的原理是什么?我们先看下代码,以0.3.2版本为例,仅保留关键代码

if (!dpr && !scale) {
    var isAndroid = win.navigator.appVersion.match(/android/gi);
    var isIPhone = win.navigator.appVersion.match(/iphone/gi);
    var devicePixelRatio = win.devicePixelRatio;
    // 如果是iphone,则根据window.devicePixelRatio获取dpr
    if (isIPhone) {
        // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
        if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {                
            dpr = 3;
        } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
            dpr = 2;
        } else {
            dpr = 1;
        }
    } else {
        // 其他设备下,仍旧使用1倍的方案
        dpr = 1;
    }
    // 计算缩放比例
    scale = 1 / dpr;
}

// 设置data-dpr属性,以便可以通过css属性选择器做一些样式处理
docEl.setAttribute('data-dpr', dpr);

// 设置meta标签
if (!metaEl) {
    metaEl = doc.createElement('meta');
    metaEl.setAttribute('name', 'viewport');
    // 设置initial-scale的值
    metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
    if (docEl.firstElementChild) {
        docEl.firstElementChild.appendChild(metaEl);
    } else {
        var wrap = doc.createElement('div');
        wrap.appendChild(metaEl);
        doc.write(wrap.innerHTML);
    }
}

function refreshRem(){
    var width = docEl.getBoundingClientRect().width;
    if (width / dpr > 540) {
        width = 540 * dpr;
    }
    // 将docuemntElement宽度分成10份,用于等比缩放
    var rem = width / 10;
    docEl.style.fontSize = rem + 'px';
    flexible.rem = win.rem = rem;
}

// 监听resize事件,如果触发resize事件,重新设置根元素的font-size
win.addEventListener('resize', function() {
    clearTimeout(tid);
    tid = setTimeout(refreshRem, 300);
}, false);


win.addEventListener('pageshow', function(e) {
    if (e.persisted) {
        clearTimeout(tid);
        tid = setTimeout(refreshRem, 300);
    }
}, false);

refreshRem();

从源码看,总共做了如下几件事

  1. 判断是否是iphone,如果是iphone则通过devicePixelRatio获取dpr,如果是安卓则dpr都默认是1,然后计算scale缩放比例

  2. 在html元素上设置data-dpr属性

  3. 如果没有meta标签则创建meta标签,且name=viewport content='initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no'

  4. 调用refreshRem方法,在html元素上设置fontSize

  5. 监听resize or pageshow事件,重新执行refreshRem方法

我们看完之可能会有如下疑问

  • 为什么需要判断iphone?

  • devicePixelRatio又是什么?

  • 在html元素上设置data-dpr属性的目的是什么?

  • 添加meta标签及meta标签上的属性都有什么作用?

  • 设置html上的font-size又是为什么?

  • 监听resize及pageshow事件的目的又是什么?

  • 做了上面这些事情之后,只需要我们将代码中的px转化为rem即可实现移动端的适配了,这又是为什么?

为了解决上面的疑惑,让我们了解下移动端布局的一些历史及一些专业名词

关于viewport

移动端刚开始的时候是没有专门针对移动端适配这一说法的,都是直接在手机上打开pc端的站点,那么导致浏览器上查看的时候就有左右滚动条,且需要我们自己手动去缩放屏幕才能够看清内容,在移动端流行之后,这样肯定不行,需要针对移动端进行适配,就出现了viewport,适口的概念;

根据ppk大神关于viewport的描述,移动端上有三个viewport,分别是layout viewport布局视口、visual viewport可视视口、ideal viewport理想视口,具体参考下图

image

image

image

ideal viewport并没有一个固定的尺寸,不同的设备拥有有不同的ideal viewport。所有的iphone的ideal viewport宽度都是320px,无论它的屏幕宽度是320还是640,也就是说,在iphone中,css中的320px就代表iphone屏幕的宽度。安卓设备就比较复杂了,有320px的,有360px的,有384px的等等

ideal viewport是最适合移动设备的viewport,ideal viewport的宽度等于移动设备的屏幕宽度,只要在css中把某一元素的宽度设为ideal viewport的宽度(单位用px),那么这个元素的宽度就是设备屏幕的宽度了,也就是宽度为100%的效果。ideal viewport 的意义在于,无论在何种分辨率的屏幕下,那些针对ideal viewport 而设计的网站,不需要用户手动缩放,也不需要出现横向滚动条,都可以完美的呈现给用户。

利用meta标签对viewport进行控制

移动设备默认的viewport是layout viewport,也就是那个比屏幕要宽的viewport,但在进行移动设备网站的开发时,我们需要的是ideal viewport。那么怎么才能得到ideal viewport呢?这就该轮到meta标签出场了

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

当我们如上设置时,既可以得到ideal viewport,至于具体原因可以参考移动前端开发之viewport的深入理解

我们还需要注意一个属性initial-scal,他的值范围时0-10之前的正数,通过移动前端开发之viewport的深入理解这篇文章可以了解到,缩放是相对于ideal viewport来缩放的,缩放值越大,当前viewport的宽度就会越小,反之亦然

以iphone5为例,ideal viewport的宽度是320px,如果设置initial-scal=1.0,则ideal viewport还是320,如果ideal viewport=2,则ideal viewport变成160,如果ideal viewport=0.5,则ideal viewport变成640

关于retina屏

在pc端布局的时候我们写100px,那么在屏幕上展示的时候往往对应着100个物理像素,所以我们一般认为css中的像素就是对应着设备的物理像素,其实不然的,pc端也不尽是这样,对于快速发展的移动端来说就更不尽然了,早期的移动端一般都是低清屏,也就是一个css像素对应一个设备像素,但是随着iphone4开始,苹果公司便推出了所谓的Retina屏,分辨率提高了一倍,变成640x960,但屏幕尺寸却没变化,这就意味着同样大小的屏幕上,像素却多了一倍,这时,一个css像素是等于两个物理像素的,之后安卓设备上的一个css像素相当于多少个屏幕物理像素,也因设备的不同而不同,没有一个定论

所以我们在写css样式的时候,同样的100px,在低清屏,即dpr为1的设备上,占据的是100个物理像素,但是在高清屏,dpr为2的设备上则会占据200个物理像素,这样就会导致1px占据2个物理像素,渲染情况被改变,可能对一个宽高固定的div看不出什么,但是对于图片来说则不一样;对于1px的边框也不一样;

我们平时使用的图片大多数是png、jpg这样格式的图片,它们称作是位图图像(bitmap),是由一个个像素点构成,缩放会失真;举个例子一张100px100px的图片,在dpr为1的屏幕上刚好占据,100100的物理像素,每一个像素一一对应,图片刚好被正常展示;但是在dpr2的屏幕上则会用200200的物理像素来展示这种图片,超过来图片本身的像素,则浏览器在处理的时候会放大该图片,导致图片变模糊,同理一张200200的图片,被展示在100*100的物理像素上,浏览器也会进行对应的压缩,将图片展示处理,导致图片略微失真

这种场景下1px的边框会占用2px的物理像素来展示,导致边框变粗,这个也就是移动端布局常说的1px边框问题

关于等比缩放

加入在html header内加入如下meta标签下,得到ideal viewport

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

我开始写代码,以iphone6 375为标准,有如下标签及样式

.wrap {
    font-size: 0;
}

.box1, .box2 {
    display: inline-block;
}

.box1 {
    width: 200px;
    height: 100px;  
    background-color: yellow;
}

.box2 {
    width: 175px;
    height: 100px;  
    background-color: blue;
}

<div class="wrap">
    <div class="box1"></div>
    <div class="box2"></div>
</div>

这种在ideal viewport为375的机型上完美展示在一行,但是到宽度比375小的屏幕时,则会出现换行现象,如下图所示

这时候可能会想到,我用百分比布局不久好了吗,但是碰到复杂的dom结构呢?那么有没有一种方式,可以让我在不同的ideal viewport上展示一样的效果呢?答案就是等比缩放,我根据不同的ideal viewport去缩放不就行了吗?那么怎么去达到等比缩放的效果呢? 答案就是rem单位

rem的具体原理可以查看Rem布局的原理解析

简单点说rem单位是相对html元素上的font-size来进行计算的,也就是说我们只要改变font-size这个基准,那么rem单位尺寸就会发生改变

还是以上面的代码为例子,以iphone6 375为准的话,我们将375分成10份,那么每一份就是37.5,那么我们可以认为在375的屏幕下,1rem=37.5px,同理在320的屏幕下,1rem=32rem,所以我们只需要根据不同的屏幕设置html上的font-size,然后将px尺寸转化成rem尺寸

.wrap {
    font-size: 0;
}

.box1, .box2 {
    display: inline-block;
}

.box1 {
    width: 5.333333333333333rem; // 1rem=375px,那么200px就是200/37.5 = 5.333333333333333rem
    height: 2.6666666666666665rem;   // 2.6666666666666665rem
    background-color: yellow;
}

.box2 {
    width: 4.666666666666667rem; // 4.666666666666667rem,也就是.box1的width+.box2的width=10rem
    height: 2.6666666666666665rem;  // 2.6666666666666665rem
    background-color: blue;
}

<div class="wrap">
    <div class="box1"></div>
    <div class="box2"></div>
</div>

<script>
document.documentElement.style.fontSize = document.documentElement.clientWidth / 10 + 'px'
</script>

所以我们可以知道通过rem可以达到等比缩放的效果

关于1px

通过上面的等比缩放,我们已经知道,通过rem已经可以适配不同的屏幕了,但是我们1px的问题还没解决,就是在高清屏下,1px会用2个物理像素来进行展示,到时1px变粗,那么怎么解决呢?

以iphone6为例,屏幕尺寸当成是375,但是因为dpr等于2,那么宽上的物理像素有750,那么有没有办法,让ideal vieport变成750呢?这样就是1px对应一个1个物理像素;通过前面我们知道initial-scale是针对ideal vieport去进行缩放的,当initial-scale=0.5时,ideal vieport会放大一倍,变成750,所以刚好满足1px对应一个物理像素,同理,我们只需要根据dpr去进行对应的缩放就能满足1px占据一个1个物理像素

.wrap {
    font-size: 0;
}

.box1, .box2 {
    display: inline-block;
}

.box1 {
    width: 5.333333333333333rem; // 1rem=375px,那么200px就是200/37.5 = 5.333333333333333rem
    height: 2.6666666666666665rem;   // 2.6666666666666665rem
    background-color: yellow;
}

.box2 {
    width: 4.666666666666667rem; // 4.666666666666667rem,也就是.box1的width+.box2的width=10rem
    height: 2.6666666666666665rem;  // 2.6666666666666665rem
    background-color: blue;
}
.list1, .list2 {
    list-style: none;
    font-size: 16px;
    line-height: 44px;
}
.list1 li {
    border-bottom: 1px solid #ccc;
}

.list2 li {
    border-bottom: 0.5px solid #ccc;
}

<div class="wrap">
    <div class="box1"></div>
    <div class="box2"></div>
</div>
<ul class="list1">
    <li>小明</li>
    <li>小黑</li>
    <li>小白</li>
</ul>
<ul class="list2">
    <li>小明</li>
    <li>小黑</li>
    <li>小白</li>
</ul>

<script>
document.documentElement.style.fontSize = document.documentElement.clientWidth / 10 + 'px'

</script>

具体展示如下图所示

image

通过initial-scale进行缩放,将ideal vieport变成实际物理尺寸的宽度,满足1px刚好适用1个物理像素来进行渲染就解决了1px的问题

const dpr = window.devicePixelRatio
const meta = document.createElement('meta')
meta.setAttribute('name', 'viewport')
meta.setAttribute('content', `initial-scale=${1 / dpr}, user-scalable=no`)
document.head.appendChild(meta)
document.documentElement.style.fontSize = document.documentElement.clientWidth / 10 + 'px'

image

当然这只是其中的一种解决1px的方式,另外还有其它很多方法解决1px的问题,详情了解再谈Retina下1px的解决方案

关于一倍图、二倍图、三倍图

从前面我们已经知道png、jpg等格式的图片时位图,而位图根矢量图的区别可以参考位图和矢量图区别

也就是100px100px的图片,刚好使用100100的物理像素来展示是刚刚好的,一一对应的关系,如果使用多于or少于100*100的物理像素来展示图片,则会出现缩放的情况,导致图片失真,如下所示

.img1 {
    width: 6.933333333333334rem;
    height: 4.8rem;
}

<div>
    <p>
        260px*180px
    </p>
    <img class="img1" src="../static/image/bg.png" alt="" />
</div>
<div>
    <p>
        520px*360px
    </p>
    <img class="img1" src="../static/image/[email protected]" alt="" />
</div>

image

在dpr2的高清屏下二倍图刚好展示,一倍图模糊

所以我们需要根据dpr来选择不同的倍图来进行展示,尽量保证图片不失真,当然还有折中方案,就是不根据dpr来进行判断,直接使用二倍图

了解来了上面这些内容之后,在回过头看前面的的问题

为什么需要判断iphone?

因为总iphone4最开始引入高清屏,又因为安卓下机型过多,放弃适配,所以只特殊判断了iphone,安卓统一为1

devicePixelRatio又是什么?

返回当前显示设备的物理像素分辨率与CSS像素分辨率之比。 此值也可以解释为像素大小的比率:一个CSS像素的大小与一个物理像素的大小。 简单来说,它告诉浏览器应使用多少屏幕实际像素来绘制单个CSS像素。比如iphone6 dpr为2,那么告诉浏览器需要使用2个物理像素来绘制单个css像素

在html元素上设置data-dpr属性的目的是什么?

有些场景下不需要使用rem单位进行等比缩放,但是又需要进行不同的大小展示,这时可以直接使用css属性选择器来进行匹配

添加meta标签及meta标签上的属性都有什么作用?

设置ideal viewport,及ideal viewport的比例,是否允许缩放

设置html上的font-size又是为什么?

进行等比缩放,适配不同机型

监听resize及pageshow事件的目的又是什么?

当我们转动横竖屏的时候,需要重新计算html上的font-size保证样式能够重新适配

做了上面这些事情之后,只需要我们将代码中的px转化为rem即可实现移动端的适配了,这又是为什么?

因为通过添加meta标签,设置ideal viewpor,通过rem实现等比缩放,所以已经基本满足移动端的适配了,剩下只需要考虑1px边框问题及不同倍图图片问题

关于vw、vh

vw、vh是css中的长度单位,更具体点就是视口的长度单位

1vw=1%的宽度、1vh=1%的高度,也就是说任何机型下任何屏幕的宽度刚好是100vw,高度是100vh;到这里是不是感觉这个单位就是用来进行等比缩放的;跟rem一样都是用来进行等比缩放,而不像rem需要依赖html元素上的font-size,vw、vh则不需要依赖其它东西

替换rem进行等比缩放

以iphone6 375为准

.wrap {
    font-size: 0;
    width: 100vw;
    height: 100vh;
}

.box1, .box2 {
    display: inline-block;
}

.box1 {
    width: 53.333333333333336vw; // vw 100vw = 375 1vw=3.75px
    height: 14.992503748125937vh;   // vh 100vh = 667 1h=6.67px
    background-color: yellow;
}

.box2 {
    width: 46.666666666666664vw;
    height: 14.992503748125937vh;   
    background-color: blue;
}

<div class="wrap">
    <div class="box1"></div>
    <div class="box2"></div>
</div>

更多使用方式参考基于vw等viewport视区单位配合rem响应式排版和布局

兼容性如下图所示

image

注意就算使用vw、vh单位进行布局,还是会存在1px及img失真问题

总结

  1. 通过meta标签实现ideal viewport
  2. 通过rem、vw、vh css长度单位实现等比缩放
  3. 缩放init-sacle的目的是解决1px的问题
  4. 关于图片的最佳展示,是根据dpr去选择不同倍图

参考链接:

lib-flexible 移动前端开发之viewport的深入理解 基于vw等viewport视区单位配合rem响应式排版和布局 使用Flexible实现手淘H5页面的终端适配 再谈Retina下1px的解决方案 再聊移动端页面的适配 HTML meta 元素 A tale of two viewports — part two Meta viewport