在数据大屏、仪表盘等场景中,全屏自适应缩放的 UI 展示越来越常见。传统的 CSS 实现方式虽然简单,但在复杂布局下可能会出现比例失衡、缩放模糊等问题。本文将分享一个使用 React + TypeScript 编写的 ScaleScreen
组件,帮助你快速实现高质量的自动缩放布局。
✨功能概述
ScaleScreen
是一个高可配置、支持全屏和按比例缩放的容器组件,具备以下特性:
- ✅ 支持宽高设置与全屏缩放
- ✅ 可配置自动缩放方向(x/y)
- ✅ 使用
MutationObserver
实时监听 DOM 样式变化 - ✅ 提供缩放防抖处理,提升性能
- ✅ 自动控制
body
的滚动隐藏 - ✅ 使用
useLayoutEffect
实现精确渲染
📦组件属性说明
interface ScaleScreenProps {
width?: number | string; // 目标宽度,默认1920
height?: number | string; // 目标高度,默认1080
fullScreen?: boolean; // 是否铺满全屏
autoScale?: boolean | { x: boolean; y: boolean }; // 缩放方向控制
delay?: number; // 缩放防抖延迟
boxStyle?: React.CSSProperties; // 最外层容器样式
wrapperStyle?: React.CSSProperties;// 包裹内容的容器样式
bodyOverflowHidden?: boolean; // 是否禁用页面滚动
children?: React.ReactNode; // 子组件内容
}
🧠核心实现思路
1️⃣ 自动缩放计算
根据 body.clientWidth
和 body.clientHeight
与目标尺寸 width/height
计算缩放比例:
const widthScale = currentWidth / realWidth;
const heightScale = currentHeight / realHeight;
const scale = Math.min(widthScale, heightScale);
再通过 transform: scale(x, y)
实现缩放:
wrapperRef.current.style.transform = `scale(${scale}, ${scale})`;
并根据是否启用 autoScale.x/y
决定是否设置 margin
来进行居中偏移。
2️⃣ 响应式 resize 监听 + MutationObserver 监听样式变化
useEffect(() => {
const handleResize = debounce(() => {
initSize();
updateSize();
updateScale();
}, delay);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [delay]);
此外通过 MutationObserver
监听 style 变化,避免用户手动修改 DOM 后造成错位:
const observer = new MutationObserver(() => {
if (resizeHandlerRef.current) {
resizeHandlerRef.current();
}
});
3️⃣ useLayoutEffect 控制尺寸和缩放
在 dimensions 更新后,使用 useLayoutEffect
立刻更新尺寸和缩放,保证子组件渲染前布局已生效,避免闪动:
useLayoutEffect(() => {
updateSize();
updateScale();
}, [dimensions, fullScreen, autoScale]);
🎨实际渲染结构
最终结构是一个 .scale-screen-box
的全屏容器包裹 .screen-wrapper
的内容区:
<div className="scale-screen-box" style={styles.box}>
<div className="screen-wrapper" style={styles.wrapper} ref={wrapperRef}>
{children}
</div>
</div>
🧩使用方式
<ScaleScreen width={1920} height={1080} fullScreen={false} autoScale={{ x: true, y: false }}>
<MyBigScreenDashboard />
</ScaleScreen>
你可以自由组合 fullScreen
和 autoScale
实现不同的自适应效果。
完整代码
import React, { useEffect, useRef, useState, useLayoutEffect } from 'react';
/**
* 自动缩放配置类型
*/
type AutoScaleType =
| boolean
| {
x: boolean;
y: boolean;
};
/**
* ScaleScreen组件属性接口
*/
interface ScaleScreenProps {
width?: number | string; // 宽度
height?: number | string; // 高度
fullScreen?: boolean; // 是否全屏
autoScale?: AutoScaleType; // 是否自动缩放
delay?: number; // 缩放延迟
boxStyle?: React.CSSProperties; // 容器样式
wrapperStyle?: React.CSSProperties; // 外层样式
bodyOverflowHidden?: boolean; // 是否隐藏body的overflow
children?: React.ReactNode; // 子组件
}
/**
* 尺寸状态接口
*/
interface DimensionsState {
width: number; // 宽度
height: number; // 高度
originalWidth: number; // 原始宽度
originalHeight: number; // 原始高度
}
/**
* 防抖函数
* @param fn - 需要防抖的函数
* @param delay - 延迟时间(毫秒)
* @returns 防抖后的函数
*/
function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null;
return function (this: any, ...args: Parameters<T>): void {
if (timer) clearTimeout(timer);
timer = setTimeout(
() => {
typeof fn === 'function' && fn.apply(this, args);
timer = null;
},
delay > 0 ? delay : 100
);
};
}
/**
* 可缩放屏幕组件
* @param width - 宽度
* @param height - 高度
* @param fullScreen - 是否全屏
* @param autoScale - 是否自动缩放
* @param delay - 缩放延迟
* @param boxStyle - 容器样式
* @param wrapperStyle - 外层样式
* @param bodyOverflowHidden - 是否隐藏body的overflow
* @param children - 子组件
*/
const ScaleScreen: React.FC<ScaleScreenProps> = ({
width = 1920,
height = 1080,
fullScreen = false,
autoScale = true,
delay = 500,
boxStyle = {},
wrapperStyle = {},
bodyOverflowHidden = true,
children
}) => {
const wrapperRef = useRef<HTMLDivElement | null>(null);
const observerRef = useRef<MutationObserver | null>(null);
const bodyOverflowRef = useRef<string>('');
const resizeHandlerRef = useRef<(() => void) | null>(null);
const [dimensions, setDimensions] = useState<DimensionsState>({
width: typeof width === 'number' ? width : parseInt(width, 10),
height: typeof height === 'number' ? height : parseInt(height, 10),
originalWidth: 0,
originalHeight: 0
});
const styles: {
box: React.CSSProperties;
wrapper: React.CSSProperties;
} = {
box: {
overflow: 'hidden',
backgroundSize: '100% 100%',
backgroundColor: '#000',
width: '100vw',
height: '100vh',
...boxStyle
},
wrapper: {
transitionProperty: 'all',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '500ms',
position: 'relative',
overflow: 'hidden',
zIndex: 100,
transformOrigin: 'left top',
...wrapperStyle
}
};
/**
* 初始化大屏容器宽高
*/
const initSize = (): void => {
if (!wrapperRef.current) return;
// 获取画布尺寸
const originalWidth = window.screen.width;
const originalHeight = window.screen.height;
setDimensions(prev => ({
...prev,
originalWidth,
originalHeight
}));
};
/**
* 更新大屏容器宽高
*/
const updateSize = (): void => {
if (!wrapperRef.current) return;
wrapperRef.current.style.width = `${dimensions.width}px`;
wrapperRef.current.style.height = `${dimensions.height}px`;
};
/**
* 处理自动缩放
* @param scale - 缩放比例
*/
const handleAutoScale = (scale: number): void => {
if (!autoScale || !wrapperRef.current) return;
const domWidth = wrapperRef.current.clientWidth;
const domHeight = wrapperRef.current.clientHeight;
const currentWidth = document.body.clientWidth;
const currentHeight = document.body.clientHeight;
wrapperRef.current.style.transform = `scale(${scale},${scale})`;
let mx = Math.max((currentWidth - domWidth * scale) / 2, 0);
let my = Math.max((currentHeight - domHeight * scale) / 2, 0);
if (typeof autoScale === 'object') {
!autoScale.x && (mx = 0);
!autoScale.y && (my = 0);
}
wrapperRef.current.style.margin = `${my}px ${mx}px`;
};
/**
* 更新缩放比例
*/
const updateScale = (): void => {
if (!wrapperRef.current) return;
// 获取真实视口尺寸
const currentWidth = document.body.clientWidth;
const currentHeight = document.body.clientHeight;
// 获取大屏最终的宽高
const realWidth = dimensions.width || dimensions.originalWidth;
const realHeight = dimensions.height || dimensions.originalHeight;
if (!realWidth || !realHeight) return;
// 计算缩放比例
const widthScale = currentWidth / +realWidth;
const heightScale = currentHeight / +realHeight;
// 若要铺满全屏,则按照各自比例缩放
if (fullScreen) {
wrapperRef.current.style.transform = `scale(${widthScale},${heightScale})`;
return;
}
// 按照宽高最小比例进行缩放
const scale = Math.min(widthScale, heightScale);
handleAutoScale(scale);
};
/**
* 初始化body样式
*/
const initBodyStyle = (): void => {
if (bodyOverflowHidden) {
bodyOverflowRef.current = document.body.style.overflow;
document.body.style.overflow = 'hidden';
}
};
/**
* 重置body样式
*/
const resetBodyStyle = (): void => {
if (bodyOverflowHidden) {
document.body.style.overflow = bodyOverflowRef.current;
}
};
/**
* 初始化MutationObserver
* @returns MutationObserver实例
*/
const initMutationObserver = (): MutationObserver | undefined => {
if (!wrapperRef.current) return;
if (observerRef.current) {
observerRef.current.disconnect();
}
const observer = new MutationObserver(() => {
if (resizeHandlerRef.current) {
resizeHandlerRef.current();
}
});
observer.observe(wrapperRef.current, {
attributes: true,
attributeFilter: ['style'],
attributeOldValue: true
});
observerRef.current = observer;
return observer;
};
// 初始化resize事件处理函数
useEffect(() => {
// 创建防抖的resize处理函数
const handleResize = debounce(() => {
initSize();
updateSize();
updateScale();
}, delay);
// 保存引用以便MutationObserver使用
resizeHandlerRef.current = handleResize;
// 添加resize事件监听
window.addEventListener('resize', handleResize);
// 组件卸载时清理
return () => {
window.removeEventListener('resize', handleResize);
};
}, [delay]);
// 处理props变化
useEffect(() => {
setDimensions(prev => ({
...prev,
width: typeof width === 'number' ? width : parseInt(width, 10),
height: typeof height === 'number' ? height : parseInt(height, 10)
}));
}, [width, height]);
// 组件挂载和卸载
useEffect(() => {
// 初始化body样式
initBodyStyle();
// 初始化尺寸
initSize();
// 初始化MutationObserver
const observer = initMutationObserver();
// 组件卸载时清理
return () => {
observer?.disconnect();
resetBodyStyle();
};
}, []);
// 当dimensions更新时更新尺寸和缩放
useLayoutEffect(() => {
updateSize();
updateScale();
}, [dimensions, fullScreen, autoScale]);
return (
<div className="scale-screen-box" style={styles.box}>
<div className="screen-wrapper" style={styles.wrapper} ref={wrapperRef}>
{children}
</div>
</div>
);
};
export default ScaleScreen;
import React, { useEffect, useRef, useState, useLayoutEffect } from 'react';
/**
* 自动缩放配置类型
*/
type AutoScaleType =
| boolean
| {
x: boolean;
y: boolean;
};
/**
* ScaleScreen组件属性接口
*/
interface ScaleScreenProps {
width?: number | string; // 宽度
height?: number | string; // 高度
fullScreen?: boolean; // 是否全屏
autoScale?: AutoScaleType; // 是否自动缩放
delay?: number; // 缩放延迟
boxStyle?: React.CSSProperties; // 容器样式
wrapperStyle?: React.CSSProperties; // 外层样式
bodyOverflowHidden?: boolean; // 是否隐藏body的overflow
children?: React.ReactNode; // 子组件
}
/**
* 尺寸状态接口
*/
interface DimensionsState {
width: number; // 宽度
height: number; // 高度
originalWidth: number; // 原始宽度
originalHeight: number; // 原始高度
}
/**
* 防抖函数
* @param fn - 需要防抖的函数
* @param delay - 延迟时间(毫秒)
* @returns 防抖后的函数
*/
function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null;
return function (this: any, ...args: Parameters<T>): void {
if (timer) clearTimeout(timer);
timer = setTimeout(
() => {
typeof fn === 'function' && fn.apply(this, args);
timer = null;
},
delay > 0 ? delay : 100
);
};
}
/**
* 可缩放屏幕组件
* @param width - 宽度
* @param height - 高度
* @param fullScreen - 是否全屏
* @param autoScale - 是否自动缩放
* @param delay - 缩放延迟
* @param boxStyle - 容器样式
* @param wrapperStyle - 外层样式
* @param bodyOverflowHidden - 是否隐藏body的overflow
* @param children - 子组件
*/
const ScaleScreen: React.FC<ScaleScreenProps> = ({
width = 1920,
height = 1080,
fullScreen = false,
autoScale = true,
delay = 500,
boxStyle = {},
wrapperStyle = {},
bodyOverflowHidden = true,
children
}) => {
const wrapperRef = useRef<HTMLDivElement | null>(null);
const observerRef = useRef<MutationObserver | null>(null);
const bodyOverflowRef = useRef<string>('');
const resizeHandlerRef = useRef<(() => void) | null>(null);
const [dimensions, setDimensions] = useState<DimensionsState>({
width: typeof width === 'number' ? width : parseInt(width, 10),
height: typeof height === 'number' ? height : parseInt(height, 10),
originalWidth: 0,
originalHeight: 0
});
const styles: {
box: React.CSSProperties;
wrapper: React.CSSProperties;
} = {
box: {
overflow: 'hidden',
backgroundSize: '100% 100%',
backgroundColor: '#000',
width: '100vw',
height: '100vh',
...boxStyle
},
wrapper: {
transitionProperty: 'all',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '500ms',
position: 'relative',
overflow: 'hidden',
zIndex: 100,
transformOrigin: 'left top',
...wrapperStyle
}
};
/**
* 初始化大屏容器宽高
*/
const initSize = (): void => {
if (!wrapperRef.current) return;
// 获取画布尺寸
const originalWidth = window.screen.width;
const originalHeight = window.screen.height;
setDimensions(prev => ({
...prev,
originalWidth,
originalHeight
}));
};
/**
* 更新大屏容器宽高
*/
const updateSize = (): void => {
if (!wrapperRef.current) return;
wrapperRef.current.style.width = `${dimensions.width}px`;
wrapperRef.current.style.height = `${dimensions.height}px`;
};
/**
* 处理自动缩放
* @param scale - 缩放比例
*/
const handleAutoScale = (scale: number): void => {
if (!autoScale || !wrapperRef.current) return;
const domWidth = wrapperRef.current.clientWidth;
const domHeight = wrapperRef.current.clientHeight;
const currentWidth = document.body.clientWidth;
const currentHeight = document.body.clientHeight;
wrapperRef.current.style.transform = `scale(${scale},${scale})`;
let mx = Math.max((currentWidth - domWidth * scale) / 2, 0);
let my = Math.max((currentHeight - domHeight * scale) / 2, 0);
if (typeof autoScale === 'object') {
!autoScale.x && (mx = 0);
!autoScale.y && (my = 0);
}
wrapperRef.current.style.margin = `${my}px ${mx}px`;
};
/**
* 更新缩放比例
*/
const updateScale = (): void => {
if (!wrapperRef.current) return;
// 获取真实视口尺寸
const currentWidth = document.body.clientWidth;
const currentHeight = document.body.clientHeight;
// 获取大屏最终的宽高
const realWidth = dimensions.width || dimensions.originalWidth;
const realHeight = dimensions.height || dimensions.originalHeight;
if (!realWidth || !realHeight) return;
// 计算缩放比例
const widthScale = currentWidth / +realWidth;
const heightScale = currentHeight / +realHeight;
// 若要铺满全屏,则按照各自比例缩放
if (fullScreen) {
wrapperRef.current.style.transform = `scale(${widthScale},${heightScale})`;
return;
}
// 按照宽高最小比例进行缩放
const scale = Math.min(widthScale, heightScale);
handleAutoScale(scale);
};
/**
* 初始化body样式
*/
const initBodyStyle = (): void => {
if (bodyOverflowHidden) {
bodyOverflowRef.current = document.body.style.overflow;
document.body.style.overflow = 'hidden';
}
};
/**
* 重置body样式
*/
const resetBodyStyle = (): void => {
if (bodyOverflowHidden) {
document.body.style.overflow = bodyOverflowRef.current;
}
};
/**
* 初始化MutationObserver
* @returns MutationObserver实例
*/
const initMutationObserver = (): MutationObserver | undefined => {
if (!wrapperRef.current) return;
if (observerRef.current) {
observerRef.current.disconnect();
}
const observer = new MutationObserver(() => {
if (resizeHandlerRef.current) {
resizeHandlerRef.current();
}
});
observer.observe(wrapperRef.current, {
attributes: true,
attributeFilter: ['style'],
attributeOldValue: true
});
observerRef.current = observer;
return observer;
};
// 初始化resize事件处理函数
useEffect(() => {
// 创建防抖的resize处理函数
const handleResize = debounce(() => {
initSize();
updateSize();
updateScale();
}, delay);
// 保存引用以便MutationObserver使用
resizeHandlerRef.current = handleResize;
// 添加resize事件监听
window.addEventListener('resize', handleResize);
// 组件卸载时清理
return () => {
window.removeEventListener('resize', handleResize);
};
}, [delay]);
// 处理props变化
useEffect(() => {
setDimensions(prev => ({
...prev,
width: typeof width === 'number' ? width : parseInt(width, 10),
height: typeof height === 'number' ? height : parseInt(height, 10)
}));
}, [width, height]);
// 组件挂载和卸载
useEffect(() => {
// 初始化body样式
initBodyStyle();
// 初始化尺寸
initSize();
// 初始化MutationObserver
const observer = initMutationObserver();
// 组件卸载时清理
return () => {
observer?.disconnect();
resetBodyStyle();
};
}, []);
// 当dimensions更新时更新尺寸和缩放
useLayoutEffect(() => {
updateSize();
updateScale();
}, [dimensions, fullScreen, autoScale]);
return (
<div className="scale-screen-box" style={styles.box}>
<div className="screen-wrapper" style={styles.wrapper} ref={wrapperRef}>
{children}
</div>
</div>
);
};
export default ScaleScreen;
JS版
import React, { useEffect, useRef, useState, useLayoutEffect } from 'react';
/**
* 防抖函数
* @param {Function} fn
* @param {number} delay
* @returns {() => void}
*/
function debounce(fn, delay) {
let timer;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(
() => {
typeof fn === 'function' && fn.apply(null, args);
clearTimeout(timer);
},
delay > 0 ? delay : 100
);
};
}
/**
* 可缩放屏幕组件
* @props {number} width - 宽度
* @props {number} height - 高度
* @props {boolean} fullScreen - 是否全屏
* @props {boolean} autoScale - 是否自动缩放
* @props {number} delay - 缩放延迟
* @props {object} boxStyle - 容器样式
* @props {object} wrapperStyle - 外层样式
* @props {boolean} bodyOverflowHidden - 是否隐藏body的overflow
* @props {children} - 子组件
*/
const ScaleScreen = ({
width = 1920,
height = 1080,
fullScreen = false,
autoScale = true,
delay = 500,
boxStyle = {},
wrapperStyle = {},
bodyOverflowHidden = true,
children
}) => {
const wrapperRef = useRef(null);
const observerRef = useRef(null);
const bodyOverflowRef = useRef('');
const resizeHandlerRef = useRef(null);
const [dimensions, setDimensions] = useState({
width: typeof width === 'number' ? width : parseInt(width, 10),
height: typeof height === 'number' ? height : parseInt(height, 10),
originalWidth: 0,
originalHeight: 0
});
const styles = {
box: {
overflow: 'hidden',
backgroundSize: '100% 100%',
backgroundColor: '#000',
width: '100vw',
height: '100vh',
...boxStyle
},
wrapper: {
transitionProperty: 'all',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '500ms',
position: 'relative',
overflow: 'hidden',
zIndex: 100,
transformOrigin: 'left top',
...wrapperStyle
}
};
/**
* 初始化大屏容器宽高
*/
const initSize = () => {
if (!wrapperRef.current) return;
// 获取画布尺寸
const originalWidth = window.screen.width;
const originalHeight = window.screen.height;
setDimensions(prev => ({
...prev,
originalWidth,
originalHeight
}));
};
/**
* 更新大屏容器宽高
*/
const updateSize = () => {
if (!wrapperRef.current) return;
wrapperRef.current.style.width = `${dimensions.width}px`;
wrapperRef.current.style.height = `${dimensions.height}px`;
};
/**
* 处理自动缩放
*/
const handleAutoScale = scale => {
if (!autoScale || !wrapperRef.current) return;
const domWidth = wrapperRef.current.clientWidth;
const domHeight = wrapperRef.current.clientHeight;
const currentWidth = document.body.clientWidth;
const currentHeight = document.body.clientHeight;
wrapperRef.current.style.transform = `scale(${scale},${scale})`;
let mx = Math.max((currentWidth - domWidth * scale) / 2, 0);
let my = Math.max((currentHeight - domHeight * scale) / 2, 0);
if (typeof autoScale === 'object') {
!autoScale.x && (mx = 0);
!autoScale.y && (my = 0);
}
wrapperRef.current.style.margin = `${my}px ${mx}px`;
};
/**
* 更新缩放比例
*/
const updateScale = () => {
if (!wrapperRef.current) return;
// 获取真实视口尺寸
const currentWidth = document.body.clientWidth;
const currentHeight = document.body.clientHeight;
// 获取大屏最终的宽高
const realWidth = dimensions.width || dimensions.originalWidth;
const realHeight = dimensions.height || dimensions.originalHeight;
if (!realWidth || !realHeight) return;
// 计算缩放比例
const widthScale = currentWidth / +realWidth;
const heightScale = currentHeight / +realHeight;
// 若要铺满全屏,则按照各自比例缩放
if (fullScreen) {
wrapperRef.current.style.transform = `scale(${widthScale},${heightScale})`;
return;
}
// 按照宽高最小比例进行缩放
const scale = Math.min(widthScale, heightScale);
handleAutoScale(scale);
};
// 初始化body样式
const initBodyStyle = () => {
if (bodyOverflowHidden) {
bodyOverflowRef.current = document.body.style.overflow;
document.body.style.overflow = 'hidden';
}
};
// 重置body样式
const resetBodyStyle = () => {
if (bodyOverflowHidden) {
document.body.style.overflow = bodyOverflowRef.current;
}
};
// 初始化MutationObserver
const initMutationObserver = () => {
if (!wrapperRef.current) return;
if (observerRef.current) {
observerRef.current.disconnect();
}
const observer = new MutationObserver(() => {
if (resizeHandlerRef.current) {
resizeHandlerRef.current();
}
});
observer.observe(wrapperRef.current, {
attributes: true,
attributeFilter: ['style'],
attributeOldValue: true
});
observerRef.current = observer;
return observer;
};
// 初始化resize事件处理函数
useEffect(() => {
// 创建防抖的resize处理函数
const handleResize = debounce(() => {
initSize();
updateSize();
updateScale();
}, delay);
// 保存引用以便MutationObserver使用
resizeHandlerRef.current = handleResize;
// 添加resize事件监听
window.addEventListener('resize', handleResize);
// 组件卸载时清理
return () => {
window.removeEventListener('resize', handleResize);
};
}, [delay]);
// 处理props变化
useEffect(() => {
setDimensions(prev => ({
...prev,
width: typeof width === 'number' ? width : parseInt(width, 10),
height: typeof height === 'number' ? height : parseInt(height, 10)
}));
}, [width, height]);
// 组件挂载和卸载
useEffect(() => {
// 初始化body样式
initBodyStyle();
// 初始化尺寸
initSize();
// 初始化MutationObserver
const observer = initMutationObserver();
// 组件卸载时清理
return () => {
observer?.disconnect();
resetBodyStyle();
};
}, []);
// 当dimensions更新时更新尺寸和缩放
useLayoutEffect(() => {
updateSize();
updateScale();
}, [dimensions, fullScreen, autoScale]);
return (
<div className="scale-screen-box" style={styles.box}>
<div className="screen-wrapper" style={styles.wrapper} ref={wrapperRef}>
{children}
</div>
</div>
);
};
export default ScaleScreen;