移动端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
就是根字体大小的两倍。
这里只是简单说下三者的关系,不做过多的展开,我相信如果来看本文一定大概知道什么是 em
和 rem
,如果还不知道赶快去补课哦~
源码解析
先看一下完整源码,源码不多,就一百行多一点。
;(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;
debugger;
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'] = {}));
代码包裹
;(function(win, lib) {
// Flexible code
})(window, window['lib'] || (window['lib'] = {}));
典型的自动执行的闭包,将 window
和 window.lib
作为实参传递匿名函数的形参上,如果 window
上没有 lib
这个对象,就创建这个对象,那么 window.lib
是干什么的呢?在移动模式打开 m.taobao.com
,控制台打印 window.lib
,会发现有一个 flexible
的对象,同时也有很多对象,所以 window.lib
应该是手机淘宝内部为了管理类库命名空间用的,防止随便污染 window
全局对象,这个方法也可以借鉴到自己的项目中管理公用类库的命名空间。
变量定义
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 = {});
变量 | 作用 |
---|---|
docEl | document缓存 |
docEl | documentElement缓存,即根元素html的dom |
metaEl | viewport属性的meta标签 |
flexibleEl | flexible属性的meta标签,不是官方标准的meta标签,手机淘宝自己定的 |
dpr | 设备像素比 |
scale | 页面缩放比例 |
tid | 缓存定时器timer |
flexible | flexible库的公用对象,一些公用的方法和属性都对定义在这个对象上 |
读取当前meta标签
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:在有
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值
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) {
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;
}
如果用户没有手动设置 viewport
或 flexible
的 meta
标签,就需要根据 devicePixelRatio
这个属性设置。计算法方法:
水平逻辑像素数 / 水平物理像素数 =
window.devicePixelRatio
。
也就是设备像素比,是 window
上的只读属性。
首先是 Android
与 iOS
的设备类型判断和获取 devicePixelRatio
值。两种平台的设备策略不一样, iPhone
存在一倍、二倍、三倍屏幕,根据不同倍率的情况使用不同的 dpr
值,但是除了 iPhone
以外的设备都是将 dpr
值设置为,好处是不用处理各种奇葩的屏幕像素比和分辨率,但是带来的缺点是在很多 Android
高清屏下页面图片会不清晰。
实际上 dpr
和 scale
是绑定的,计算方法是这样: scale=1/dpr;
。
dpr
主要是为了不同像素比屏幕下显示文字用的,而 scale
是为了解决 1px
的线过于粗的问题。
设置 dpr
和 scale
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);
}
}
计算出 dpr
和 scale
,就设置到页面中。
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
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;
}
docEl.getBoundingClientRect().width
和 document.documentElement.clientWidth
获得的数值是一样的,只不过在屏幕旋转和尺寸变化的时候,这个属性获取速度更快,而 clientWidth
会稍微有点延迟,淘宝能把技术做到这么细,也是厉害了。
width/dpr
的出来的是设备的 ideal viewport
,也就是设备的最理想宽度,如果超过 540
说明基本上已经不是移动设备了,因为 iPhone6Plus
的 ideal viewport
也只有 414
,比 iPhone6Plus
再大的设备基本上是平板级别的设备了。
这里的 1rem
的基准是屏幕宽度的1/10,也就是 layout viewport
的大小的1/10,选1/10官网说是为了兼容 vh
、 vw
这样的长度单位,不过我认为这带来了计算复杂度,为什么计算复杂,我们下面再说。
计算出 1rem
的像素数之后设置到 html
根元素上并导出到 window
和 flexible
对象上。
我们来看几张图,更直观的理解下如果处理缩放后的尺寸:
屏幕是几倍屏幕是根据 window.devicePixelRatio
的数值决定的。
其中 iPhone3GS
是一倍屏, iPhone5
、 iPhone6
是二倍屏, iPhone6Plus
是三倍屏。
在二倍屏的 iPhone6
中,会设置
<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
的二倍,所以 1rem
是 75px
。
在三倍屏的 iPhone6Plus
中,会设置
<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
的二倍,所以 1rem
是 124.2px
。
可以看到,缩小比例越大,最后设置的 html
的 font-size
也就越大,这就保证了,无论在什么尺寸,什么缩放比例的屏幕下, 1rem
的大小都是屏幕的十分之一的大小。
另外,大家自己做实验的时候,切换设备一定要刷新,因为 devicePixelRatio
不会动态变化和获取, devicePixelRatio
的变化也没有回调函数通知。
设置计算数值到页面中
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();
这块比较简单了,监听了 resize
和 pageshow
这两个会引起页面尺寸发生变化的事件,然后运行 refreshRem
方法重新计算。
在页面的DOM加载完成的情况下,给 body
设置了 12*dpr
大小的文字大小,一倍屏下是 12px
、二倍屏是 24px
,三倍屏是 36px
,文字不用 rem
作为单位是为了让更大的屏幕能够展示更多的文字,当然这样文字的大小就需要动态去算了,这就需要用构建工具处理,如果嫌麻烦文字用 rem
作为单位也不会有什么特别大的问题。
首次要运行一下 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;
}
导出 dpr
和计算根元素字体的 refreshRem
方法。又导出了 rem2px
和 px2rem
两个方法,通过名字能够看得出来这两个方法是做 px
与 rem
相互转换的,下面我们来说说这两个方法和转换规则。
以 iPhone6
为例,它的理想视口宽度是 375
,由于为了适配二倍屏,我们将它缩小到原来的 0.5
,那么页面的视口就变成了 750
了, 1rem
是 75px
。
加入设计师根据 iPhone6
的尺寸提供设计稿,下面几个小学计算题我们来一下:
设计稿中的
75px
,我们要写 75/75 = 1rem设计稿中的
100px
,我们要写 100/75 = 1.33rem设计稿中的
180px
,我们要写 150/75 = 2.4rem
我们现在的计算过程就是 px2rem
做的事情,用 设计稿中的px
/ 1rem的px数
= 代码中的rem数值
而 rem2px
就比较好理解了,直接用 1rem的px数
* 传入的rem数值
= 实际的px大小
。
可以看到,我们将 px
转化为 rem
的时候计算已经不是很方便了,实际开发难道要算 180/75=2.4这种算术题吗?不,一定不可以!所有有 postcss-px2rem
这样的插件,也可以自己写 sass
的函数来计算。这也算是 Flexible
的一个麻烦的地方吧,我们可以改 Flexible
的源代码自己重写 refreshRem
方法,让计算变的简单点,这个我们后续有机会再说。
结束
Flexible
的源码已经精解完了,大家明白了吗?
以上是关于移动端REM方案-Flexible源码分析的主要内容,如果未能解决你的问题,请参考以下文章