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+ |
最佳实践
- 优先 Container Query:纯布局适配用 CSS 容器查询,无需 JS
- ResizeObserver 处理逻辑:需要数据/行为变化时使用 JS
- 及时 disconnect:组件卸载时断开观察,避免泄漏
- 避免循环:回调中修改尺寸可能触发新回调,需防抖
- container-type 选择:仅需要宽度适配用
inline-size,比size性能更好
总结
元素级响应式是现代组件化设计的核心需求。CSS Container Queries 实现纯 CSS 的容器适配,ResizeObserver 提供精确的 JS 尺寸感知,两者配合实现完整的元素级响应式方案。告别"视口断点硬编码",让组件真正做到"放哪都适配"。
推荐使用 Flexbox 生成器 设计容器内布局,渐变生成器 制作响应式背景,阴影生成器 调试容器内卡片样式。
本站提供浏览器本地工具,免注册即可试用 →
#ResizeObserver#响应式#容器查询#元素尺寸#布局