多端适配之rem的最佳实践

3,869 阅读6分钟

抛出一个需求

相信使用rem做移动端适配大家都手到擒来了,那么如果我现在提出这样一个需求大家看看该如何解决:

在一个项目内同时包含一张pc端页面A和一张移动端页面B。A、B两个页面的设计图宽度分别为1920px和375px。设计师期望在可视区域宽度为1920px的pc设备上可以1:1显示页面A(实际尺寸:设计图尺寸),而在其他宽度的pc设备上对页面A做等比缩放。同样的,还希望在可视区域宽度为375px的移动设备上可以1:1显示页面B,而在其他宽度的移动设备上对页面B做等比缩放。

如果感觉这个需求有点匪夷所思,那么不妨设想下这是个官网项目,而页面A和B分别是pc端和移动端首页,这样可能就很容易理解了。另外,两个页面虽然都是首页这个主题,但它们的布局一定是差别较大的,否则也没必要单独做成两个页面了。

难点

我相信各位读者一定有各种办法实现上面抛出的需求,但大家的方案能不能保证当可视区域宽度 = 设计图宽度的时候1:1还原设计图呢?

1:1代表的是0误差。我举个例子,假设设计图的宽度用户浏览器可视区域的宽度恰巧都是1920px,并且设计图上存在一个99px宽的矩形,那么这个矩形在用户的屏幕上实际渲染出来的宽度必须也是99px,不能有小数点! 另外,真正的难点在于,我们要在同一个项目内的pc端页面和移动端页面同时做到0误差。

优雅地解决这些难题就是本方案的目的。

解决方案

既然要求我们使用等比缩放来做适配,那目前最优的技术手段一定是rem。建议clone这个仓库跑跑看我的例子。

下面来介绍下我的方案,我的方案只有两步。

第1步. 配置postcss-pxtorem插件

使用此插件的目的是利用postcss在打包过程中将px转化为rem。所谓转化其实就是将样式中以px为单位的数值 ➗ rootValue得到以rem为单位的数值。仍然以前文提到的宽度99px的矩形为例,经由插件转换,这个矩形在代码中的实际尺寸 = 以px为单位的数值 ➗ rootValue=99px ➗ 1 = 99rem。至于rootValue为何是1,请阅读原理部分。

插件的配置方法如下:

  1. 控制台执行yarn add postcss postcss-pxtorem --dev
  2. 根目录下创建postcss.config.js文件
  3. postcss.config.js文件内引入插件并将rootValue设置为1。文件内容如下
    module.exports = {
       plugins: [
         require("postcss-pxtorem")({
           rootValue: 1,
           propList: ['*', '!font-size'],//字体不需要缩放
         })
       ],
     };
    

大家使用其他插件也都是可以的,每个插件都有类似rootValue的配置项,但我还是推荐这个pxtorem,因为他配置项更全,并且下载量非常多。

第2步. 动态设置html的fontsize

既然我们的项目包含pc和移动两端,那么就需要根据用户目前浏览的页面来动态修改html标签的font-size样式,因为只有这样才能让页面元素调整到正确的尺寸。此处计算公式为:html标签的font-size = 可视区域宽度 ➗ 此页面在设计图上的尺寸

公式虽然只有一个,但是设计图尺寸可有1920px和375px两种呢,因此需要写个逻辑在恰当时机调整font-size,我下面的例子是先根据用户的ua判断出设备类型,然后再依据不同设备的设计图尺寸计算出font-size。

import { isMobile } from "./helper"
import _ from 'lodash'

//我这里是根据ua来区分移动端和pc端,您可以选择适合自己的方案
export function setDomFontSizeByUa() {
  const isMobileDev = isMobile()
  let fontsize = '';
  let width = document.documentElement.clientWidth || document.body.clientWidth;
  if (isMobileDev) {
    fontsize = width / 375 + 'px' //重点在这,375是移动端设计稿的宽度
  }
  else {
    fontsize = width / 1920 + 'px' //重点在这,1920是pc端设计稿的宽度
  }
  (document.getElementsByTagName('html')[0].style)['font-size'] = fontsize;
}

let setDomFontSizeDebounce = _.debounce(setDomFontSizeByUa, 400) //做个防抖
window.addEventListener('resize', setDomFontSizeDebounce); 

可以看出,即使你需要支持更多的设计图尺寸,拓展起来也相当简单,因为只需要加几个数字就好了。

那么到此为止,以上就是我的最佳实践了。如果希望了解更多细节的话,不妨clone这个仓库跑一下看看效果。 下面我想解释一下为什么这样做是可行的,原理很简单。

原理

我会将数值变化的完整过程一步步写下来,相信各位朋友看完这个过程就能够清楚的理解其原理了。

首先,仍然假设A和B两个页面的设计图上各有一个宽度为99px的div。

  1. 我们按照设计图,在A和B两个页面上分别写一个宽度为99px的div。
  2. 在热更新或是build过程中,pxtorem插件会对99px这个值进行转化。转化结果 = 原始值 ➗ rootValue = 99px ➗ 1 = 99rem。也就是说,在构建结果的代码逻辑中,这个div的宽度已经被改成99rem了,而非我们写的99px。
  3. 假设某位用户浏览器的可视区域宽度是1920px,并且此用户打开了页面A。此时动态设置fontsize的代码会被执行,根据公式html的font-size = 可视区域宽度 ➗ 设计图尺寸 = 1920px ➗ 1920px = 1px,html的font-size被修改成了1px。
  4. 在渲染那个宽度为99rem的div时,浏览器是一定会先将99rem转换成实际的px才能进行渲染的(这是在回流过程中进行的)。这个转化过程是这样的实际的px = rem * html的fontSize = 99rem * 1px = 99px。因此,最终在可视区域宽度是1920px的设备上,宽度为99px的div仍然显示为99px宽。做到了1:1还原设计图。

如果各位读者还没看出精髓的话,我们可以继续尝试着在移动端打开项目。让我来修改下第3步。

  1. 假设某位用户使用手机打开了页面B,手机恰巧是375px宽。此时动态设置fontsize的代码会被执行,根据公式html的font-size = 可视区域宽度 ➗ 设计图尺寸 = 375px ➗ 375px = 1px,html的font-size被修改成了1px。
  2. 浏览器在渲染那个99px的div时候,根据公式实际的px = rem * html的fontSize = 99rem * 1px = 99px。因此,最终在可视区域宽度是375px的设备上,宽度为99px的div显示为99px宽。仍然做到了1:1还原设计图。

整个过程中用户设备的可视区域宽度是个变量,因此各位朋友可以将其更改为任意值感受下它的变化过程。

全文完。