关于移动端适配

377 阅读7分钟
之前写移动端页面基本都是直接rem布局,对其中很多原理没有去详细分析了解,这几天看了一些文档,和同事进行了一些讨论,下面是我的一些梳理和记录,首次博客,如有幸被大家阅读,不对的地方请温柔指出,互相学习,废话不多说~ 

 移动端适配涉及的基本概念理解

 viewport(视窗)

移动端的viewport包括layout viewport、visual viewport、ideal viewport

layout viewport:布局layout(也就是你的html页面实际可以展示的宽度),一般layout viewport默认大小980px, 明显这个值大于移动端浏览器的可视区域宽度。我们可以通过document.documentElement.clientWidth来获取layout viewport的宽度。当你设置meta标签的content属性的时候也会改变layout viewport的大小。  

visual viewport:表示浏览器可视区域的大小

 ideal viewport:设备理想viewport,它需要满足用户在不需要缩放和滚动条就能查看所有内容,显示的文字和图片的大小是合适的,要达到这种效果方法(这也是我们移动端适配需要实现的效果),要使layout viewport = visual viewport

 实现方法有

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

设置width=device-width,代表将layout viewport设置为设备宽度,例如iphone6的设备宽度为375px,此时layout viewport = 375px。这也是为什么我们写移动端页面的时候,会二话不说,先把这个meta标签引到head里面

设备像素(也叫物理像素)

他是显示设备中一个最微小的物理部件。

设备独立像素

可以认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用的虚拟像素(比如说CSS像素),然后由相关系统转换为物理像素。

设备像素比(device pixel ratio/dpr)

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

在JavaScript中,可以通过window.devicePixelRatio获取到当前设备的dpr。而在CSS中,可以通过-webkit-device-pixel-ratio-webkit-min-device-pixel-ratio-webkit-max-device-pixel-ratio进行媒体查询,对不同dpr的设备,做一些样式适配。

普通屏和Retina屏

Retina屏也叫视网膜屏和高清屏(最初的设定是dpr=2的屏幕,但由于手机制造厂商的不同,不同的品牌手机的dpr各有不同,下面附件一些手机的设备参数,最右边一列表示设备的dpr,糟心的是android手机的dpr很混乱,有1.75、3等各种比较特殊的dpr值,这也是最初为什么淘宝在设计移动端适配的时候直接忽略android机的dpr,全部默认为1【下面会有代码说明】),Retina屏作用显而易见,更高清。在普通屏幕下1个CSS像素对应1个设备像素,而在Retina屏幕下,1个CSS像素对应的却是dpr * dpr 个设备像素。


大概一些基本概念的了解就上面这些,接下来进入代码分析,flexble.js和vw到底是如何实现移动端适配的呢?

关于淘宝的适配方案flexble.js

github地址:github.com/amfe/lib-fl…
官方文档地址:github.com/amfe/articl…

关于flexble.js,它主要做的事情其实就是三件

1、动态设置html的font-size

2、动态设置meta的content 属性,也就是页面的scale(缩放比例)

3、动态设置html的data-dpr

下面我将附上代码和注解

;(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 = {});
    
   // 个人觉得可以省略以下这些代码(/*  */内的),上面文字中有提到过flexble.js作用包括:根据手
   // 机的不同dpr动态设置meta标签和html的data-dpr,如果你自己写上meta标签,并设置它的initial
   // -scale属性,就等同你强行设置了initial-scale和data-dpr
 /* 
 if (metaEl) {
     // 根据已有的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标签
    if (!dpr && !scale) {
        // 淘宝的flexble.js只对ios机的dpr做了动态设置,android机则默认设置为1
        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;
        // 因为现在很多android机的dpr不再是1,也可能是2或者3等一些值,所以我个人觉得为了兼容,
        // 可以将以上代码稍做改造,不区分ios和android
        var devicePixelRatio = win.devicePixelRatio;
        if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) { 
            dpr = 3;
        } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
           dpr = 2;
        } else {
           dpr = 1;
        }
        scale = 1 / dpr;
    }
    // 设置html的data-dpr属性是为了设置不同dpr下的css样式,例如你要设置dpr=1的div下的字体为12px
    // 那dpr=2下的字体应该为24px才能显示正常
    [data-dpr="1"] div{
       font-size: 12px;
    }
    [data-dpr="2"] div {
       font-size: 24px;
    }
    // 为了方便开发,可以用sass来实现
    @mixin font-dpr($font-size){
         font-size: $font-size;
         [data-dpr="2"] & {
            font-size: $font-size * 2
         }
         [data-dpr="3"] & {
            font-size: $font-size * 3
         }    }
    @include font-dpr(12px)
    docEl.setAttribute('data-dpr', dpr);
    // 不存在viewport的meta标签时,进行meta的标签的动态设置,如直接覆盖原有meta标签,
    // 用flexble.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);
        }
    }
    // 设置rem大小
    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;
    }
    // 当浏览器视窗大小发生改变时触发resize时间,重新计算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);
    // 设置body的字体大小,以12px为基准,防止页面元素未设置字体大小时,字体过大
    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;
    // rem 与 px的相互转换
    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'] = {}));

vw适配方案

淘宝flexble官方有这样一段话

由于viewport单位得到众多浏览器的兼容,lib-flexible这个过渡方案已经可以放弃使用,不管是现在的版本还是以前的版本,都存有一定的问题。建议大家开始使用viewport来替代此方案。vw的兼容方案可以参阅《如何在Vue项目中使用vw实现移动端适配》一文。所以我们可以开始在项目中试着使用vw来做移动端的兼容,本人只是偶尔用一下vw和vh,还没有完全脱离rem来进行移动适配。

vw的概念

vw 是基于Viewport视窗的长度单位,这里的视窗(Viewport)指的是浏览器可视化的区域。vw来替代以前Flexible中的rem缩放方案。先来回归到我们的实际业务中来。目前出视觉设计稿,我们都是使用750px宽度的,那么100vw = 750px,即1vw =7.5px。那么我们可以根据设计图上的px值直接转换成对应的vw值。看到这里,很多同学开始感到崩溃,又要计算,这是插件的作用就体现了,我们可以使用PostCSS的插件postcss-px-to-viewport,让我们可以直接在代码中写px。

关于插件如何用可以参考文章 www.w3cplus.com/css/vw-for-…

                                         https://github.com/evrone/postcss-px-to-viewport

Viewport不足之处

采用vw来做适配处理并不是只有好处没有任何缺点。有一些细节之处还是存在一定的缺陷的。比如当容器使用vw单位,margin采用px单位时,很容易造成整体宽度超过100vw,从而影响布局效果。对于类似这样的现象,我们可以采用相关的技术进行规避。比如将margin换成padding,并且配合box-sizing。只不过这不是最佳方案,随着将来浏览器或者应用自身的Webview对calc()函数的支持之后,碰到vwpx混合使用的时候,可以结合calc()函数一起使用,这样就可以完美的解决。

另外一点,px转换成vw单位,多少还会存在一定的像素差,毕竟很多时候无法完全整除。