移动端适配

移动端适配

Posted by SkioFox on December 15, 2017

px和视口

  1. 像素

像素是网页布局的基础,一个像素表示了计算机屏幕所能显示的最小区域,像素分为两种类型:css像素和物理像素。

我们在js或者css代码中使用的px单位就是指的是css像素,物理像素也称设备像素,只与设备或者说硬件有关,同样尺寸的屏幕,设备的密度越高,物理像素也就越多。下表表示css像素和物理像素的具体区别:

css像素:为web开发者提供,在css中使用的一个抽象单位

物理像素:只与设备的硬件密度有关,任何设备的物理像素都是固定的

那么css像素与物理像素的转换关系是怎么样的呢?为了明确css像素和物理像素的转换关系,必须先了解视口是什么。

  1. 视口

广义的视口,是指浏览器显示内容的屏幕区域,狭义的视口包括了布局视口、视觉视口和理想视口。

(1) 布局视口(layout viewport)

布局视口定义了pc网页在移动端的默认布局行为,因为通常pc的分辨率较大,布局视口默认为980px。也就是说在不设置网页的viewport的情况下,pc端的网页默认会以布局视口为基准,在移动端进行展示。因此我们可以明显看出来,默认为布局视口时,根植于pc端的网页在移动端展示很模糊。

(2) 视觉视口(visual viewport)

视觉视口表示浏览器内看到的网站的显示区域,用户可以通过缩放来查看网页的显示内容,从而改变视觉视口。视觉视口的定义,就像拿着一个放大镜分别从不同距离观察同一个物体,视觉视口仅仅类似于放大镜中显示的内容,因此视觉视口不会影响布局视口的宽度和高度。

(3) 理想视口(ideal viewport)

理想视口或者应该全称为“理想的布局视口”,在移动设备中就是指设备的分辨率。换句话说,理想视口或者说分辨率就是给定设备物理像素的情况下,最佳的“布局视口”。

为了理清分辨率和物理像素之间的联系,我们介绍一个用DPR(Device pixel ratio)设备像素比来表示,则可以写成:

dpr = 物理像素/分辨率

在不缩放的情况下,一个css像素就对应一个dpr,也就是说,在不缩放条件下

CSS像素 = 物理像素/分辨率

此外,在移动端的布局中,我们可以通过viewport元标签来控制布局,比如一般情况下,我们可以通过下述标签使得移动端在理想视口下布局:

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

其中我们来看width属性,在移动端布局时,在meta标签中我们会将width设置称为device-width,device-width一般是表示分辨率的宽,通过width=device-width的设置我们就将布局视口设置成了理想的视口。

举例:

在pc端的布局视口通常情况下为980px,移动端以iphone6为例,分辨率为375 * 667,也就是说布局视口在理想的情况下为375px。比如现在我们有一个750px * 1134px的视觉稿,那么在pc端,一个css像素可以如下计算:

PC端: 1 CSS像素 = 物理像素/分辨率 = 750 / 980 =0.76 px

而在iphone6下:

iphone6:1 CSS像素 = 物理像素 /分辨率 = 750 / 375 = 2 px

也就是说在PC端,一个CSS像素可以用0.76个物理像素来表示,而iphone6中一个CSS像素表示了2个物理像素。此外不同的移动设备分辨率不同,也就是1个CSS像素可以表示的物理像素是不同的,因此如果在css中仅仅通过px作为长度和宽度的单位,造成的结果就是无法通过一套样式,实现各端的自适应。

媒体查询

不同端的设备下,在css文件中,1px所表示的物理像素的大小是不同的,因此通过一套样式,是无法实现各端的自适应。使用@media媒体查询可以针对不同的媒体类型定义不同的样式,特别是响应式页面,可以针对不同屏幕的大小,编写多套样式,从而达到自适应的效果。举例来说:

@media screen and (max-width: 960px){
    body{
      background-color:#FF6699
    }
}

@media screen and (max-width: 768px){
    body{
      background-color:#00FF66;
    }
}

@media screen and (max-width: 550px){
    body{
      background-color:#6633FF;
    }
}

@media screen and (max-width: 320px){
    body{
      background-color:#FFFF00;
    }
}

