把「1920 ≠ 375」写进一行代码——前端响应式 rem 方案实战与踩坑记

为什么要自己造轮子?

市面上现成的 lib/flexible、postcss-px-to-rem 都很好,但我们的项目有三个"小脾气":

  1. 设计稿宽度随业务而变:PC 1920、PAD 768、Mobile 375,中间还有 590 的"折叠屏"。
  2. 设计师坚持「文字在任何视口都不能小于 12px,最大不超过 42px」。
  3. 项目历史包袱:大量旧页面仍用 px,需要渐进式迁移。

于是,我们决定手写一个"极简、渐进、可插拔"的响应式脚本,并把它封装成 <RemScaler /> 组件,随 React / Vue / 原生项目都能即插即用。


代码 30 行,原理 3 句话

js 复制代码
// RemScaler.js
(function (win) {
  const doc = win.document
  const docEl = doc.documentElement
  const baseSize = 14                // 14px = 1rem
  const maxScale = 3                 // 防止大屏字体爆炸

  function setRem () {
    const clientWidth = docEl.clientWidth

    // 1. 先选「设计稿宽度」
    let designWidth = 1920
    if (clientWidth < 500) designWidth = 375
    else if (clientWidth < 769) designWidth = 590

    // 2. 再算「缩放比例」
    const scale = Math.min(clientWidth / designWidth, maxScale)

    // 3. 最后把比例变成 rem
    docEl.style.fontSize = `${baseSize * scale}px`
  }

  setRem()
  win.addEventListener('resize', setRem, { passive: true })
})(window)

为什么要分段写 designWidth?

视口宽度 设计稿宽度 场景举例
< 500 375 手机竖屏
500--768 590 Pad 竖屏、折叠屏
≥ 769 1920 PC、横屏 Pad

这样划分后,任何设备看到的 1rem 都是"最接近设计稿比例"的物理大小,不会在小屏上字太小,也不会在大屏上字太大。


三步接入

1. 把脚本丢进项目

bash 复制代码
src/
├── utils/
│   └── RemScaler.js   // 上面那段
└── main.js            // 入口

2. 入口文件里 import

js 复制代码
// React 项目
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './utils/RemScaler'   // 只需 import 一次

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)

Vue、Angular、Svelte 都一样,只要在首屏之前跑起来即可。

3. 写样式

css 复制代码
/* 设计稿 1920 下宽度 300px */
.card {
  width: 21.4286rem;   /* 300 / 14 = 21.4286 */
  font-size: 1rem;     /* 14px */
}

踩坑与补丁

坑 1:SSR 时 window is not defined

解决:加一个判断,Node 环境不执行。

js 复制代码
if (typeof window !== 'undefined') {
  (function (win) { ... })(window)
}

坑 2:用户手动缩放浏览器字体

解决:监听 window.orientationchangepageshow,双保险。

js 复制代码
;['resize', 'orientationchange', 'pageshow'].forEach(evt =>
  win.addEventListener(evt, setRem, { passive: true })
)

坑 3:横竖屏切换时闪一下

解决:在 setRem 里加 100ms 节流。

js 复制代码
let tid
function setRem() {
  clearTimeout(tid)
  tid = setTimeout(() => { /* 原逻辑 */ }, 100)
}

进阶:把变量暴露出去,让业务层可配置

js 复制代码
window.RemScaler = {
  setBaseSize: (size) => { baseSize = size; setRem() },
  setMaxScale: (scale) => { maxScale = scale; setRem() }
}

业务代码里随时改:

js 复制代码
// 大屏活动页,允许字体放大 4 倍
window.RemScaler.setMaxScale(4)

总结

  • 30 行代码搞定"多设计稿 + 渐进迁移"的响应式方案。
  • 不依赖 webpack / vite 插件,无构建成本。
  • 可插拔:迁移完成后直接删掉 import './RemScaler' 即可回到纯 rem 方案。

响应式不是"把 1920 压成 375",而是"在 1920 与 375 之间找到最舒服的那个点"。

希望这篇小记能帮你少踩一个坑,早点下班去撸猫 🐱

相关推荐
拾光拾趣录6 分钟前
WebSocket:断线、心跳与重连
前端·websocket
阿眠30 分钟前
vue3实现web端和小程序端个人签名
前端·小程序·apache
哎呦薇43 分钟前
从开发到发布:手把手教你将Vue组件上传npm
前端·vue.js
Z7676_1 小时前
静态路由技术
服务器·前端·javascript
慧一居士1 小时前
npm 和 npx 区别对比
前端
用户3802258598241 小时前
vue3源码解析:生命周期
前端·vue.js·源码阅读
遂心_1 小时前
前端路由进化论:从传统页面到React Router的SPA革命
前端·javascript
前端菜鸟杂货铺1 小时前
前端首屏优化及可实现方法
前端
遂心_1 小时前
React Fragment与DocumentFragment:提升性能的双剑合璧
前端·javascript·react.js
ze_juejin1 小时前
ionic、flutter、uniapp对比
前端