webview布局适配实践

4,329 阅读6分钟

一、相关概念

1、viewport:移动设备(包括webview)用来显示网页的那一块区域;
2、devicePixelRatio属性(别名像素比,简称dpr):window.devicePixelRatio =物理像素 / 独立像素(css中的px);
3、rem:相对于根元素(即html元素)font-size计算值的倍数(我以前真不大清楚)。

上面许多概念请看这篇 www.cnblogs.com/2050/p/3877…

二、淘宝flexible.js

下面是flexible.js的源码:

;(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'] = {}));

我以前做移动端适配都采用百分百布局,通常粘贴复制下面一段代码,而且还乐此不疲

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

实际网上大部分移动端适配的方案都是rem布局外加使用flexble.js原理,其核心要点(以iPhone6为例)如下:
1、手动设置上述meta viewport( initial-scale=1.0)标签,加载flexble.js成功后会在html标签设置data-dpr=1,font-size=37.5px;
  1rem = 37.5px = doc.documentElement.getBoundingClientRect().width/10 + “px”;
2、如果没有手动添加meta viewport标签,flexble.js会动态设置此标签,但是和上面的情况有区别,会执行如下流程:
  a、获取devicePixelRatio(dpr)值,iPhone6值为2;
  b、设置scale = 1/dpr = 0.5;
  c、渲染viewport标签,动态设置html标签属性data-dpr=2,font-size=75px;;
  d、rem与1的获取方式相同,只不过这里面的1rem=75px;

所以flexible.js通过这两种方式布局,如果在css文件中设置一个div标签的宽度,设计图的长度是75px,第一种方式需设置width=2rem,第二种需设置width=1rem。

  以上呈现的方式适合苹果手机,安卓手机flexble.js默认dpr值为1,所以网上大部分的结论是flexible.js不适合安卓手机,既然安卓手机devicePixelRatio 也有约等于1,2,3,为什么就不能用呢。而且flexble.js只考虑竖屏的情况,设置doc宽度不大于540px。

三、我遇到的情况

  工作中一直在安卓webview中进行web开发,从接手项目的css样式中都是100%布局,后来机器越来越多,分辨率600到1920不等,而且rom层默认设置也会对机型的分辨率有影响。关键布局是否合理,一般由产品和UI决定,原项目通过media标签对各种机型不同样式适配,冗余度和复杂度高,可维护性差。
   UI设计师通常以一种机型为基准进行设计,比如说UI设计师给的设计图是1920 x 1200,那么她会以这个机型衡量其它机型,如960 x 600的机型就应该等比例缩放。布局从视觉上看不会有差异,这才达到她理想的兼容适配。

四、修改flexible.js

  根据上面遇到的情况,如果以rem布局,在1920 x 1200分辨率下是1rem=192px,那么在960 x 600分辨率的机型中1rem = 96px,这里将document宽度十等分。既然这样我也可以运用flexble.js,只需修改相关逻辑就行,而且改动不大,修改后如下:

'use strict'
const doc = window.document;
const docEl = doc.documentElement;
let metaEl = doc.querySelector('meta[name="viewport"]');
let dpr=0,scale=0,tid=null;
let msoFlex = {};

if (!dpr && !scale) {
    const devicePixelRatio = window.devicePixelRatio;
    if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {                
        dpr = 3;
    } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
        dpr = 2;
    } else {
        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 {
        const wrap = doc.createElement('div');
        wrap.appendChild(metaEl);
        doc.write(wrap.innerHTML);
    }
}
const refreshRem = function(){
    const width = docEl.getBoundingClientRect().width;
    const rem = width / 10;
    docEl.style.fontSize = rem + 'px';
    msoFlex.rem = rem;
}

window.addEventListener('resize', function() {
    clearTimeout(tid);
    tid = setTimeout(refreshRem, 300);
}, false);
window.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();
msoFlex.dpr = dpr;
msoFlex.refreshRem = refreshRem;
msoFlex.rem2px = function(d) {
    var val = parseFloat(d) * this.rem;
    if (typeof d === 'string' && d.match(/rem$/)) {
        val += 'px';
    }
    return val;
}
msoFlex.px2rem = function(d) {
    var val = parseFloat(d) / this.rem;
    if (typeof d === 'string' && d.match(/px$/)) {
        val += 'rem';
    }
    return val;
}

修改点如下:
  1、将flexible.js手动设置viewport标签的逻辑删除,如果不删除,dpr不是1的情况,在viewport标签缩放scale值是默认的1.0,1px可能就不是真正1像素宽度,那么随之而来就是你要适配1像素宽度,网上有很多方案,比如伪元素,图片代替,另一种就是缩放,所以用代码动态设置viewport标签,scale值会随dpr值变化,1px也会缩放的,这里面我一般在样式中设置1像素宽度的单位就是px,而非rem;
  2、将flexible.js对iPhone的retain屏1,2,3设置三种情况同样适用安卓机,并且document宽度由540改为不限制宽度。
  3、命名改变了,flexible改成msoFlex,为了区别flexible.js。

五、项目微调

  1在vue工程中引用mso-flex,先npm install mso-flex --save-dev,然后在main.js头部引用import "mso-flex/mso-flex.js"即可(可以用yarn)。
  2、字体的问题,一般如果UI和产品不介意字体有瑕疵的话可以不用修改,因为用rem换算的过程中,字体可能会出现奇数和小数的字体(13px或者13.343px),这样的字体像素眼看起来有模糊。如果你用的是scss样式处理器@mixin方法对data-dpr判断,根据属性值的不同,设置字体大小。
  3、rem和px转换,需安装postcss-pxtorem,在书写样式的时候直接在psd文件中量大小像素作为长宽的数值,不需要每次用计算器将px换算成rem,方便快捷。

六、vue-cli3.0使用配置

  为了观察效果,直接准备运行vue-cli工程,发现vue-cli大变样,真的体会到在前端日益发展的过程中学不动了。
  通过官网 yarn 命令启动vue服务,先后安装
  yarn add -D mso-flex;
  yarn add -D sass-loader node-sass;
  yarn add -D postcss-pxtorem;
  下面是工程目录(真的简洁很多)


上面vue.config.js是新建配置相关loader,但是3.0不需要配sass-loader,直接可以引用,mso-flex.js在main.js 头部直接可以引用

postcss-pxtorem在package.json配置,其中“rootValue":192的值是根据设计稿的长度除10设置的。 pxtorem中,对于想忽略的px写成大写即可,诸如 width:50PX;

以上就是在webview 安卓机布局实践,没有太多原理,也非长篇大论,如果各位大神看到开心,麻烦点个赞,谢谢哈~_~,实践的代码地址:github.com/yuelinghuny…