0. 前言

flexible.js 是淘宝15年推出的移动端适配方式,虽然其官方文档已经建议不再使用了,但是因为兼容性好、上手易等原因,依旧被广泛使用,来看下它的实现原理学习下这套曾经最牛逼的移动端适配方案。

1. 优缺点

1.1 优点:

  • 上手容易,只需要加载一个打包好的 flexible.min.js ,然后牢记 1rem === 10/1 屏幕,拿到的设计图是750宽度的话就 /75 换算成 rem 适配(这一步也可以通过 px2rem 来做,那就更简单了,直接拷贝即可)
  • 兼容性好,只要兼容 rem 单位的机型都可以兼容这种方案。

    1.2 缺点:

  • flexible 会将写好的 px 转换成 rem ,转换结果多半是带小数点的,在浏览器中对于浮点数的计算非常不统一,很可能出现「代码中写的一样,实际展示大小有1px的偏差」这样的情况。

2. 原理

  • 将可见视口的宽度分为10份,通过设置 html 元素的 font-size 为「可见视口宽度 / 10」,使1rem为1份。
  • 通过在书写时使用 rem 进行样式书写,达到按比例缩放,也就是移动端适配。
  • ps:通过这个角度也可以明白为什么官方说 lib-flexible 方案只是 viewport 方案的兼容了,viewport 即是把可见视口宽度分为100份,且不需要 js 去专门计算 dpr 等等,直接写就完事。

3. 先导概念

本部分概念可以仅作查漏补缺之用,可以跳过直接看源码解析。

3.1 Viewport/视口

通俗的讲,移动设备上的viewport就是设备的屏幕上能用来显示我们的网页的那一块区域[1],但不一定是我们可见的区域。具体来说,分为以下三种。

3.1.1 Visual Viewport

Visual Viewport: 可见视口。就是移动设备上可以被我们看见的部分。宽度在移动端通过window.innerWidth获得(仅限移动端,PC上哪怕是chrome模拟也会有不同的结果)。

pic1.png

3.1.2 Layout Viewport

Layout Viewport: 布局视口。

pic2.png
如果把PC上的页面放到移动端,以iphone8为例,如果只展示为可见视口的宽度(375px),那么页面会被压缩的特别窄而显示错乱,所以移动浏览器就决定默认情况下把viewport设为一个较宽的值,比如980px,这样的话即使是那些为桌面设计的网站也能在移动浏览器上正常显示了。
而事实上,我们一般看不到如上图这样出现横向滚动条的界面;在手机上访问页面时,往往是下图的样子:
pic3.jpeg

3.1.3 Ideal Viewport

Ideal Viewport:理想视口,其实就是设备的可见区域,和可见视口一致。

设置Ideal Viewport的好处是,只要按照Ideal Viewport来设计样式稿,用户就不用能最完美的查看网站的内容——既不用左右滑动,也不用放大缩小。
设置理想视口:

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

这段代码的意思是将布局视口的宽度设置为设备宽度,初始缩放比例为1,最大缩放比例为1,用户不能缩放。

3.2 像素

3.2.1 物理像素

物理像素:一个物理像素是显示器(手机屏幕)上最小的物理显示单元,在操作系统的调度下,每一个设备像素都有自己的颜色值和亮度值。[2]

3.2.2 设备独立像素

设备独立像素:又称为CSS像素,就是我们日常代码中使用的像素。浏览器内的一切长度都是以CSS像素为单位的,CSS像素的单位是px。

3.2.3 设备像素比

设备像素比(简称dpr)定义了物理像素和设备独立像素的对应关系。比如说对于iOS的retina屏,一个设备独立像素就对应着4个物理像素。这样的设计可以使画面更加清晰锐利,如下图:
pic4.png

