react大屏适配方案(利用scale)

1,255 阅读1分钟

封装一个ScaleContainer缩放组件

// ScaleContainer
/** @description 缩放容器组件 */
import React, { useCallback, useEffect, useRef, useState } from 'react';
import styles from './index.less';
import { observer, unobserver } from './observerUtils';

export interface ScaleContainerProps {
  width: number;
  height: number;
  children: React.ReactElement;
  disabled?: boolean;
  maxScale?: number;
  minScale?: number;
}

const ScaleContainer: React.FC<ScaleContainerProps> = (props) => {
  const { children, disabled } = props;
  const wrapDomRef = useRef<HTMLDivElement>(null);
  // 计算缩放比 触发更新
  const [scale, setScale] = useState<number>(1);
  // 因为缩放
  const [translateArr, setTranslateArr] = useState([0, 0]);

  // 监听容器变化的回调 处理缩放
  const onInternalResize = useCallback((target: HTMLDivElement) => {
    const { width, height } = target.getBoundingClientRect();
    const fixedWidth = Math.floor(width);
    const fixedHeight = Math.floor(height);

    const w = fixedWidth / props.width;
    const h = fixedHeight / props.height;
    const s = Math.min(w, h);
    const leftNum = (fixedWidth - props.width * h) / 2;
    const topNum = (fixedHeight - props.height * w) / 2;
    setTranslateArr([leftNum <= 0 ? 0 : leftNum, topNum <= 0 ? 0 : topNum]);
    setScale(s);
  }, []);

  useEffect(() => {
    const dom = wrapDomRef.current;
    // 监听容器的缩放
    if (!disabled && dom) {
      // 开始监听
      observer(dom, onInternalResize);
    }
    return () => {
      // 取消监听
      unobserver(dom, onInternalResize);
    };
  }, [disabled, wrapDomRef.current]);

  return (
    <div className={styles.scaleContainer} ref={wrapDomRef}>
      <div
        style={{
          width: props.width + 'px',
          height: props.height + 'px',
          transform: `scale(${scale})`,
          transformOrigin: 'top left',
          left: translateArr[0],
          top: translateArr[1],
          position: 'absolute',
        }}
      >
        {children}
      </div>
    </div>
  );
};

export default ScaleContainer;

ScaleContainer的样式

// index.less
.scaleContainer {
position: relative;
width: 100%;
height: 100%;
}

核心逻辑

// observerUtils.ts
import ResizeObserver from 'resize-observer-polyfill';
export type ResizeListener = (element: HTMLDivElement) => void;

// 定义一个数据结构 用来装载缓存我们监听的函数 key为元素 value为对该元素的监听函数
const elementListeners = new Map<HTMLDivElement, Set<ResizeListener>>();

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ro = new ResizeObserver((entries, _observer) => {
  // entries 是一个数组,通过 ro.observe(element)注入
  // observer
  for (const entry of entries) {
    // entry 表示监听的dom
    const listeners = elementListeners.get(entry.target as HTMLDivElement);
    if (listeners && listeners.size) {
      // 其实我们还可以把entry中的 contentRect返回给回调函数
      listeners.forEach((listen) => listen(entry.target as HTMLDivElement));
    }
  }
});
export const observer = (element: HTMLDivElement | null, callback: ResizeListener) => {
  if (!element) {
    return;
  }
  // elementListeners 中没有记录则添加key
  if (!elementListeners.has(element)) {
    elementListeners.set(element, new Set());
    ro.observe(element);
  }
  // 对呀key中添加函数
  elementListeners.get(element)?.add(callback);
};

export const unobserver = (element: HTMLDivElement | null, callback: ResizeListener) => {
  if (!element) {
    return;
  }
  if (elementListeners.has(element)) {
    // 删除存储的 回调函数
    elementListeners.get(element)?.delete(callback);
    // 如果存储的 回调函数不存在了 移除监听 并且移除缓存
    if (!elementListeners.get(element)?.size) {
      ro.unobserve(element);
      elementListeners.delete(element);
    }
  }
};

具体的使用方式

  // 使用缩放组件
  // 该组件的父级一定要是随着显示器的宽高变化而变化的
  // 所以可以是百分比 或者 网格布局
  <ScaleContainer width={1920} height={1080}>
    <div ref={mapRef} className={styles.mapBox} />
  </ScaleContainer>