移动端布局最佳实践(viewport+rem)
通过前几天写的两篇博客(浅谈移动端三大viewport和移动端em和rem区别),我们现在来总结一下如何实现一个最佳方案。
之前在第二篇博客中提到过我们可以使用媒体查询来针对不同设备及做适配,如下图
但是这个可能不会太精准,比如我的设备布局viewport可能是370px,这样只能使用320这一版本。而事实上,他们的viewport并不相同,所以他们的布局也得不一样。也就是说我们如果用css媒体查询只能说是可以用,但不是最佳实践。其实通过看某些牛逼的移动端网站,可以看到他们的共同点,他们都是使用js方式来实现的。
可以看到,慕课网以及我们现在做的今日十大H5页面,他们共同的地方就是在根html上设置data-dpr以及font-size。
为什么要设置这两个东西呢?
我们可以回归移动端的本质,就是根据不同的设备规划不同的布局,这里面有一点需要注意,就是任何分辨率下我们的字体要保持一致。所以data-dpr是针对字体设置的,font-size是我们不同设备上用rem的基准值。其实有一个规范的js文件可以帮我们解决以上问题,flexible.js(可以到github上copy)基本满足我们的需求。
;(function(win, lib) { 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)); } } } 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; } docEl.setAttribute('data-dpr', dpr); 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); } } 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); if (doc.readyState === 'complete') { doc.body.style.fontSize = 12 * dpr + 'px'; } else { doc.addEventListener('DOMContentLoaded', function(e) { doc.body.style.fontSize = 12 * dpr + 'px'; }, false); } refreshRem(); flexible.dpr = win.dpr = dpr; flexible.refreshRem = refreshRem; flexible.rem2px = function(d) { var val = parseFloat(d) * this.rem; if (typeof d === 'string' && d.match(/rem$/)) { val += 'px'; } return val; } flexible.px2rem = function(d) { var val = parseFloat(d) / this.rem; if (typeof d === 'string' && d.match(/px$/)) { val += 'rem'; } return val; } })(window, window['lib'] || (window['lib'] = {}));
简单的说一下这里面的重点吧,我们先设置页面viewport中的scale,这个用来解决border 1px问题,下面是我的一个css demo
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <title>Document</title> <style> ul li { width: 100px; padding: 8px 0 8px 15px; color: #7c7c7c; position: relative; } ul li + li:before { position: absolute; top: -1px; left: 0; content: ''; width: 100%; height: 1px; border-top: 1px solid #ddd; /*transform: scaleY(0.5);*/ } </style> </head> <body> <ul> <li></li> <li></li> <li></li> </ul> </body> </html>
然后我们下面展示的样式第一个是注释掉 transform: scaleY(0.5)的,第二个是打开的。
为什么会这样子呢,我在浅谈移动端三大viewport说过,dpr为2的高清屏下,1个css像素=4个物理像素(1px = dpr²*dp)
所以,设计师想要的retina下border: 1px;
其实就是1物理像素宽,对于css而言,可以认为是border: 0.5px;
,这是高清屏下(dpr=2)下能显示的最小单位,但并不是所有的浏览器都支持border:0.5px,所以我们需要scale(0.5),也就是上面flexible.js提到的scale,通过不同的dpr(devicePixelRatio)来设置不同的scale。
然后我们的主角rem是通过refreshRem函数中的docEl.getBoundingClientRect().width来获取的,这个值跟document.body.clientWidth是相同的,也就是布局viewport,之后的width/10纯属是为了便于计算。等下,当我们在iphone5下查看布局viewport时,为什么是640,而不是320,
这是因为我们在前面设置了scale,在iphone5下,通过获取dpr(devicePixelRatio)是2,然后计算出initial-scale是0.5,因为在initial-scale为1情况下,我们知道布局viewport等于设备宽度,也就是等于理想视口(screen.width),iphone5下的理想视口是320,然后缩放比是0.5,所以最终的布局viewport是640。移动端大神PPK曾经说initial-scale是理想视口与视觉视口之比,但我感觉既然设置了initial-scale,我们的视觉视口与布局视口就相等了,所以我们在计算时用的document.body.clientWidth,用window.innerWidth(视觉视口)其结果也一样,只不过是为了给大家演示。最后根据我们计算出来的html的基值可以在css编码中还原视觉稿的真实宽高。
比如我们拿到一个iphone6的高清设计稿(750×1334),在psd中量出一个div宽为750,如何转换为rem单位呢?
公式—— rem = px / 基值
通过flexible.js计算根html的font-size为75px,所以我们可以这样写css
div {
width: 10rem;
}
转换成html就是这样
width: 10rem; // -> 750px
最后因为dpr为2,scale了0.5,所以手机屏幕上的真实宽度为375px,就刚刚好。试想一下,假如我们不用initial-scale=0.5,那样我们得在这个设计稿上量完div后,再口算除以2,才会写width=375px,想想就累。
最后说一下字体的设置吧,这个比较特殊。以下分别是我们现在做的项目(今日十大)中不同设备的字体展示,可以清楚地看到任何手机屏幕上字体大小都要统一。
当然为了方便,我们使用sass做了一次遍历。在不同的data-dpr中遍历font-size(这里我们就看到了之前html中data-dpr的作用了)