Vue 3 与 Resize Observer API

概述

Resize Observer API 是现代浏览器提供的一组 API,允许开发者监听元素的大小变化。与传统的 resize 事件相比,Resize Observer API 具有以下优势:

  • 更精确:可以监听任意元素的大小变化,而不仅仅是 window
  • 更高性能:使用高效的观察机制,避免了频繁的布局计算
  • 更灵活:支持监听内容盒、边框盒或滚动盒的变化
  • 更易用:提供了简洁的 API,便于集成到现代前端框架中

本教程将介绍如何在 Vue 3 应用中集成 Resize Observer API,实现响应式布局和组件自适应。

核心知识点

1. API 基本概念

Resize Observer API 主要包含以下核心接口:

  • ResizeObserver:用于观察元素大小变化的主要接口
  • ResizeObserverEntry:表示被观察元素的大小变化信息
  • ResizeObserverOptions:用于配置观察选项

2. 基本用法

创建观察器

const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    console.log('元素大小变化:', entry);
  }
});

观察元素

const element = document.querySelector('.target-element');
resizeObserver.observe(element);

停止观察

resizeObserver.unobserve(element);

断开所有观察

resizeObserver.disconnect();

3. 观察选项

Resize Observer API 支持以下观察选项:

const options = {
  box: 'content-box' // 可选值: 'content-box', 'border-box', 'device-pixel-content-box'
};

const resizeObserver = new ResizeObserver(callback, options);
  • content-box:观察内容盒的大小变化
  • border-box:观察边框盒的大小变化
  • device-pixel-content-box:观察设备像素内容盒的大小变化

4. ResizeObserverEntry 属性

每个 ResizeObserverEntry 对象包含以下属性:

  • target:被观察的元素
  • contentRect:元素的内容矩形信息
  • borderBoxSize:边框盒的大小信息
  • contentBoxSize:内容盒的大小信息
  • devicePixelContentBoxSize:设备像素内容盒的大小信息

5. Vue 3 组合式 API 封装

创建一个 useResizeObserver 组合式函数来封装 Resize Observer API:

import { ref, onMounted, onUnmounted } from 'vue';

export function useResizeObserver(options = {}) {
  const element = ref(null);
  const resizeObserver = ref(null);
  const size = ref({ width: 0, height: 0 });
  const contentRect = ref(null);

  // 处理大小变化
  const handleResize = (entries) => {
    for (const entry of entries) {
      // 获取内容盒大小
      const contentBoxSize = entry.contentBoxSize?.[0] || { inlineSize: 0, blockSize: 0 };
      
      // 更新大小信息
      size.value = {
        width: contentBoxSize.inlineSize,
        height: contentBoxSize.blockSize
      };
      
      // 更新 contentRect
      contentRect.value = entry.contentRect;
    }
  };

  // 开始观察
  const startObserving = () => {
    if (element.value && !resizeObserver.value) {
      resizeObserver.value = new ResizeObserver(handleResize, options);
      resizeObserver.value.observe(element.value);
    }
  };

  // 停止观察
  const stopObserving = () => {
    if (resizeObserver.value) {
      resizeObserver.value.disconnect();
      resizeObserver.value = null;
    }
  };

  // 组件挂载时开始观察
  onMounted(() => {
    startObserving();
  });

  // 组件卸载时停止观察
  onUnmounted(() => {
    stopObserving();
  });

  return {
    element,
    size,
    contentRect,
    startObserving,
    stopObserving
  };
}

6. 观察多个元素

创建一个可以观察多个元素的组合式函数:

import { ref, onUnmounted } from 'vue';

export function useMultiResizeObserver(options = {}) {
  const elements = ref([]);
  const resizeObserver = ref(null);
  const sizes = ref(new Map());

  // 处理大小变化
  const handleResize = (entries) => {
    for (const entry of entries) {
      const contentBoxSize = entry.contentBoxSize?.[0] || { inlineSize: 0, blockSize: 0 };
      
      sizes.value.set(entry.target, {
        width: contentBoxSize.inlineSize,
        height: contentBoxSize.blockSize,
        contentRect: entry.contentRect
      });
    }
  };

  // 添加观察元素
  const addElement = (el) => {
    if (el && !elements.value.includes(el)) {
      elements.value.push(el);
      
      // 如果观察器已创建,立即观察新元素
      if (resizeObserver.value) {
        resizeObserver.value.observe(el, options);
      } else {
        // 否则创建观察器
        resizeObserver.value = new ResizeObserver(handleResize, options);
        // 观察所有已添加的元素
        elements.value.forEach(element => {
          resizeObserver.value.observe(element, options);
        });
      }
    }
  };

  // 移除观察元素
  const removeElement = (el) => {
    const index = elements.value.indexOf(el);
    if (index !== -1) {
      elements.value.splice(index, 1);
      sizes.value.delete(el);
      
      // 如果观察器存在,停止观察该元素
      if (resizeObserver.value) {
        resizeObserver.value.unobserve(el);
        
        // 如果没有元素需要观察,断开观察器
        if (elements.value.length === 0) {
          resizeObserver.value.disconnect();
          resizeObserver.value = null;
        }
      }
    }
  };

  // 获取元素大小
  const getSize = (el) => {
    return sizes.value.get(el) || { width: 0, height: 0, contentRect: null };
  };

  // 组件卸载时清理资源
  onUnmounted(() => {
    if (resizeObserver.value) {
      resizeObserver.value.disconnect();
      resizeObserver.value = null;
    }
    elements.value = [];
    sizes.value.clear();
  });

  return {
    addElement,
    removeElement,
    getSize,
    sizes
  };
}

