登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

前端 ResizeObserver 怎样读取内容盒和边框盒尺寸

来源:17golang原创

时间:2026-10-20 02:45:22 114浏览 收藏

我在做可伸缩卡片时遇到过一个很容易误判的问题:CSS 里把 box-sizing、内边距和边框都配好了,ResizeObserver 回调里却仍然拿到“少了一圈”的宽度。后来我把目标拆成一句话:需要内容区就读 contentBoxSize,需要元素外轮廓就读 borderBoxSize,旧实现再回退到 contentRect。这样组件的尺寸计算才不会和视觉边界错位。

官方资料地址:https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserverEntry

要点速览
  • contentBoxSize 表示内容盒,通常不包含 padding 和 border;borderBoxSize 包含内容、内边距和边框。
  • 现代实现中的尺寸字段是数组,实际读取时优先取第一个碎片的 inlineSize 与 blockSize。
  • 需要兼容较早实现时,保留 contentRect.width 与 contentRect.height 回退,但不要把它和 border box 混为一谈。

先选对要读取的尺寸盒

ResizeObserver 观察的是元素尺寸,不是只有窗口变化才触发的全局事件。真正开始写回调前,先确认下游要回答哪个问题:如果要决定文字区域、绘图区域或内容列数,通常关心内容盒;如果要让浮层、描边或卡片外框和元素整体对齐,通常关心边框盒。

CSS 盒模型可以从内向外理解:内容盒是内容所在的区域,padding 在它外面,border 再包住 padding。于是同一个元素可能同时有两个合理的宽度。把内容区宽度拿去和外框定位,会出现间距被少算;把边框盒宽度拿去计算文字列数,又可能让内容判断偏大。

ResizeObserver 内容盒与边框盒包含关系的静态结构说明图
图1:ResizeObserver 两种尺寸盒的静态结构说明图,不是浏览器截图或运行证据。

最小配方:用 box 选项观察元素

observe 的第二个参数可以指定观察的盒类型。需要内容盒时传 content-box,需要边框盒时传 border-box。如果省略选项,默认观察内容盒,这正是很多“看起来少了边框”的来源。

const panel = document.querySelector('.panel');

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const size = entry.borderBoxSize?.[0];
    if (!size) continue;

    // 用边框盒驱动外框相关布局,避免把 padding 和 border 漏掉。
    panel.dataset.width = String(Math.round(size.inlineSize));
    panel.dataset.height = String(Math.round(size.blockSize));
  }
});

// box 只影响观察目标的尺寸盒,不会改变元素本身的 CSS 盒模型。
observer.observe(panel, { box: 'border-box' });

这里使用 inlineSize 和 blockSize,而不是把宽高写死成 width 与 height。逻辑尺寸字段能适应不同书写模式;如果你的组件明确只服务水平书写,也可以在最终业务层把它映射成横向宽度和纵向高度。

兼容数组与旧实现

现代 ResizeObserverEntry 的 contentBoxSize 和 borderBoxSize 是数组,原因是一个元素可能被分成多个布局碎片,例如多列场景。普通卡片大多只有一个碎片,因此常用 [0]。但较早的实现曾经把字段暴露成单个对象,所以兼容读取函数应同时处理数组和对象。

function firstBoxSize(value) {
  if (!value) return null;

  // 兼容现代数组形态,也兼容旧实现直接返回单个对象的情况。
  return Array.isArray(value) ? value[0] ?? null : value;
}

function readObservedSize(entry, box = 'content') {
  const source = box === 'border' ? entry.borderBoxSize : entry.contentBoxSize;
  const logical = firstBoxSize(source);
  if (logical) {
    // 逻辑尺寸字段比硬编码 width/height 更适合不同书写模式。
    return {
      inline: logical.inlineSize,
      block: logical.blockSize,
      source: `${box}-box`
    };
  }

  if (entry.contentRect) {
    // contentRect 是兼容回退,语义接近内容盒,不要当成边框盒。
    return {
      inline: entry.contentRect.width,
      block: entry.contentRect.height,
      source: 'content-rect-fallback'
    };
  }

  return null;
}

