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 回调中避免执行昂贵的操作
- 使用防抖/节流:如果需要执行复杂操作,考虑使用防抖或节流
- 及时停止观察:不再需要观察时,及时调用
unobserve或disconnect - 限制观察数量:避免观察过多元素,只观察必要的元素
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 和父窗口之间传递大小信息
进阶学习资源
- MDN 文档:Resize Observer API
- Web.dev 教程:Resize Observer API: Observe Element Resizes
- W3C 规范:Resize Observer
- GitHub 示例:Resize Observer Examples
- Vue 3 组合式 API 文档:Composition API
实战练习
练习:构建响应式仪表板
目标:使用 Vue 3 和 Resize Observer API 构建一个响应式仪表板,包含自适应大小的组件
功能要求:
响应式布局:
- 仪表板组件根据父容器大小自适应调整
- 支持不同屏幕尺寸和设备
- 实现流畅的布局转换
自适应组件:
- 卡片组件根据可用空间调整大小
- 图表组件根据容器大小自动重绘
- 网格布局根据容器宽度调整列数
性能优化:
- 使用 Resize Observer API 监听大小变化
- 实现组件级别的观察
- 优化 resize 回调中的操作
用户体验:
- 平滑的大小过渡动画
- 清晰的视觉层次
- 支持拖拽调整大小
实现步骤:
- 创建一个 Vue 3 项目
- 实现
useResizeObserver组合式函数 - 构建仪表板布局组件
- 实现自适应卡片组件
- 添加图表组件并实现自动重绘
- 测试不同屏幕尺寸和设备
示例代码结构:
<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>进阶学习资源
MDN Web Docs:
Web.dev 文章:
GitHub 仓库:
视频教程:
规范文档:
总结
Resize Observer API 为 web 应用提供了强大的元素大小观察能力,结合 Vue 3 的组合式 API,可以构建出功能丰富、性能优良的响应式应用。在使用过程中,需要注意浏览器兼容性、性能优化和用户体验,始终以用户为中心,提供流畅的自适应体验。
通过本教程的学习,你应该能够:
- 理解 Resize Observer API 的核心概念和基本用法
- 实现 Vue 3 组合式函数封装 API
- 构建响应式仪表板应用
- 遵循最佳实践,确保应用的性能和兼容性
- 处理常见问题,提供良好的用户体验
Resize Observer API 是现代前端开发中实现响应式设计的重要工具,它为开发者提供了更多可能性,可以构建出更加灵活和自适应的 web 应用。