4. 源码注释

  • flexible.js有两套在实现上差别比较大的源码,这里都掏出来看一下
  • 但是,首先对比一下两套源码没变化的部分:

    1. var rem = docEl.clientWidth / 10
    2. docEl.style.fontSize = rem + 'px'
  • 这里就是flexible的核心:「将设备宽度分成10份,1rem = 10分之一设备宽度」,然后我们就可以把750的设计图按照 / 75 的方法算出正确的px数(这里常用px2rem插件),从而达到移动端适配的目的了。

    v2.0

    1. (function flexible (window, document) {
    2. var docEl = document.documentElement
    3. // 获取当前显示设备的「物理像素分辨率」与「css像素分辨率」之比,
    4. // 它告诉浏览器应使用多少屏幕实际像素来绘制单个CSS像素
    5. // 初版是android恒定为1,2017年的update修改了这一点
    6. var dpr = window.devicePixelRatio || 1
    7. // adjust body font size
    8. function setBodyFontSize () {
    9. if (document.body) {
    10. // 设置body的font-size为「(12 * dpr) + 'px'」
    11. // 这是为了方便后续写样式时的默认字号不会乱
    12. document.body.style.fontSize = (12 * dpr) + 'px'
    13. }
    14. else {
    15. // 内容未加载完的兼容
    16. document.addEventListener('DOMContentLoaded', setBodyFontSize)
    17. }
    18. }
    19. setBodyFontSize();
    20. // set 1rem = viewWidth / 10
    21. // core!!
    22. function setRemUnit () {
    23. // 将整个屏幕的 Visual viewport 分成10份
    24. // 不用 innerWidth 的原因:避免缩放的影响
    25. var rem = docEl.clientWidth / 10
    26. // 根节点(rem基点)的 font-size 设置为 viewWidth / 10
    27. docEl.style.fontSize = rem + 'px'
    28. }
    29. setRemUnit()
    30. // reset rem unit on page resize
    31. window.addEventListener('resize', setRemUnit)
    32. window.addEventListener('pageshow', function (e) {
    33. // 回退/前进时,页面取缓存时,重新获取基点
    34. if (e.persisted) {
    35. setRemUnit()
    36. }
    37. })
    38. // detect 0.5px supports
    39. if (dpr >= 2) {
    40. var fakeBody = document.createElement('body')
    41. var testElement = document.createElement('div')
    42. testElement.style.border = '.5px solid transparent'
    43. fakeBody.appendChild(testElement)
    44. docEl.appendChild(fakeBody)
    45. if (testElement.offsetHeight === 1) {
    46. // 如果在dpr>=2的时候.5px被实际解析为1,那么就给根元素添加hairlines类名
    47. docEl.classList.add('hairlines')
    48. }
    49. docEl.removeChild(fakeBody)
    50. }
    51. }(window, document))

    v1.0

    ```javascript ;(function(win, lib) { // 文档节点,为了下文写选择器好写 var doc = win.document; // 根节点 var docEl = doc.documentElement;

    1. // 获取声明了 viewport 属性的 meta 元素

    var metaEl = doc.querySelector(‘meta[name=”viewport”]’);

    1. // 获取含有属性name为 flexible 的 meta 元素,目前不知道是干嘛的

    var flexibleEl = doc.querySelector(‘meta[name=”flexible”]’); var dpr = 0; var scale = 0; var tid;

    1. // 声明 flexible 为 {}

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

    if (metaEl) {

    1. console.warn('将根据已有的meta标签来设置缩放比例');
    2. // 如果有 initial-scale 属性,那么获取到的结果会是一个数组['initial-scale=1', '1']
    3. var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
    4. if (match) {
    5. // 如果是一个设置了 initial-scale 属性的
    6. // 将 initial-scale 属性的值作为 scale 的值
    7. scale = parseFloat(match[1]);
    8. dpr = parseInt(1 / scale);
    9. }

    } else if (flexibleEl) {

    1. // 如果有 flexible 这个meta,怀疑是用来兼容内部旧版本
    2. var content = flexibleEl.getAttribute('content');
    3. if (content) {
    4. var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
    5. var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
    6. if (initialDpr) {
    7. dpr = parseFloat(initialDpr[1]);
    8. scale = parseFloat((1 / dpr).toFixed(2));
    9. }
    10. if (maximumDpr) {
    11. dpr = parseFloat(maximumDpr[1]);
    12. scale = parseFloat((1 / dpr).toFixed(2));
    13. }
    14. }

    }

    1. // dpr 和 scale 都是初始值

    if (!dpr && !scale) {

    1. var isAndroid = win.navigator.appVersion.match(/android/gi);
    2. var isIPhone = win.navigator.appVersion.match(/iphone/gi);
    3. var devicePixelRatio = win.devicePixelRatio;
    4. if (isIPhone) {
    5. // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
    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;

    }

    1. // 根元素设置 data-dpr 属性存储各种逻辑修改过的 dpr

    docEl.setAttribute(‘data-dpr’, dpr); if (!metaEl) {

    1. metaEl = doc.createElement('meta');
    2. metaEl.setAttribute('name', 'viewport');
    3. // 创建一个 meta 元素,将所有的缩放值设置为计算后的scale值,也就是禁止二次缩放/放大
    4. metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
    5. if (docEl.firstElementChild) {
    6. // html元素
    7. docEl.firstElementChild.appendChild(metaEl);
    8. } else {
    9. // 低版本polyfill
    10. var wrap = doc.createElement('div');
    11. wrap.appendChild(metaEl);
    12. doc.write(wrap.innerHTML);
    13. }

    }

    function refreshRem(){

    1. // 获取可视区域的宽度
    2. var width = docEl.getBoundingClientRect().width;
    3. if (width / dpr > 540) {
    4. // 淘宝认为设备宽度大于540的就不算移动设备了,走另一套兼容
    5. // 更详细的可以看 https://github.com/amfe/lib-flexible/issues/12
    6. width = 540 * dpr;
    7. }
    8. // 设备宽度分10份
    9. var rem = width / 10;
    10. docEl.style.fontSize = rem + 'px';
    11. flexible.rem = win.rem = rem;

    }

    win.addEventListener(‘resize’, function() {

    1. clearTimeout(tid);
    2. tid = setTimeout(refreshRem, 300);

    }, false); win.addEventListener(‘pageshow’, function(e) {

    1. if (e.persisted) {
    2. clearTimeout(tid);
    3. tid = setTimeout(refreshRem, 300);
    4. }

    }, false);

    if (doc.readyState === ‘complete’) {

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

    } else {

    1. doc.addEventListener('DOMContentLoaded', function(e) {
    2. doc.body.style.fontSize = 12 * dpr + 'px';
    3. }, false);

    }

  1. refreshRem();
  2. flexible.dpr = win.dpr = dpr;
  3. flexible.refreshRem = refreshRem;
  4. flexible.rem2px = function(d) {
  5. var val = parseFloat(d) * this.rem;
  6. if (typeof d === 'string' && d.match(/rem$/)) {
  7. val += 'px';
  8. }
  9. return val;
  10. }
  11. flexible.px2rem = function(d) {
  12. var val = parseFloat(d) / this.rem;
  13. if (typeof d === 'string' && d.match(/px$/)) {
  14. val += 'rem';
  15. }
  16. return val;
  17. }

})(window, window[‘lib’] || (window[‘lib’] = {})); ```

5. 参考文章