由于手机不同的屏幕尺寸,在做移动端H5页面时,为了能够更好的适配不同的屏幕,使其达到相对较好的显示效果,我们就需要对H5页面进行适配。

常规的适配方案包括使用百分比、em单位、rem单位、手淘的flexible等方案。其中百分比和em是大家已经耳熟能详的的解决方案,这里将不再介绍。本文将着重介绍rem的使用,和手淘flexible解决方案的实现思路。

在文章开始前,我们先了解一些有关移动端屏幕的基本概念,来做一些简单的铺垫。

概念简述

viewport

viewport是大家经常看到的名词,它表示浏览器的视窗大小,一般都是使用meta标签来定义网页相对于视窗的规格。我们经常会使用下面的方式来定义页面的视窗规格:

<meta name="viewport" content="width=device-width, initial-scale=1" />

表示页面的宽度即为浏览器视窗的宽度,初始化大小为1(不进行缩放)。

物理像素

物理像素又被称为设备像素,它是显示设备中一个最微小的物理部件。我们经常看到的一倍屏、二倍屏(Retina)、三倍屏,指的是设备以多少物理像素来显示一个CSS像素,也就是说,多倍屏以更多更精细的物理像素点来显示一个CSS像素点。例如,在普通屏幕(一倍屏)下1个CSS像素对应1个物理像素,而在Retina屏幕(二倍屏)下,1个CSS像素对应的却是4个物理像素。多倍屏的物理像素与CSS像素的关系,是以“田”字型关联的,二倍屏即为横向2*纵向2的物理像素表示一个CSS像素。

CSS像素

CSS像素是一个抽像的单位,主要使用在浏览器上,用来精确度量Web页面上的内容。一般情况下,CSS像素称为与设备无关的像素(device-independent pixel),简称DIPs。在Web端写页面样式的时候,我们一般会以1px来表示1个CSS像素。

设备独立像素dp

设备独立像素表示屏幕可视区宽度,简称dip或dp。移动端设备分为非视网膜和视网膜屏幕。

非视网膜屏幕是指,屏幕可视区域宽度是320像素,就只能显示320像素的内容。

视网膜屏幕是指,屏幕可视区域宽度是320像素,却能够显示640像素、960像素的内容。

设备像素比dpr

设备像素比全称device pixel ratio,简称dpr,用来定义物理像素和设备独立像素的对应关系。计算公式为:

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

在Retina屏上,设备像素比=2,也就是说1个CSS像素相当于2个物理像素。

移动端适配方案

通过上面概念的梳理,我们大致了解了移动端的一些屏幕基础知识。接下来,我们来探讨下移动端H5页面的适配方案。

rem

rem是CSS的一个单位。相对于px来说,它不是固定的一个像素大小;相对于em来说,它的计算依赖于根元素(html)而非父元素。

rem是根据根元素(html)的font-size值来动态计算的,假设此时根元素的font-size为16px,则1rem=16px。根据这个特点,可以根据设备宽度动态计算根元素的font-size,使得以rem为单位的元素可以动态响应屏幕宽度来展示。

viewport

通过设置viewport的值,来进行缩放。例如在一倍屏下,可以这样设置:

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

设置页面的显示比例和浏览器视窗1:1。对于二倍屏,我们可以这样设置:

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

然后我们把页面上的元素的css像素乘以2,相当于是我页面内容放大两倍,然后页面再整体缩小一倍。

手淘的实现方式

手淘flexible的实现思路:

1、动态修改标签

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

3、给<html>元素添加font-size属性,并且动态改写font-size的值

页面加载前,通过执行一段js脚本,来根据手机屏幕的宽度、dpr和设计稿宽度设置根元素的data-dpr和font-size。

摘要一段工作中使用到的js实现方法:

(function(global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
        typeof define === 'function' && define.amd ? define(factory) :
        (global.rem = factory());

}(this, (function() {

    //'use strict';

    //@todo  设计图尺寸不同机型等比例缩放模式,基础html尺寸不同机型适配模式

    var rem = {
        designWidth: 750, //设计稿宽px值
        px2rem: 100, //px to rem为100倍比例转换,宽度建议用百分比
        defaultFontSize: 20, //默认1rem字体大小
        maxWidth: 0, //rem适配最大尺寸(屏幕再大也就那个尺寸了)
        dpr: parseInt(window.devicePixelRatio || 1),
    };

    try {
        if (rem.android) {
            //android设置dpr不生效
            rem.dpr = 1;
        }

        // 设置默认最大尺寸
        var clientWidth = window.screen.width;
        if(clientWidth >= 480){
            rem.maxWidth = 360;
        }
    } catch (e) {}

    //根据devicePixelRatio自定计算scale
    //可以有效解决移动端1px这个世纪难题。
    var remEl = document.querySelector('meta[name="rem"]');

    //允许通过自定义name为rem的meta头,通过initial-dpr来强制定义页面缩放
    if (remEl) {
        var remCon = remEl.getAttribute('content');
        if (remCon) {
            var initialDprMatch = remCon.match(/initial\-dpr=([\d\.]+)/);
            if (initialDprMatch) {
                rem.dpr = parseFloat(initialDprMatch[1]);
            }
            var designWidthMatch = remCon.match(/design\-width=([\d\.]+)/);
            if (designWidthMatch) {
                rem.designWidth = parseFloat(designWidthMatch[1]);
            }

            var maxWidthMatch = remCon.match(/max\-width=([\d\.]+)/);
            if (maxWidthMatch) {
                rem.maxWidth = parseFloat(maxWidthMatch[1]);
            }
        }
    }

    document.documentElement.setAttribute('data-dpr', rem.dpr);
    document.documentElement.setAttribute('max-width', rem.maxWidth);


    var viewportEl = document.querySelector('meta[name="viewport"]');
    var scale = 1 / rem.dpr;
    var content = 'width=device-width, initial-scale=' + scale + ', minimum-scale=' + scale + ', maximum-scale=' + scale + ', user-scalable=no';

    if (viewportEl) {
        viewportEl.setAttribute('content', content);
    } else {
        viewportEl = document.createElement('meta');
        viewportEl.setAttribute('name', 'viewport');
        viewportEl.setAttribute('content', content);
        document.head.appendChild(viewportEl);
    }

    var d = document.createElement('div');
    d.style.width = '1rem';
    d.style.display = 'none';
    document.head.appendChild(d);
    try {
        rem.defaultFontSize = parseFloat(window.getComputedStyle(d, null).getPropertyValue('width'));
    } catch (e) {}

    rem.get_px = function(px_num) {
        var px = parseInt(px_num);
        if (!px) {
            return false;
        }
        return rem.dpr * px + 'px';
    }

    rem.resize = function() {
        //对,这个就是核心方法了,给HTML设置font-size。
        var measureWidth = 0;
        try {
            measureWidth = document.documentElement.getBoundingClientRect().width;
        } catch (e) {}

        if (!measureWidth) {
            measureWidth = window.innerWidth;
        }

        if (!measureWidth) { return false; }

        //设置最大尺寸
        if (rem.maxWidth) {
            var tp = rem.maxWidth * rem.dpr;
            if (measureWidth > tp) {
                measureWidth = tp;
            }
        }

        var fontSize = measureWidth / (rem.designWidth / rem.px2rem) / rem.defaultFontSize * 100;
        var elName = 'rem';
        var remStyle = document.getElementById(elName);
        if (remStyle) {
            remStyle.innerHTML = "html{font-size:" + fontSize + "% !important;}";

        } else {
            remStyle = document.createElement('style');
            remStyle.setAttribute('id', elName);
            remStyle.innerHTML = "html{font-size:" + fontSize + "% !important;}";
            document.head.appendChild(remStyle);

        }

        rem.callback && rem.callback();

    };

    rem.resize();
    //直接调用一次

    window.addEventListener('resize', function() {
        clearTimeout(rem.tid);
        rem.tid = setTimeout(rem.resize, 33);
    }, false);
    //绑定resize的时候调用

    window.addEventListener('load', rem.resize, false);
    //防止不明原因的bug。load之后再调用一次。

    setTimeout(function() {
        rem.resize();
        //防止某些机型怪异现象,异步再调用一次
    }, 333);

    return rem;
})));