lib-flexible与viewport--关于移动端适配方案的替换

5,536 阅读6分钟

前言

做移动端开发经常会用到 lib-flexible + postcss-pxtorem 这两个插件,对于他的使用我们都已经比较熟悉了,这里就不在介绍使用方法了。本文内容主要是介绍 lib-flexible 的源码内容,以及他的替代方法viewport 的使用。

由于 viewport 单位得到众多浏览器的兼容,lib-flexible这个过渡方案已经可以放弃使用,官方已经不再维护了,不管是现在的版本还是以前的版本,都存有一定的问题。建议大家开始使用vw的兼容方案来替代此方案。

flexible

先上一段 flexible2.0 版本的代码,解析我都写在代码的关键位置:

/**
 * lib-flexible 2.0版本
 * 此版本十分简约
 * 
 * 添加注释 @lizhiyu
 * 时间 2021-5-13
 *
 * 由于viewport单位得到众多浏览器的兼容,lib-flexible这个过渡方案已经可以放弃使用,不管是现在的版本还是以前的版本,都存有一定的问题。
 * 建议大家开始使用vw的兼容方案来替代此方案。
 */
(function flexible (window, document) {
  var docEl = document.documentElement
  var dpr = window.devicePixelRatio || 1 // 当前显示设备的物理像素分辨率与 CSS 像素分辨率的比率

  // adjust body font size
  function setBodyFontSize () {
    if (document.body) {
      document.body.style.fontSize = (12 * dpr) + 'px'
    }
    else {
      // 等页面的元素加载完之后再调用一下这个函数。因为浏览器执行代码是从上到下,当解析到script标签的时候,可能body还没构建好。所以需要考虑全面。
      document.addEventListener('DOMContentLoaded', setBodyFontSize)
    }
  }
  setBodyFontSize();

  // set 1rem = viewWidth / 10
  // 设置 html 元素的文字大小,因为 rem 是相对于根节点而定的
  function setRemUnit () {
    var rem = docEl.clientWidth / 10 // 除以10是为了方便计算
    docEl.style.fontSize = rem + 'px' // 将html元素的fontsize设置为10分之1像素
  }

  setRemUnit()

  // reset rem unit on page resize
  // resize事件是 当页面尺寸大小发生变化的时候,要重新设置rem的大小
  window.addEventListener('resize', setRemUnit)
  // pageshow事件是 重新加载页面触发的事件
  window.addEventListener('pageshow', function (e) {
    // 如果是从缓存取过来的页面也重新设置一下rem的大小(主要是火狐浏览器有往返缓存机制,这个缓存保存了DOM和JS的状态)
    if (e.persisted) {
      setRemUnit()
    }
  })

  // detect 0.5px supports
  // 有些移动端的浏览器不支持0.5像素的写法
  if (dpr >= 2) {
    var fakeBody = document.createElement('body')
    var testElement = document.createElement('div')
    testElement.style.border = '.5px solid transparent'
    fakeBody.appendChild(testElement)
    docEl.appendChild(fakeBody)
    if (testElement.offsetHeight === 1) {
      docEl.classList.add('hairlines')
    }
    docEl.removeChild(fakeBody)
  }
}(window, document))

代码块的注释基本上都写好了,这段代码比较简洁,我们现在用的基本上还是经典版 flexible0.3.2

下面我放上我已经写好注释的 flexible0.3.2 的源码:


/**
 * lib-flexible 0.3.2经典版
 * 目前都是使用这个版本开发
 * 
 * 大部分页面 meta name="viewport" 的配置
 * <meta name="viewport" content="width=device-width, viewport-fit=cover, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
 * 
 * 添加注释 @lizhiyu
 * 时间 2021-5-13
 */
(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); // html根标签上获取
  // 没有设置<meta name="viewport">的会默认添加一个
  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) { // 添加到head
          docEl.firstElementChild.appendChild(metaEl);
      } else {
          var wrap = doc.createElement('div');
          wrap.appendChild(metaEl);
          doc.write(wrap.innerHTML);
      }
  }

  function refreshRem(){
      var width = docEl.getBoundingClientRect().width; // getBoundingClientRect可以获取页面的宽高
      if (width / dpr > 540) {
          width = 540 * dpr; // 默认的适配最大宽度界限
      }
      var rem = width / 10; // 方便计算除以10
      docEl.style.fontSize = rem + 'px'; // 设置根元素的fontsize,rem是基于根元素的fontsize来设定的
      flexible.rem = win.rem = rem;
  }

  // 监听页面,窗口或框架被调整大小时修改页面的fontsize
  win.addEventListener('resize', function() {
      clearTimeout(tid);
      tid = setTimeout(refreshRem, 300);
  }, false);
  // pageshow事件是 重新加载页面触发的事件
  win.addEventListener('pageshow', function(e) {
      // 如果是从缓存取过来的页面也重新设置一下rem的大小(主要是火狐浏览器有往返缓存机制,这个缓存保存了DOM和JS的状态)
      if (e.persisted) {
          clearTimeout(tid);
          tid = setTimeout(refreshRem, 300);
      }
  }, false);

  if (doc.readyState === 'complete') { // 文档加载完成
      doc.body.style.fontSize = 12 * dpr + 'px'; // 设置body里文字的基本fontsize
  } 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; // 根据rem计算出px值
      if (typeof d === 'string' && d.match(/rem$/)) {
          val += 'px';
      }
      return val;
  }
  flexible.px2rem = function(d) {
      var val = parseFloat(d) / this.rem; // 根据px计算出rem值
      if (typeof d === 'string' && d.match(/px$/)) {
          val += 'rem';
      }
      return val;
  }

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

与2.0版本相比,经典版的代码多一些,基本都是一些处理和默认赋值的方法,核心代码都是一样的。

viewport

这里我介绍一下viewport的使用方法,viewport配置起来很容易,只需要新增一个postcss.config.js文件,然后下载yarn add -D # postcss-px-to-viewport-8-plugin,这样在代码中写入的px会自动转换成vw单位,同样可以达到移动端适配的效果。

// postcss.config.js文件内容
module.exports = {
  plugins: {
    "# postcss-px-to-viewport-8-plugin": {
      unitToConvert: "px", // 要转化的单位
      viewportWidth: 375, // UI设计稿的宽度
      unitPrecision: 6, // 转换后的精度,即小数点位数
      propList: ["*"], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
      viewportUnit: "vw", // 指定需要转换成的视窗单位,默认vw
      fontViewportUnit: "vw", // 指定字体需要转换成的视窗单位,默认vw
      selectorBlackList: ["wrap"], // 指定不转换为视窗单位的类名,
      minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
      mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
      replace: true, // 是否转换后直接更换属性值
      exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
      landscape: false, // 是否处理横屏情况
    },
  },
};

px会自动转换成vw的效果:

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <div class="box">这是一个box</div>
  </div>
</template>
<style scoped>
.box {
  width: 200px;
  height: 100px;
  border: 1px solid black;
  font-size: 16px;
}
</style>

image.png

总结

之前的项目我基本都是使用lib-flexible开发,但是阅读其源码的时候看到作者的说明,所以我就去了解了viewport适配方案,目前viewport的浏览器兼容度已经达到92%以上,所以在日常开发使用基本已经没有什么问题,所以我后续的项目 应该都会使用viewport适配方案来开发。

image.png

但是viewport也有不足之处,vwvh会在 pc 与移动端均产生效果,而不像flexible只会转换一定宽度(dpr=1就是540px)以下设备的pxrem,因此若设计稿为移动端而生,全权使用vw单位会使得页面在 pc 端出现字体过大等现象,需酌情处理,根据应用场景自行选择。