1. 为什么 rem 布局总让人似懂非懂
很多前端开发者第一次接触 rem,都是从「移动端适配」这个词开始的。网上教程一搜一大把,代码复制过来也能跑,但一旦设计稿换了宽度、或者某个元素在 iPhone SE 上突然变大变小,就完全不知道从哪下手。问题的根源在于:大多数人只记住了「动态设置 html 的 font-size」这个动作,却没搞清楚 rem 到底相对谁计算、根字号为什么能控制整页缩放、以及 px 和 rem 之间那层换算关系是怎么建立的。
rem 全称 root em,是 CSS3 引入的相对长度单位。它和 em 最大的区别在于参照物:em 参照的是当前元素或父元素的字体大小,嵌套一多就会层层放大,算起来非常反直觉;而 rem 永远只参照根元素 html 的 font-size,不管元素嵌套多深,1rem 始终等于 html 上设定的那个值。这个「只认根」的特性,让 rem 成了做等比缩放的天然工具。
这篇文章面向正在做移动端 H5、活动页、或者需要一套代码适配多尺寸屏幕的前端开发者。我会从 em 和 rem 的差异讲起,把根字号动态计算的脚本、px 到 rem 的换算配置、以及浏览器里怎么验证结果完整拆一遍。读完你不仅能理解 rem 布局的原理,还能直接把这套方案落到自己的项目里。
2. 先搞懂 em 和 rem 的参照差异
在讲 rem 之前,必须先把 em 说清楚,因为混淆这两个单位是 rem 布局翻车的头号原因。
em 作为 font-size 的单位时,参照的是父元素的字体大小;em 作为其他属性(比如 line-height、width)的单位时,参照的是元素自身的字体大小。这个规则来自 MDN 的定义,但光看文字很难有体感,我们直接看一段代码。
<div class="p1"> <div class="s1">1</div> <div class="s2">1</div> </div> <div class="p2"> <div class="s5">1</div> <div class="s6">1</div> </div>.p1 { font-size: 16px; line-height: 32px; } .s1 { font-size: 2em; } .s2 { font-size: 2em; line-height: 2em; } .p2 { font-size: 16px; line-height: 2; } .s5 { font-size: 2em; } .s6 { font-size: 2em; line-height: 2em; }先看第一组。p1 的 font-size 是 16px,line-height 是 32px。s1 的 font-size 是 2em,参照父元素 p1 的 16px,算出来是 32px;它的 line-height 没有单独设置,继承父元素的 32px。s2 的 font-size 同样是 32px,但它自己写了 line-height: 2em,这里的 em 参照的是自身字体大小 32px,所以行高变成 64px。
再看第二组,这里藏着最容易踩的坑。p2 的 line-height 写的是无单位数字 2,它表示自身字体大小的两倍,也就是 32px。s5 的 font-size 是 2em,等于 32px,但它的 line-height 继承的是父元素那个「无单位的 2」,而不是 32px 这个计算值。无单位行高会被子元素重新计算,所以 s5 的行高是 32px × 2 = 64px。s6 自己写了 line-height: 2em,参照自身 32px,同样是 64px。
注意:无单位 line-height 会作为原始值被继承,子元素用自己的 font-size 重新计算;带单位的 line-height 则直接继承计算后的固定值。这个差异在 rem 布局里同样存在,别踩。
理解了 em 的「相对父级、层层传递」,rem 就好懂了。rem 把参照物固定成 html 的 font-size,无论元素嵌套多深,1rem 永远等于 html 上那个值。这意味着只要改 html 的 font-size,整页所有用 rem 描述的元素就会同步等比缩放。这就是 rem 布局的本质:等比缩放,通常基于屏幕宽度。
3. TaoToken 前置:把模型对话接进调试流程
做 rem 适配时,经常需要快速验证一段换算逻辑、或者让模型帮忙解释某个尺寸在不同设备上的表现。我习惯把这类问题丢给模型对话来快速确认,省去反复查文档的时间。如果你也想在调试过程中随时问一句,可以先把访问凭证准备好。
TaoToken 的模型对话入口在 https://taotoken.net/api ,控制台和密钥管理分别在 https://taotoken.net/console 和 https://taotoken.net/api-keys 。拿到 API Key 之后,就可以在本地脚本或调试工具里调用模型,让它帮你算 rem 值、检查换算比例,或者解释某段 CSS 的继承结果。
这一步不是 rem 布局的必需环节,但如果你经常需要边写边验证,把模型对话接进工作流会顺手很多。密钥只在服务端或本地环境使用,不要写进前端代码提交到仓库。
4. 可复制的根字号动态计算脚本
rem 布局的核心动作只有一个:根据设备宽度和设计稿宽度的比例,动态设置 html 的 font-size。下面这份脚本可以直接复制到项目里。
(function (designWidth, baseFontSize) { function setRem() { var html = document.documentElement; var deviceWidth = document.body.clientWidth || html.clientWidth; var rem = (deviceWidth / designWidth) * baseFontSize; html.style.fontSize = rem + 'px'; } setRem(); window.addEventListener('resize', setRem); window.addEventListener('pageshow', function (e) { if (e.persisted) setRem(); }); })(750, 100);这段代码做了几件事。designWidth 传 750,代表设计稿宽度是 750px;baseFontSize 传 100,代表在 750px 宽的设备上,html 的 font-size 会被设成 100px。这样 1rem 就等于 100px,设计稿上 200px 的元素写成 2rem 即可,换算比例是 100。
为什么选 100 而不是 1?如果 baseFontSize 设成 1,设计稿 200px 就要写 200rem,数值太大不好读也不好维护。选 100 之后,px 转 rem 只需要除以 100,心算就能完成。你也可以选 50 或 75,只要和后续的换算工具配置保持一致就行。
脚本里额外监听了 pageshow 事件,是为了处理部分浏览器从缓存恢复页面时 resize 不触发的情况。移动端还有横竖屏切换、软键盘弹出等场景会改变可视宽度,resize 监听基本能覆盖。
提示:如果你的项目需要限制最大宽度(比如在平板或桌面上不希望无限放大),可以在 setRem 里加一个上限判断,超过某个宽度就固定 font-size,避免元素被拉得过大。
5. px 与 rem 换算配置:编辑器插件与构建工具
脚本跑起来之后,写样式时还要手动把设计稿的 px 除以 100 换成 rem,时间长了容易出错。有两种方式可以自动化这个换算。
第一种是编辑器插件。VS Code 里可以装 px 转 rem 的插件,安装后在设置里把换算基数改成 100,之后在样式里输入 px 值,插件会自动提示对应的 rem 值。默认基数通常是 16,一定要改成和你脚本里 baseFontSize 一致的数字,否则换算全错。
第二种是构建工具配置。以 PostCSS 为例,可以用 postcss-pxtorem 插件在打包时自动把 px 转成 rem。
// postcss.config.js module.exports = { plugins: { 'postcss-pxtorem': { rootValue: 100, propList: ['*'], selectorBlackList: ['.no-rem'], minPixelValue: 2 } } };rootValue 设成 100,和脚本里的 baseFontSize 对应。propList 用['*']表示所有属性都转换;如果只想转部分属性,可以写成['font', 'font-size', 'width', 'height', 'margin', 'padding']。selectorBlackList 里的类名不会被转换,适合处理那些必须用 px 的场景,比如 1px 边框。minPixelValue 设成 2,表示小于 2px 的值不转换,避免把细边框转成小数导致渲染模糊。
这里有个容易忽略的点:postcss-pxtorem 转换的是你写的 px,而设计稿的 px 和 CSS 的 px 在概念上要对齐。如果你的设计稿是 750px 宽,rootValue 是 100,那么设计稿上量出来 200px,代码里写 200px,插件会自动转成 2rem。整个过程你只需要照着设计稿写 px,剩下的交给插件。
6. 浏览器验证:确认 rem 真的生效了
配置写完,怎么确认 rem 布局真的按预期工作?打开 Chrome DevTools,按下面几步验证。
第一步,选中 html 元素,在 Computed 面板里看 font-size 的实际计算值。假设当前设备宽度是 375px,designWidth 是 750,baseFontSize 是 100,那么 font-size 应该是 375 / 750 × 100 = 50px。如果看到的是 50px,说明脚本生效了。
第二步,选中一个用了 rem 的元素,在 Computed 面板里看它的 width 或 font-size。比如你写了 2rem,在 375px 设备上应该显示 100px。如果显示的是 32px 或其他值,说明换算基数对不上,检查 rootValue 和 baseFontSize 是否一致。
第三步,用 DevTools 的设备模拟器切换不同宽度,观察 html 的 font-size 是否跟着变。从 375px 切到 414px,font-size 应该从 50px 变成 55.2px。如果不变,检查 resize 监听是否被移除,或者脚本是否在 DOM 加载前就执行了。
第四步,在 Console 里直接输入getComputedStyle(document.documentElement).fontSize,回车看返回值。这是最直接的验证方式,返回的字符串就是当前根字号。
// 在 Console 里快速验证 const rootFontSize = getComputedStyle(document.documentElement).fontSize; console.log('当前根字号:', rootFontSize); console.log('设备宽度:', document.documentElement.clientWidth); console.log('预期根字号:', document.documentElement.clientWidth / 750 * 100 + 'px');如果预期值和实际值对不上,优先检查脚本里的 designWidth 和 baseFontSize 是否和你的设计稿、插件配置一致。这三个数字必须完全对齐,任何一个错了,整页尺寸都会偏。
7. 本篇常见错误排查
rem 布局跑不起来,绝大多数问题集中在下面几种情况。
根字号没生效:最常见的原因是脚本执行时机太早,DOM 还没解析到 html 元素。把脚本放在 body 末尾,或者用 DOMContentLoaded 包一层。另外检查有没有其他样式覆盖了 html 的 font-size,比如某个全局样式里写了html { font-size: 16px }。
换算比例对不上:设计稿 750px,脚本 baseFontSize 是 100,但插件 rootValue 写成了 75,结果所有元素都小了四分之一。这三个值必须一致:设计稿宽度、脚本的 designWidth、插件的 rootValue 对应的基数。
1px 边框变粗或消失:postcss-pxtorem 把 1px 也转成了 0.01rem,在某些设备上渲染不出来。解决办法是把 minPixelValue 设成 2,或者把边框样式加进 selectorBlackList,让它保持 px。
横屏后布局错乱:横屏时设备宽度变大,根字号跟着变大,元素被拉得很宽。如果业务不需要横屏适配,可以在脚本里判断横屏时固定一个根字号,或者用媒体查询限制最大宽度。
字体大小不跟随缩放:有些浏览器(尤其是部分安卓 WebView)会限制最小字体大小,导致 rem 算出来的小字号被强制放大。可以在 html 上加-webkit-text-size-adjust: 100%来关闭自动调整。
resize 触发过于频繁:拖动窗口时 resize 会连续触发,每次都改 font-size 会造成大量重排。可以加一个简单的防抖,或者用 requestAnimationFrame 包一层。
let ticking = false; function onResize() { if (!ticking) { requestAnimationFrame(function () { setRem(); ticking = false; }); ticking = true; } } window.addEventListener('resize', onResize);8. 继续深入:把 rem 方案接进你的工作流
rem 布局的原理说到底就一句话:改 html 的 font-size,整页等比缩放。但真正落地时,脚本、插件、设计稿三者之间的数字对齐才是关键。我自己的习惯是,项目初始化时先把 designWidth、baseFontSize、rootValue 这三个值写在 README 里,后面任何人接手都不会搞混。
如果你在调试过程中想快速验证某段换算逻辑,或者让模型帮你检查一段 CSS 在不同设备上的表现,可以用模型对话来辅助。需要长期在编码和 Agent 场景里使用的话,Coding Plan 会更合适,接入方式和密钥管理都在控制台里。接入文档里有完整的调用示例,照着配一遍就能跑通。
最后留一个实用技巧:在项目里建一个rem.config.js,把设计稿宽度和基数集中管理,脚本和构建工具都从这个文件读,改一处就全改,比散落在各个配置里靠谱得多。