移动端REM方案-Flexible源码分析

Posted 本期节目

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了移动端REM方案-Flexible源码分析相关的知识,希望对你有一定的参考价值。

大漠老师讲(大漠老师在2017.8月发文中):Flexible已经完成了他自身的历史使命,我们可以放下Flexible,拥抱新的变化。不过目前手淘首页依然还在使用这个方案(发文时间2018.3),依然是值得学习,其实这个文章是我2017年春节假期写的,只不过现在才发出来,逃:>

Flexible是目前业界比较知名和成熟的 REM布局方案,源码不多,今天来做一下源码解析。 首先要知道 viewport的概念,我在之前有特意写过。

首先告诉大家, Flexible主要做了下面的事情:

  • 根据屏幕情况在 html根元素上设置 dpr

  • 根据屏幕情况在 html根元素上设置 font-size

  • 根据屏幕情况设置 meta标签

简单说下 px、 em、 rem

无论是 em还是 rem,最后都会换算成 px

  • px是PC端主要使用的尺寸单位, 1px是一个逻辑像素。

  • em是主要用在移动端字体上。使用了em的父元素的像素大小 × 自己的rem数值 = 终转换出来的像素的数值。由于嵌套多层 em的元素之后, em的计算会变得不可控,所以一般很少大范围布局使用,只会用在字体上。

  • rem是根据根字体,也就是 html根元素上设置的 font-size大小来确定最终像素数字的, 1rem就是根字体的大小, 2rem就是根字体大小的两倍。

这里只是简单说下三者的关系,不做过多的展开,我相信如果来看本文一定大概知道什么是 emrem,如果还不知道赶快去补课哦~

源码解析

先看一下完整源码,源码不多,就一百行多一点。

 
   
   
 
  1. ;(function(win, lib) {

  2.    var doc = win.document;

  3.    var docEl = doc.documentElement;

  4.    var metaEl = doc.querySelector('meta[name="viewport"]');

  5.    var flexibleEl = doc.querySelector('meta[name="flexible"]');

  6.    var dpr = 0;

  7.    var scale = 0;

  8.    var tid;

  9.    var flexible = lib.flexible || (lib.flexible = {});

  10.    if (metaEl) {

  11.        console.warn('将根据已有的meta标签来设置缩放比例');

  12.        var match = metaEl.getAttribute('content').match(/initial-scale=([d.]+)/);

  13.        if (match) {

  14.            scale = parseFloat(match[1]);

  15.            dpr = parseInt(1 / scale);

  16.        }

  17.    } else if (flexibleEl) {

  18.        var content = flexibleEl.getAttribute('content');

  19.        if (content) {

  20.            var initialDpr = content.match(/initial-dpr=([d.]+)/);

  21.            var maximumDpr = content.match(/maximum-dpr=([d.]+)/);

  22.            if (initialDpr) {

  23.                dpr = parseFloat(initialDpr[1]);

  24.                scale = parseFloat((1 / dpr).toFixed(2));

  25.            }

  26.            if (maximumDpr) {

  27.                dpr = parseFloat(maximumDpr[1]);

  28.                scale = parseFloat((1 / dpr).toFixed(2));

  29.            }

  30.        }

  31.    }

  32.    if (!dpr && !scale) {

  33.        var isandroid = win.navigator.appVersion.match(/android/gi);

  34.        var isIPhone = win.navigator.appVersion.match(/iphone/gi);

  35.        var devicePixelRatio = win.devicePixelRatio;

  36.        if (isIPhone) {

  37.            // ios下,对于2和3的屏,用2倍的方案,其余的用1倍方案

  38.            if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {

  39.                dpr = 3;

  40.            } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){

  41.                dpr = 2;

  42.            } else {

  43.                dpr = 1;

  44.            }

  45.        } else {

  46.            // 其他设备下,仍旧使用1倍的方案

  47.            dpr = 1;

  48.        }

  49.        scale = 1 / dpr;

  50.    }

  51.    docEl.setAttribute('data-dpr', dpr);

  52.    if (!metaEl) {

  53.        metaEl = doc.createElement('meta');

  54.        metaEl.setAttribute('name', 'viewport');

  55.        metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');

  56.        if (docEl.firstElementChild) {

  57.            docEl.firstElementChild.appendChild(metaEl);

  58.        } else {

  59.            var wrap = doc.createElement('div');

  60.            wrap.appendChild(metaEl);

  61.            doc.write(wrap.innerHTML);

  62.        }

  63.    }

  64.    function refreshRem(){

  65.        var width = docEl.getBoundingClientRect().width;

  66.        debugger;

  67.        if (width / dpr > 540) {

  68.            width = 540 * dpr;

  69.        }

  70.        var rem = width / 10;

  71.        docEl.style.fontSize = rem + 'px';

  72.        flexible.rem = win.rem = rem;

  73.    }

  74.    win.addEventListener('resize', function() {

  75.        clearTimeout(tid);

  76.        tid = setTimeout(refreshRem, 300);

  77.    }, false);

  78.    win.addEventListener('pageshow', function(e) {

  79.        if (e.persisted) {

  80.            clearTimeout(tid);

  81.            tid = setTimeout(refreshRem, 300);

  82.        }

  83.    }, false);

  84.    if (doc.readyState === 'complete') {

  85.        doc.body.style.fontSize = 12 * dpr + 'px';

  86.    } else {

  87.        doc.addEventListener('DOMContentLoaded', function(e) {

  88.            doc.body.style.fontSize = 12 * dpr + 'px';

  89.        }, false);

  90.    }

  91.    refreshRem();

  92.    flexible.dpr = win.dpr = dpr;

  93.    flexible.refreshRem = refreshRem;

  94.    flexible.rem2px = function(d) {

  95.        var val = parseFloat(d) * this.rem;

  96.        if (typeof d === 'string' && d.match(/rem$/)) {

  97.            val += 'px';

  98.        }

  99.        return val;

  100.    }

  101.    flexible.px2rem = function(d) {

  102.        var val = parseFloat(d) / this.rem;

  103.        if (typeof d === 'string' && d.match(/px$/)) {

  104.            val += 'rem';

  105.        }

  106.        return val;

  107.    }

  108. })(window, window['lib'] || (window['lib'] = {}));

