前端开发中的各种距离和宽高计算

518 阅读4分钟

在前端开发中,计算各种距离和尺寸是常见需求。以下是一些典型场景和相关属性的详细介绍。

1 常见场景

  1. OnBoarding 组件:需要获取每一步高亮元素的位置和尺寸。
  2. Popover 组件:需要获取元素位置以确定浮层位置。
  3. 滚动加载:需要获取滚动距离和页面高度以判断是否触底。

2 浏览器中的距离和宽高属性

2.1 可视区域与滚动条

页面通常超过一屏,右侧会出现滚动条,表示当前可视区域的位置。可视区域也称为视口(viewport)。

2.2 鼠标事件位置属性

当点击可视区域内的元素时,可以通过事件对象获取以下属性:

  • pageY:点击位置到文档顶部的距离。
  • clientY:点击位置到可视区域顶部的距离。
  • offsetY:点击位置到触发事件元素顶部的距离。
  • screenY:点击位置到屏幕顶部的距离。

2.3 示例代码

// 导入必要的 React 钩子
import { MouseEventHandler, useEffect, useRef } from 'react';

function App() {
  // 创建一个 ref 用于引用 DOM 元素
  const boxRef = useRef<HTMLDivElement>(null);

  // 定义点击事件处理函数
  const handleClick: MouseEventHandler<HTMLDivElement> = event => {
    if (boxRef.current) {
      const boxRect = boxRef.current.getBoundingClientRect();
      const offsetY = event.clientY - boxRect.top;

      console.log('鼠标事件坐标信息:');
      console.log('pageY (相对于整个文档):', event.pageY);
      console.log('clientY (相对于浏览器视口):', event.clientY);
      console.log('offsetY (相对于目标元素):', offsetY);
      console.log('screenY (相对于屏幕):', event.screenY);
    }
  };

  // 使用 useEffect 钩子添加原生事件监听器
  useEffect(() => {
    const boxElement = boxRef.current;
    if (boxElement) {
      const nativeClickHandler = (e: MouseEvent) => {
        console.log('原生事件监听器获取的坐标信息:');
        console.log('pageY:', e.pageY);
        console.log('clientY:', e.clientY);
        console.log('offsetY:', e.offsetY);
        console.log('screenY:', e.screenY);
      };

      boxElement.addEventListener('click', nativeClickHandler);

      // 清理函数
      return () => {
        boxElement.removeEventListener('click', nativeClickHandler);
      };
    }
  }, []); // 空依赖数组表示这个效果只在组件挂载时运行一次

  return (
    <div>
      <div
        ref={boxRef}
        style={{
          marginTop: '800px', // 设置上边距使元素远离页面顶部
          width: '100px',
          height: '100px',
          background: 'blue',
        }}
        onClick={handleClick} // 绑定点击事件处理函数
      ></div>
    </div>
  );
}

export default App;

2.4 React 合成事件的坑点

React 的合成事件缺少一些原生事件属性,如 offsetY

可以使用 react-use 提供的 useMouse hook 来解决:

const clickHandler: MouseEventHandler<HTMLDivElement> = (e) => {
  const top = document.getElementById('box')!.getBoundingClientRect().top;
  console.log('box offsetY', e.pageY - top - window.scrollY);
};

2.5 元素的滚动距离

  • window.scrollY:窗口滚动的距离。
  • element.scrollTop:元素内容的滚动距离。

2.6 示例代码

import { MouseEventHandler, useRef, useEffect } from 'react';

function App() {
  const containerRef = useRef<HTMLDivElement>(null);
  const boxRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const handleScroll = () => {
      console.log('窗口滚动距离:', window.scrollY);
    };

    window.addEventListener('scroll', handleScroll);

    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);

  const clickHandler: MouseEventHandler<HTMLDivElement> = () => {
    console.log('元素内容滚动距离:', boxRef.current?.scrollTop);
  };

  return (
    <div ref={containerRef} style={{ height: '2000px' }}>
      <h2 style={{ position: 'fixed', top: 0, left: 0 }}>滚动页面和内部元素来查看效果</h2>
      <div
        ref={boxRef}
        style={{
          marginTop: '800px',
          width: '300px',
          height: '200px',
          background: 'pink',
          overflow: 'auto',
          padding: '10px',
        }}
        onClick={clickHandler}
      >
        <p>点击此元素查看内部滚动距离</p>
        {Array(20)
          .fill(null)
          .map((_, index) => (
            <p key={index}>这是第 {index + 1} 行内容</p>
          ))}
      </div>
    </div>
  );
}

