跳至主要內容
Skip to content

偵測元素尺寸變化:用 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,我們能以極低的效能開銷,實現複雜且流暢的動態佈局。


延伸閱讀