2024 年，我终于用上 rem 了……

国际惯例，________。

rem 要说起来也不是啥新东西了，面试的时候总是被问 vw/em/rem 的区别，但仔细想想我似乎一次 rem 都没用过…… 趁着最近刚把公司项目里的样式方案整了下，赶紧来水一篇（bushi（今年一定做高产的博主

打了一套组合拳…… 然后开发体验极差

公司项目最初是以 720px 出稿的，为了能在不同的机型上有个相对正常的表现，用了大家的老朋友 postcss-px-to-viewport。

它能在编译时将 px 按照配置的设计稿基础款转换为 vw ，以 320px 的设计稿为例（你问我为啥是 320px，因为我抄的 postcss-px-to-viewport官网的例子……），书写的代码如下：

Copy
.class {
margin: -10px 0.5vh;
padding: 5vmin 9.5px 1px;
border: 3px solid black;
border-bottom-width: 1px;
font-size: 14px;
line-height: 20px;
}

在编译编译后的结果为：

Copy
.class {
margin: -3.125vw 0.5vh;
padding: 5vmin 2.96875vw 1px;
border: 0.9375vw solid black;
border-bottom-width: 1px;
font-size: 4.375vw;
line-height: 6.25vw;
}

这个方案的缺点是在 IPad 等设备中样式显的偏大，屏效低，被我们戏称为大号手机。

后来有一天，PM 们一拉数据，发现我们有不少用户都是平板设备，于是单独出了一期需求来适配。设计给出的整体方案思路其实就是定宽适配，体现在设计稿上的元素尺寸几乎就没啥区别，只是针对不同设备给定不同的设计稿基础尺寸，再有就是不同设备上少数几个地方的边距、背景图做了些调整。

设备类型 | 设计稿基础尺寸
Phone | 720px
Pad 竖屏 | 1536px
Pad 横屏 | 2048px
某个有侧边栏的履约产品 | 1888px

众所周知 postcss-px-to-viewport 的转换是编译时进行的，尽管有 114514 个用户都希望维护者增加上支持配置多个基准值的 feature ，但结果却是事与愿违，该项目目前几乎处于停止维护的状态。因而之前的需求实际上以一种非常蹩脚的方式完成了适配，即使用 less 预处理器，在 postcss-px-to-viewport 转换过后再生成一套缩放的样式。

为了实现不同规格设备之间的尺寸换算，项目里封装了一堆 less mixin：

.attr: 屏宽大于 431px 时将目标尺寸缩放至原来的 0.6 倍以展示更多的内容；

.attr-pad: 将 vw * 720px / 2048px 转换为基于 2048px 设计稿的尺寸；

.attr-pad-tab: 将 vw * 720px / 1888px 转换为基于 1888px 设计稿的尺寸；

.attr-pad-portrait: 将 vw * 720px / 1536px 转换为基于 1536px 设计稿的尺寸；

看起来有些绕，上面的 vw 实际为设计稿上的元素尺寸 / 720px，实际上换算的思路就是先复原设计稿上的尺寸，然后计算与对应设备的基准宽度的比例，之后使用 vw 作为单位即可。

而基于上述 less Mixin 方案编写一个多端适配的页面则需要至多书写 4 份代码，如：

Copy
@import "~@/assets/less/common.less";

.article-wrapper {
font-size: 40px;
padding: 0 32px 148px;
line-height: 72px;
// ... other style
}

.article-wrapper-tablet-portrait {
.attr-pad-portrait(--var-container-pad-pading, 624px);
width: calc(100% - var(--var-container-pad-pading));
.attr-pad-portrait(font-size, 40px);
.attr-pad-portrait(line-height, 72px);
.attr-pad-portrait(padding, 0, 12px, 198px);
}

.article-wrapper-tablet-landscape {
.attr-pad(--var-container-pad-pading, 624px);
width: calc(100% - var(--var-container-pad-pading));
.attr-pad(font-size, 40px);
.attr-pad(line-height, 72px);
.attr-pad-3(padding, 0, 12px, 198px);
}

