前端大屏原理系列:流畅的自适应预览

704 阅读4分钟

本文是《前端大屏原理系列》第五篇:流畅的自适应预览。

本系列所有技术点,均经过本人开源项目 react-big-screen 实际应用,欢迎 star (*´▽`)ノノ.

一、效果演示

2025-05-23_16-36-10.gif

二、原理简介

使用前端可拖拽大屏开发应用,一般期望所见即所得。所以需要固定页面的宽高比,在页面大小改变时同步缩放页面组件,以保持预览区域整体布局不变。我们可以设计这样一个自适应容器,专门负责处理预览区域的缩放和定位,用来支持在不同尺寸屏幕上显示。即:页面布局不变,撑满容器。总共有3种显示情景:

1比1缩放,直接计算宽度与设计宽度的比例即可(或高度与设计高度的比例)。 1比1缩放.png

内部容器两边先触底,计算宽度与设计宽度的比例。

内部容器两边先触底.png

内部容器上下先触底,计算高度与设计高度的比例。 内部容器上下先触底.png

上述可以做到布局保持不变并撑满容器。但想要实现 流畅过渡,需要尽量减小节流(throttle)间隔或完全取消,并且开启GPU加速。例如:transform: translate3d(...)will-change 等。

三、源码实现

首先封装 React-Hook useResizeDom,负责处理容器元素的resize监听、卸载,并执行callback回调函数。此处使用ResizeObserver来实现,ResizeObserver 很适合用来监视元素盒或边框盒或SVG元素边界尺寸的变化。

// 监听dom大小变化
import { RefObject, useEffect, useRef } from "react";

function useResizeDom(domRef: RefObject<HTMLElement>, callback: ResizeObserverCallback) {
  // 保存实时回调函数,避免闭包
  const callbackRef = useRef<ResizeObserverCallback>(callback);
  callbackRef.current = callback;
  
  useEffect(() => {
    if (!domRef.current) {
      return;
    }
    const resizeObserver = new ResizeObserver(
      (entries: ResizeObserverEntry[], observer: ResizeObserver) => {
        callbackRef?.current?.(entries, observer);
      },
    );
    resizeObserver.observe(domRef.current);
    return () => {
      resizeObserver.disconnect();
    };
  }, []);
}

如何实现自适应容器?我们可以通过比较内部元素宽高比容器宽高比来计算缩放比例。两者相等,1:1缩放,此时缩放比率为 内部元素宽度 / 设计宽度(或 内部元素高度 / 设计高度)。如果内部元素宽高比较大,则其宽度撑满容器,缩放比例为 内部元素宽度 / 设计宽度。反之,高度撑满容器。缩放比例为 内部元素高度 / 设计高度。(小技巧:哪边先撑满就用哪边计算)

// 大屏适配容器
import React, { useMemo, useRef } from "react";
import styles from "./index.module.less";
import classNames from "classnames";
import { useResizeDom } from "@/hooks";

export interface FitScreenProps {
  dw?: number; // 设计宽度 (例如:1920)
  dh?: number; // 设计高度 (例如:1080)
  className?: string;
  style?: React.CSSProperties;
  children?: React.ReactNode;
  bodyClassName?: string;
  bodyStyle?: React.CSSProperties;
}

export default function FitScreen(props: FitScreenProps) {
  const { dw = 1920, dh = 1080 } = props;
  const containerDomRef = useRef<HTMLDivElement>(null); // 容器dom
  const domRef = useRef<HTMLDivElement>(null); // 画布 dom
  const widthHeightRate = useMemo(() => dw / dh, [dw, dh]); // 画布宽高比

  useResizeDom(containerDomRef, () => {
    if (!containerDomRef.current || !domRef.current) {
      return;
    }
    const containerRect = containerDomRef.current.getBoundingClientRect();
    const containerWidthHeightRate = containerRect.width / containerRect.height;
    // 如果容器宽高比 > 画布宽高比,则画布高度占满容器。
    // 如果容器宽高比 < 画布宽高比,则画布宽度占满容器。
    if (containerWidthHeightRate > widthHeightRate) {
      // 高度对齐
      const scale = containerRect.height / dh;
      const width = scale * dw;
      // 子元素距离左侧距离
      const left = (containerRect.width - width) / 2;
      domRef.current.style.transform = `scale(${scale}) translate3d(${left / scale}px, 0, 0)`;
    } else {
      // 宽度对齐
      const scale = containerRect.width / dw;
      domRef.current.style.transform = `scale(${scale})`;
    }
    domRef.current.style.width = `${dw}px`;
    domRef.current.style.height = `${dh}px`;
  });

  return (
    <div className={props?.className} ref={containerDomRef} style={props?.style}>
      <div
        className={classNames(props?.bodyClassName, styles.fitScreenBody)}
        style={props?.bodyStyle}
        ref={domRef}
      >
        {props?.children}
      </div>
    </div>
  );
}
// index.module.less
.fitScreenBody {
  transform-origin: left top; // 转换点(左上角)
  position: relative;
  background: white;
}

使用示例:

export default function Page() {
  return (
    <FitScreen
      dw={1920}
      dh={1080}
      style={{
        top: 0,
        left: 0,
        width: '100%',
        height: '100%',
        position: 'fixed',
        background: 'black'
      }}
    >
      {/* 此处渲染预览组件 */}
      <div style={{ width: '100%', height: '100%', background: 'white' }}>
         Hello Screen!
      </div>
    </FitScreen>
  )
}

example.png

【前端大屏原理系列】

react-big-screen 是一个从0到1使用React开发的前端拖拽大屏开源项目。此系列将对大屏的关键技术点一一解析。包含了:拖拽系统实现、自定义组件、收藏夹、快捷键、可撤销历史记录、加载远程组件/本地组件、自适应预览页、布局容器组件、多组件联动(基于事件机制)、成组/取消成组、多子页面切换、i18n国际化语言、鼠标范围框选、... ... 等等。

演示地址:点击访问