如何优雅地跳出移动端布局的浅坑

如何优雅地跳出移动端布局的浅坑

最近在移动端H5开发中踏坑无数,在这里打算把问题整理一遍跟大家好好讨论。

  • 响应式只是适配PC、平板与手机之间的解决方案,在移动端H5开发中并非是很好的解决办法
  • 通过Javascript操作DOM属性来辅助布局的弊端

还是不得不从基本概念入手…

像素

物理像素(physical pixel)

设备像素(device pixel)指的就是物理像素,一个物理像素是设备上的最小物理显示单元。

设备逻辑像素(density-independent pixel)

CSS像素(css pixel)指的就是逻辑像素,可以理解为系统为设备安排的虚拟显示单元,此单元是为系统程序所使用,它与物理像素之间存在一定的转换关系。

每英寸像素(pixel per inch)

每英寸像素(ppi)又称为像素密度,是表示设备单位面积上像素数量的指数,一般来说ppi越高,屏幕显示的图像越精细。(详见维基百科

前端开发中与ppi联系较大的是retina这个概念,超过300ppi的屏幕被常称为Retina显示屏,iphone4及之后的版本都采用了Retina屏(@2x)。

设备像素比(device pixel ratio)

设备像素比(dpr)定义了物理像素和逻辑像素的对应关系,换算公式为:

dpr = 物理像素 / 逻辑像素

譬如,iphone6的物理像素为750,逻辑像素为375,dpr = 2。

  • Javascript
    window.devicePixelRatio可以获取当前屏幕的dpr;
  • CSS
    可以通过以下条件进行媒体查询:
    -webkit-device-pixel-ratio
    -webkit-min-device-pixel-ratio
    -webkit-max-device-pixel-ratio

在逻辑像素相同的不同屏幕(普通屏幕 vs Retina屏@2x)上,CSS像素所呈现的大小(物理尺寸)是一样的,只是一个CSS像素所对应的物理像素的个数是不一样的,见下图(原谅我的盗图):

Retina屏与普通屏

渲染像素(rendered pixel)

渲染像素是在iphone6使用Display Zoom功能后引入的概念。

  • iphone6

未使用Display Zoom功能时,逻辑像素为375×667(@2x);

开启Display Zoom功能后,逻辑像素与iphone5相同(320×568),渲染像素为640×1136,但iphone6的物理像素是750×1334,比渲染像素高,则设备会将画面“拉大”显示,导致画面模糊!

  • iphone6 plus

未使用Display Zoom时,逻辑像素为414×736(6p采用了新的Retina屏:@3x),则渲染像素应该为1242×2208,然而在实际生产中6p使用了1080p(1080×1920)屏幕,所以需要使用Downscale技术对画面进行压缩(downsampling / 1.15)。

使用Display Zoom后,逻辑像素为375×667,渲染像素为1125×2001,则需要使用Downscale技术对画面进行压缩(downsampling × 0.96)。

由于6与6p的像素密度非常高,压缩对画面呈现效果没有太大的影响。

位图像素

一个位图像素是栅格图像(png,jpg,gif等)最小的数据单位,每个位图像素中都包含显示信息(显示位置,颜色值,透明度等)。

那么问题来了,在Retina屏中,由于位图像素不能再分割,所以只能就近取色,导致图片模糊。这种问题在我们用chrome开发者工具抓取元素颜色值的时候很容易发现,见下图(再次原谅我的盗图):

这里写图片描述

比较好的解决方案就是使用两倍像素大小的图片。

那么问题又来了,两倍像素图片在普通屏幕下时会经过downsampling过程(通过算法进行取色),会产生一点点色差(或者说失去锐利度)。

这里写图片描述

在移动端H5开发时,最好针对不同dpr的屏幕准备不同的图片!

唉,总之在普通屏跟Retina屏不统一之前,前端布局都会有各种大大小小的问题,譬如border:1px等,在此不一一探究,各位可以在参考文章的链接里面找到部分分析贴。

在此真诚附上:各大流行手机的屏幕参数


各种单位扫盲

px与pt

px(pixel):像素,是屏幕上显示数据的最基本的点,是相对长度;
pt(point):印刷行业常用单位,等于1/72英寸,是绝对长度。

转换关系:1 pt = ( ppi / 72 ) px

rem与em

em:相对与当前对象内文本的字体尺寸(如当前行内文本的字体尺寸未被人为设置,则相对于浏览器的默认尺寸);
rem(root em):相对于HTML根元素的字体尺寸。

vh、vw、vmin、vmax

vh(viewport width):1vw等于视窗宽度的1%;
vw类似。
vmin:vw,vh中较小的那一个;
vmax类似。

浏览器兼容情况:
vh、vw、vmin、vmax兼容性

再次真诚附上:CSS属性兼容性查询


各平台移动端适配方案介绍

网易适配方案

精髓在于用过js获取device-width,然后动态计算根节点的font-size值。

因为网易使用的是640px的设计稿,计算rem的基准为100,则计算方法如下:

  • device-width = 320
    font-size = 320 / ( 640 / 100 ) px = 50px

  • device-width = 375
    font-size = 375 / ( 640 / 100 ) px = 58.5975px

手机淘宝适配方案 —— flexible

机智的手淘团队,一上来先判断是否已有meta标签设置了缩放比例和data-dpr值;

PS:常见的meta标签设置:

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

<meta name="flexible" content="initial-dpr=2" />
// flexible.debug.js
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector('meta[name="viewport"]');
var flexibleEl = doc.querySelector('meta[name="flexible"]');
var dpr = 0;
var scale = 0;
var tid;
var flexible = lib.flexible || (lib.flexible = {});

if (metaEl) {
    console.warn('将根据已有的meta标签来设置缩放比例');
    var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
    if (match) {
        scale = parseFloat(match[1]);
        dpr = parseInt(1 / scale);
    }
} else if (flexibleEl) {
    var content = flexibleEl.getAttribute('content');
    if (content) {
        var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
        var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
        if (initialDpr) {
            dpr = parseFloat(initialDpr[1]);
            scale = parseFloat((1 / dpr).toFixed(2));    
        }
        if (maximumDpr) {
            dpr = parseFloat(maximumDpr[1]);
            scale = parseFloat((1 / dpr).toFixed(2));    
        }
    }
}

当页面meta标签未设置时 ——

dpr设置

// flexible.debug.js
docEl.setAttribute('data-dpr', dpr);
if (!dpr && !scale) {
    var isAndroid = win.navigator.appVersion.match(/android/gi);
    var isIPhone = win.navigator.appVersion.match(/iphone/gi);
    var devicePixelRatio = win.devicePixelRatio;
    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;
}

那么问题又来了,为什么只对ios设备进行dpr判断,而对安卓设备设置dpr始终为1呢?这是赤裸裸的鄙视么?

iphone4~6的dpr都是2;
iphone6+的dpr是3;
而安卓(dpr = 1 , 1.5 , 1.75 , 2 , 2.5 , 3 , 4 ……)

说多了都是泪。。。

动态设置meta标签

// flexible.debug.js
if (!metaEl) {
    metaEl = doc.createElement('meta');
    metaEl.setAttribute('name', 'viewport');
    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);
    }
}

