
1. 后台管理系统为什么需要“整体缩放”1.1 混乱的终端尺寸逼出来的缩放需求做后台管理系统做到一半最让人头疼的往往不是业务逻辑而是各种分辨率下页面“看起来不对”。同一个登录页在 1366×768 的办公笔记本上正常放到 1920×1080 的显示器上可能左侧留白一大片放到带系统缩放 125% 的 Windows 上布局又挤成一团。传统做法是给每个页面写一堆媒体查询内容稍微多一点就改不过来了。“整体缩放”的本质是当屏幕尺寸或容器宽度变化时页面上所有元素——按钮、表格、表单、字体、间距——按照同一个比例同步放大或缩小。后台管理系统不像 C 端官网那样需要“流式布局”它对视觉还原度要求很高设计稿画成什么样线上就应该是什么样。与其让每个组件自己响应式不如让整棵 DOM 树的尺寸跟着一个全局基准联动。这个需求在数据大屏项目里尤其明显。大屏通常会嵌入到展馆的拼接屏或浏览器窗口里窗口宽度一变所有图表和标题必须跟着等比缩放否则画面要么放不下要么空出一大块。后台管理系统的适配逻辑虽然没大屏那么极端但核心思路完全一致先把设计稿上的绝对值转成相对值再由一个全局基准把它们统一放大或缩小。1.2 三种常见方案对比transform: scale、zoom、rem先说结论不要一听到“整体缩放”就想着 transform: scale。这个方案确实简单给根容器加一行transform: scale(0.8)就能把整个页面缩小原理解起来也容易但实际用起来坑非常多。transform 缩放只是视觉上缩小布局计算占的空间还是原来的。也就是说你的页面宽度仍然是 1920px滚动条、弹层定位、固定定位都会出问题。举个例子某个按钮用position: fixed固定在右下角当根容器被缩放之后它计算定位时用的还是未缩放的坐标系位置就会偏移。弹层也是重灾区Element UI 的 MessageBox、Drawer 都挂在 body 或指定容器上它们不在缩放的根容器里弹层出现后和触发它的按钮位置对不上遮罩层也盖不全。zoom 是非标准属性Chrome 支持得不错但 Firefox 和旧版 Safari 的行为不一致多数团队不敢在生产环境依赖它。CSS Zoom 虽然确实能做到“布局也跟着缩放”但它会让页面里某些交互的坐标计算变得很诡异而且一旦用了 zoom后续排查问题的时候很难分辨到底是浏览器的锅还是自己的锅。rem 方案绕开了前面的问题它不是去“缩放”某个容器而是把全局的html根字号调大调小页面上所有用 rem 写的尺寸都会跟着根字号变化。标准、内容、可维护这块后面会细说。这也是我在 Vue2 后台管理系统里最终选择 pxtorem 的原因——不入侵布局模型不依赖非标准属性改造成本可控并且能直接沉淀到后续项目里复用。1.3 为什么最终锁定 pxtorem选择 pxtorem 还有一层很现实的原因团队里不是每个开发都熟悉自适应那套换算逻辑。直接让他们在样式里写 rem很容易写出font-size: 0.14rem这种看着就头疼的数字。pxtorem 是在 CSS 编译阶段做转换的开发者日常写样式仍然写font-size: 14px构建完成后自动变成0.14rem。也就是说改造成本对业务开发几乎是透明的。而且 pxtorem 的转换精度比手写 rem 稳定得多。它基于 PostCSS 的 AST 处理能够识别px出现的位置只处理 CSS 属性值里的单位不会误伤文本内容或者注释。配合自动溢出、小数位精度控制这些参数基本能做到构建之后打开页面就是设计稿形态不用每个人动脑子去算。当时我还对比过把设计稿所有尺寸都手动改成 vw 的方案。vw 更“原生”但管理后台高度方向的问题没法用一个统一比例解决而且写起来不如 rem 直观。rem 的另一个隐形好处是如果某个场景你真的不想缩放可以随时在根字号脚本里做控制全局只动一个地方非常灵活。下面我从原理开始讲把 rootValue 这个最关键的概念先捋清楚。2. rem 整体缩放的核心原理rootValue 这个概念不能含糊2.1 一个公式讲清楚 px、rem、html font-size 的关系很多人把 rem 理解成“等于网页根字体大小”这没错但实际操作时经常在 rootValue 上翻车。pxtorem 的工作原理是扫描 CSS 文件里的 px 数值在构建期把数值 px除以rootValue换算成数值 rem。这个 rem 的大小在运行时取决于html元素的font-size。所以全程只有两个关键变量rootValuepxtorem 把 px 转 rem 时使用的除数html font-size浏览器计算 rem 实际像素时的基数举个例子如果rootValue设为 100那么14px会被转成0.14rem。运行时若html font-size恰好是 100px这个元素实际就是 14px若html font-size调整到 80px这个元素实际就是 11.2px整体等比缩小了 20%。后台管理系统整体缩放方案的核心就在于让html font-size成为一块“动态比例尺”。屏幕越宽比例尺越大屏幕越窄比例尺越小。样式里凡是写 px 的尺寸构建后全部变成 rem最终都跟着这根比例尺走。2.2 rootValue 到底该填多少100 还是 192这是最多人犯迷糊的地方。同样是 1920 设计稿有人填 100有人填 192还都对原因就是他们配的html font-size计算方式不同。第一种做法是设计稿宽度除以 10。1920 设计稿下html font-size被设为 192px此时rootValue填 192。意思是设计稿上 192px 等于 1rem整个页面宽度约等于 10rem。第二种做法是把设计稿宽度换成 100 的倍率。1920 宽度下html font-size设为 100px此时rootValue填 100。设计稿上 100px 等于 1rem整个页面宽度约等于 19.2rem。两种做法数学上等价区别主要在心态上填 192 更贴近 rem 的字面含义根字号就是 192px填 100 更好心算1rem 约等于 1 个“百像素”。我在团队内部长期用 100 这个口径因为 1920px 设计稿除以 19.2 得到动态根字号时开发可以在浏览器控制台里直接看document.documentElement.style.fontSize一眼就知道页面当前被缩放到了多少。结论就是rootValue不是拍脑袋定的它必须等于“设计稿宽度下你希望 html font-size 是多少”。如果你计划在 1366 宽屏幕上让整个页面保持 1366 的设计比例公式应该是html font-size 浏览器当前视口宽度 / 设计稿宽度 * rootValue设计稿 1920rootValue 100在 1366 宽度下就是1366 / 1920 * 100 ≈ 71.15px页面整体缩小到 71.15%。2.3 有哪些样式不归 pxtorem 管pxtorem 不是银弹它只管 PostCSS 能看到的 CSS。下面几种情况它一概不管必须在落地前想清楚方案内联样式。:style{ width: 200px }这种写在模板里的 px 不会被转换因为 PostCSS 处理的是.css/.vue里的style块不处理 JS 字符串。Canvas 图表内部的像素坐标。ECharts 实例里的字体大小、图形尺寸都是 JS 运行时算的pxtorem 感知不到。第三方 JS 库动态插入的 DOM 内联样式。var()自定义属性里的 px 数值。例如--space: 20pxpxtorem 可能不会去改var()内部的值使用时要单独处理。个别经过计算的边框颜色、阴影等场景转换出来可能是 0.05rem 这种极小数视觉上会有偏差。所以落地 pxtorem 不只是加一个 npm 包那么简单还需要配套处理“非 CSS 文件里的 px”。这是后话先把配置跑通再说。3. Vue2 项目接入 pxtorem 的完整落地步骤3.1 环境与依赖清单这里讲的流程基于 Vue2 Vue CLI 3/4 Webpack这是目前存量后台管理系统最常见的组合。需要的依赖只有两个postcss-pxtoremPostCSS 插件负责把 px 转 remamfe-flexible动态设置根字号也可以不用它自己写代理脚本后面会给出自定义版本安装命令npm install postcss-pxtorem --save-dev npm install amfe-flexible --save如果你的项目还在用更老的postcss-px2rem建议换掉。那个插件对 PostCSS 8 的兼容性不好Vue CLI 4 默认带的 PostCSS 版本容易和它产生冲突。我用postcss-pxtorem的版本是 5.1.0在 Vue CLI 4.5 和 5.0 下都验证过没有明显问题。3.2 postcss 配置和 vue.config.js 实战Vue CLI 项目里接入 postcss 配置有两条路postcss.config.js或者通过vue.config.js的css.loaderOptions.postcss。我更推荐后者因为它在同一个文件里能管理 vue-cli 的编译行为避免出现两个配置文件互相覆盖的情况。在vue.config.js里添加如下配置const pxtorem require(postcss-pxtorem); module.exports { css: { loaderOptions: { postcss: { plugins: [ pxtorem({ rootValue: 100, unitPrecision: 5, propList: [*], selectorBlackList: [.ignore-rem], minPixelValue: 2, replace: true, mediaQuery: false, exclude: /node_modules/i }) ] } } } };这里面几个参数要逐个说清楚rootValue按前面的约定填 100。propList[*]表示所有属性都转。如果只想转字体和宽度可以写成[*, !border*]但管理后台通用场景基本用[*]就够。selectorBlackList数组里的选择器不转换。我习惯把.ignore-rem留出来给一些确实需要固定像素的边界场景用比如登录页的极细分割线。minPixelValue小于等于 2px 的不转。设计稿里 1px、2px 主要用来画边框转了 rem 之后在低分辨率下可能变成 0.01rem 这种接近 0 的值视觉上会闪。设成 2 能保住一部分细节。exclude这里我写了/node_modules/i会让 Element UI 这类组件库的样式不转。但是要注意如果你的公司对 UI 还原度要求很高Element UI 全部保持 px 会破坏整体缩放效果。这个取舍放到第 4 节讲因为这里的取舍直接决定你会在哪些场景踩坑。如果你的项目没有vue.config.js也可以创建postcss.config.jsmodule.exports { plugins: { autoprefixer: {}, postcss-pxtorem: { rootValue: 100, propList: [*], selectorBlackList: [], minPixelValue: 2, replace: true, mediaQuery: false, exclude: /node_modules/i } } };两种方式二选一即可同时存在的时候以vue.config.js里的配置为准。3.3 写一套可控的根字号自适应脚本amfe-flexible开箱即用但它默认按“视口宽度 / 10”来设置根字号和rootValue: 100的口径对不上。所以我不建议直接引入它更推荐用一个 10 来行代码的脚本让逻辑完全可控。新建src/utils/flexible.js(function (doc, win) { const designWidth 1920; const rootValue 100; function setRem() { const docEl doc.documentElement; let clientWidth docEl.clientWidth || win.innerWidth; // 按需限制最小宽度避免页面缩到看不清 const minWidth 1366; if (clientWidth minWidth) { clientWidth minWidth; } // 设计稿宽度下根字号正好等于 rootValue const fontSize (clientWidth / designWidth) * rootValue; docEl.style.fontSize fontSize.toFixed(2) px; } function debounce(fn, delay) { let timer null; return function () { if (timer) clearTimeout(timer); timer setTimeout(fn, delay); }; } win.addEventListener(resize, debounce(setRem, 150)); win.addEventListener(orientationchange, setRem); setRem(); })(window, document);然后到main.js里直接引入import ./utils/flexible.js;这里我把最小宽度设成 1366是考虑到后台管理系统在低于 1366 的窗口里继续等比缩小表格里的文字会小到没法用。你可以根据实际终端范围调整这一行。注意宽于 1920 时我没有做上限限制因为很多后台页面是给大屏或者高分屏准备的放大反而合适。如果你明确不希望超宽屏放大可以再加一个maxWidth 1920的判断。3.4 接入后如何快速验证效果接入完后不要急着写业务先做一轮快速验证。打开项目在 Chrome DevTools 的 Device Toolbar 里切换几种常见宽度比如 1920、1680、1440、1366观察两个地方第一document.documentElement.style.fontSize是否在随宽度变化。如果没变化多半是 flexible 脚本没引用或者放在了某个不执行的位置。第二看某个已经写了固定 px 的元素在样式面板里是不是变成了 rem。如果还是 px说明 postcss-pxtorem 没有生效这时候检查vue.config.js里 plugins 的写法。Vue CLI 的loaderOptions.postcss如果写成数组会覆盖掉项目里原有的 postcss 配置包括 Autoprefixer建议把require(autoprefixer)也加进去const autoprefixer require(autoprefixer); plugins: [ autoprefixer(), pxtorem({ ... }) ]另外一个常见问题是dist目录下生成的 CSS 里看到大量0.xxxxxrem但页面样式没变化。这种情况优先检查 CSS 是哪个插件生成的有些内部封装的构建配置会在vue.config.js之外另起一套chainWebpack配置postcss 的 loader 没有被走通。这时候直接搜项目里是否还有postcss.config.js被两个位置读到把它们统一起来就好。4. 我从实战里踩出来的检查清单弹层、表格、1px 和其他4.1 弹层挂载节点与 rem 的“断了传”问题使用 transform: scale 方案时弹层和遮罩是最痛苦的用了 rem 方案之后理论上不会再出现“容器缩放导致弹层定位偏离”但 Element UI 的弹层仍然有它自己的一套问题需要确认。Element UI 的 Dialog、MessageBox、Select 下拉框默认挂载在body下。它的定位基于 body 和平铺的 DOM 位置计算弹层内部的尺寸用的是 CSS 里定义的 px。如果node_modules里的 Element UI 样式没有被 pxtorem 转换那弹层的宽度、内边距、字体都是固定像素不会跟着根字号变化。视觉上就会出现页面主体整体缩到 80%弹层却还是 100% 的大小看起来很不协调。解决办法是把exclude: /node_modules/i去掉让 Element UI 的样式一起参与转换。但去掉之后又有新的坑Element UI 的一些组件靠精确像素对齐比如表格表头、输入框的 padding转换后可能出现 1px 的偏差。实际项目里我一般会先不排除node_modules在完整回归一轮后再决定是否加上selectorBlackList来局部补救。宁可让弹层同步缩放也不要出现主体缩放、弹层不缩放的割裂效果。如果你对引入的第三方组件库有强约束比如团队要求项目里所有弹层都用自定义组件不依赖 Element UI那排除node_modules也说得过去。重点是要形成统一约定别文本列的某个组件缩放了、另一个没缩放。4.2 第三方表格、图表和 JS 内联样式怎么处理后台管理系统绕不开表格和图表。el-table 的列宽很多是通过内联样式设置的也就是 JS 在运行时算出width然后拼到 style 属性里比如stylewidth: 180px。这种内联样式 PostCSS 根本碰不到。碰到的第一反应是“那我表格的列宽度是不是就没法缩放了”其实不一定。如果表格所在的容器宽度也是等比缩放后的宽度表格列宽保持固定 px反而会让横向滚动条出现这还算能接受。但如果你的表格设置了 fixed 列在缩放过的小窗口里左侧固定列和内联宽度的普通列之间会出现对不齐这就是用户能直接感知的 bug。处理方式是在业务代码里写一个统一的转换函数让所有通过 JS 设置尺寸的地方都走一层“px 转 rem”再拼接export function getRemBase() { return parseFloat(document.documentElement.style.fontSize) || 100; } export function pxToReal(px) { const rem px / 100; return rem * getRemBase(); }比如原本写this.$nextTick(() { this.$refs.table.style.width 200px; });改成this.$nextTick(() { this.$refs.table.style.width pxToReal(200) px; });图表的处理类似。ECharts 初始化时通过 option 设置的字体、图形尺寸都是 JS 运行时的值你需要把设计稿里的像素值先除以 100 再乘当前根字号得到实际的像素值。更省事的方式是在同一份工具函数里维护一个fontSize(px)例如function echartsFontSize(px) { return px / 100 * getRemBase(); }然后 ECharts 的textStyle、axisLabel都走这个函数。注意图表 resize 的时候要重新设置一遍并且监听容器尺寸变化来触发chart.resize()否则整套缩放里图表会“慢半拍”。4.3 1px 边框和部分样式不转换的取舍minPixelValue: 2的意思是 1px、2px 不转。这里有个细节要提醒设计稿上很细的分割线比如border-top: 1px solid #e5e5e5在整体缩放之后本来应该是 0.5px 或者 0.6px浏览器不会渲染看起来就像线消失了。设成不转之后这条线始终保持 1px物理像素上虽然比设计稿“粗了一点点”但用户感知反而更稳定。如果确实需要精确还原可以在项目里统一用伪元素加 transform 缩放来画细线。不要指望 pxtorem 给你一个银弹解决所有边框问题。另一个容易忽略的点是box-shadow。box-shadow: 0 2px 8px rgba(0,0,0,0.1)里的 2px 和 8px 会被 pxtorem 转成 rem导致阴影也随着屏幕大小放缩。视觉上通常没问题但在一些明暗分明的橙色按钮上阴影等比放大的效果反而更自然。如果你不想让阴影缩放可以直接在selectorBlackList里加专门的类。4.4 设计稿不是 1920 的项目怎么改有些后台管理系统的设计稿是 1600 宽甚至还有一部分团队按 1440 出图。这种情况下不要照搬前文的脚本关键还是回到第一节说的那个公式rootValue必须等于设计稿宽度下你希望的 html font-size 大小。设计稿 1600仍然想让 100px 对应 1rem就把 flexible.js 里的designWidth改成 1600rootValue保持 100。设计稿 1440同理改成 1440。pxtorem 的rootValue也必须跟着改成 100不需要因为设计稿变窄而改成 160 或 144因为你的根字号定义在 1600 宽时依然被脚本设成 100px。要注意的是团队内部不要混用。如果一台显示器宽度恰好是 1600一个页面按 1600 设计稿写另一个按 1920 设计稿写两者在同一屏上表现出的缩放比例会差 20%。项目启动时就要定好统一基线最好是沿用公司最主流的显示器分辨率。5. 和 Vue3 / 新工程迁移时这套方案能不能带走5.1 Vue3 项目里的配置变化Vue3 的环境比 Vue2 干净尤其是新的工程基本都切到了 Vite。Vite 下不走 vue.config.js 那一套postcss 配置统一放在postcss.config.js里插件还是同一个postcss-pxtoremrootValue、propList 这些参数完全兼容。唯一要留神的是 Vite 原生支持 CSS variables 和未来 CSS 特性有些样式会走postcss-preset-env如果你在postcss.config.js里配置插件务必把postcss-preset-env也带上否则部分新语法会失效。一个更干净的配置长这样export default { plugins: { postcss-preset-env: {}, postcss-pxtorem: { rootValue: 100, propList: [*], exclude: /node_modules/i } } };flexible.js 脚本在 Vue3 里可以原封不动搬到src/main.ts的入口引入因为它只是给html设置 font-size不依赖 Vue 的运行时机制。5.2 pxtorem 和 viewport 单位方案之间的迁移成本思考 Vue2 转 Vue3 的时候很多人会顺势把自适应方案从 rem 换成 vw。这个方向没错但要意识到迁移成本主要在存量代码上。如果你在一个成熟项目里动手转所有写 px 的样式都得重新过一遍pxtorem 的编译期转换其实帮你把大量工作做了一旦去掉它团队就要开始人肉改写。vw 方案有一个优势是更“长线”不依赖 JS 介入也不会因为某个动态脚本没执行就失效。但对于后台管理系统这种以 PC 端为主、更看重全页面等比的场景rem 的成熟度和生态支持依然是最好的。真要从 Vue2 迁移到 Vue3我会建议保留 pxtorem只是把配置从vue.config.js挪到postcss.config.js业务代码不动迁移风险最小。5.3 我的建议不要为了缩放牺牲代码可读性最后一个经验是方法论层面的。无论用 rem、vw 还是 transform都别写出让人看不懂的样式。有人为了保证几个像素的适配硬是在组件里塞了几十个!!条件表达式把样式逻辑和业务逻辑搅在一起后患无穷。总体缩放方案容易掩盖代码质量问题因为“反正最后都会按比例缩”开发者就容易忽略间距的理性设计。我的习惯是先把组件在设计稿宽度下做到像素级还原再考虑缩放。视觉还原OK整体缩放才不会在边缘分辨率下露馅。反过来如果设计稿本身间距就是乱的缩放只会把乱放大。最后分享一个调试技巧接入 flexile 脚本后可以在浏览器控制台执行document.documentElement.style.fontSize 100px立刻把页面切回设计稿尺寸方便对比设计稿和实际渲染差异。上线前我通常会写一段临时脚本对不同宽度下的关键页面截图比对把表格、图表、弹层、固定底部工具栏这几个高风险区域过一遍。多花一个小时做这个回归比上线后被业务方截图反馈问题再修的代价小得多。