上述的代码通过媒体查询定义了几套样式,通过max-width设置样式生效时的最大分辨率,上述的代码分别对分辨率在0~320px,320px~550px,550px~768px以及768px~960px的屏幕设置了不同的背景颜色。

通过媒体查询,可以通过给不同分辨率的设备编写不同的样式来实现响应式的布局,比如我们为不同分辨率的屏幕,设置不同的背景图片。比如给小屏幕手机设置@2x图,为大屏幕手机设置@3X图,通过媒体查询就能很方便的实现。

但是媒体查询的缺点也很明显,如果在浏览器大小改变时,需要改变的样式太多,那么多套样式代码会很繁琐。

百分比

了用px结合媒体查询实现响应式布局外,我们也可以通过百分比单位 “ % “ 来实现响应式的效果。

比如当浏览器的宽度或者高度发生变化时,通过百分比单位,通过百分比单位可以使得浏览器中的组件的宽和高随着浏览器的变化而变化,从而实现响应式的效果。

为了了解百分比布局,首先要了解的问题是:

css中的子元素中的百分比(%)到底是谁的百分比?

直观的理解,我们可能会认为子元素的百分比完全相对于直接父元素,height百分比相对于height,width百分比相对于width。当然这种理解是正确的,但是根据css的盒式模型,除了height、width属性外,还具有padding、border、margin等等属性。那么这些属性设置成百分比,是根据父元素的那些属性呢?此外还有border-radius和translate等属性中的百分比,又是相对于什么呢?下面来具体分析。

  1. 百分比的具体分析

(1)子元素height和width的百分比

子元素的height或width中使用百分比,是相对于子元素的直接父元素,width相对于父元素的width,height相对于父元素的height。

(2) top和bottom 、left和right

子元素的top和bottom如果设置百分比,则相对于直接非static定位(默认定位)的父元素的高度,同样

子元素的left和right如果设置百分比,则相对于直接非static定位(默认定位的)父元素的宽度。

(3)padding 和 margin

子元素的padding/margin如果设置百分比,不论是垂直方向或者是水平方向,都相对于直接父亲元素的width,而与父元素的height无关。

(5)border-radius

border-radius不一样,如果设置border-radius为百分比,则是相对于自身的宽度。

除了border-radius外,还有比如translate、background-size等都是相对于自身的,

  1. 百分比单位布局应用

百分比单位在布局上应用还是很广泛的,这里介绍一种应用。

比如我们要实现一个固定长宽比的长方形,比如要实现一个长宽比为4:3的长方形,我们可以根据padding属性来实现,因为padding不管是垂直方向还是水平方向,百分比单位都相对于父元素的宽度,因此我们可以设置padding-top为百分比来实现,长宽自适应的长方形:

<div class="trangle"></div>

.trangle{
  height:0;
  width:100%;
  padding-top:75%;
}

通过设置padding-top:75%,相对比宽度的75%,因此这样就设置了一个长宽高恒定比例的长方形。

百分比单位缺点:

(1)计算困难,如果我们要定义一个元素的宽度和高度,按照设计稿,必须换算成百分比单位。

(2)从小节1可以看出,各个属性中如果使用百分比,相对父元素的属性并不是唯一的。比如width和height相对于父元素的width和height,而margin、padding不管垂直还是水平方向都相对比父元素的宽度、border-radius则是相对于元素自身等等,造成我们使用百分比单位容易使布局问题变得复杂。

自适应场景下的rem解决方案

  1. rem单位

首先来看,什么是rem单位。rem是一个灵活的、可扩展的单位,由浏览器转化像素并显示。与em单位不同,rem单位无论嵌套层级如何,都只相对于浏览器的根元素(HTML元素)的font-size。默认情况下,html元素的font-size为16px,所以:

1 rem = 16px

为了计算方便,通常可以将html的font-size设置成:

html{ font-size: 62.5% }

这种情况下:

1 rem = 10px
  1. 通过rem来实现响应式布局

rem单位都是相对于根元素html的font-size来决定大小的,根元素的font-size相当于提供了一个基准,当页面的size发生变化时,只需要改变font-size的值,那么以rem为固定单位的元素的大小也会发生响应的变化。

因此,如果通过rem来实现响应式的布局,只需要根据视图容器的大小,动态的改变font-size即可。

    function refreshRem() {

        var docEl = doc.documentElement;
        var width = docEl.getBoundingClientRect().width;
        var rem = width / 10;
        docEl.style.fontSize = rem + 'px';
        flexible.rem = win.rem = rem;
    }
    win.addEventListener('resize', refreshRem);