最佳实践

1. 浏览器兼容性

  • 检查 API 支持:在使用前检查浏览器是否支持 Resize Observer API
  • 提供降级方案:对于不支持的浏览器,使用传统的 resize 事件或其他替代方案
  • 使用 polyfill:考虑使用 ResizeObserver polyfill 来支持旧浏览器

2. 性能优化

  • 避免频繁更新:在 resize 回调中避免执行昂贵的操作
  • 使用防抖/节流:如果需要执行复杂操作,考虑使用防抖或节流
  • 及时停止观察:不再需要观察时,及时调用 unobservedisconnect
  • 限制观察数量:避免观察过多元素,只观察必要的元素

3. 组件设计

  • 封装为自定义指令:将 Resize Observer 封装为 Vue 自定义指令,便于复用
  • 使用组合式 API:利用 Vue 3 的组合式 API,将观察逻辑封装为可复用的组合式函数
  • 响应式更新:结合 Vue 的响应式系统,实现组件的自适应更新

4. 观察策略

  • 选择合适的观察盒模型:根据需求选择 content-box、border-box 或 device-pixel-content-box
  • 考虑嵌套元素:注意观察嵌套元素时的大小变化传递
  • 处理动态元素:对于动态创建的元素,确保在元素挂载后开始观察

常见问题与解决方案

1. 浏览器不支持 Resize Observer API

问题:在某些旧浏览器中,Resize Observer API 不被支持

解决方案

if (!('ResizeObserver' in window)) {
  console.error('Resize Observer API is not supported in this browser');
  // 使用 polyfill 或降级方案
  import('resize-observer-polyfill').then(ResizeObserver => {
    window.ResizeObserver = ResizeObserver.default || ResizeObserver;
    // 初始化观察器
  });
}

2. 观察的元素还未挂载

问题:尝试观察还未挂载到 DOM 的元素

解决方案

<template>
  <div ref="targetElement">目标元素</div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { useResizeObserver } from './composables/useResizeObserver';

const targetElement = ref(null);
const { size, startObserving } = useResizeObserver();

// 确保元素挂载后再开始观察
onMounted(() => {
  // 可以在这里手动开始观察,或者通过 ref 绑定自动观察
});
</script>

3. 频繁触发 resize 回调

问题:某些情况下,resize 回调会被频繁触发,影响性能

解决方案

import { ref, watch } from 'vue';

const { size } = useResizeObserver();
const debouncedSize = ref({ width: 0, height: 0 });

// 使用 watch 和防抖处理
watch(size, (newSize) => {
  // 防抖逻辑
  clearTimeout(window.resizeTimeout);
  window.resizeTimeout = setTimeout(() => {
    debouncedSize.value = { ...newSize };
    // 执行昂贵的操作
  }, 100);
}, { deep: true });

4. 观察多个元素时性能问题

问题:观察大量元素时,可能会导致性能问题

解决方案

  • 只观察必要的元素
  • 使用虚拟滚动或分页技术减少同时观察的元素数量
  • 对观察结果进行批量处理

5. 观察 iframe 内容

问题:无法直接观察 iframe 内部元素的大小变化

解决方案

  • 在 iframe 内部使用 Resize Observer API
  • 通过 postMessage 在 iframe 和父窗口之间传递大小信息

进阶学习资源

  1. MDN 文档Resize Observer API
  2. Web.dev 教程Resize Observer API: Observe Element Resizes
  3. W3C 规范Resize Observer
  4. GitHub 示例Resize Observer Examples
  5. Vue 3 组合式 API 文档Composition API

实战练习

练习:构建响应式仪表板

目标:使用 Vue 3 和 Resize Observer API 构建一个响应式仪表板,包含自适应大小的组件