所以其实flexible的精髓就在于动态改写meta标签。

flexible里面用到的resize和pageshow事件:

function refreshRem(){
    var width = docEl.getBoundingClientRect().width;
    //这个判断基本针对某些大屏非高清安卓手机
    if (width / dpr > 540) {
        width = 540 * dpr;
    }
    var rem = width / 10;
    docEl.style.fontSize = rem + 'px';
    flexible.rem = win.rem = rem;
}
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);

上面这段代码中有一部分在chrome开发者工具中测试时会出现一点小问题,当用工具把iphone5调成iphone6p时,width的yan jiu值为828,而刷新页面后才是正确值1242,这个问题在6p跟其他型号切换时都会出现,个人认为这跟6p retina的特殊性有关,详细原因有待研究。

resize:当浏览器窗口被调整大小时触发;
pageshow:当窗口成为可见时触发—— pageshow事件详见

不过需要注意的是,一般文本字号都使用px而不使用rem,一是因为在非retina和retina屏下面,我们希望看到大小相同的字号;二是因为绝大多数的文字字体都自带点阵尺寸,不希望出现13px和15px这种奇葩尺寸。

所以,我们可以通过meta标签设置的data-dpr属性来设置字号:

[data-dpr="1"]{
    font-size: 14px;
}
[data-dpr="2"]{
    font-size: 28px;
}
[data-dpr="3"]{
    font-size: 42px;
}

在这里就可以看到less跟sass这些css预处理器的优势所在了!


多屏适配最佳方案

在Google搜刮了各种移动端适配方案后,个人最青睐手淘的做法,下述方法很大程度参考flexible。