代码包裹

 
   
   
 
  1. ;(function(win, lib) {

  2.    // Flexible code

  3. })(window, window['lib'] || (window['lib'] = {}));

典型的自动执行的闭包,将 windowwindow.lib作为实参传递匿名函数的形参上,如果 window上没有 lib这个对象,就创建这个对象,那么 window.lib是干什么的呢?在移动模式打开 m.taobao.com,控制台打印 window.lib,会发现有一个 flexible的对象,同时也有很多对象,所以 window.lib应该是手机淘宝内部为了管理类库命名空间用的,防止随便污染 window全局对象,这个方法也可以借鉴到自己的项目中管理公用类库的命名空间。

变量定义

 
   
   
 
  1.    var doc = win.document;

  2.    var docEl = doc.documentElement;

  3.    var metaEl = doc.querySelector('meta[name="viewport"]');

  4.    var flexibleEl = doc.querySelector('meta[name="flexible"]');

  5.    var dpr = 0;

  6.    var scale = 0;

  7.    var tid;

  8.    var flexible = lib.flexible || (lib.flexible = {});

变量 作用
docEl document缓存
docEl documentElement缓存,即根元素html的dom
metaEl viewport属性的meta标签
flexibleEl flexible属性的meta标签,不是官方标准的meta标签,手机淘宝自己定的
dpr 设备像素比
scale 页面缩放比例
tid 缓存定时器timer
flexible flexible库的公用对象,一些公用的方法和属性都对定义在这个对象上

读取当前meta标签

 
   
   
 
  1.    if (metaEl) {

  2.        console.warn('将根据已有的meta标签来设置缩放比例');

  3.        var match = metaEl.getAttribute('content').match(/initial-scale=([d.]+)/);

  4.        if (match) {

  5.            scale = parseFloat(match[1]);

  6.            dpr = parseInt(1 / scale);

  7.        }

  8.    } else if (flexibleEl) {

  9.        var content = flexibleEl.getAttribute('content');

  10.        if (content) {

  11.            var initialDpr = content.match(/initial-dpr=([d.]+)/);

  12.            var maximumDpr = content.match(/maximum-dpr=([d.]+)/);

  13.            if (initialDpr) {

  14.                dpr = parseFloat(initialDpr[1]);

  15.                scale = parseFloat((1 / dpr).toFixed(2));

  16.            }

  17.            if (maximumDpr) {

  18.                dpr = parseFloat(maximumDpr[1]);

  19.                scale = parseFloat((1 / dpr).toFixed(2));

  20.            }

  21.        }

  22.    }

  • if:在有 viewport的 meta标签下,如果发现设置了缩放比例,获取 initial-scale设置的值,并根获取到的据缩放比例设置dpr的数值。

  • else if:在有 flexible的 meta标签下,获取 initial-dpr, maximum-dpr的值,根据获取到的数值设置 dpr和 scale。以 maximum-dpr的 dpr值为基准, toFixed是为了小数点后取两位第三位四舍五入。 flexible的 meta标签是为了能够让用户自己设置缩放比例,而不是使用通过屏幕尺寸实际计算出来的 dpr和 scale值,一般很少用到。

