移动端适配这件事说大不大说小也不小。我见过不少项目PC端跑得稳稳当当一到手机上就原形毕露字体忽大忽小、按钮挤成一团、图片要么溢出要么留白一大片。更让人头疼的是团队里每个人对适配的理解都不一样有人用百分比有人用弹性盒子还有人干脆写死像素值最后拼出来的页面在各类机型上表现参差不齐。rem这套方案之所以值得单独拿出来讲是因为它用一套非常朴素的思路把设计稿尺寸和设备实际宽度之间的换算关系给统一了。你只要理解了根字号这个杠杆剩下的都是机械操作。这篇文章我会从 rem 的本质讲起把动态根字号的几种主流做法拆开对比再补上实际项目里最容易翻车的几个细节包括 1px 边框、字体缩放、横竖屏切换这些老生常谈但又绕不开的问题。不管你是刚接触移动端的新手还是想把手头项目适配方案梳理一遍的老手应该都能从中找到能直接用的东西。1. rem 到底在解决什么问题以及它和 em、px 的本质区别1.1 从一次真实的适配翻车说起早些年我接手过一个活动页项目设计稿给的是 750px 宽度标注得清清楚楚。当时图省事所有尺寸直接按标注写死 px在模拟器上看效果完美。结果上线后收到反馈小屏手机上标题挤成两行大屏手机上内容缩在左上角一小块留白大得离谱。问题的根源很直白——px 是绝对单位它不会随设备宽度变化。你在 375px 宽的设备上写font-size: 32px它占的比例是 32/375换到 414px 宽的设备上同样的 32px 占比就变成了 32/414视觉上明显变小了。设计稿是按固定宽度画的但设备宽度是浮动的这两者之间缺一个换算桥梁。em看起来能解决这个问题因为它相对父元素字号。但 em 有个要命的连锁反应父元素字号一变子元素全部跟着变嵌套层级一深计算就变成了一场灾难。你写padding: 1em这个 1em 到底等于多少像素得一层层往上追溯父元素的 font-size维护成本极高。rem的 r 是 root 的意思它只认根元素html的字号不管嵌套多深1rem 永远等于 html 的 font-size。这一刀切的设计恰恰是它适合做全局适配的根本原因。1.2 根字号就是那把统一的尺子把 rem 理解成一把会伸缩的尺子最直观。假设你把 html 的 font-size 设成 16px那 1rem 就是 16px2rem 就是 32px。现在你把 html 的 font-size 改成 20px所有用 rem 写的尺寸会等比例放大 1.25 倍。页面里所有元素共用同一把尺子整体缩放时比例关系不会乱。这就是 rem 适配的核心机制用 JavaScript 动态修改根字号让 1rem 所代表的实际像素值随屏幕宽度线性变化。具体到数值关系假设设计稿宽度是 750px你在稿子上量到某个元素宽 150px。如果约定 1rem 对应设计稿上的 75px也就是把设计稿分成 10 份那这个元素就是 2rem。到了实际设备上屏幕宽 375px你把根字号设成 375/10 37.5px那 2rem 实际渲染出来就是 75px正好占屏幕宽度的 1/5和设计稿上的比例完全一致。整个换算链条里唯一需要动态计算的就是根字号其余尺寸全部写成 rem 固定值一次写完处处适配。1.3 三种单位的适用边界对照单位参照对象是否随屏幕变化典型适用场景主要风险px物理像素否边框、极细线条、不随屏幕缩放的图标大屏小屏比例失调em父元素字号间接组件内部与字号联动的间距嵌套层级导致计算失控rem根元素字号是配合动态根字号布局宽高、字号、间距的全局适配根字号计算错误导致整体偏移这张表不是让你死记而是提醒一个判断原则需要跟屏幕宽度成比例变化的用 rem需要保持物理尺寸稳定的用 px。比如 1px 的细边框你要的是它在任何屏幕上都看起来是一根细线而不是随屏幕放大成粗线这种就该用 px 或者后面会讲的缩放方案而不是 rem。2. 动态根字号的三种主流写法与选型逻辑2.1 方案一JS 监听 resize 直接算最原始也最透明的做法就是在页面加载和窗口尺寸变化时用 JavaScript 读取document.documentElement.clientWidth除以一个约定好的份数把结果赋给 html 的 font-size。代码大致是这样(function () { function setRootFontSize() { var clientWidth document.documentElement.clientWidth || window.innerWidth; // 设计稿 750px分成 10 份每份 75px var rootSize clientWidth / 10; document.documentElement.style.fontSize rootSize px; } setRootFontSize(); window.addEventListener(resize, setRootFontSize); window.addEventListener(orientationchange, setRootFontSize); })();这段代码的好处是一目了然出了问题你能立刻定位到是哪一行算错了。但它的短板也很明显resize事件在移动端触发频率很高尤其是拖动浏览器地址栏收起展开的时候会连续触发几十次每次都改根字号会引起大量重排。稳妥的做法是加一层防抖或者用requestAnimationFrame把多次调用合并到一帧里执行。另外orientationchange在部分老机型上触发时机不可靠横竖屏切换后拿到的宽度可能是旧值需要延迟一小段时间再读一次。2.2 方案二CSS 媒体查询分段设置不想引入 JS 的话可以用媒体查询按常见屏幕宽度分段写死根字号html { font-size: 20px; } media screen and (min-width: 375px) { html { font-size: 25px; } } media screen and (min-width: 414px) { html { font-size: 27.6px; } } media screen and (min-width: 750px) { html { font-size: 50px; } }这种写法的优点是纯 CSS、无脚本依赖首屏不会因为 JS 未执行而出现字号跳变。缺点是它是阶梯式的屏幕宽度在两个断点之间时根字号是固定的适配精度不如连续计算。而且断点越多维护越麻烦遇到折叠屏、平板这些非标准宽度很容易落在某个断点里表现不佳。我的经验是如果项目对适配精度要求不高、页面结构简单媒体查询够用但凡涉及复杂布局和精细还原还是得上 JS 连续计算。2.3 方案三结合视口单位的混合思路vw是视口宽度的百分之一1vw 等于屏幕宽度的 1%。有人会想那直接html { font-size: 10vw; }不就行了纯 CSS 搞定。思路没错但vw在部分浏览器里对滚动条宽度的处理不一致而且当根字号完全由 vw 决定时用户手动调整系统字体大小的设置会被忽略无障碍体验打折扣。更稳的混合做法是用 JS 算出根字号后再乘一个用户偏好系数或者用clamp()给根字号设一个上下限防止在超宽屏上字号大到离谱、在超小屏上小到看不清。html { font-size: clamp(12px, 2.6667vw, 50px); }这行的意思是根字号最小 12px、最大 50px中间按 2.6667vw 走对应 375px 屏下约 10px 的基准可按设计稿调整。clamp()的兼容性现在主流机型都没问题用它兜底能省掉不少边界判断。2.4 选型的判断依据三种方案没有绝对优劣关键看你的约束条件。我一般按这几条来定项目是否允许引入额外脚本、首屏渲染对字号跳变是否敏感、设计稿宽度是否统一、是否需要支持用户系统字号缩放。如果设计稿是 750px 且团队统一JS 连续计算最省心如果是多套设计稿混用媒体查询分段反而更好控制如果追求零脚本和首屏稳定clamp()加 vw 是折中优选。选型时把这些条件列清楚比盲目抄某篇教程靠谱得多。3. 从设计稿到代码rem 换算的完整落地流程3.1 先定基准再谈换算换算的第一步不是写代码而是和设计确认一个基准关系。常见约定有两种一是设计稿宽度 / 10 1rem 对应的设计稿像素750px 稿就是 75px 对应 1rem二是设计稿宽度 / 100750px 稿就是 7.5px 对应 1rem。前者数值好记后者精度更高、小数更少。我倾向用除以 10 的方式因为 75 这个数在口算时比较友好量到 150px 就是 2rem量到 225px 就是 3rem心算不容易出错。定了基准之后实际设备上的根字号公式就是根字号 设备宽度 / 设计稿份数。750px 稿分 10 份375px 设备上根字号就是 37.5px414px 设备上就是 41.4px。这个公式要写进团队文档所有人按同一套来否则有人按 75 算、有人按 7.5 算页面比例立刻乱套。3.2 换算工具与手动计算的取舍设计稿上标注的尺寸是 px代码里要写 rem中间需要一个除法。手动算当然可以150 / 75 2225 / 75 3但遇到 137px 这种除不尽的就麻烦了137 / 75 1.8266...写进代码里一长串小数既难看又容易出错。实际项目里我一般用两种方式处理一是用构建工具里的插件自动把 px 转成 rem写代码时照抄设计稿标注编译时统一转换二是用编辑器插件或在线换算表量一个转一个。自动转换的配置要点在于基准值要和 JS 里的份数严格对应。比如 JS 里是clientWidth / 10那转换插件的基准就得设成设计稿宽度 / 10 75。这两处一旦对不上页面会整体放大或缩小一个固定倍数而且这种错误在单一机型上不容易发现往往要换几台设备对比才暴露出来。我踩过这个坑当时插件基准设成了 37.5JS 里却除以 10结果所有元素小了一半排查了半天才定位到是配置不一致。3.3 哪些属性该转、哪些不该转不是所有 px 都该无脑转成 rem。我的划分原则是这样的该转 rem 的容器宽高、内外边距、字号、行高、圆角、大部分间距类属性。这些需要跟屏幕宽度成比例转 rem 后整体缩放才协调。不该转 rem 的1px 边框、极细分割线、阴影偏移量中的细微值、以及需要保持物理尺寸的图标描边。这些如果转成 rem在小屏上可能细到看不见在大屏上又粗得突兀。视情况而定的图片宽高。如果图片需要铺满容器用百分比或width: 100%更合适如果是固定比例的图标用 rem 控制尺寸可以但要配合object-fit防止变形。这个划分不是死规矩但能帮你避开大部分整体缩放后局部比例失调的问题。尤其是边框后面第 5 节会专门讲 1px 的处理方案。3.4 一个完整的换算示例假设设计稿 750px某卡片标注宽 690px、内边距 30px、标题字号 36px、圆角 12px、边框 1px。按 75px 对应 1rem 换算.card { width: 9.2rem; /* 690 / 75 */ padding: 0.4rem; /* 30 / 75 */ border-radius: 0.16rem; /* 12 / 75 */ border: 1px solid #e5e5e5; /* 边框保持 px */ } .card__title { font-size: 0.48rem; /* 36 / 75 */ }在 375px 设备上根字号 37.5px卡片实际宽 9.2 × 37.5 345px正好是屏幕宽度的 92%和设计稿上 690/750 92% 的比例完全吻合。这就是 rem 适配想要达到的效果——比例一致而非像素一致。4. 实际项目里最容易翻车的几个细节4.1 根字号被浏览器最小字号限制截断这是最隐蔽的坑之一。部分浏览器有最小字号设置比如默认不小于 12px。当你按公式算出来的根字号小于这个值时比如在 320px 小屏上320 / 10 32px 还好但如果份数设成 100320 / 100 3.2px 就完蛋了浏览器会强行把根字号拉到最小值导致所有 rem 尺寸集体偏大页面比例全乱。解决办法有两个一是份数别设太大保证最小屏上根字号也大于 12px二是用clamp()给根字号设下限同时接受小屏上比例略有偏差的事实。我一般选前者份数控制在 10 左右320px 屏上根字号 32px安全。4.2 横竖屏切换后的重算时机手机横过来之后clientWidth变成了原来的高度值根字号需要重算。但orientationchange事件触发时部分浏览器还没完成布局更新此时读到的宽度可能是旧值。稳妥做法是在事件回调里加一个短延迟或者监听resize并在requestAnimationFrame里读取window.addEventListener(orientationchange, function () { setTimeout(setRootFontSize, 100); });这个 100ms 不是随便定的是给浏览器留出完成视口更新的时间。延迟太短读到旧值太长用户会看到明显的字号跳变。实测下来 100ms 左右在主流机型上比较稳。4.3 字体大小与 rem 的联动陷阱字号用 rem 写意味着用户无法通过系统设置放大字体——因为根字号被 JS 锁死了。这对无障碍不友好。折中方案是布局尺寸用 rem正文文字用px或em配合媒体查询或者给根字号乘一个用户偏好系数。有些团队会在 JS 里读取window.getComputedStyle的初始字号作为基准再叠加缩放但这套逻辑在不同浏览器上表现不一实现成本较高。我的建议是正文可读性优先的场景字号别完全交给 rem至少保留媒体查询做兜底放大。4.4 第三方组件的 rem 污染引入的 UI 组件库如果内部也用 rem而它的基准和你项目的基准不一致就会出现组件尺寸和页面其他部分对不上的情况。这种问题排查起来很费劲因为样式是组件库内部写的。应对办法是优先选支持主题定制的组件库把它的 rem 基准改成和你一致或者用 CSS 变量把根字号抽出来组件库引用同一个变量。实在改不动的就在组件外层包一个容器用font-size重置局部基准把组件隔离在一个独立的 rem 上下文里。5. 1px 边框与高清屏rem 方案绕不开的补丁5.1 为什么 1px 在高清屏上会变粗rem 适配解决的是比例问题但 1px 边框是另一个维度的事。在设备像素比DPR为 2 或 3 的高清屏上CSS 里的 1px 会被渲染成 2 个或 3 个物理像素视觉上就是一条粗线。设计稿上那条精致的细分割线到了真机上变得笨重。这个问题和 rem 无关但移动端适配绕不开它所以必须一并处理。5.2 几种主流处理方案的对比方案实现方式优点缺点伪元素 transform scale用::after画 1px 线再缩放 0.5兼容性好视觉精准代码略繁琐需处理定位直接写 0.5pxborder: 0.5px solid写法简单部分安卓机型不支持会渲染成 0 或 1pxbox-shadow 模拟用阴影代替边框不占布局空间圆角处理麻烦性能略差背景渐变linear-gradient画线可控性强只适合直线圆角边框难做我实际项目里用得最多的是伪元素加transform: scaleY(0.5)这套因为它兼容性最稳视觉上也最接近设计稿。核心思路是给元素加一个绝对定位的伪元素高度设为 1px然后用transform沿 Y 轴缩放一半再通过transform-origin控制缩放基点让线贴在正确的位置。.hairline-bottom { position: relative; } .hairline-bottom::after { content: ; position: absolute; left: 0; bottom: 0; width: 100%; height: 1px; background-color: #e5e5e5; transform: scaleY(0.5); transform-origin: 0 100%; }5.3 媒体查询按 DPR 分档的写法如果项目需要更精细的控制可以按设备像素比用媒体查询分档处理media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) { .hairline-bottom::after { transform: scaleY(0.5); } } media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 3dppx) { .hairline-bottom::after { transform: scaleY(0.333); } }这样在 DPR 为 3 的设备上1px 的线缩放成 0.333物理上正好是 1 个像素视觉上就是最细的线。需要注意的是缩放后的线在部分机型上可能出现颜色偏淡这是因为亚像素渲染导致的可以通过调整背景色透明度来补偿。6. 适配方案的上线前自检清单6.1 多机型实测的覆盖策略rem 方案在模拟器上看着对不代表真机就对。上线前我一般会覆盖这几类设备做实测小屏320px 到 360px 宽、主流屏375px 到 414px、大屏430px 以上、以及至少一台折叠屏或平板。每类设备上重点看三件事整体比例是否协调、文字是否清晰可读、边框和分割线是否正常。模拟器只能验证逻辑真机才能暴露渲染差异尤其是安卓阵营机型碎片化严重同一套代码在不同厂商的浏览器内核上表现可能不同。6.2 关键指标的核对方法实测时我会打开浏览器的开发者工具用元素检查逐个核对根字号的实测值是否等于设备宽度 / 份数、关键容器的宽度占比是否和设计稿一致、字号换算后是否落在可读区间正文不小于 12px 实际渲染值。如果发现某个元素比例不对先查它是不是漏转了 rem再查是不是被某个固定 px 的父容器限制了宽度。这个排查顺序能快速定位大部分适配问题。6.3 常见问题的快速对照现象可能原因排查方向整体偏大或偏小根字号份数与转换基准不一致核对 JS 份数和构建插件基准局部元素比例失调该元素漏转 rem 或写了固定 px检查该元素及其父级样式小屏上字号过小根字号被最小字号限制或份数过大调整份数或加 clamp 下限横屏后布局错乱重算时机过早读到旧宽度延迟重算或监听 resize边框过粗未处理高清屏 1px用伪元素缩放方案这张表是我自己排查时常用的对照大部分适配问题都能归到这几类里。真正难缠的是那种只在某台特定机型上出问题的情况那往往不是 rem 逻辑本身的问题而是该机型浏览器对某个 CSS 属性的解析有偏差需要单独做兼容处理。6.4 文档与团队约定最后一点容易被忽略但很重要把基准关系、份数、转换规则、例外情况哪些用 px、哪些用 rem写进团队的前端规范文档。适配方案本身不复杂复杂的是多人协作时各自理解不一致。我见过项目里两个人一个按 75 换算、一个按 37.5 换算合并代码后页面一半正常一半错位排查成本极高。约定写清楚比任何技术方案都管用。我个人在实际操作中的体会是rem 这套方案的价值不在于它多先进而在于它足够简单、足够统一。它把移动端适配从每个元素单独调变成了调一个根字号全局跟着走。真正花时间的从来不是写那几行 JS而是把基准约定、例外规则、高清屏补丁这些边角料处理干净。把这些细节提前想清楚后面改需求、加页面的时候会省下大量返工。