移动端页面适配之rem

552 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

常见的移动端适配方法

  • 固定高度,宽度百分百:这种方法只适合简单的、要求不高的 webApp,几乎达不到大型项目的要求,属于过时方法;
  • 媒体查询:这是一种比较主流的适配方案,很多优秀的 UI 框架都采用这种方案(如 Bootstrap),它能完成几乎大部分项目的需求,但是编写起来比较复杂;
  • flex 布局 + rem:主流的布局方式,不仅适用于移动 Web,PC 端表现也良好,它能够完成大部分项目的需求,同时编写也比较简单,比较推荐使用

rem 适配原理

rem 是 CSS3 引入的一个相对单位,它是指相对于根元素的字体大小的单位,它的值取决于<html>元素设定的 font-size 值的大小,即:1rem = 1(font-size 值);

根据 rem 的这个特性,我们得出来一种比较好的适配思路,即根据不同屏幕的宽度,以相同的比例动态的修改 html 的 font-size,从而实现根据屏宽控制根元素的字体大小,进而控制 rem 的显示比例,达到各种屏幕基本一致的体验效果。

rem 计算原理

  1. 屏宽为 clientWidth(px)。设计稿宽度为 750px,n = clientWidth / 750;
  2. 将 html 的 font-size 设置为 n(px)
  3. 此时已经确定 1(rem) = n(px)
  4. 假设有一个 div,在设计稿上测量的宽度 ruleW(px),则需要写入的宽度 width:w(px)
  5. 则有 w / clientWidth(px) = ruleW(px) / 750(px) 推导出 w = ruleW * n(px)
  6. 则有 w = ruleW * 1rem 推导出 w = ruleW(rem);

结论:当我们设置 html 的 font-size 为(屏宽 / 设计稿宽度)的 px 时,我们在设计稿上测量的 px 单位值可以直接转换为 rem 单位写到代码里面即可。

注意: 有一个很重要也很容易被忽略的问题,那就是浏览器存在最小字体,即字体大小不得小于 12px,而当我们设置根节点的 font-size 时,屏幕宽度/750 基本上 100%小于 12px,这个比例是无法通过浏览器的,所以我们要把它放大一百倍。

放大比例的计算规则如下:

  1. n = clientWidth / 750; n2 = clientWidth*100 / 750; n2 = 100n,即 n2 为放大 100 倍的 n;
  2. 将 html 元素的 font-size 设置为 n2;
  3. 1(rem) = n2(px) = 100n(px); 则 n = 1/100(rem);
  4. 假设有一个 div,在设计稿上测量的宽度 ruleW(px),则需要写入的宽度 width:w(px)
  5. 则有 w / clientWidth(px) = ruleW(px)/750(px) 推导出 w = (clientWidth / 750) * ruleW(px)
  6. w = n * ruleW(px) 推导出 w = 1/100 * n2 * ruleW(rem) = ruleW/100(rem);

结论:当我们设置 html 的 font-size 为(屏宽 * 100 / 设计稿宽度)的 px 时,当我们在设计稿上测得的 px 单位值,直接除以 100 换为 rem 单位写入代码即可。

配适方法

js 配适

<script type="text/javascript">
  //手机端的适配
  document.addEventListener("DOMContentLoaded", function() {
    document.getElementsByTagName("html")[0].style.fontSize =
      (document.documentElement.clientWidth / 750) * 100 + "px";
  });
​
  window.onresize = function() {
    document.getElementsByTagName("html")[0].style.fontSize =
      (document.documentElement.clientWidth / 750) * 100 + "px";
  };
</script>

css 配适

移动端的rem布局需要通过JS设置不同屏幕宽高比的font-size,而采用这种方式就可以摆脱js的控制。不兼容低版本移动端系统。

html {
  font-size: calc(100vw / 7.5);
}  

less 配适

.re(@width) {
    @xs: 100px/(750px/@width);
    @media (max-width:(@width + 1px)) {
        html {
            font-size: @xs;
        }
    }
}
.re(1600px);
.re(1440px);
.re(1280px);
.re(1024px);
.re(960px);
.re(950px);
.re(900px);
.re(800px);
.re(773px);
.re(768px);
.re(736px);
.re(732px);
.re(731px);
.re(667px);
.re(640px);
.re(600px);
.re(568px);
.re(533px);
.re(435px);
.re(414px);
.re(411px);
.re(384px);
.re(375px);
.re(360px);
.re(320px);  

scss

// 计算rem的基准字体
$rem-base-font-size: 100px;
​
// UI设计图的分辨率宽度
$UI-resolution-width: 750px;
​
// 需要适配的屏幕宽度
$device-widths: 240px, 320px, 360px, 375px, 390px, 414px, 480px, 540px, 640px, 720px, 768px,1080px, 1024px;
​
@mixin html-font-size() {
  @each $current-width in $device-widths {
    @media only screen and (min-width: $current-width) {
      html {
        $x: $UI-resolution-width / $current-width; //计算出是几倍屏
        font-size: $rem-base-font-size / $x;
      }
    }
  }
}
​
@include html-font-size();
​
@function pxToRem($px) {
  @return $px / $rem-base-font-size * 1rem;
} 

vue 移动端自适应实例

移动端设计稿一般都是以 750px 宽度为基础进行设计。页面自适应的大致思路就是在代码中 css 样式都用 px 为单位,然后再将 px 转换为 rem,再根据页面大小动态设置根元素 htmlfont-size,即可达到页面的自适应的效果。

vuecli4 创建的项目为例子,先安装 postcss-pxtorem 至项目中

npm install postcss-pxtorem

然后在 vue.config.js 文件中添加以下代码,即可将代码中的 px 转换为 rem 单位

module.exports = {
  css: {
    loaderOptions: {
      postcss: {
        plugins: [
          require("postcss-pxtorem")({
            rootValue: 75, //75px=1rem,这个值可以自行修改
            propWhiteList: [],
            minPixelValue: 2,
          }),
        ],
      },
    },
  },
};

然后在 public/index.html 中添加以下代码,设置 htmlfont-size,即可自动完成适配

(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) {
    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;
    //可配置安卓和ios不同的适配
    if (isIPhone) {
      dpr = 1;
    } else {
      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;
    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() {
        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"] = {}));

有些特殊的地方比如 echart 中 tooltip 的文字大小,是无法进行自适应的。那就需要动态获取当前页面的分辨率再进行计算

// 同样是以750px为基础进行计算
export let GetDpr = function() {
  let fontData = document.querySelector("html").style.fontSize;
  return fontData.slice(0, fontData.length - 2) / 37.5;
};