偵測元素尺寸變化:用 Resize Observer API 打造響應式元件
在開發複雜的網頁介面時,我們經常需要根據元素的尺寸來調整佈局或功能。傳統上,我們可能會監聽 window.resize 事件,但這只能偵測瀏覽器視窗的變化,無法偵測特定元素(如:側邊欄摺疊時的主內容區)的尺寸改變。
Resize Observer API 正是為此而生。它讓我們能直接監聽「元素」的邊框盒(Border Box)或內容盒(Content Box)的尺寸變化,且效能遠優於傳統的輪詢(polling)或監聽視窗事件。
一、 Resize Observer 核心概念
1.1 為什麼需要它?
- 視窗 vs 元素:
window.resize只能反應視窗大小,無法反應元素因內部內容改變或側邊欄開關產生的尺寸變化。 - 效能優勢:它是原生瀏覽器 API,採用異步觀察模式,避免了頻繁觸發 Layout Thrashing。
- 靈活性:可以選擇觀察不同的盒子模型(Content Box, Border Box, Device Pixel Content Box)。
1.2 關鍵物件與方法
typescript
// 1. 建立觀察者
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
// entry.contentRect - 內容區域尺寸
// entry.borderBoxSize - 邊框區域尺寸
// entry.target - 被觀察的元素
console.log('尺寸改變了:', entry.contentRect);
}
});
// 2. 開始觀察
observer.observe(element);
// 3. 停止觀察特定元素
observer.unobserve(element);
// 4. 完全斷開連線
observer.disconnect();二、 設計 useResizeObserver Composable
為了在 Vue 3 中方便地使用,我們將其封裝成一個 Composable。
2.1 介面定義
typescript
import type { Ref } from 'vue';
export interface ResizeObserverOptions {
box?: 'content-box' | 'border-box' | 'device-pixel-content-box';
}
export interface UseResizeObserverReturn {
isSupported: boolean;
width: Ref<number>;
height: Ref<number>;
stop: () => void;
}2.2 完整實作
typescript
// composables/useResizeObserver.ts
import { ref, watch, onUnmounted, computed } from 'vue';
export function useResizeObserver(
target: Ref<HTMLElement | null | undefined>,
callback?: (entry: ResizeObserverEntry) => void,
options: ResizeObserverOptions = {}
) {
const isSupported = typeof window !== 'undefined' && 'ResizeObserver' in window;
const width = ref(0);
const height = ref(0);
let observer: ResizeObserver | null = null;
const stop = () => {
if (observer) {
observer.disconnect();
observer = null;
}
};
const start = () => {
stop();
if (!isSupported || !target.value) return;
observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry) {
// 更新響應式尺寸
const { width: w, height: h } = entry.contentRect;
width.value = w;
height.value = h;
// 如果有回呼則執行
if (callback) callback(entry);
}
});
observer.observe(target.value, options);
};
// 監聽目標元素的變化(如:從 null 到掛載)
watch(
() => target.value,
(el) => {
if (el) start();
else stop();
},
{ immediate: true }
);
onUnmounted(() => stop());
return {
isSupported,
width,
height,
stop
};
}三、 實戰範例:響應式圖表容器
最常見的情境是:當容器尺寸改變時,自動重新繪製圖表或調整內容。
vue
<script setup lang="ts">
import { ref } from 'vue';
import { useResizeObserver } from '@/composables/useResizeObserver';
const containerRef = ref<HTMLElement | null>(null);
const { width, height } = useResizeObserver(containerRef, (entry) => {
console.log('容器尺寸更新:', entry.contentRect.width);
});
</script>
<template>
<div class="demo-wrapper">
<div ref="containerRef" class="resizable-box">
<p>寬度: {{ Math.round(width) }}px</p>
<p>高度: {{ Math.round(height) }}px</p>
<!-- 這裡可以放圖表,並根據 width 進行 resize -->
<div class="chart-mock" :style="{ opacity: width > 500 ? 1 : 0.5 }">
{{ width > 500 ? '寬螢幕模式' : '窄螢幕模式' }}
</div>
</div>
</div>
</template>
<style scoped>
.resizable-box {
border: 2px solid #42b883;
padding: 20px;
resize: both; /* 讓元素在開發時可手動拉伸測試 */
overflow: auto;
min-width: 200px;
min-height: 100px;
background: rgba(66, 184, 131, 0.1);
border-radius: 8px;
}
</style>四、 進階:斷點偵測 (Breakpoints)
我們可以在 Composable 基礎上更進一步,實作一個簡單的斷點偵測:
typescript
export function useElementBreakpoints(target: Ref<HTMLElement | null>, breakpoints: Record<string, number>) {
const { width } = useResizeObserver(target);
const currentBreakpoint = computed(() => {
const sorted = Object.entries(breakpoints).sort((a, b) => b[1] - a[1]);
for (const [name, value] of sorted) {
if (width.value >= value) return name;
}
return 'xs';
});
return { currentBreakpoint };
}五、 技術細節與注意事項
5.1 循環觸發 (Infinite Loops)
如果 ResizeObserver 的回呼中修改了被觀察元素的大小,可能會導致無限循環。瀏覽器會拋出 ResizeObserver loop limit exceeded 錯誤。
解決方案:
- 避免在回呼中直接修改寬高。
- 使用
requestAnimationFrame延遲更新。
5.2 瀏覽器支援狀況
| 瀏覽器 | 支援狀況 |
|---|---|
| Chrome | 支援 (64+) |
| Edge | 支援 (79+) |
| Safari | 支援 (13.1+) |
| Firefox | 支援 (69+) |
TIP
Polyfill:如果需要支援更舊的瀏覽器(如舊版 Safari),可以使用 resize-observer-polyfill。
總結
Resize Observer API 為我們提供了微觀層面的響應式設計能力。透過封裝成 Vue Composable,我們能以極低的效能開銷,實現複雜且流暢的動態佈局。