功能要求

  1. 响应式布局

    • 仪表板组件根据父容器大小自适应调整
    • 支持不同屏幕尺寸和设备
    • 实现流畅的布局转换
  2. 自适应组件

    • 卡片组件根据可用空间调整大小
    • 图表组件根据容器大小自动重绘
    • 网格布局根据容器宽度调整列数
  3. 性能优化

    • 使用 Resize Observer API 监听大小变化
    • 实现组件级别的观察
    • 优化 resize 回调中的操作
  4. 用户体验

    • 平滑的大小过渡动画
    • 清晰的视觉层次
    • 支持拖拽调整大小

实现步骤

  1. 创建一个 Vue 3 项目
  2. 实现 useResizeObserver 组合式函数
  3. 构建仪表板布局组件
  4. 实现自适应卡片组件
  5. 添加图表组件并实现自动重绘
  6. 测试不同屏幕尺寸和设备

示例代码结构

<template>
  <div class="dashboard">
    <h1>响应式仪表板</h1>
    
    <div class="grid-container" ref="gridContainer">
      <div 
        v-for="card in cards" 
        :key="card.id"
        class="card"
        :style="getCardStyle(card)"
      >
        <h3>{{ card.title }}</h3>
        <div class="card-content">
          <ChartComponent 
            :data="card.data" 
            :width="getCardSize(card.id).width" 
            :height="getCardSize(card.id).height - 60"
          />
        </div>
      </div>
    </div>
    
    <div class="size-info">
      <p>容器大小: {{ containerSize.width }}px × {{ containerSize.height }}px</p>
      <p>列数: {{ columns }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue';
import { useMultiResizeObserver } from './composables/useMultiResizeObserver';
import ChartComponent from './components/ChartComponent.vue';

const gridContainer = ref(null);
const { size: containerSize } = useResizeObserver({ box: 'border-box' });
const { addElement, getSize } = useMultiResizeObserver();

// 仪表板卡片数据
const cards = ref([
  { id: 1, title: '卡片 1', data: [10, 20, 30, 40, 50] },
  { id: 2, title: '卡片 2', data: [50, 40, 30, 20, 10] },
  { id: 3, title: '卡片 3', data: [25, 35, 15, 45, 5] },
  { id: 4, title: '卡片 4', data: [30, 10, 40, 20, 50] }
]);

// 根据容器宽度计算列数
const columns = computed(() => {
  if (containerSize.width < 600) return 1;
  if (containerSize.width < 1200) return 2;
  return 4;
});

// 卡片引用映射
const cardRefs = ref(new Map());

// 获取卡片样式
const getCardStyle = (card) => {
  const cardWidth = containerSize.width / columns.value - 20;
  return {
    width: `${cardWidth}px`,
    height: `${cardWidth * 0.75}px`
  };
};

// 获取卡片大小
const getCardSize = (cardId) => {
  const cardEl = cardRefs.value.get(cardId);
  return cardEl ? getSize(cardEl) : { width: 0, height: 0 };
};

// 组件挂载时初始化
onMounted(() => {
  // 观察卡片元素
  cards.value.forEach(card => {
    const cardEl = document.getElementById(`card-${card.id}`);
    if (cardEl) {
      cardRefs.value.set(card.id, cardEl);
      addElement(cardEl);
    }
  });
});
</script>

<style scoped>
.dashboard {
  max-width: 1400px;
  margin: 0 auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}

h1 {
  text-align: center;
  margin-bottom: 30px;
}

.grid-container {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  justify-content: center;
  margin-bottom: 30px;
}

.card {
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 15px;
  transition: all 0.3s ease;
  overflow: hidden;
}

.card:hover {
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  transform: translateY(-2px);
}

.card h3 {
  margin-top: 0;
  margin-bottom: 15px;
  font-size: 18px;
  color: #333;
}

.card-content {
  width: 100%;
  height: calc(100% - 60px);
}

.size-info {
  background-color: #f5f5f5;
  padding: 15px;
  border-radius: 8px;
  text-align: center;
  font-size: 14px;
  color: #666;
}
</style>

进阶学习资源

  1. MDN Web Docs

  2. Web.dev 文章

  3. GitHub 仓库

  4. 视频教程

  5. 规范文档

总结

Resize Observer API 为 web 应用提供了强大的元素大小观察能力,结合 Vue 3 的组合式 API,可以构建出功能丰富、性能优良的响应式应用。在使用过程中,需要注意浏览器兼容性、性能优化和用户体验,始终以用户为中心,提供流畅的自适应体验。

通过本教程的学习,你应该能够:

  • 理解 Resize Observer API 的核心概念和基本用法
  • 实现 Vue 3 组合式函数封装 API
  • 构建响应式仪表板应用
  • 遵循最佳实践,确保应用的性能和兼容性
  • 处理常见问题,提供良好的用户体验

Resize Observer API 是现代前端开发中实现响应式设计的重要工具,它为开发者提供了更多可能性,可以构建出更加灵活和自适应的 web 应用。

« 上一篇 Vue 3 与 Screen Capture API 下一篇 » Vue 3 与 Intersection Observer API 高级应用