计算dpr和scale值

 
   
   
 
  1.    if (!dpr && !scale) {

  2.        var isAndroid = win.navigator.appVersion.match(/android/gi);

  3.        var isIPhone = win.navigator.appVersion.match(/iphone/gi);

  4.        var devicePixelRatio = win.devicePixelRatio;

  5.        if (isIPhone) {

  6.            if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {

  7.                dpr = 3;

  8.            } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){

  9.                dpr = 2;

  10.            } else {

  11.                dpr = 1;

  12.            }

  13.        } else {

  14.            // 其他设备下,仍旧使用1倍的方案

  15.            dpr = 1;

  16.        }

  17.        scale = 1 / dpr;

  18.    }

如果用户没有手动设置 viewportflexiblemeta标签,就需要根据 devicePixelRatio这个属性设置。计算法方法:

水平逻辑像素数 / 水平物理像素数 = window.devicePixelRatio

也就是设备像素比,是 window上的只读属性。

首先是 AndroidiOS的设备类型判断和获取 devicePixelRatio值。两种平台的设备策略不一样, iPhone存在一倍、二倍、三倍屏幕,根据不同倍率的情况使用不同的 dpr值,但是除了 iPhone以外的设备都是将 dpr值设置为,好处是不用处理各种奇葩的屏幕像素比和分辨率,但是带来的缺点是在很多 Android高清屏下页面图片会不清晰。

实际上 dprscale是绑定的,计算方法是这样: scale=1/dpr;

dpr主要是为了不同像素比屏幕下显示文字用的,而 scale是为了解决 1px的线过于粗的问题。

设置 dpr和 scale

 
   
   
 
  1.    docEl.setAttribute('data-dpr', dpr);

  2.    if (!metaEl) {

  3.        metaEl = doc.createElement('meta');

  4.        metaEl.setAttribute('name', 'viewport');

  5.        metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');

  6.        if (docEl.firstElementChild) {

  7.            docEl.firstElementChild.appendChild(metaEl);

  8.        } else {

  9.            var wrap = doc.createElement('div');

  10.            wrap.appendChild(metaEl);

  11.            doc.write(wrap.innerHTML);

  12.        }

  13.    }

计算出 dprscale,就设置到页面中。

  • dpr会设置在 html根元素上,如果是二倍屏,eg: <htmldata-dpr="2"></html>

  • scale会设置在 viewport的 meta标签中,如果是三倍屏,eg: <metaname="viewport"content="initial-scale=0.3333333333333333, maximum-scale=0.3333333333333333, minimum-scale=0.3333333333333333, user-scalable=no">

说到这里,可能有疑问,为什么要缩放,上面说了,就是为了解决 1px的线在高清屏幕中太粗的问题,但是缩放后会导致 viewport的尺寸变大,关于 viewport的事情,之前的文章已经解释过了。下面设置 html根元素的 font-size也会根据这个 scale的数值进行调整。

计算 html根元素的 font-size

 
   
   
 
  1.    function refreshRem(){

  2.        var width = docEl.getBoundingClientRect().width;

  3.        if (width / dpr > 540) {

  4.            width = 540 * dpr;

  5.        }

  6.        var rem = width / 10;

  7.        docEl.style.fontSize = rem + 'px';

  8.        flexible.rem = win.rem = rem;

  9.    }

docEl.getBoundingClientRect().widthdocument.documentElement.clientWidth获得的数值是一样的,只不过在屏幕旋转和尺寸变化的时候,这个属性获取速度更快,而 clientWidth会稍微有点延迟,淘宝能把技术做到这么细,也是厉害了。

width/dpr 的出来的是设备的 ideal viewport,也就是设备的最理想宽度,如果超过 540说明基本上已经不是移动设备了,因为 iPhone6Plusideal viewport也只有 414,比 iPhone6Plus再大的设备基本上是平板级别的设备了。

这里的 1rem的基准是屏幕宽度的1/10,也就是 layout viewport的大小的1/10,选1/10官网说是为了兼容 vhvw这样的长度单位,不过我认为这带来了计算复杂度,为什么计算复杂,我们下面再说。

计算出 1rem的像素数之后设置到 html根元素上并导出到 windowflexible对象上。

我们来看几张图,更直观的理解下如果处理缩放后的尺寸:

移动端REM方案-Flexible源码分析

屏幕是几倍屏幕是根据 window.devicePixelRatio的数值决定的。

