rem 布局 - 最流行、最成熟的移动端布局方案

127 阅读6分钟

rem 布局 - 最流行、最成熟的移动端布局方案

rem

  • 在网页设计中,我们希望页面元素的大小能够根据设备屏幕的大小进行自适应调整。
  • 这时我们就需要一个相对单位来表示尺寸。比如 rem.
  • rem 是相对于 html 根元素的字体大小。
  • 使用 rem 单位结合 html 根元素字体大小的动态设置可以达到这个目的。
  • 例如,如果我们将 html 根元素的字体大小设置为 16px(这是浏览器的默认值),1rem 就等于 16px。如果一个元素的宽度被设置为 10rem,那么它实际的宽度就是 160px。当我们将 html 根元素字体大小改为 32px 时,这个元素的宽度就会自动变为 320px,实现了自适应。
  • 当设备屏幕变化时,我们可以通过 JavaScript 动态地改变 html 根元素的字体大小,而所有使用 rem 单位的元素大小也会随之按比例变化。

像素

打开手机可以看到现在手机的像素都是非常高的,分辨率为 2712 * 1220。按照这个分辨率 750px 的设计稿在手机上渲染,岂不是很小,但是实际情况又不是这样。

想搞懂这点要先知道几个基本概念

  1. 像素

    • 像素就是构成图像的最小单位,指显示屏上的最小单位, 图像由像素组成,单位面积内的像素越多 效果就越好 像素的大小不是绝对的,是根据设备的分辨率决定的。
  2. 分辨率

    • 屏幕分辨率 : 屏幕横向和纵向的像素点数,单位为 px。

    • 相同大小的屏幕 分辨率越低,单位像素尺寸越大,分辨率越高,单位像素尺寸越小。

    • 图像分辨率 : 指图片含有的像素数 , 表示图片分别在垂直和水平上所具有的像素点数。

    • 同一尺寸的图片,分辨率越高,图片越清晰。

  3. 设备物理像素 (物理分辨率)

    • 设备的真实分辨率,屏幕有多少个像素点就是多少分辨率。

    • 以 iphone6 为例 物理分辨率为 750 * 1334,也就是显示屏内部 led 灯的个数。

  4. 设备独立像素 dips (逻辑分辨率)

    • 一种单位来告诉不同分辨率的手机,它们在界面上显示元素的大小是多少即设备几个像素当成一个像素使用。

    • 以 iphone6 为例, dips 为 375* 667 即是 2*2 个物理像素当做一个设备独立像素

    • 各种设备,手机,平板,笔记本等逻辑像素

    • 手机:逻辑像素在 3xx-4xx(短边)之间

    • 平板:10 寸平板 7xx-8xx(短边)

    • 笔记本:13 寸 1280 (长边)

    • 24 寸显示屏:1920(长边)

px 转 rem

  • 在开发的时候,我们写的 css 像素也是根据逻辑像素来写的。

  • 比如设计稿是 750px, 屏幕宽度也是 750px, 为了计算方便我们把 1rem = 100px; 也就是把屏幕宽分成了 7.5 份 = 750/100 当设置一个 18px 的字号时,我们可以直接设置成 0.18rem; 226px 就是 2.26rem;


如果你就想直接复制设计稿上的 css 样式。即使设置成 1 rem = 100px, 正在写样式的时候,还是要脑子动一下把 px 转成 rem 的。但是 px 转 rem 又很无脑。

此时可以写一个函数来实现

  • 获取到 css 文本
  • 正则匹配到 px, 并按规则替换成 rem
import css from "css";

type Config = {
  rootValue: number;
  unitPrecision: number;
  propList: string[];
  selectorBlackList: string[];
  mediaQuery: boolean;
  minPixelValue: number;
  exclude: RegExp;
};

const pxRegExp = /\b(\d+(\.\d+)?)px\b/g;

class Px2rem {
  config: Config;
  constructor(config) {
    this.config = config;
  }

  generateRem(cssText) {
    const processRule = (rules: css.Rule[]) => {
      rules.forEach((rule) => {
        rule.declarations.forEach((declaration) => {
          if (pxRegExp.test(declaration.value)) {
            declaration.value = this._getCalcValue("rem", declaration.value);
          }
        });
      });
    };

    const astObj = css.parse(cssText);
    processRule(astObj.stylesheet.rules);
    return css.stringify(astObj);
  }