回退分支的关键不是“保证一定拿到一个数字”,而是保留尺寸语义。contentRect 适合兼容只提供旧字段的浏览器;如果你的业务必须精确读取 border box,就应当在不支持 borderBoxSize 时明确降级策略,而不是静默把内容盒当作边框盒。

ResizeObserverEntry 中 contentBoxSize borderBoxSize 与 contentRect 回退关系的静态说明图
图2:ResizeObserverEntry 尺寸字段与兼容回退的静态关系图,不是截图或运行证据。

把尺寸用于组件布局时,我会这样分层

读取尺寸后不要在回调里到处修改样式。我的习惯是先把观察结果归一化成一个小对象,再交给组件自己的布局函数。这样下游不需要知道当前浏览器返回的是数组、单对象还是 contentRect。

const card = document.querySelector('.card');

function applyCardLayout(size) {
  if (!size) return;

  // 这里的阈值只服务示例:内容区足够宽时显示双列。
  card.classList.toggle('is-wide', size.inline >= 560);
  card.style.setProperty('--observed-inline-size', `${size.inline}px`);
}

const cardObserver = new ResizeObserver(([entry]) => {
  // 组件只消费归一化结果,不关心 ResizeObserverEntry 的兼容细节。
  applyCardLayout(readObservedSize(entry, 'content'));
});

cardObserver.observe(card, { box: 'content-box' });

// 组件销毁时停止观察,避免保留不再使用的节点引用。
function destroyCardObserver() {
  cardObserver.disconnect();
}

这段代码的选择是有条件的:双列判断依赖内容区,因此观察 content-box;如果要计算卡片外框与相邻元素的贴合位置,就把观察选项和读取参数一起改为 border。不要只改一处,否则代码会出现“订阅了边框盒,读取却按内容盒解释”的隐性错位。

几个容易踩到的兼容坑

为什么读取 entry.contentBoxSize.width 会是 undefined?

现代字段通常是数组,应先取 entry.contentBoxSize[0]。为了兼容较早实现,可以用上面的 firstBoxSize 同时处理数组和单对象。

contentRect 能代替 borderBoxSize 吗?

不能直接等价替代。它更接近内容盒语义,适合作为旧实现回退;边框宽度或内边距不为零时,两者的数值可能不同。

什么时候需要关心多个数组项?

普通单列卡片通常只读第一项即可。元素存在多列或碎片化布局时,应该根据业务决定是累加、取最大值,还是逐碎片处理,不能默认第一项代表全部布局结果。

观察器回调里能不能直接改尺寸?

可以更新布局,但要避免让更新再次改变被观察元素的尺寸并形成反馈循环。更稳妥的做法是比较新旧值、只在状态确实变化时写回,并在组件销毁时调用 disconnect()。

完整片段:统一读取两种尺寸盒

function readBox(entry, box) {
  const raw = box === 'border' ? entry.borderBoxSize : entry.contentBoxSize;
  const item = Array.isArray(raw) ? raw[0] : raw;

  if (item) {
    // 优先使用标准逻辑尺寸字段。
    return { inline: item.inlineSize, block: item.blockSize };
  }

  if (entry.contentRect) {
    // 旧实现回退到内容区矩形;调用方应记录这是降级结果。
    return { inline: entry.contentRect.width, block: entry.contentRect.height };
  }

  return null;
}

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const size = readBox(entry, 'border');
    if (!size) continue;

    // 只在这里消费归一化尺寸,避免把兼容判断散落到业务组件。
    entry.target.style.setProperty('--box-inline', `${size.inline}px`);
  }
});

observer.observe(document.querySelector('.panel'), { box: 'border-box' });

记忆这套配方就够用了:先问组件要内容盒还是边框盒,再在 Entry 中优先读取对应数组的第一个逻辑尺寸对象,最后保留 contentRect 的明确回退。尺寸来源说清楚,ResizeObserver 就不会再变成“数字拿到了,但不知道它代表哪一圈”的黑盒。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>