数据大屏怎样适配不同分辨率?
大屏通常按固定设计稿(如 1920×1080)开发,再用 transform scale 整体缩放到窗口大小。按宽高中较小的比例等比缩放会在一边留白,分别按宽高缩放会拉伸变形。rem/vw 方案让布局随宽度变化,但宽高比不同时高度难以对齐。窗口或容器尺寸变化时用 resize 或 ResizeObserver 重新计算比例,用 rem、vw 等让布局尺寸真正变化的方案时,图表也要跟着重绘。
简答
数据大屏一般有一张 1920×1080 的设计稿,可实际要在 4K 电视、带鱼屏、笔记本上展示。最常用的做法是按设计稿的固定像素开发一个 1920×1080 的画布,再用
transform: scale()把整个画布缩放到窗口大小,里面的字号、图表、间距一起等比变化。比例怎么取,是留白和拉伸的取舍。用宽和高两个比例里较小的那个等比缩放,画布能完整放进窗口、不变形,但宽高比不一致时左右或上下会留出空白,一般把画布居中。分别按宽、高缩放可以铺满屏幕,但图形和文字会被压扁或拉长。
另一种思路是 rem 或 vw:把尺寸写成相对视口宽度的单位,布局随宽度连续变化,元素按真实尺寸排版。可是它只跟宽度挂钩,屏幕比例和设计稿不一样时,高度方向容易放不下或空出一截,所以大屏用整体缩放更多。
要注意,窗口或容器尺寸变化时要重新计算比例,可以监听
resize,或者用ResizeObserver观察容器。用 rem、vw 这类方案时,容器尺寸真的变了,图表还要按新尺寸重绘。另外 transform 不改变元素在布局里占的位置,鼠标坐标和弹层定位要按缩放后的坐标换算。
算出缩放比例和居中偏移
下面的函数根据窗口大小和设计稿尺寸,算出等比缩放的比例,以及让画布居中需要的偏移。它是纯计算,在 Node 里也能运行。
function getScreenScale(viewportWidth, viewportHeight, designWidth = 1920, designHeight = 1080) {
const scaleX = viewportWidth / designWidth;
const scaleY = viewportHeight / designHeight;
// 取较小的比例,保证画布完整放进窗口,多出来的一边留白。
const scale = Math.min(scaleX, scaleY);
return {
scale,
offsetX: (viewportWidth - designWidth * scale) / 2,
offsetY: (viewportHeight - designHeight * scale) / 2,
};
}
console.log(getScreenScale(3840, 2160)); // { scale: 2, offsetX: 0, offsetY: 0 }
console.log(getScreenScale(1920, 1200)); // { scale: 1, offsetX: 0, offsetY: 60 }
console.log(getScreenScale(2560, 1080)); // { scale: 1, offsetX: 320, offsetY: 0 }
4K 屏和设计稿都是 16:9,放大 2 倍正好铺满。1920×1200 是 16:10,更高一些,按宽度算是 1、按高度算是 1.11,取较小的 1,上下各空 60 像素。2560×1080 是带鱼屏,更宽,按高度算是 1,左右各空 320 像素。
为什么取较小的比例?因为取较大的那个,画布的另一边就会超出窗口被裁掉。取较小的,内容完整可见,代价是留白。
在页面里应用缩放
画布写成固定尺寸,用 transform-origin: 0 0 让缩放从左上角开始,再用偏移把它挪到中间:
const screen = document.querySelector('.screen'); // width: 1920px; height: 1080px
function fit() {
const { scale, offsetX, offsetY } = getScreenScale(window.innerWidth, window.innerHeight);
screen.style.transformOrigin = '0 0';
screen.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
}
fit();
window.addEventListener('resize', fit);
为什么要改 transform-origin?因为它默认在元素中心,从中心缩放时,画布左上角的位置会跟着变,偏移就不好算。从左上角缩放,缩放后的左上角仍在原处,再平移 offsetX、offsetY 就居中了。
transform 有一个特点要知道。MDN 写明,CSS transform 改变内容的形状和位置,但不打乱正常的文档流,元素在布局里仍然占着原来 1920×1080 的位置。所以外层容器最好设置 overflow: hidden,避免出现滚动条。另外 transform 不为 none 的元素会成为 position: fixed 后代的包含块,画布里的 fixed 弹层会相对画布定位、跟着一起缩放,不再相对视口。
留白、拉伸,还是让布局自己伸缩
等比缩放加留白最常见,留白部分可以填成和大屏一致的背景色,看起来不突兀。如果甲方要求必须铺满,可以分别用 scale(scaleX, scaleY) 拉伸,适合以色块、数字为主、对变形不敏感的页面,地图和圆形图表会明显变形。
还有一种折中:画布按较小比例等比缩放,但把画布的宽或高按窗口比例放大,让里面的 Flex 或 Grid 布局自己把多出来的空间分给各个模块。这样不变形也不留白,代价是各模块的位置要按弹性布局重新设计,不能完全照着设计稿。
rem 和 vw 方案的原理见《移动端怎样做屏幕适配?1px 边框为什么看起来变粗了?》。MDN 定义 1vw 是视口宽度的 1%,rem 是根元素的字号,所以把根字号设成和视口宽度成比例,所有 rem 尺寸就会跟着宽度变。它的问题是高度不受控:在 16:10 或者带鱼屏上,按宽度算出的高度和窗口实际高度对不上。
为什么要用 ResizeObserver
大屏不一定占满整个窗口,比如嵌在后台系统的一个区域里,侧边栏收起时容器变宽,窗口却没变,resize 事件不会触发。ResizeObserver 观察的是元素自身的尺寸,容器变了就会回调:
const observer = new ResizeObserver((entries) => {
const { inlineSize, blockSize } = entries[0].contentBoxSize[0];
applyScale(getScreenScale(inlineSize, blockSize));
});
observer.observe(container);
要注意观察的是外层容器,修改的是里面画布的 transform。如果在回调里改被观察元素自己的尺寸,又会触发回调,MDN 说浏览器这时会把通知推迟到下一帧,并在 window 上报出 “ResizeObserver loop completed with undelivered notifications” 错误。页面销毁时要调用 disconnect()。
常见追问
图表库在缩放的画布里要注意什么?
图表库按容器的布局尺寸绘制,transform 不改变布局尺寸,所以画布整体缩放时图表不需要知道比例。可是用 rem、vw 或弹性布局时,容器的布局尺寸真的变了,就要在尺寸变化后调用图表实例的 resize 方法,让它按新尺寸重绘。
缩放后鼠标点击位置为什么和图表里的坐标对不上?
因为事件给出的是视口坐标,而画布内部还是按 1920×1080 布局的。要先减去画布的偏移,再除以缩放比例,换算回设计稿坐标。
窗口拖动时频繁 resize,会不会卡?
整体缩放时每次只改一个 transform,开销很小。用弹性布局、图表要跟着重绘时,重绘才是耗时的部分,可以把它放进防抖里,等用户停止拖动再执行。