其中 iPhone3GS是一倍屏, iPhone5iPhone6是二倍屏, iPhone6Plus是三倍屏。

在二倍屏的 iPhone6中,会设置

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

缩小了两倍, layout viewport变成了 750px,是 ideal viewport``375px的二倍,所以 1rem75px

在三倍屏的 iPhone6Plus中,会设置

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

layout viewport变成了 1242px,是 ideal viewport``414px的二倍,所以 1rem124.2px

可以看到,缩小比例越大,最后设置的 htmlfont-size也就越大,这就保证了,无论在什么尺寸,什么缩放比例的屏幕下, 1rem的大小都是屏幕的十分之一的大小。

另外,大家自己做实验的时候,切换设备一定要刷新,因为 devicePixelRatio不会动态变化和获取, devicePixelRatio的变化也没有回调函数通知。

设置计算数值到页面中

 
   
   
 
  1.    win.addEventListener('resize', function() {

  2.        clearTimeout(tid);

  3.        tid = setTimeout(refreshRem, 300);

  4.    }, false);

  5.    win.addEventListener('pageshow', function(e) {

  6.        if (e.persisted) {

  7.            clearTimeout(tid);

  8.            tid = setTimeout(refreshRem, 300);

  9.        }

  10.    }, false);

  11.    if (doc.readyState === 'complete') {

  12.        doc.body.style.fontSize = 12 * dpr + 'px';

  13.    } else {

  14.        doc.addEventListener('DOMContentLoaded', function(e) {

  15.            doc.body.style.fontSize = 12 * dpr + 'px';

  16.        }, false);

  17.    }

  18.    refreshRem();

这块比较简单了,监听了 resizepageshow这两个会引起页面尺寸发生变化的事件,然后运行 refreshRem方法重新计算。

在页面的DOM加载完成的情况下,给 body设置了 12*dpr大小的文字大小,一倍屏下是 12px、二倍屏是 24px,三倍屏是 36px,文字不用 rem作为单位是为了让更大的屏幕能够展示更多的文字,当然这样文字的大小就需要动态去算了,这就需要用构建工具处理,如果嫌麻烦文字用 rem作为单位也不会有什么特别大的问题。

首次要运行一下 refreshRem方法。

导出属性与方法

 
   
   
 
  1.    flexible.dpr = win.dpr = dpr;

  2.    flexible.refreshRem = refreshRem;

  3.    flexible.rem2px = function(d) {

  4.        var val = parseFloat(d) * this.rem;

  5.        if (typeof d === 'string' && d.match(/rem$/)) {

  6.            val += 'px';

  7.        }

  8.        return val;

  9.    }

  10.    flexible.px2rem = function(d) {

  11.        var val = parseFloat(d) / this.rem;

  12.        if (typeof d === 'string' && d.match(/px$/)) {

  13.            val += 'rem';

  14.        }

  15.        return val;

  16.    }

导出 dpr和计算根元素字体的 refreshRem方法。又导出了 rem2pxpx2rem两个方法,通过名字能够看得出来这两个方法是做 pxrem相互转换的,下面我们来说说这两个方法和转换规则。

iPhone6为例,它的理想视口宽度是 375,由于为了适配二倍屏,我们将它缩小到原来的 0.5,那么页面的视口就变成了 750了, 1rem75px

加入设计师根据 iPhone6的尺寸提供设计稿,下面几个小学计算题我们来一下:

  • 设计稿中的 75px,我们要写 75/75 = 1rem

  • 设计稿中的 100px,我们要写 100/75 = 1.33rem

  • 设计稿中的 180px,我们要写 150/75 = 2.4rem

我们现在的计算过程就是 px2rem做的事情,用 设计稿中的px / 1rempx = 代码中的rem数值

rem2px就比较好理解了,直接用 1rempx * 传入的rem数值 = 实际的px大小

可以看到,我们将 px转化为 rem的时候计算已经不是很方便了,实际开发难道要算 180/75=2.4这种算术题吗?不,一定不可以!所有有 postcss-px2rem这样的插件,也可以自己写 sass的函数来计算。这也算是 Flexible的一个麻烦的地方吧,我们可以改 Flexible的源代码自己重写 refreshRem方法,让计算变的简单点,这个我们后续有机会再说。

结束

Flexible的源码已经精解完了,大家明白了吗?


以上是关于移动端REM方案-Flexible源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Vant移动端rem适配方案

「前端」rem 缩放方案 flexible-js 兼容 375px 方案的思路

移动端REM布局方案

Vue移动端适配方案.md

移动端页面适配问题

基于vue-cli配置手淘的lib-flexible + rem,实现移动端自适应