.article-wrapper-tablet-landscape-with-sidebar {
.attr-pad-tab(--var-container-pad-pading, 624px);
width: calc(100% - var(--var-container-pad-pading));
.attr-pad-tab(font-size, 40px);
.attr-pad-tab(line-height, 72px);
.attr-pad-tab-3(padding, 0, 12px, 198px);
}

别觉得荒诞…… 就是从项目代码里摘抄出来的，实际上比这个长不知道多少倍

其中 article-wrapper-tablet-portrait/article-wrapper-tablet-landscape/reading-article-wrapper-tablet-landscape-with-sidebar 三个 css 类名仅仅是把 .article-wrapper 复制以后，将 px 替换为了使用对应 .attr Mixin 转化后的数值而已。另外眼见的朋友可能已经看出来了，受限于 less Mixin 灵活性的限制，里面还用 css 变量来做了一些曲线救国的操作……

之后在页面结构中需要通过客户端公参判断设备类型，并挂载上对应的类名来实现样式的兼容。

一套组合拳打下来我都快忘了自己在干啥了……

总而言之这个方案只能用辣眼睛来形容，经常出现一个类名里加了条样式其他的里面没加，或者有个数值修改了但某个类名忘改了之类的，维护成本极高。

终于用上 rem 了……

rem 是相对于 html 根元素 font-size 的大小来计算的，本身就具备运行时调整的能力，不依赖类似 postcss-px-to-viewport 这样编译时的行为。只需要在页面打开时根据设备类型在 html 根元素上按照比例换算 font-size 即可。

动态调整 font-size 的部分也没啥复杂的，随便糊一下：

Copy
export enum EDeviceType {
/** 移动端 */
PHONE = "phone",
/** Pad 横屏 */
TABLE_LANDSCAPE = "tabletLandscape",
/** Pad 横屏带侧边导航栏 */
TABLE_LANDSCAPE_WITH_SIDEBAR = "tabletLandscapeWithSidebar",
/** Pad 竖屏 */
TABLE_PORTRAIT = "tabletPortrait",
}

const sizes: Record<string, number> = {
[EDeviceType.PHONE]: 720,
[EDeviceType.TABLE_LANDSCAPE]: 2048,
[EDeviceType.TABLE_LANDSCAPE_WITH_SIDEBAR]: 1888,
[EDeviceType.TABLE_PORTRAIT]: 1536,
};

/** 根据屏幕宽度自动计算出 rem 的基准 font-size */
export const autoRem = (device: EDeviceType): (() => void) => {
const baseScreenWidth = sizes[device];
const docEl: HTMLElement = window.document.documentElement;

const resizeCall = () => {
const clientWidth: number = docEl.clientWidth;

if (!clientWidth) {
docEl.style.fontSize = 100 + "px";
} else {
docEl.style.fontSize = 100 * (clientWidth / baseScreenWidth) + "px";
}
};
let dpr: number = window.devicePixelRatio || 1;

resizeCall();
dpr = dpr >= 3 ? 3 : dpr >= 2 ? 2 : 1;
docEl.setAttribute("data-dpr", `${dpr}`);

return resizeCall;
};

之后在页面中，只需要按照设计稿的尺寸，将 px / 100 替换为 rem 即可（因为默认的 font-size 是 100px）。

Copy
.article-wrapper {
font-size: 0.4rem;
padding: 0 0.32rem 1.48rem;
line-height: 0.72rem;
// ... other style
}

由于页面挂载时已经根据比例修改了 font-size 了，rem 能够呈现出基于 720px/1536px/1888px/2048px 基础宽的尺寸以实现自适应。同时 rem 不会受到项目已有的 postcss-px-to-viewport 插件的影响，亦不需要担心对已有的代码造成影响。

之后如果需要针对设计稿中不同设备上的样式区别进行处理，直接按照之前的方式覆写即可，如：

Copy
.article-wrapper {
font-size: 0.4rem;
padding: 0 0.32rem 1.48rem;
line-height: 0.72rem;
background: url(~@assets/images/bg@1x.png) no-repeat;
// ... other style
}

.article-wrapper-tablet {
background: url(~@assets/images/bg@2x.png) no-repeat;
}