上述代码中将视图容器分为10份,font-size用十分之一的宽度来表示,最后在header标签中执行这段代码,就可以动态定义font-size的大小,从而1rem在不同的视觉容器中表示不同的大小,用rem固定单位可以实现不同容器内布局的自适应。

  1. rem2px和px2rem

如果在响应式布局中使用rem单位,那么存在一个单位换算的问题,rem2px表示从rem换算成px,这个就不说了,只要rem乘以相应的font-size中的大小,就能换算成px。更多的应用是px2rem,表示的是从px转化为rem。

比如给定的视觉稿为750px(物理像素),如果我们要将所有的布局单位都用rem来表示,一种比较笨的办法就是对所有的height和width等元素,乘以相应的比例,现将视觉稿换算成rem单位,然后一个个的用rem来表示。另一种比较方便的解决方法就是,在css中我们还是用px来表示元素的大小,最后编写完css代码之后,将css文件中的所有px单位,转化成rem单位。

px2rem的原理也很简单,重点在于预处理以px为单位的css文件,处理后将所有的px变成rem单位。可以通过两种方式来实现:

1) webpack loader的形式:

npm install px2rem-loader

在webpack的配置文件中:

    module.exports = {
    // ...
    module: {
        rules: [{
        test: /\.css$/,
        use: [{
            loader: 'style-loader'
        }, {
            loader: 'css-loader'
        }, {
            loader: 'px2rem-loader',
            // options here
            options: {
            remUni: 75,
            remPrecision: 8
            }
        }]
        }]
    }
    }

2)webpack中使用postcss plugin

npm install postcss-loader 在webpack的plugin中:

var px2rem = require('postcss-px2rem');

module.exports = {
  module: {
    loaders: [
      {
        test: /\.css$/,
        loader: "style-loader!css-loader!postcss-loader"
      }
    ]
  },
  postcss: function() {
    return [px2rem({remUnit: 75})];
  }
}

3)CSS处理器

除了使用编辑器的插件之外,还可以使用CSS的处理器来帮助大家处理。比如说Sass、LESS以及PostCSS这样的处理器。

4)CSSREM

CSSREM是一个CSS的px值转rem值的自动完成插件。

  1. 手淘的flexible方案适配

整个手淘团队,我们有一个名叫lib-flexible的库,而这个库就是用来解决H5页面终端适配的。lib-flexible是一个制作H5适配的开源库。

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

建议对JS做内联处理,在所有资源加载之前执行这个JS。执行这个JS后,会在<html>元素上增加一个data-dpr属性,以及一个font-size样式。JS会根据不同的设备添加不同的data-dpr值,比如说2或者3,同时会给html加上对应的font-size的值,比如说75px。

如此一来,页面中的元素,都可以通过rem单位来设置。他们会根据html元素的font-size值做相应的计算,从而实现屏幕的适配效果。

除此之外,在引入lib-flexible需要执行的JS之前,可以手动设置meta来控制dpr值,如:

<meta name="flexible" content="initial-dpr=2" />

其中initial-dpr会把dpr强制设置为给定的值。如果手动设置了dpr之后,不管设备是多少的dpr,都会强制认为其dpr是你设置的值。在此不建议手动强制设置dpr,因为在Flexible中,只对ios设备进行dpr的判断,对于Android系列,始终认为其dpr为1。

flexible实际上就是能过JS来动态改写meta标签,代码类似这样:

var metaEl = doc.createElement('meta');
var scale = isRetina ? 0.5:1;
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
    document.documentElement.firstElementChild.appendChild(metaEl);
} else {
    var wrap = doc.createElement('div');
    wrap.appendChild(metaEl);
    documen.write(wrap.innerHTML);
}

事实上他做了这几样事情:

动态改写<meta>标签

给<html>元素添加data-dpr属性,并且动态改写data-dpr的值

给<html>元素添加font-size属性,并且动态改写font-size的值
  1. rem 布局的缺点

通过rem单位,可以实现响应式的布局,特别是引入相应的postcss相关插件,免去了设计稿中的px到rem的计算。rem单位在国外的一些网站也有使用,这里所说的rem来实现布局的缺点,或者说是小缺陷是:

在响应式布局中,必须通过js来动态控制根元素font-size的大小。

也就是说css样式和js代码有一定的耦合性。且必须将改变font-size的代码放在css样式之前。

  1. 视觉稿中的px转换成rem

目前日常工作当中,视觉设计师给到前端开发人员手中的视觉稿尺寸一般是基于640px、750px以及1125px宽度为准。甚至为什么?大家应该懂的(考虑Retina屏)。

正如文章开头显示的示例设计稿,他就是一张以750px为基础设计的。那么问题来了,我们如何将设计稿中的各元素的px转换成rem。

目前Flexible会将视觉稿分成100份(主要为了以后能更好的兼容vh和vw),而每一份被称为一个单位a。同时1rem单位被认定为10a。

那么我们这个示例的稿子就分成了10a,也就是整个宽度为10rem,<html>对应的font-size为75px。

  1. 字号不使用rem

前面大家都见证了如何使用rem来完成H5适配。那么文本又将如何处理适配。是不是也通过rem来做自动适配。

显然,我们在iPhone3G和iPhone4的Retina屏下面,希望看到的文本字号是相同的。也就是说,我们不希望文本在Retina屏幕下变小,另外,我们希望在大屏手机上看到更多文本,以及,现在绝大多数的字体文件都自带一些点阵尺寸,通常是16px和24px,所以我们不希望出现13px和15px这样的奇葩尺寸。

如此一来,就决定了在制作H5的页面中,rem并不适合用到段落文本上。所以在Flexible整个适配方案中,考虑文本还是使用px作为单位。只不过使用[data-dpr]属性来区分不同dpr下的文本字号大小。

div {
    width: 1rem; 
    height: 0.4rem;
    font-size: 12px; // 默认写上dpr为1的fontSize
}
[data-dpr="2"] div {
    font-size: 24px;
}
[data-dpr="3"] div {
    font-size: 36px;
}

为了能更好的利于开发,在实际开发中,我们可以定制一个font-dpr()这样的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(16px);

通过vw/vh来实现自适应

  1. 什么是vw/vh ?

css3中引入了一个新的单位vw/vh,与视图窗口有关,vw表示相对于视图窗口的宽度,vh表示相对于视图窗口高度,除了vw和vh外,还有vmin和vmax两个相关的单位。各个单位具体的含义如下:

单位 含义

vw 相对于视窗的宽度,视窗宽度是100vw

vh 相对于视窗的高度,视窗高度是100vh

vmin vw和vh中的较小值

vmax vw和vh中的较大值

这里我们发现视窗宽高都是100vw/100vh,那么vw或者vh,下简称vw,很类似百分比单位。vw和%的区别为:

%: 大部分相对于祖先元素,也有相对于自身的情况比如(border-radius、translate等) vw/vh: 相对于视窗的尺寸

从对比中我们可以发现,vw单位与百分比类似,但确有区别,前面我们介绍了百分比单位的换算困难,这里的vw更像”理想的百分比单位”。任意层级元素,在使用vw单位的情况下,1vw都等于视图宽度的百分之一。

  1. vw单位换算

同样的,如果要将px换算成vw单位,很简单,只要确定视图的窗口大小(布局视口),如果我们将布局视口设置成分辨率大小,比如对于iphone6/7 375*667的分辨率,那么px可以通过如下方式换算成vw:

1px = (1/375)*100 vw

此外,也可以通过postcss的相应插件,预处理css做一个自动的转换,postcss-px-to-viewport可以自动将px转化成vw。 postcss-px-to-viewport的默认参数为:

var defaults = {
  viewportWidth: 320,
  viewportHeight: 568, 
  unitPrecision: 5,
  viewportUnit: 'vw',
  selectorBlackList: [],
  minPixelValue: 1,
  mediaQuery: false
};

通过指定视窗的宽度和高度,以及换算精度,就能将px转化成vw。

vw和vh都是根据视口来确定实际像素的,视口上的宽度和高度比例不一定是1:1 。一般来说,vh和vw不用同时使用,使用vw一般就满足移动端适配的要求了。

  1. vw/vh单位的兼容性

可以在https://caniuse.com/ 查看各个版本的浏览器对vw单位的支持性。