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'
// 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;
if (width < 300) {
this.showSummary();
} else {
this.showFullData();
}
});
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#响应式#容器查询#元素尺寸#布局