国际惯例,________。

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

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

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

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

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

在编译编译后的结果为:

1
2
3
4
5
6
7
8
.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 们一拉数据,发现我们有不少用户都是平板设备,于是单独出了一期需求来适配。设计给出的整体方案思路其实就是定宽适配,体现在设计稿上的元素尺寸几乎就没啥区别,只是针对不同设备给定不同的设计稿基础尺寸,再有就是不同设备上少数几个地方的边距、背景图做了些调整。

设备类型设计稿基础尺寸
Phone720px
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 份代码,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@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 的部分也没啥复杂的,随便糊一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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)。

1
2
3
4
5
6
.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 插件的影响,亦不需要担心对已有的代码造成影响。

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

1
2
3
4
5
6
7
8
9
10
11
.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;
}