如何优雅地跳出移动端布局的浅坑
最近在移动端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)定义了物理像素和逻辑像素的对应关系,换算公式为:
譬如,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像素所对应的物理像素的个数是不一样的,见下图(原谅我的盗图):
渲染像素(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类似。
浏览器兼容情况:
再次真诚附上:CSS属性兼容性查询
各平台移动端适配方案介绍
网易适配方案
精髓在于用过js获取device-width,然后动态计算根节点的font-size值。
因为网易使用的是640px的设计稿,计算rem的基准为100,则计算方法如下:
device-width = 320
font-size = 320 / ( 640 / 100 ) px = 50pxdevice-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/