ResizeObserver 与容器查询:元素级响应式布局方案

前端工程(更新于 2026年6月5日)

从视口响应到元素响应

传统响应式基于视口宽度@media),但组件应根据自己的容器尺寸自适应:

对比项 @media 视口查询 Container Query ResizeObserver
响应依据 视口宽度 容器宽度 元素尺寸
作用域 全局 容器内 JS 回调
CSS 驱动
JS 驱动
组件可移植性
性能 浏览器优化 浏览器优化 需注意回调频率

ResizeObserver API 详解

基本用法

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    console.log(`${entry.target.className}: ${width}×${height}`);

    // 边框盒尺寸
    const borderBox = entry.borderBoxSize?.[0];
    console.log(`含边框: ${borderBox?.inlineSize}×${borderBox?.blockSize}`);
  }
});

observer.observe(document.querySelector('.card'));

监听选项

observer.observe(element, {
  box: 'content-box'  // 默认,内容区
  // box: 'border-box'  // 含 padding + border
  // box: 'device-pixel-content-box'  // 设备像素级
});

实战:自适应卡片布局

class ResponsiveCard {
  private observer: ResizeObserver;

  constructor(private card: HTMLElement) {
    this.observer = new ResizeObserver((entries) => {
      const { width } = entries[0].contentRect;
      this.applyLayout(width);
    });
    this.observer.observe(card);
  }

  private applyLayout(width: number) {
    if (width < 200) {
      this.card.setAttribute('data-layout', 'compact');
    } else if (width < 400) {
      this.card.setAttribute('data-layout', 'normal');
    } else {
      this.card.setAttribute('data-layout', 'expanded');
    }
  }

  destroy() {
    this.observer.disconnect();
  }
}
.card[data-layout="compact"] {
  flex-direction: column;
  font-size: 12px;
}
.card[data-layout="normal"] {
  flex-direction: column;
  font-size: 14px;
}
.card[data-layout="expanded"] {
  flex-direction: row;
  font-size: 16px;
}

实战:响应式图表

class ResponsiveChart {
  private observer: ResizeObserver;
  private chart: Chart | null = null;

  constructor(private container: HTMLElement) {
    this.observer = new ResizeObserver((entries) => {
      const { width, height } = entries[0].contentRect;
      this.resizeChart(width, height);
    });
    this.observer.observe(container);
  }

  private resizeChart(width: number, height: number) {
    if (this.chart) {
      this.chart.resize({ width, height });
    } else {
      this.chart = new Chart(this.container, {
        type: 'line',
        data: this.getChartData(width),
        options: {
          responsive: false,
          maintainAspectRatio: false,
        }
      });
    }
  }

  private getChartData(width: number) {
    const pointCount = width < 300 ? 5 : width < 600 ? 10 : 20;
    return { labels: generateLabels(pointCount), datasets: [...] };
  }

  destroy() {
    this.observer.disconnect();
    this.chart?.destroy();
  }
}

实战:文本截断检测

class TextOverflowDetector {
  private observer: ResizeObserver;

  constructor(
    private element: HTMLElement,
    private onOverflow: (isOverflow: boolean) => void
  ) {
    this.observer = new ResizeObserver(() => {
      const isOverflow = this.checkOverflow();
      this.onOverflow(isOverflow);
    });
    this.observer.observe(element);
  }

  private checkOverflow(): boolean {
    return this.element.scrollWidth > this.element.clientWidth;
  }

  destroy() {
    this.observer.disconnect();
  }
}

const detector = new TextOverflowDetector(
  titleElement,
  (overflow) => {
    tooltipElement.style.display = overflow ? 'inline' : 'none';
  }
);

CSS Container Queries

基本语法

.card-container {
  container-type: inline-size;
  container-name: card;
}

@container card (min-width: 400px) {
  .card {
    flex-direction: row;
    gap: 16px;
  }
  .card-title {
    font-size: 20px;
  }
}

@container card (min-width: 200px) and (max-width: 399px) {
  .card {
    flex-direction: column;
  }
  .card-title {
    font-size: 16px;
  }
}

@container card (max-width: 199px) {
  .card-title {
    font-size: 14px;
  }
  .card-meta {
    display: none;
  }
}