以前在做项目的时候,基本都是媒体查询+rem,然而没有考虑到dpr的问题(毕竟即使iphone已成街机,但国内安卓手机的使用量仍不容小觑,更别提那些还停留在苹果4和安卓2.3的了……),所以即使使用了rem,最后还是不得不使用js操作dom来辅助定位(这种方式会因为js在文档中的渲染顺序导致页面出现问题,这一点之后我会另外写一篇博客分析)

因此,我们需要借用dpr来做更多的事情:

针对不同屏幕尺寸和dpr的手机动态改变文档根节点的font-size大小

网上很多人总结的一条公式:

rem = device-width * dpr / 10

附带的例子:
iphone3gs:320px /10 = 32px;
iphone4/5:320px * 2 /10 = 64px;
iphone6:375px * 2 /10 =75px;
iphone6p:414px * 3/10 =124.2px;(注意6p的dpr是3而非2.6!)

然而,这个例子是有问题的,device-width是指设备的物理像素,同时js的DOM对象document.documentElement.clientWidth获取的也是设备的物理像素!

所以,按照公式实际的rem变量值(iphone5)是128px。

那么问题又来了,这里该不该乘以dpr值,或者说乘以dpr值的意义在哪?

网上说乘以dpr是为了解决retina屏下border:1px问题( retina(@2x)下border:1px将会变粗 ),但其实这个问题已经通过根据dpr动态设置meta标签的缩放解决了,所以个人认为没必要乘以dpr。

CSS实现

媒体查询:

// 适配iphone4、5
@media screen and (max-width: 374px) {
    html{font-size: 32px;}
}
// 适配iphone6
@media screen and (min-width: 375px) and (max-width: 413px) {
    html{font-size: 64px;}
}
// 适配iphone6p
@media screen and (min-width: 414px) {
    html{font-size: 75px;}  
}

此方法的缺点显而易见,在同一逻辑像素区间跨度下可能有不同宽度的设备,然而他们的基准值却是一样大的。

Javascript实现

通过这个API获取设备的宽度:

document.documentElement.clientWidth;
document.documentElement.getBoundingClientRect().width;

获取设备dpr

window.devicePixelRatio

自己写程序就无需仿效flexible的做法针对苹果手机进行dpr设置,若读者有意,则需要用到正则表达式和browers对象,在此不作深究,大家可以看看参考flexible源码。

navigator.appVersion

贴上高仿flexible代码:

(function(win){
    var dpr = 0 , scale = 0 , rem = 0,tid;
    dpr = win.devicePixelRatio || 1;
    scale = 1 / dpr ;

    // meta element settings
    var doc = win.document ;
    var doc_el = doc.documentElement ; 
    doc_el.setAttribute( 'data-dpr' , dpr ) ;
    var meta_el = doc.createElement( 'meta' ) ;
    meta_el.setAttribute( 'name' , 'viewport' ) ;
    meta_el.setAttribute( 'content' , 'initial-scale=' + scale + ',minimum-scale=' + scale + ',maximum-scale=' + scale + ',user-scalable=no') ;
    doc_el.firstElementChild.appendChild( meta_el ) ;

    refresh();
    function refresh(){
        rem = doc_el.clientWidth / 10 ;
        doc_el.style.fontSize = rem + 'px';
    }
    // refresh for development test
    win.addEventListener('resize', function() {
        clearTimeout(tid);
        tid = setTimeout(refresh, 300);
    }, false);
    win.addEventListener('pageshow', function(e) {
        if (e.persisted) {
            clearTimeout(tid);
            tid = setTimeout(refresh, 300);
         }
    }, false);
})( window );

rem与px 转换

转换公式只需看上文中的js代码对于rem变量是怎么定义的就行了,这里作一下提醒,设计师基本只会以某一型号手机为基准画视觉稿,通常以iphone6为准,则只需把元素的尺寸除以视觉稿的基准值(iphone6的rem变量值为75)就是元素的rem尺寸了,通常我们都会用预处理器的mixin写一个处理函数,很简单,大家可自行去尝试。

如果有粉sublime text3的“道友”,推荐一个解决这个问题的插件:CSSREM

兴起写的第一篇博客,各位如有不同或更好的见解务必提出来,3Q~


关于js操作DOM动态定位元素的问题,由于js在html文档加载渲染顺序的问题,导致页面刚刷新的时候会出现元素漂移的现象,这个问题待我在下一篇博客中详谈。


参考文章

http://www.cocoachina.com/webapp/20150715/12585.html?utm_source=tuicool&utm_medium=referral
http://www.kuqin.com/shuoit/20140725/341390.html
https://www.zhihu.com/question/27261444/answer/35898885
http://web.jobbole.com/84285/


  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值