  _getCalcValue(type, value) {
    return value.replace(pxRegExp, (_, $1) => {
      const val = (parseFloat($1) / this.config.rootValue).toFixed(
        this.config.unitPrecision
      );
      return val + type;
    });
  }
}
pxtorem({
  rootValue: 100,
  // 允许 px 转换为 rem 精确到小数点后几位
  unitPrecision: 5,
  // 存储哪些将被转换的属性列表,这里设置为 ['*'] 全部。
  propList: ["*"],
  // 对 css 选择器进行忽略的数组。
  // 比如你设置为 ['el-'],那所有el-类名里面有关 px 的样式将不被转换
  selectorBlackList: [],
  // 媒体查询( @media screen 之类的)中不生效
  mediaQuery: false,
  // px 绝对值小于 0 的不会被转换
  minPixelValue: 0,
  // 忽略转换的文件路径
  exclude: /node_modules/i,
});

动态设置根元素字号

如果屏幕宽度是 375px, 为了等比例缩放,也把屏幕分成 7.5 份,即 7.5rem = 375px; 那么 1rem = 50px; 那么 0.18rem 就是 9px, 2.26rem 就是 113px; 这样就实现了等比例缩放。

但是移动端设备的宽有很多种 3xx - 4xx 之间 那么 1rem 应该设置成多少呢。

把屏幕分成 7.5 份就可以了,1 rem = dw/7.5;

(function flexible(window, document) {
  var docEl = document.documentElement;

  // 设置 documentElement 的 font-size。为屏幕宽度的7.5分之一。
  function setRemUnit() {
    var rem = docEl.clientWidth / 7.5;
    docEl.style.fontSize = rem + "px";
  }
  setRemUnit();

  // 窗口大小改变,就重新调用 setRemUnit 方法
  window.addEventListener("resize", setRemUnit);
})(window, document);

这样就可以实现不同尺寸下的自适应了。

踩到的坑

  1. 当页面在 webview 内打开的,回退的话 app 会缓存页面, 可能导致布局混乱。这时就需要在 pageShow 事件中再次调用 setRemUnit 方法。
window.addEventListener("pageshow", function (e) {
  if (e.persisted) {
    // 如果页面是从缓存中加载
    setRemUnit();
  }
});
  1. 并不是所有的元素都设置了 font-size 属性,造成元素使用的默认继承的字号大小会非常大。

我们可以根据设备像素比手动设置一个默认字号

// 设备像素比
var dpr = window.devicePixelRatio || 1;
// 设置 body 的 font-size
// 这里根据 dpr 设置 body 标签的字体大小,是为了设置默认继承的字体大小。
// 否则默认的字体大小会非常大。
function setBodyFontSize() {
  if (document.body) {
    document.body.style.fontSize = 12 * dpr + "px";
  } else {
    document.addEventListener("DOMContentLoaded", setBodyFontSize);
  }
}
setBodyFontSize();
  1. 有些设备支持 0.5px 的样式,但是有些设备不支持,需要手动检测。
(function flexible(window, document) {
  var docEl = document.documentElement;
  // 检测当前设备是否支持 0.5px。如果支持,docEl 将会增加类名 hairlines
  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);

在使用的时候就可以这样

/* 不支持 0.5px 的设备 */
.element {
  border: 1px solid #000; /* 显示为 1px */
}

/* 支持 0.5px 的设备 */
.hairlines .element {
  border: 0.5px solid #000; /* 显示为 0.5px,更细 */
}
  1. 另外还有一个问题,用户可能手动放大或缩小网页

这里可以借助 meta 标签来解决

<meta
  name="viewport"
  content="width=device-width; initial-scale=1.0; maximum-scale=1; minimum-scale=1; user-scalable=no;"
/>

该 meta 标签的作用是让当前布局视口的宽度等于设备的宽度,同时不允许用户手动缩放

视觉视口等于理想视口时,1 个 CSS 像素就等于 1 个逻辑像素,而且我们也是基于理想视口来进行布局的,所以呈现出来的页面布局在各种设备上都能大致相似。

理想视口: 布局视口的一个理想尺寸,当布局视口的尺寸等于设备屏幕的尺寸时就是理想视口。

把咱写的两个方法打包成 npm 包就可以给别人使用了。

补充

既然 rootValue 的值可以随便设置,那就设置成设计稿的 1/10 吧。

pxtorem({
    rootValue: 75
})

 function setRemUnit() {
    var rem = docEl.clientWidth / 10;
    docEl.style.fontSize = rem + "px";
  }
  setRemUnit();

上传到 npm 包就是。

postcss-pxtorem 和 amfe-flexible

在项目中直接引入使用就可以了