容器查询单位

.card-container {
  container-type: inline-size;
}

.card-title {
  font-size: clamp(14px, 5cqi, 24px);
  padding: 2cqi;
}
单位 含义 等价于
cqw 容器宽度的 1% container-query-width
cqh 容器高度的 1% container-query-height
cqi 容器内联尺寸的 1% container-query-inline
cqb 容器块尺寸的 1% container-query-block
cqmin min(cqi, cqb) 较小维度
cqmax max(cqi, cqb) 较大维度

容器查询 vs 媒体查询

同一组件在不同场景

<div class="sidebar">
  <product-card />  <!-- 侧边栏:窄容器 → 紧凑布局 -->
</div>

<div class="main-content">
  <product-card />  <!-- 主内容:宽容器 → 展开布局 -->
</div>
.product-card-container {
  container-type: inline-size;
}

@container (min-width: 350px) {
  .product-card { flex-direction: row; }
}

@container (max-width: 349px) {
  .product-card { flex-direction: column; }
}

媒体查询无法做到:同一组件在不同位置自动适配。

对比总结

场景 @media @container
全局布局(导航/页脚) ✅ 合适 ❌ 不需要
组件自适应 ❌ 需手动断点 ✅ 自动适配
组件可复用性 差(依赖视口) 好(依赖容器)
嵌套组件 ❌ 无法感知父容器 ✅ 逐层适配
性能 视口变化触发 容器变化触发

ResizeObserver + Container Query 配合

class SmartResponsive {
  private observer: ResizeObserver;

  constructor(private container: HTMLElement) {
    this.observer = new ResizeObserver((entries) => {
      const { width } = entries[0].contentRect;

      // JS 逻辑:数据密度调整
      if (width < 300) {
        this.showSummary();
      } else {
        this.showFullData();
      }

      // CSS 自动通过 container query 处理布局
    });

    this.observer.observe(container);
  }

  private showSummary() {
    this.container.setAttribute('data-density', 'sparse');
  }

  private showFullData() {
    this.container.setAttribute('data-density', 'dense');
  }

  destroy() {
    this.observer.disconnect();
  }
}

性能优化

防抖 ResizeObserver 回调

function debouncedResizeObserver(
  callback: (entries: ResizeObserverEntry[]) => void,
  delay = 100
): ResizeObserver {
  let timer: number;
  let latestEntries: ResizeObserverEntry[];

  return new ResizeObserver((entries) => {
    latestEntries = entries;
    clearTimeout(timer);
    timer = window.setTimeout(() => callback(latestEntries), delay);
  });
}

批量处理

const observer = new ResizeObserver((entries) => {
  requestAnimationFrame(() => {
    for (const entry of entries) {
      updateLayout(entry.target as HTMLElement, entry.contentRect);
    }
  });
});

浏览器兼容性

API Chrome Firefox Safari Edge
ResizeObserver 64+ 69+ 13.1+ 79+
Container Queries 105+ 110+ 16+ 105+
Container Units (cqw/cqi) 105+ 110+ 16+ 105+
container-type 105+ 110+ 16+ 105+
borderBoxSize 84+ 69+ 13.1+ 84+

最佳实践

  1. 优先 Container Query:纯布局适配用 CSS 容器查询,无需 JS
  2. ResizeObserver 处理逻辑:需要数据/行为变化时使用 JS
  3. 及时 disconnect:组件卸载时断开观察,避免泄漏
  4. 避免循环:回调中修改尺寸可能触发新回调,需防抖
  5. container-type 选择:仅需要宽度适配用 inline-size,比 size 性能更好

总结

元素级响应式是现代组件化设计的核心需求。CSS Container Queries 实现纯 CSS 的容器适配,ResizeObserver 提供精确的 JS 尺寸感知,两者配合实现完整的元素级响应式方案。告别"视口断点硬编码",让组件真正做到"放哪都适配"。

推荐使用 Flexbox 生成器 设计容器内布局,渐变生成器 制作响应式背景,阴影生成器 调试容器内卡片样式。

本站提供浏览器本地工具,免注册即可试用 →

#ResizeObserver#响应式#容器查询#元素尺寸#布局