export default App;

2.7 元素的其他属性

  • offsetTop:元素距离最近的有 position 属性的父元素的内容顶部的距离。
  • clientTop:元素上边框的高度。

2.8 示例代码

import { useEffect, useRef, useState } from 'react';

// App 组件:展示一个带有边框的盒子,并显示其 offsetTop 和 clientTop 值
function App() {
  // 创建一个引用,用于访问 DOM 元素
  const ref = useRef<HTMLDivElement>(null);
  // 状态hooks,用于存储 offsetTop 和 clientTop 值
  const [offsetTop, setOffsetTop] = useState<number | undefined>(undefined);
  const [clientTop, setClientTop] = useState<number | undefined>(undefined);

  // 副作用hook,在组件挂载后获取并设置 offsetTop 和 clientTop 值
  useEffect(() => {
    if (ref.current) {
      setOffsetTop(ref.current.offsetTop);
      setClientTop(ref.current.clientTop);
    }
  }, []);

  return (
    // 外层容器,设置相对定位和边框
    <div
      style={{
        position: 'relative',
        margin: '50px',
        padding: '100px',
        border: '1px solid blue',
      }}
    >
      <p>
        offsetTop: {offsetTop}px {/* offsetTop 表示元素相对于其定位父元素的顶部偏移量 */}
      </p>
      <p>
        clientTop: {clientTop}px {/* clientTop 表示元素上边框的宽度 */}
      </p>
      {/* 内部盒子,应用ref并设置样式 */}
      <div
        id="box"
        ref={ref}
        style={{
          position: 'relative',
          top: '30px',
          left: '20px',
          border: '10px solid #000',
          width: '100px',
          height: '100px',
          background: 'pink',
        }}
      ></div>
    </div>
  );
}

export default App;

2.9 计算元素到根元素的总距离

/**
 * 计算元素相对于文档顶部的总偏移量
 * @param element 要计算偏移量的HTML元素
 * @returns 元素相对于文档顶部的总偏移量(以像素为单位)
 */
function getTotalOffsetTop(element: HTMLElement): number {
  let totalOffsetTop = 0;
  while (element) {
    // 累加元素的边框宽度(除了第一个元素)
    if (totalOffsetTop > 0) {
      totalOffsetTop += element.clientTop;
    }
    // 累加元素的offsetTop
    totalOffsetTop += element.offsetTop;
    // 移动到父级定位元素
    element = element.offsetParent as HTMLElement;
  }
  return totalOffsetTop;
}

测试下:

import { useRef, useEffect, useState } from 'react';
import { getTotalOffsetTop } from './assets/utils';

function App() {
  const targetRef = useRef<HTMLDivElement>(null);
  const [offsetTop, setOffsetTop] = useState<number | null>(null);

  useEffect(() => {
    if (targetRef.current) {
      const offset = getTotalOffsetTop(targetRef.current);
      setOffsetTop(offset);
    }
  }, []);

  return (
    <div style={{ padding: '50px' }}>
      <h1>getTotalOffsetTop 示例</h1>
      <div style={{ marginTop: '100px', border: '1px solid black', padding: '20px' }}>
        <div ref={targetRef} style={{ backgroundColor: 'lightblue', padding: '10px' }}>
          目标元素
        </div>
      </div>
      {offsetTop !== null && <p>目标元素距离文档顶部的偏移量:{offsetTop}px</p>}
    </div>
  );
}

export default App;

3 总结

掌握以下属性,可以处理各种需要计算位置和尺寸的需求:

  • e.pageY:鼠标距离文档顶部的距离。
  • e.clientY:鼠标距离可视区域顶部的距离。
  • e.offsetY:鼠标距离触发事件元素顶部的距离。
  • e.screenY:鼠标距离屏幕顶部的距离。
  • window.scrollY:页面滚动的距离。
  • element.scrollTop:元素内容的滚动距离。
  • element.clientTop:元素上边框的高度。
  • element.offsetTop:元素距离最近有 position 属性的父元素的内容顶部的距离。
  • clientHeight:内容高度,不包括边框。
  • offsetHeight:包含边框的高度。
  • scrollHeight:滚动区域的高度,不包括边框。
  • window.innerHeight:窗口的高度。
  • element.getBoundingClientRect():获取元素的宽高和位置。

通过这些属性,可以精确计算和处理各种前端布局和交互需求。