第一次给指挥中心做投屏,我用 Vue 写的大屏在本地开发机(1920×1080)上分毫不差,接到现场一块 3840×2160 的屏幕上,整个页面缩在左上角只占四分之一,右边和下边是两大片黑。当时我以为是浏览器的问题,折腾了半天才发现,是页面宽度被写死成 1920px,而视口变成了 3840px。后来这些年,从会议室一体机到展厅竖屏,从数据看板到视频监控墙,Vue 大屏自适应这件事我基本踩遍了能踩的坑,也试过 rem、vw/vh、transform scale、容器查询四条技术路线。
这篇东西不打算给你一个"复制粘贴就能用"的万能公式,因为大屏适配从来就没有万能公式。我想做的是把这几套方案的边界说清楚——它们各自在什么场景下成立、在哪里会崩、崩了以后怎么补救,然后把我们线上跑了两年多的那套 transform scale 方案,连同配套的图表像素补偿、弹层定位修正、非 16:9 妥协策略,完整地摊开讲一遍。不管你是刚接到第一个大屏需求的新人,还是已经被模糊的 ECharts 和乱飞的弹窗折磨过的老手,应该都能从里面捞到点能直接落地的东西。
1. 大屏适配到底在适配什么:先把物理场景和设计稿对齐
1.1 三种大屏场景,物理特性完全不同
"大屏"这个词在不同团队嘴里含义差得很远,方案选型从第一步就该分叉。我接触最多的是这三类:
第一类是会议室或展厅的一体机,常见 65 到 98 寸,面板物理分辨率 3840×2160,跑 Windows 或者安卓系统。这种场景最麻烦的变量不是分辨率,而是Windows 的显示缩放。当系统缩放设成 150% 时,浏览器全屏后的window.innerWidth只有 2560,你的 4K 面板实际按 2560 的逻辑像素在排版。同一个项目在 A 会议室正常,搬到 B 会议室就错位,八成是两台的显示缩放设置不一样。
第二类是指挥中心的 LED 拼接墙,物理分辨率可能是 3×3 拼接的 5760×3240。但实际交付时,绝大多数项目走的是拼接处理器方案:一台主机输出 1920×1080,由硬件把这一路信号拉伸铺满整墙。这意味着你的页面设计稿还是 1920×1080,只是最终会被物理放大三倍左右。这里有个容易被忽略的点:1px 的发丝线物理放大后会变成三四个像素宽,看着发糊;反过来如果整体缩小,1px 的线可能直接消失。所以给拼接墙做的设计稿,线条我会统一用 2px 起步。
第三类是展会竖屏,1080×1920 那种立式广告机。这种基本算另一套产品了,布局逻辑跟横屏完全不同,我一般会单独出一版设计稿,而不是指望同一套代码翻转。还有一类是超宽屏,比如 21:9 甚至 32:9 的弧形屏,常见于地铁站或者展厅。
1.2 第一步不是写代码,是量屏幕
我现在的固定动作是:拿到项目后,第一时间要求在目标设备上打开浏览器控制台,把这三个值读出来,写进项目的 README 里。
console.log({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, dpr: window.devicePixelRatio, screenW: screen.width, screenH: screen.height })为什么要这么较真?因为window.innerWidth和screen.width经常不是一回事,设备像素比devicePixelRatio又决定了你的 canvas 该开多大位图。这三个数决定了后面所有的取舍,比讨论用什么框架重要得多。
顺手把设计稿的尺寸也在这个环节敲定。设计稿宽高一旦确定,就是整个项目的"世界坐标系",后面所有布局都以它为基准,不允许中途改。
1.3 等比缩放、弹性布局、混合策略的分界线
真正决定方案走向的,其实是一个业务问题:你的大屏能不能接受留白或者变形?
- 指挥中心监控墙:绝对不能变形,地图、雷达、拓扑图一旦拉伸就失真,留黑边可以接受。
- 展会宣传屏:也不能变形,但也不能留黑边,因为太难看了,所以倾向于背景铺满、内容等比。
- 内部数据看板:可以接受弹性布局,左右两栏随屏幕宽度伸缩反而更好用。
这三种业务诉求,对应三条完全不同的技术路线。很多团队一上来就问"哪个方案最好",其实应该先问"这块屏允许我留白吗",答案出来,方案基本就锁定一半了。
2. 四套主流方案的实测对比:rem、vw/vh、transform scale、容器查询
2.1 每套方案到底在做什么
先说 rem。核心就一行 JS:把根元素的字号按视口宽度动态算出来,然后所有尺寸都用 rem。
function setRootFontSize() { const w = document.documentElement.clientWidth document.documentElement.style.fontSize = (w / 1920) * 100 + 'px' }设计稿上 200px 宽的元素,写 2rem 就行,配合postcss-pxtorem自动转换,改造成本很低。它的好处是像素是像素,没有 GPU 拉伸那回事,字体边缘永远锐利。
再说 vw/vh。这是纯 CSS 路线,用postcss-px-to-viewport把 px 全部转成 vw,连 JS 都不用写。构建期完成转换,运行时零开销,理论上最优雅。代价是它对"非等比"这件事完全没有办法——屏幕比例一变,元素要么挤要么散。
第三条是 transform scale,也就是大家都在用的那套:整块内容按设计稿写死 1920×1080,外层套一个缩放容器,按视口算一个缩放比,靠 CSS 变换整体放大缩小。优点是开发体验极好,设计稿上量多少就写多少,所见即所得,出了一点像素级偏差都看得见。缺点集中在渲染质量上,后面单独开一章讲。
第四条是容器查询@container。给容器声明container-type: inline-size,然后内部按容器宽度写断点。它是目前唯一"组件级自适应"的原生方案,适合做设计系统,但对于追求像素级还原的大屏,说实话性价比不高——大屏要的是精确复刻,不是响应式优雅降级。
2.2 四套方案的关键指标对照
| 维度 | rem | vw/vh | transform scale | 容器查询 |
|---|---|---|---|---|
| 像素级还原 | 一般 | 较差 | 优秀 | 较差 |
| 改造成本 | 中 | 低 | 低 | 高 |
| 第三方组件兼容 | 需单独处理 | 需单独处理 | 天然兼容 | 天然兼容 |
| 非等比屏幕表现 | 会变形 | 会变形 | 可留白可延展 | 取决于断点 |
| 字体清晰度 | 锐利 | 锐利 | 有模糊风险 | 锐利 |
| canvas/视频质量 | 正常 | 正常 | 需补偿 | 正常 |
| 最小字号限制 | 会受影响 | 会受影响 | 不受影响 | 不受影响 |
表格里有两个点值得展开。一个是第三方组件兼容:rem 和 vw 方案下,Element Plus 这类组件库内部的 px 不会跟着你的设计稿走,一个 32px 高的输入框在 4K 屏上看着像颗芝麻,你得额外配置插件的转换范围,风险不小。transform scale 方案下,组件库的东西连带你自己的布局一起被缩放了,视觉比例天然一致。
另一个是最小字号限制。rem 方案在视口宽度很小时,根字号会被算得很小,此时部分浏览器环境的字号下限会跳出来兜底,把文字强制拉大,导致排版直接错乱。我确实在几个中文环境的机器上撞到过这个问题,排查起来相当费劲。transform scale 方案因为字号本身写的是设计稿值(比如 14px),缩放发生在渲染层,反而不受字号下限影响——这是我后来坚定选它的一个重要原因。
2.3 我的选型判断表
把上面的东西压缩成几条经验:
- 需求里有"像素级还原"四个字,直接上 transform scale,别犹豫。
- 项目是通用后台,只是偶尔投到大屏上看看,用弹性布局 + 少量断点,别搞缩放。
- 团队前端规范成熟、组件库自研、追求长期可维护,可以考虑容器查询,但别指望它一晚上搞定。
- 项目里全是第三方图表、地图、视频,而且必须清晰,做好像素补偿的心理准备再上 scale。
3. transform scale 方案的完整落地链路
3.1 容器结构的四个关键属性
先看 DOM 结构。外层负责铺满视口和裁切,内层负责承载设计稿尺寸的内容。
<div class="screen-wrapper"> <div class="screen-stage" :style="stageStyle"> <router-view /> </div> </div>外层样式只有三件事:铺满、裁切、铺底色。
.screen-wrapper { position: fixed; inset: 0; overflow: hidden; background: #0a1428; // 与设计稿边缘同色,避免留白突兀 }内层的关键是四个属性:固定宽高对应设计稿、绝对定位到左上角、变换原点设在左上角、由 JS 动态注入变换矩阵。
.screen-stage { position: absolute; top: 0; left: 0; width: 1920px; height: 1080px; transform-origin: 0 0; }这里必须强调一次:transform-origin选 0 0 还是 center center,决定了你后面的偏移计算方式,两者不能混用。我见过有人在样式里写了transform-origin: center center,JS 里又按左上角原点算偏移,结果页面永远对不齐,查了一整天。
3.2 useScreenScale:把缩放逻辑封装成一个组合式函数
Vue3 里我会把它做成 composable,Vue2 项目可以改写成 mixin 或者全局指令,思路完全一样。
// useScreenScale.ts import { ref, computed, onMounted, onBeforeUnmount } from 'vue' export const DESIGN_WIDTH = 1920 export const DESIGN_HEIGHT = 1080 type Mode = 'contain' | 'cover' | 'width' | 'height' export function useScreenScale( designWidth = DESIGN_WIDTH, designHeight = DESIGN_HEIGHT, mode: Mode = 'contain' ) { const scale = ref(1) const offsetX = ref(0) const offsetY = ref(0) let rafId = 0 function calc() { const vw = document.documentElement.clientWidth const vh = document.documentElement.clientHeight let s = 1 if (mode === 'contain') s = Math.min(vw / designWidth, vh / designHeight) if (mode === 'cover') s = Math.max(vw / designWidth, vh / designHeight) if (mode === 'width') s = vw / designWidth if (mode === 'height') s = vh / designHeight scale.value = s offsetX.value = (vw - designWidth * s) / 2 offsetY.value = (vh - designHeight * s) / 2 } function schedule() { cancelAnimationFrame(rafId) rafId = requestAnimationFrame(calc) } const stageStyle = computed(() => ({ transform: `translate(${offsetX.value}px, ${offsetY.value}px) scale(${scale.value})` })) onMounted(() => { calc() window.addEventListener('resize', schedule) window.addEventListener('orientationchange', schedule) }) onBeforeUnmount(() => { cancelAnimationFrame(rafId) window.removeEventListener('resize', schedule) window.removeEventListener('orientationchange', schedule) }) return { scale, offsetX, offsetY, stageStyle, recalc: calc } }三个细节解释一下。第一,为什么用requestAnimationFrame而不是防抖定时器?拖动窗口时resize会连续触发几十上百次,如果每次都同步读写布局,会引发布局抖动。rAF 把计算压到下一帧之前,一次渲染周期只算一次,比定时器更贴合渲染节奏,也不会出现"松手后还延迟几百毫秒才对齐"的观感。
第二,translate必须写在scale前面。CSS 的变换按矩阵顺序右乘,写在前面的变换作用在最终的视觉位移上,不会被后面的缩放放大。如果你写成scale(s) translate(x, y),那个 x 和 y 就会被乘上 s,偏移量彻底错位。这个坑我踩过,当时现象是"屏幕越宽,页面偏得越离谱"。
第三,translate的偏移值用未缩放的视口像素算,所以offsetX = (vw - 1920 * s) / 2直接就是视觉上的居中偏移,不用再除以 s。
3.3 双轴策略:让侧栏弹性、让主视觉等比
上面这套是"整体等比缩放",适合监控墙。但如果需求是"左右两侧列表随屏幕变宽多显示几行内容,中间地图保持比例",整体缩放就不合适了。我用的变体是这样:缩放比仍按高度算,但舞台宽度反向撑开。
const s = vh / designHeight scale.value = s stageWidth.value = vw / s // 关键:把视口宽度换算回设计稿坐标系 offsetX.value = 0 offsetY.value = 0舞台的高度固定 1080,宽度变成一个动态值(比如 2600)。此时页面里左右两侧的列表容器用flex: 1自动吃掉多余宽度,中间的地图容器给固定宽度、保持设计比例。缩放回去之后,整块内容正好铺满屏幕,没有黑边,两侧列表还多了可用空间。
这个思路我在多个指挥中心项目里用过,配合display: grid的三栏布局非常顺。要注意的是:宽度撑开之后,所有依赖"设计稿 1920 宽"的绝对定位元素都会错位,所以这套方案下布局必须用 flex 或 grid 相对布局,不能用绝对定位堆。
4. 缩放的代价:ECharts、视频流、3D 场景的像素比补偿
4.1 图表为什么不跟着放大:一次典型的误判
transform scale 方案上线后第一个报障,十有八九是图表变糊。而且诡异的地方在于:你监听窗口 resize,调用了chart.resize(),图表明明"响应"了,还是糊。
根因在于CSS transform 不改变布局尺寸。图表容器的clientWidth永远是设计稿里写的那个值(比如 600px),无论外面把它放大到了 1.5 倍还是 3 倍。ECharts 内部判定"尺寸没变",于是既不重绘也不调整 canvas 位图。浏览器拿着一张 600×300 的位图,直接拉到 1800×900 显示,结果就是文字边缘糊成一片,折线像毛边。
所以正确的补偿方向不是让图表 resize,而是让 canvas 的位图分辨率跟上放大的倍数。
4.2 三种补偿写法的取舍
第一种,在初始化时把缩放比乘进设备像素比:
import * as echarts from 'echarts' function createChart(el: HTMLElement, scale: number) { return echarts.init(el, undefined, { renderer: 'canvas', // 关键:把 CSS 缩放倍数补进 dpr devicePixelRatio: Math.min(window.devicePixelRatio * scale, 3) }) }注意我加了Math.min(..., 3)做上限。4K 屏上devicePixelRatio本身可能是 2,再乘个 2 倍缩放就是 4,canvas 面积直接膨胀到 16 倍,显存和绘制耗时都会爆掉,大屏一卡就全卡。压到 3 是性能和效果的平衡点,实测 3 倍位图在全屏观看距离下已经看不出模糊。
第二种,缩放比变化时重建实例。ECharts 官方的resize()不接受devicePixelRatio参数,也就是说窗口大小变了、缩放比变了之后,旧的 dpr 不会自动更新。我的做法是在缩放比变化超过阈值(比如 0.05)时,把实例 dispose 掉重新 init:
watch(scale, (next, prev) => { if (Math.abs(next - prev) > 0.05) { chartRef.value?.dispose() chartRef.value = createChart(elRef.value!, next) bindOption(chartRef.value) } })窗口拖拽过程中缩放比会连续变化,所以要加防抖,一般 200 毫秒足够。重建实例有成本,但窗口尺寸变化本身就不是高频事件,可以接受。
第三种是直接改内部 painter 的 dpr,属于非公开 API,我不建议在正式项目里用,升级一次版本可能就废了。这里提一句只是让你在别人的代码里看到时不至于懵。
4.3 视频流、WebGL 和地图的额外处理
视频流在大屏里极其常见,无论是 HLS 切片播放还是 WebRTC 低延迟推流,最后落到 DOM 上都是一个<video>元素。它跟 canvas 不太一样:现代浏览器对视频纹理的缩放处理更聪明,放大后看着是"柔"而不是"糊"。真正的问题往往出在源头分辨率上——你拿一路 720p 的流,放到 4K 拼接墙上放大六倍,怎么补偿都没用。我的处理顺序是:先确认能不能拿到更高分辨率的源,源没问题再考虑适配层的优化。
WebGL 场景就更直接了,Three.js 和 Cesium 这类库必须显式告诉渲染器像素比:
const renderer = new THREE.WebGLRenderer({ antialias: true }) renderer.setPixelRatio(Math.min(window.devicePixelRatio * scale, 2)) renderer.setSize(width, height, false)具体倍数按现场机器显卡能力定,大屏主机普遍用的是核显或者入门独显,像素比给高了帧率掉到 20 以下,看着比模糊更难受。我的经验值是 2 封顶。
顺便说一句 1px 边框。设计稿里 1px 的描边,在 2 倍缩放下变成 2 个物理像素,在拼接墙上再被硬件放大 3 倍,就是 6 个像素宽,粗得刺眼。我现在的做法是:给大屏做的设计稿,边框统一 2px 起,分割线用背景色块或者渐变模拟,别用发丝线。
5. 缩放容器里的定位陷阱:fixed 弹层、鼠标坐标与表格滚动
5.1 一个反直觉的规范:transform 会改变 fixed 的包含块
这个坑我认为是 transform scale 方案里最隐蔽的一个。按 CSS 规范,只要元素的transform不是none,它就会成为其后代position: fixed元素的包含块。也就是说,缩放舞台内部写position: fixed的遮罩层,不再相对视口定位,而是相对这块 1920×1080 的舞台定位。
表现是什么?你在缩放容器里手写了一个全屏加载遮罩,position: fixed; inset: 0,本地开发看着好好的,因为本地视口刚好接近设计稿尺寸,偏差看不出来。一上大屏,遮罩只盖住了舞台范围内的一部分,边缘露出一圈没遮住的内容。
知道原理之后处理起来很简单:在缩放容器内部,一律不用 fixed,需要铺满就用position: absolute; inset: 0。覆盖范围跟着舞台走,缩放之后视觉上就是铺满的。
5.2 坐标换算:从 clientX 到设计稿坐标
第二个高频问题是鼠标坐标。只要你做的是自定义绘制、热点区域、跟随鼠标的提示框,就一定会遇到。event.clientX给的是视口坐标,而你的业务逻辑是按设计稿坐标系写的,中间差着一个偏移和一个缩放比。
function toStageCoord(e: MouseEvent) { const wrapper = document.querySelector('.screen-wrapper') as HTMLElement const rect = wrapper.getBoundingClientRect() const x = (e.clientX - rect.left - offsetX.value) / scale.value const y = (e.clientY - rect.top - offsetY.value) / scale.value return { x, y } }这里的offsetX和offsetY就是useScreenScale里算出来的那两个值。如果舞台是"宽度延展"模式,偏移为 0,公式退化成只除以缩放比。
还有个更隐蔽的坑:getBoundingClientRect()返回的是变换之后的矩形。你写el.getBoundingClientRect().width,在缩放 2 倍的屏幕上拿到的是 1200,而不是元素本来的 600。要拿设计稿尺寸,用el.offsetWidth或el.clientWidth。我在一个拖拽排序需求里被这个坑了大半天,手柄一直吸不准位置,最后发现是尺寸拿错了坐标系,除以缩放比之后就正常了。
5.3 组件库浮层的实际表现与我的处理偏好
拿 Element Plus 举例。它的下拉、气泡、弹窗默认会传送到body下,因为这些浮层挂在缩放容器外面,所以它们不受缩放影响——位置计算是对的,但尺寸是 1:1 的。在 2 倍缩放的界面上,一个 14px 字体、32px 高的下拉框显得特别小,和周围被放大过的界面明显不在一个层里,视觉上很割裂。
我的处理偏好是:把浮层留在缩放容器内部。Element Plus 的下拉支持:teleported="false",对话框支持:append-to-body="false",关掉传送之后,浮层就跟着容器一起缩放,尺寸和位置同时对齐。代价是要注意两件事——一是容器的overflow: hidden不能把浮层裁掉(大屏本身不滚动,一般没影响);二是浮层的层级要在容器内部足够高,别被别的卡片盖住。
表格也是重灾区。el-table的自动列宽计算基于clientWidth,缩放不影响它,所以列宽算得是对的,这点可以放心。真正别扭的是滚动条——滚动条宽度会被一起缩放,2 倍缩放下原本 8px 的滚动条变成 16px 视觉宽度,看着很粗;反过来缩小时可能细到不好拖。大屏一般是鼠标操作或者遥控操作,滚动条本身用得少,我通常直接美化掉,用隐藏滚动条 + 自定义指示器替代。
6. 非 16:9 屏幕的三种妥协策略与实现
6.1 居中留白:最安全也最保守
设计稿 16:9,屏幕 21:9,等比缩放必然在左右各留一条黑边,这是contain模式。它的好处是零风险,任何比例都能正确显示,内容永远不变形。指挥中心、演示汇报这类场景我首选它。
要减轻黑边的观感,可以做两件事。一是底色对齐:把外层 wrapper 的背景色设成跟设计稿边缘一样的深色,黑边和内容边界就模糊掉了。二是背景往外延:设计稿边缘那圈装饰性的光效、网格、粒子,不要用普通 DOM 画,改用一张background-size: cover的背景图层铺满整个视口,内容层再等比缩放叠在上面。视觉上几乎看不出有留白。
6.2 高度铺满 + 宽度延展:内容型大屏的最优解
如果业务能接受"两侧多显示点内容",那第 3.3 节讲的宽度延展模式是最舒服的。核心就是把舞台宽度设成vw / scale,然后让布局容器的弹性生效。
具体到三栏布局:
.stage-layout { display: grid; grid-template-columns: 1fr 1200px 1fr; // 中间地图锁死,两侧弹性 height: 1080px; gap: 24px; }中间地图锁 1200px,保证比例不变;两侧用1fr,屏幕越宽列表越长。16:9 的屏幕上侧栏各 336px,21:9 的屏幕上能到 600 多 px,正好多放两行数据。这套方案实际用下来,客户满意度明显高于留黑边。
要注意的是,如果侧栏里是图表,图表宽度变了之后必须重新 resize,而且因为舞台宽度是按设计稿坐标系算的,图表的容器宽度变化是真实的布局变化,ECharts 的resize()这次是有效的——这也是它跟第 4 章场景的区别:同样的图表组件,在整体缩放模式下要靠 dpr 补偿,在宽度延展模式下靠 resize 就能解决。两套逻辑别搞混。
6.3 内容等比 + 背景铺满:展会场景的常用组合
展会屏幕不允许出现任何黑边,但内容也不能变形,剩下的办法就是把背景层和内容层拆开。背景层固定铺满视口,内容层按contain缩放居中。这样即使两侧有区域没放内容,观众看到的也是完整的背景,不会觉得"这屏没做完"。
内容层如果实在偏窄,还可以做一档轻微的补偿:把缩放比在contain和cover之间插值。比如scale = minRatio + (maxRatio - minRatio) * 0.2,允许内容被拉伸 20%,肉眼几乎看不出来,但能吃掉一大截留白。这个比例我是凭经验给的,超过 0.3 圆形的指示灯会明显变椭圆,慎用。
7. 上线前的检查清单:打包、定时器与长跑稳定性
7.1 本地正常、打包后布局异常的三个常见原因
这个现象太常见了,我整理了三个高频原因,按排查优先级排:
第一,CSS 压缩工具吃掉了 calc 里的空格。早期版本的压缩器会把calc(100% - 16px)压成calc(100%-16px),减号两侧没有空格,整个表达式直接失效,元素宽度变成 auto,布局全乱。这类问题在开发环境不会出现,因为 dev 阶段不做压缩。排查方法很简单:打开生产环境的 CSS,搜一下calc(,看两边有没有空格。
第二,静态资源路径不对。打包后部署到子目录,背景图 404 了,看起来就像"布局错乱",其实只是背景丢了。检查构建配置里的资源基础路径,以及 CSS 里引用的图片是否走了正确的解析规则。
第三,插件配置在生产和开发之间不一致。比如 px 转 rem/vw 的插件只在开发配置里挂了,生产构建漏了,本地一切正常,发布后所有尺寸按原始 px 渲染。这个最容易排查,搜一下打包产物里有没有出现 vw 单位就知道了。
7.2 7×24 长跑的稳定性问题
大屏项目跟普通后台最大的不同是:它要连续跑几个月不关。这类问题里,内存泄漏排第一。
最常见的泄漏点就是图表实例。每分钟刷新数据、重建一次 ECharts 实例,如果没调dispose(),一天的累积足够把内存吃到几个 G。我的做法是给每个图表封一个统一的生命周期:onBeforeUnmount里dispose,切换数据源时先clear()再setOption,尽量不重建实例。同理,setInterval和addEventListener都必须在卸载时清掉。
第二个问题是浏览器后台节流。用户切走标签页或者屏幕上弹了个系统窗口,浏览器会把定时器大幅降频,requestAnimationFrame干脆完全暂停。切回来的时候,如果你的轮播逻辑是靠"每次 tick 加一"实现的,就会出现一段诡异的静止和一次突兀的跳跃。我的处理方式是基于时间戳算进度,而不是靠次数累加,并且在visibilitychange里主动暂停和恢复动画。
document.addEventListener('visibilitychange', () => { if (document.hidden) { pauseAll() } else { resumeFromTimestamp(Date.now()) } })第三是Windows 显示缩放带来的跨机器差异。部署脚本里我没法控制这个,所以我会在页面启动时把innerWidth、devicePixelRatio这些值打到控制台,工作人员截个图发过来,我就能判断现场环境是否异常。
7.3 上线前的巡检清单
| 检查项 | 判断标准 | 常见后果 |
|---|---|---|
| 目标设备视口尺寸 | innerWidth/innerHeight 是否与预期一致 | 整体缩放比算错 |
| 显示缩放设置 | 是否为 100%,或已纳入计算 | 跨机器表现不一致 |
| 图表位图补偿 | 全屏后文字是否锐利 | 图表发虚、折线毛边 |
| 浮层层级与定位 | 下拉、弹窗是否跟随缩放 | 尺寸割裂或位置偏移 |
| 1px 线条 | 视觉宽度是否可接受 | 过粗或消失 |
| 定时器清理 | 路由切换后是否持续增长 | 内存持续上涨 |
| 后台恢复 | 切走再切回是否正常 | 动画错乱 |
| 生产构建产物 | calc 空格、vw 单位、资源路径 | 布局突然错乱 |
这张表我基本每次交付前都会过一遍,其中前三项每次必查。
最后分享一个我现在的固定动作:在项目根目录放一份screen-info.md,写下目标屏幕的物理分辨率、逻辑视口、设备像素比、显示缩放设置、适配模式和缩放比计算公式,再附上一张现场实拍图。这玩意儿看着不起眼,但半年后要加一块新屏、或者换个人接手的时候,能省掉整整一天的现场排查。我上一个项目就是因为没这东西,为了搞清楚"为什么客户说字变小了"来回跑了三趟。