那些被忽略但很好用的 Web API / GetBoundingClientRect

小孩才做选择,成年人当然是尺寸、座标全都要!

昨天介绍的 ResizeObserver API 可以观察到元素的尺寸变动时执行回呼,并且会提供元素变动後的「尺寸」、「座标」等资讯,而今天介绍的 GetBoundingClientRect 虽然无法再观察元素变动了,但我们可以主动出击,直接索取目前元素的相关资讯。


GetBoundingClientRect

GetBoundingClientRect 可以使我们取得 Element 元素的宽高以及相对於视窗可视范围(Viewport)的座标位置,对於前端老鸟来说可能不是那麽陌生,但对於新手来说,它不太会是第一批认识的 Web API,大部分可能都会先认识 clientWidthscrollWidthoffsetWidthscrollTopclientTopoffsetTop...等等,这一大堆眼花撩乱让人容易混淆的系列属性。

# 元素的各种宽高与位置

为了让各位更好的理解 GetBoundingClientRect,先来帮各位整理及复习前面提到的这一大堆元素属性,

  • offsetWidth / offsetHeight: 元素 borderBox 的宽/高
  • clientWidth / clientHeight: 元素 paddingBox 的宽/高
  • scrollWidth / scrollHeight: 元素包含 padding 及外溢内容的宽/高

https://ithelp.ithome.com.tw/upload/images/20211006/20125431WaySPYpodl.png

  • offsetTop / offsetLeft: 元素 borderBox 相对於 offsetParent 的垂直/水平距离
  • clientTop / clientLeft: 元素 paddingBox 相对於 borderBox 的垂直/水平距离
  • scrollTop / scrollLeft: 元素 paddingBox 被卷动的垂直/水平距离

https://ithelp.ithome.com.tw/upload/images/20211006/20125431sCZ9ken5x4.png

 

# Element.getBoundingClientRect

其实刚刚介绍的众多属性,相信大家或多或少都有使用过,尤其在一些卷动事件中常常会出现它们的身影,不过在今天之後,你使用它们的机会可能会渐渐减少了。

getBoundingClientRect 的使用方式非常简单,它属於 Element 的原生 method,直接互叫即可:

const div = document.querySelector("div");
console.log(div.getBoundingClientRect());

执行过後便会回传一个 DOMRect 物件,该物件中就会有指定元素的相关尺寸与座标讯息:

  • width: 元素的 borderBox 宽度,相当於 offsetWidth
  • height: 元素的 borderBox 高度,相当於 offsetHeight
  • x: 元素的 borderBox 左上角相对於视窗的水平(X)座标
  • y: 元素的 borderBox 左上角相对於视窗的垂直(Y)座标
  • left: 元素的 borderBox 左上角相对於视窗的水平(X)座标,等同 x
  • top: 元素的 borderBox 左上角相对於视窗的垂直(Y)座标,等同 y
  • right: 元素的 borderBox 右下角相对於视窗的水平(X)座标
  • bottom: 元素的 borderBox 右下角相对於视窗的垂直(Y)座标

DOMRect 跟昨天介绍的 ResizeObserverEntry.contentRect 所回传的 DOMRectReadOnly 格式是一样的,但当中数值所代表的意义完全不同,不要被混淆罗。

https://ithelp.ithome.com.tw/upload/images/20211006/20125431bGB4Yw6Chv.png

有了这些资讯後,针对一些卷动事件的需求其实就会变得简单许多,像是我们常常会做的事情是「判断某元素是否进入可视范围」,就可以来看看使用 getBoundingClientRect 後的差别:

const div = document.querySelector("div");
// 不使用 getBoundingClientRect
window.addEventListener("scroll", function (e) {
  if (div.offsetTop + div.offsetHeight <= window.pageYOffset) {
    console.log("元素底端已离开画面");
  } else if (div.offsetTop <= window.pageYOffset + window.innerHeight) {
    console.log("元素顶端已进入画面");
  }
});
// 使用 getBoundingClientRect
window.addEventListener("scroll", function (e) {
  const { top, bottom } = div.getBoundingClientRect();
  if (bottom <= 0) {
    console.log("元素底端已离开画面");
  } else if (top <= window.innerHeight) {
    console.log("元素顶端已进入画面");
  }
});

可以看到,如果不使用 getBoundingClientRect 需进行较复杂的计算,而且如果该元素的 offsetParent 不是 body 的话,这个计算就会出现 Bug。反之使用 getBoundingClientRect 的程序码简洁又容易理解,而且因为 topbottom 的数值是直接相对视窗计算出来的,所以也不用去顾虑元素的 offsetParent

小技巧: 利用 window.pageYOffset + getBoundingClientRect().top 就可以计算出元素相对於文件(document)的绝对座标喔。

 

# 实际练习

为了让大家更能感受到 getBoundingClientRect 的强大,我们来做一个 「动态 Highlight」 的小练习,需求是「当游标滑到文章中的粗体字时会自动添加底色,且底色在不同关键字之间切换时,要有移动的过渡效果」。先看效果:

.highlight {
  position: fixed;
  background: yellowgreen;
}
const highlight = document.querySelector(".highlight");
const bold = document.querySelectorAll("b");

let hoverElement;

bold.forEach((el) => {
  el.addEventListener("mouseenter", function () {
    hoverElement = this;
    highlight.style.transition = "0.3s";
    setHighlight();
  });
});

// 为了在视窗滚动时不会跑版,要在 scroll 进行重新定位
window.addEventListener("scroll", function () {
  highlight.style.transition = "0s";
  if (hoverElement) setHighlight();
});

function setHighlight() {
  const { width, height, top, left } = hoverElement.getBoundingClientRect();
  highlight.textContent = hoverElement.textContent;
  highlight.style.width = width + "px";
  highlight.style.height = height + "px";
  highlight.style.top = top + "px";
  highlight.style.left = left + "px";
}

整体概念就是在指定的元素上绑定 mouseenter 事件,并在事件发生时使用 getBoundingClientRect 来取得该元素的尺寸座标资讯,然後将其设定在 Highlight 元素的样式上。

不要认为这样的功能效果好像很简单,如果没有 getBoundingClientRect 的话,做起来是特别麻烦的,想要实际玩玩看的话,这边提供我已经写好的 CodePen,并且也鼓励大家发挥创意来试试看其他的应用,感受一下它的好用之处。

 

希望经过今天的介绍,各位已经开始爱上 getBoundingClientRect 了,我本身就蛮常使用的,比较记下一堆容易搞混的属性,只需要一行我就可以取得那些经常使用的资讯,而且还可以剩下很多麻烦的计算,何乐不为呢?


<<:  [Day21]Geolocation based Speedometer and Compass

>>:  如果你真的够渴望做点什麽,任何事情你都可以持续做三十天。

[Day28]一寸光阴不可轻-修好你的资料,补值初学上线

今天我们要来解决空缺的部份,我们要使用的素材如下,是一张每隔五分钟就纪录温度的资料表,我结图整张表最...

Day19:【技术篇】无障碍检测(freego)方式

一、前言   这篇要介绍的是当你的网站要申请无障碍标章时,必须要做的「无障碍检测」。一个好的网站,是...

[DAY27] 功能型团队 VS 需求型团队

前面的篇章大部分着重 DDD 的战术设计,这篇来说说战略设计。 功能型团队 在导入 DDD 前,我们...

【把玩Azure DevOps】Day12 Artifacts应用:上传第一个nuget package

前一篇文章简单介绍了Azure DevOps Artifacts,知道了它就是用来存放私有套件的套件...

Day 12:封装 OkHttp

本篇文章同步发表在 HKT 线上教室 部落格,线上影音教学课程已上架至 Udemy 和 Youtu...