资讯动态

JS动态设置rem:PC端与移动端适配的完整实践指南

发布时间:2026/10/17 5:51:41 来源:尧图企业网站定制
1. 为什么要用 JS 设置 rem而不是纯 CSS先说结论纯 CSS 也可以做 rem 适配但做不彻底。rem 这个单位的本质是“相对于根元素 html 的 font-size”所以你只需要把 html 的 font-size 改掉整个页面里所有用 rem 做单位的元素都会跟着等比缩放。问题在于CSS 怎么知道当前设备的屏幕宽度是多少靠媒体查询。媒体查询的问题在于它是断点式的比如 375px、414px、768px、1920px你在断点之间只能取一个近似值永远做不到“屏幕多宽根字号就精确对应多宽”。JS 就不一样了。JS 可以直接读取document.documentElement.clientWidth拿到当前视口的真实宽度然后按设计稿的宽高比实时算出根字号。这样一来不管你的屏幕是 320px 还是 1920px甚至是 2560px 的带鱼屏页面里的 rem 元素都会严格等比缩放中间没有任何断点跳变的感觉。另一个选 JS 而不是纯 CSS 的原因是PC 端和移动端的适配逻辑其实不一样纯 CSS 很难在一套代码里同时兼顾两种场景。移动端的经典做法是“按 750 设计稿等比缩放”而 PC 端的常见做法是“按设计稿宽度等比放大但要限制最大宽度和最小宽度防止内容拉伸变形”。这个差异用 CSS 写会非常绕用 JS 也就十几行代码。我几年前接手过一个后台管理系统设计稿是 1920 宽但用户的显示器从 1366 到 2560 都有早期用媒体查询写了七八个断点结果每次改版都要反复调断点后来全部推翻换成 JS 动态设置 rem整个适配层就干净了。所以这篇文章的核心就一句话用 JS 统一管理根字号把适配逻辑从 CSS 里捞出来。2. PC 端设置 rem 的核心思路与 JS 实现2.1 PC 端 rem 适配的基本逻辑PC 端设置 rem 和移动端最大的不同在于移动端设计稿宽度是固定的 750 或 375而 PC 端设计稿宽度五花八门常见的从 1440 到 1920 都有。更麻烦的是PC 端的浏览器窗口是可以随意resize的用户不会像手机那样固定在一个宽度。PC 端 rem 适配的核心公式是根字号 (当前视口宽度 / 设计稿宽度) * 基准字号举个例子设计稿宽度 1920基准字号 100px。当视口宽度是 1920 时根字号 (1920 / 1920) * 100 100px当视口宽度是 1366 时根字号 (1366 / 1920) * 100 ≈ 71.15px这样一来如果设计稿里某个盒子宽度是 300px你在代码里写成3rem它在 1920 宽下渲染为 300px在 1366 宽下渲染为 213.45px比例完全一致。很多人会问为什么基准字号要选 100因为 100 好算。设计稿里 1px 对应 0.01rem设计稿里 320px 对应 3.2rem心算都能算出来开发效率高。你选 10 也行选 16 也行但 100 是最好口算的。2.2 处理 PC 端的最大宽度和最小宽度PC 端不能像移动端那样无脑等比缩放原因是你在 2560 的显示器上如果还按比例放大整个页面的字会大到离谱。同样在 1366 的笔记本上如果按比例缩小到 71px 根字号有些加了min-width的限制的组件会先崩掉。所以 PC 端 rem 适配必须加两个限制最大根字号和最小根字号。我的习惯是const maxFontSize 100; // 1920 设计稿下根字号上限 100px const minFontSize 75; // 对应 1440px 视口宽度逻辑是视口宽度超过设计稿宽度时根字号不再增大保持 100px视口宽度小于某个阈值比如 1440时根字号不再缩小保持 75px。这样设计业务上有个好处内容区域不会因为屏幕太宽而拉得无边无际也不会因为屏幕太窄而挤成一团。配合容器的max-width: 1920px; margin: 0 auto;整个页面在超宽屏上呈现的是居中的固定宽度布局而不是无脑拉伸。2.3 PC 端不想全站 rem可以只让内容区缩放有一种更精细的做法也是我在实际项目里用得比较多的全局布局还是用 px只有内容区域比如大屏图表区、表格区使用 rem。思路是给内容区设置一个固定的设计稿宽度基准然后用 JS 计算缩放比例通过transform: scale()来实现内容的整体缩放外层再包一层占位容器撑起高度。这个方案和 rem 方案并不冲突反而验证了一个道理适配方案没有银弹核心是找准“变化的是什么、不变的是什么”。rem 解决的是长度单位的等比换算transform: scale()解决的是整个容器视觉等比缩放。两种手段都离不开 JS 对当前视口宽度的实时感知。3. 移动端设置 rem 的经典方案与完整代码3.1 移动端 rem 适配的经典比例逻辑移动端的设计稿宽度一般就是 750二倍图或 375一倍图。不管你拿到的设计稿是哪种核心逻辑只有一句话把设计稿的总宽度映射到设备的物理像素宽度然后按比例算出根字号。假设设计稿是 750 宽设备逻辑宽度是 375px比如 iPhone 13 的 CSS 像素宽度根字号 (375 / 750) * 100 50px假设设备逻辑宽度是 412px许多国产安卓机根字号 (412 / 750) * 100 ≈ 54.93px设备越宽根字号越大页面里的所有 rem 元素同步放大最终视觉上就是“设计稿等比缩放”。这里有个容易踩坑的点为什么有些项目要处理devicePixelRatio因为 750 宽的设计稿通常是在 2 倍屏下出的图如果你直接按 375 逻辑宽度做 rem图片的视觉效果其实是宽的但因为 CSS 像素和物理像素的换算关系实际显示会偏小。更严谨的做法是把根字号计算与window.devicePixelRatio联动但这个不是所有项目都必须的具体取决于你的设计稿是标注了物理像素还是 CSS 像素。我建议的做法是先和设计确认设计稿的基准是物理像素还是逻辑像素再决定要不要乘 dpr。3.2 移动端 rem 设置的三个必备环节移动端 rem 适配只做根字号计算是不够的你至少还需要考虑三个环节少了任何一个都容易出问题。第一个是viewport 配置。必须有meta nameviewport contentwidthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalableno这样的基础配置否则页面在手机上会先按默认的 980px 渲染一遍再缩放你的 rem 计算拿到的clientWidth根本不准。第二个是移动端触摸屏的缩放行为。很多 Android 浏览器在双击时会触发页面缩放如果你没有显式禁用user-scalableno用户误触后整个 rem 布局会被浏览器的缩放机制破坏掉。当然这个设置对无障碍访问不友好所以你也可以用touch-action: manipulation来禁止双击缩放同时保留系统字体缩放的入口。第三个是根字号的动态更新。移动端和 PC 端不一样手机上浏览器的工具栏显示/隐藏会导致视口高度变化但宽度一般不变。所以移动端的 resize 监听主要是防横竖屏切换代码上建议同时监听resize和orientationchange两个事件。3.3 移动端 rem 设置的标准 JS 实现下面这段代码是我在多个移动端项目里复用过的基础版本思路清晰改动成本低(function (doc, win) { const designWidth 750; // 设计稿宽度 const baseFontSize 100; // 基准字号方便计算 function setRem() { const clientWidth doc.documentElement.clientWidth; if (!clientWidth) return; // 如果屏幕宽度超过设计稿一般也保持最大宽度防止页面无限放大 const width Math.min(clientWidth, designWidth); const fontSize (width / designWidth) * baseFontSize; doc.documentElement.style.fontSize fontSize px; } win.addEventListener(resize, function () { clearTimeout(tid); const tid setTimeout(setRem, 300); }, false); win.addEventListener(pageshow, function (e) { if (e.persisted) { clearTimeout(tid); const tid setTimeout(setRem, 300); } }, false); setRem(); })(document, window);这段代码做了三件核心事读取document.documentElement.clientWidth这是当前视口的逻辑宽度不受浏览器缩放影响。计算根字号并直接写到html的style.fontSize上。注意这里用的是html元素本身不是body因为 rem 的参考基准就是html的字体大小。在resize和pageshow浏览器前进/后退缓存页恢复时重新计算保证切换横竖屏或跳过缓存后页面依然是正确的比例。我在代码里做了 300ms 的防抖因为真机上 resize 会高频触发每次都去改html的font-size会引发大面积重排。防抖后只有停止改变尺寸了才去计算一次体验会顺滑很多。4. PC 端与移动端 rem 设置方案合并的实操过程4.1 同一套代码兼容 PC 和移动端的关键点如果你维护的是一个需要同时跑在 PC 浏览器和手机浏览器上的项目最省事的方案是写一个通用函数内部通过navigator.userAgent或视口宽度判断当前是 PC 还是移动端然后走不同的适配分支。我建议的判定逻辑很简单优先用pointer: coarse媒体查询来判断是否支持粗指针触摸设备其次是视口宽度小于等于某个阈值比如 768时按移动端逻辑处理。为什么不用userAgent因为 iPad 的userAgent非常容易伪装而且现在很多 Windows 触屏笔记本userAgent完全和 PC 一样但实际上是触摸设备只靠 UA 判断会踩坑。4.2 合并方案的完整示例代码下面这个函数我在一个实时监控大屏项目里用过同时兼容 PC 端和移动端浏览器访问(function (win, doc) { // 配置信息可按具体项目调整 const CONFIG { pc: { designWidth: 1920, // PC 设计稿宽度 maxFontSize: 100, // 根字号上限 minFontSize: 72 // 根字号下限防止过小 }, mobile: { designWidth: 750, // 移动端设计稿宽度 maxFontSize: 100 } }; function isMobile() { // 优先看触摸能力其次看尺寸 const hasTouch ontouchstart in win || navigator.maxTouchPoints 0; const narrow doc.documentElement.clientWidth 768; return hasTouch narrow; } function setRootFontSize() { const clientWidth doc.documentElement.clientWidth; if (!clientWidth) return; const mobile isMobile(); const config mobile ? CONFIG.mobile : CONFIG.pc; const designWidth config.designWidth; let fontSize (clientWidth / designWidth) * 100; if (!mobile) { fontSize Math.min(fontSize, CONFIG.pc.maxFontSize); fontSize Math.max(fontSize, CONFIG.pc.minFontSize); } else { fontSize Math.min(fontSize, CONFIG.mobile.maxFontSize); } doc.documentElement.style.fontSize fontSize.toFixed(2) px; } let timer null; win.addEventListener(resize, function () { clearTimeout(timer); timer setTimeout(setRootFontSize, 200); }, false); win.addEventListener(pageshow, function (e) { if (e.persisted) { clearTimeout(timer); timer setTimeout(setRootFontSize, 200); } }, false); setRootFontSize(); })(window, document);这段代码把所有钥匙逻辑浓缩在一个函数里。PC 端和移动端用同一个入口但走不同的配置和不同的边界策略。PC 端走最小/最大字号限制移动端只做上限保护防止 5K 屏手机把根字号放得过大。4.3 动效与 rem 搭配的一个实践建议你可能遇到过一个问题使用 rem 适配后某些 CSS 动画比如位移、放大会糊。原因其实不在 rem 本身而在于 rem 适配后根字号大多不是整数比如 71.15px元素尺寸和位移值就带了小数位浏览器合成动画时要对非整数像素做抗锯齿处理性能相对变差。我的经验是给根字号计算结果保留两位小数并且尽量让基准字号为 100这样大多数常见宽度下算出来的根字号误差很小。另外动画涉及的变换如果用transform: translateX(rem)会比left: rem的性能好很多因为transform走的是合成器线程不触发重排。4.4 实际联调时如何验证适配效果我这里有一个笨但有效的方法用浏览器开发者工具的设备模拟器分别切几个关键尺寸看效果。PC 端至少要过三档1366、1440、1920。移动端至少要过四档320、375、414、768。验证的核心指标是设计稿里的某个固定元素比如一个 300px 宽的卡片在所有尺寸下视觉上都保持大约 300/750 或 300/1920 的比例。你可以直接在控制台执行getComputedStyle(document.documentElement).fontSize看根字号是否在预期范围内。5. 设置 rem 时的常见问题与排查技巧实录这几类问题我都在实际项目里遇到过列出来供你排查时参照。5.1 rem 写成 px 了怎么办最常见的翻车现场是根字号设置成功了但组件里部分样式还是写死了 px特别是border、box-shadow、letter-spacing这类属性。这些写成 px 通常问题不大因为 1px 的边框在大多数屏幕上差异不明显但如果你把宽度、高度、padding、margin写死成 px那 rem 缩放时这些元素就会“不老实”有的跟着缩有的不跟着缩布局直接乱掉。排查思路是在浏览器控制台执行下面这段代码它会找出所有包含 px 的样式规则你逐个看哪些是布局类属性const styles document.styleSheets; for (let sheet of styles) { try { for (let rule of sheet.cssRules) { if (rule.style rule.style.cssText.includes(px)) { const rules rule.style.cssText.match(/[a-z-]: [0-9.]px/g); if (rules) console.log(rule.selectorText, rules); } } } catch (e) { // 跨域样式表会抛错忽略 } }5.2 字号计算出现 100px 或极小值这是边界条件没控制好。PC 端 4K 屏的clientWidth可能到 3840不设上限的话根字号会变成 200px整个页面放大一倍视觉完全失控。反过来老的安卓机或小折叠屏clientWidth才 320根字号会算出来 42px 左右设计稿里 100px 的字会缩到没法看。所以我在合并方案里特意加了minFontSize和maxFontSize这属于经验值你可以根据实际项目落地。5.3 图片的宽高和 rem 比例对不上这个坑出在设备像素比上。移动端设计稿如果是 750 宽二倍图图片标注的宽高是物理像素。你写img { width: 3.5rem }算下来渲染宽度是 350px 的一半即 175px 逻辑像素但物理像素是 350px这是正常的。问题往往出在部分安卓机的devicePixelRatio是 2.75、3.5 这类非整数这时用固定的 750 基准会导致部分图片过锐或模糊。处理办法是按 dpr 缩放根字号。我给你一个思路计算clientWidth * window.devicePixelRatio作为新的“视口宽度”然后再除以设计稿宽度去算字号。但注意如果这样做你的viewport的initial-scale也要对应设置为1 / dpr否则会出现页面被放大、点不中按钮的问题。这个方案改动较大建议只在图片精度要求高的项目里用。5.4 移动端横竖屏切换后 rem 不刷新某些 Android 浏览器的resize事件在横竖屏切换时不会触发或者触发得很慢。这时orientationchange事件就派上用场了。如果你用的是我上面给的合并方案需要确保两个事件都被监听。另外还有一个冷门场景部分浏览器对pageshow事件的理解不一致它有些时候不会在普通跳转后触发但在前进/后退缓存恢复时一定会触发所以不能漏掉。我给你的建议是写一个setRootFontSize()函数然后在resize、orientationchange、pageshow三种情况下都调用它。配合防抖不会产生性能问题。5.5 Chrome 的字体最小字号限制打断缩放Chrome 浏览器有个默认设置最小字体大小是 12px。如果你的根字号经过计算后在极端小屏上低于 12pxChrome 会强制把html的font-size显示成 12px导致整个页面缩放比例失真。虽然现在很多浏览器已经放宽了这个限制但老版本的浏览器还是会遇到。解决方案有两种一是用-webkit-text-size-adjust: 100%降低影响二是保证根字号的下限不小于 12px。你可以用下面这段代码在控制台快速验证当前页面的实际根字号console.log(parseFloat(getComputedStyle(document.documentElement).fontSize));5.6 iframe 内嵌页面的 rem 计算做后台系统时经常遇到 iframe 内嵌看板的情况。iframe 里的页面如果也用 rem 适配它拿到的clientWidth是 iframe 自身的宽度而不是父页面的宽度这其实是正确的。但如果你在 iframe 里用了window.parent的尺寸来计算页面在某些布局模式下会误判。我的经验是iframe 内的 rem 适配应该只依赖自身窗口尺寸除非你有强烈的跨页面联动需求。否则父页面调整侧边栏宽度导致 iframe 尺寸变化时iframe 页面里的 rem 计算会乱。如果有联动需求需要在父页面通过postMessage把新的宽度传给 iframe而不是在 iframe 内部去读父级的getBoundingClientRect()。6. 根字号计算后的单位换算与开发技巧6.1 rem 和 px 的快速换算方法设定基准字号为 100 后最爽的体验就是换算的心智负担基本为零设计稿标注 16px代码就写0.16rem设计稿标注 100px代码就写1rem设计稿标注 375px代码就写3.75rem如果是 750 宽设计稿但团队代码想用 375 作为设计基准也只需要在换算时统一除以 2。比如设计稿标注 300px代码写1.5rem实际渲染宽度是 150px——这个你开发时一定要心里有数不然视觉走查时会觉着元素偏小。如果团队统一用基准字号 16虽然数值是整数但换算心智负担重不建议。你自己做无所谓带团队做适配一定选 100。6.2 在 SCSS/LESS 中封装 px 转 rem 的工具函数既然 JS 里能做换算CSS 预处理器里也可以封装一个函数这样写样式时就完全不用手动换算直接写设计稿的 px编译时自动转 rem。以 SCSS 为例$design-width: 750; $base-font-size: 100; function px2rem($px) { return ($px / $base-font-size) * 1rem; } .box { width: px2rem(300px); height: px2rem(150px); }编译出来的结果是.box { width: 3rem; height: 1.5rem; }这类工具的要点是它和 JS 里的设计稿宽度、基准字号必须保持同一个值。如果你 SCSS 里写$design-width: 750JS 里写designWidth375那两者全部对不上页面直接崩。6.3 使用 CSS 变量做局部适配如果你希望某些组件在移动端和 PC 端有不同的渲染比例比如移动端更紧凑、PC 更宽松可以在根字号方案之上叠加一层 CSS 变量。JS 在判断到是移动端时给html加一个>:root { --card-padding: 2rem; } [data-devicemobile] { --card-padding: 1.2rem; }基础长度用 rem 跟随全局缩放组件的局部间距用 CSS 变量做差值修正。这个技巧实际开发中非常有用尤其是响应式改版阶段不需要动 JS 逻辑只改样式变量就能快速调整。7. 从这套 rem 方案还能扩展什么如果你的项目已经跑通了 PC 端和移动端 rem 适配下一步可以自然延伸到几个更有价值的方向。第一个方向是大屏可视化适配。大屏通常运行在固定分辨率的显示器或拼接屏上逻辑上和 PC 端 rem 适配一致但多了“拼接缝补偿”和“安全边距”的问题。你可以基于 rem 方案叠加一个transform: scale()来适配非标准分辨率保证内容不被裁切。第二个方向是局部容器 rem 缩放。组件库或复杂业务页面中可能有某个卡片列表、图表容器固定要按自身的宽度而不是整页宽度进行 rem 变换。这种场景下你可以把根字号计算从全局改成局部在某个容器内临时调整 font-size子元素依然用 rem。但要注意CSS 的 rem 永远只认html的 font-size所以局部缩放实际上要靠em而不是rem。如果你既想保留 rem 的全站一致性又想局部缩放就得用内联样式动态改了。第三个方向是子页面与设计稿的配合。我经历过的项目里设计团队有时会把 PC 设计稿和移动端设计稿的基准宽度定得不一样比如 PC 是 1920移动是 750。这个时候你需要和设计约定所有间距、字号、圆角都尽量用整数像素避免奇奇怪怪的 3.333px否则换算后会有严重的精度损失。这个看起来是规范问题但在 rem 适配的项目里特别重要因为它直接影响 CSS 渲染的物理像素对齐。第四个方向是服务端渲染场景的适配。如果你的项目用了服务端渲染JS 设置 rem 会面临一个“首屏闪烁”的问题服务端返回的 HTML 里没有根字号浏览器要先渲染一版默认字号再执行 JS 设置根字号用户会看到页面跳动一下。解决办法是在head里内联一小段 JS来尽早设置根字号。但尽量减少内联脚本的大小并且避免阻塞渲染。常用的做法是script (function (doc, win) { var clientWidth doc.documentElement.clientWidth; var fontSize (clientWidth / 1920) * 100; doc.documentElement.style.fontSize fontSize px; })(document, window); /script这段代码放在head的 CSS 之前虽然内联脚本会有一定的阻塞但因为这个脚本只做一次计算和一次样式写入代价极小收益是首屏没有闪烁。我个人在实际操作中的体会是rem 适配方案最大的价值不是“像素级还原”而是把“适配”这个关注点从杂乱无章的 CSS 断点里抽出来变成一段可以用逻辑验证的 JavaScript。只要设计稿基准、视口宽度获取、事件监听这三个环节不出错PC 和移动端的比例问题就解决了一大半。最后再分享一个小技巧把设置根字号的代码封装成独立模块放出来不要只在某个页面里局部定义因为适配这件事在项目生命周期里一定会被反复调用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