useWindowSize 📐
介绍
想要实时掌握浏览器窗口的尺寸变化吗?useWindowSize 就像一个智能的窗口测量师,时刻监控着视口的宽度和高度!📏
无论是响应式布局、动态调整组件大小,还是根据屏幕尺寸优化用户体验,这个 Hook 都能让你的应用变得更加灵活和智能!
代码演示
基础用法 🚀
让我们从最简单的窗口尺寸监控开始:
html
<template>
<div class="window-size-demo">
<div class="size-display">
<h3>当前窗口尺寸 📐</h3>
<div class="size-info">
<div class="size-item">
<span class="label">宽度:</span>
<span class="value">{{ width }}px</span>
</div>
<div class="size-item">
<span class="label">高度:</span>
<span class="value">{{ height }}px</span>
</div>
<div class="size-item">
<span class="label">比例:</span>
<span class="value">{{ aspectRatio }}</span>
</div>
</div>
</div>
<div class="device-info">
<h4>设备类型: {{ deviceType }} {{ deviceIcon }}</h4>
<p>{{ deviceDescription }}</p>
</div>
<div class="resize-tip">
<p>💡 试试调整浏览器窗口大小,看看数值的实时变化!</p>
</div>
</div>
</template>js
import { computed, watch } from 'vue';
import { useWindowSize } from '@vant/use';
export default {
setup() {
// 获取窗口尺寸
const { width, height } = useWindowSize();
// 计算宽高比
const aspectRatio = computed(() => {
if (height.value === 0) return '0:0';
const ratio = width.value / height.value;
return `${ratio.toFixed(2)}:1`;
});
// 判断设备类型
const deviceType = computed(() => {
if (width.value < 768) return '移动设备';
if (width.value < 1024) return '平板设备';
if (width.value < 1440) return '桌面设备';
return '大屏设备';
});
// 设备图标
const deviceIcon = computed(() => {
if (width.value < 768) return '📱';
if (width.value < 1024) return '📱';
if (width.value < 1440) return '💻';
return '🖥️';
});
// 设备描述
const deviceDescription = computed(() => {
if (width.value < 768) return '适合移动端布局,建议使用单列设计';
if (width.value < 1024) return '适合平板布局,可以使用两列设计';
if (width.value < 1440) return '适合桌面布局,可以使用多列设计';
return '大屏幕设备,可以展示更多内容';
});
// 监听窗口尺寸变化
watch([width, height], ([newWidth, newHeight], [oldWidth, oldHeight]) => {
console.log(`窗口尺寸变化: ${oldWidth}x${oldHeight} -> ${newWidth}x${newHeight}`);
// 可以在这里执行一些响应式逻辑
if (newWidth !== oldWidth) {
console.log('宽度变化,可能需要调整布局');
}
if (newHeight !== oldHeight) {
console.log('高度变化,可能需要调整滚动区域');
}
});
return {
width,
height,
aspectRatio,
deviceType,
deviceIcon,
deviceDescription
};
}
};响应式布局控制 📱
根据窗口尺寸动态调整布局和样式:
html
<template>
<div class="responsive-layout" :class="layoutClass">
<header class="header">
<h1>响应式布局演示</h1>
<div class="layout-info">
当前布局: {{ currentLayout }} ({{ width }}x{{ height }})
</div>
</header>
<main class="main-content">
<aside v-if="showSidebar" class="sidebar">
<h3>侧边栏</h3>
<nav class="nav-menu">
<a href="#" v-for="item in menuItems" :key="item">{{ item }}</a>
</nav>
</aside>
<section class="content">
<div class="content-grid" :style="gridStyle">
<div
v-for="item in contentItems"
:key="item.id"
class="content-card"
>
<h4>{{ item.title }}</h4>
<p>{{ item.description }}</p>
</div>
</div>
</section>
</main>
<footer class="footer">
<p>当前断点: {{ currentBreakpoint }}</p>
</footer>
</div>
</template>js
import { computed, watch } from 'vue';
import { useWindowSize } from '@vant/use';
export default {
setup() {
const { width, height } = useWindowSize();
// 定义断点
const breakpoints = {
xs: 0,
sm: 576,
md: 768,
lg: 992,
xl: 1200,
xxl: 1400
};
// 当前断点
const currentBreakpoint = computed(() => {
if (width.value >= breakpoints.xxl) return 'xxl';
if (width.value >= breakpoints.xl) return 'xl';
if (width.value >= breakpoints.lg) return 'lg';
if (width.value >= breakpoints.md) return 'md';
if (width.value >= breakpoints.sm) return 'sm';
return 'xs';
});
// 当前布局类型
const currentLayout = computed(() => {
switch (currentBreakpoint.value) {
case 'xs':
case 'sm':
return '移动端布局';
case 'md':
return '平板布局';
case 'lg':
case 'xl':
case 'xxl':
return '桌面布局';
default:
return '默认布局';
}
});
// 布局样式类
const layoutClass = computed(() => ({
'layout-mobile': ['xs', 'sm'].includes(currentBreakpoint.value),
'layout-tablet': currentBreakpoint.value === 'md',
'layout-desktop': ['lg', 'xl', 'xxl'].includes(currentBreakpoint.value)
}));
// 是否显示侧边栏
const showSidebar = computed(() => {
return ['md', 'lg', 'xl', 'xxl'].includes(currentBreakpoint.value);
});
// 网格样式
const gridStyle = computed(() => {
let columns = 1;
switch (currentBreakpoint.value) {
case 'sm':
columns = 2;
break;
case 'md':
columns = 2;
break;
case 'lg':
columns = 3;
break;
case 'xl':
case 'xxl':
columns = 4;
break;
}
return {
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: currentBreakpoint.value === 'xs' ? '12px' : '16px'
};
});
// 菜单项
const menuItems = ['首页', '产品', '服务', '关于', '联系'];
// 内容项
const contentItems = Array.from({ length: 12 }, (_, i) => ({
id: i + 1,
title: `内容卡片 ${i + 1}`,
description: `这是第 ${i + 1} 个内容卡片的描述信息。`
}));
// 监听断点变化
watch(currentBreakpoint, (newBreakpoint, oldBreakpoint) => {
console.log(`断点变化: ${oldBreakpoint} -> ${newBreakpoint}`);
// 可以在这里执行断点变化的逻辑
if (newBreakpoint === 'xs' || newBreakpoint === 'sm') {
console.log('切换到移动端布局');
} else if (newBreakpoint === 'md') {
console.log('切换到平板布局');
} else {
console.log('切换到桌面布局');
}
});
return {
width,
height,
currentBreakpoint,
currentLayout,
layoutClass,
showSidebar,
gridStyle,
menuItems,
contentItems
};
}
};图表自适应 📊
根据窗口尺寸动态调整图表大小:
html
<template>
<div class="chart-container">
<h3>自适应图表演示 📊</h3>
<div class="chart-info">
<p>窗口尺寸: {{ width }} x {{ height }}</p>
<p>图表尺寸: {{ chartWidth }} x {{ chartHeight }}</p>
<p>图表比例: {{ chartRatio }}</p>
</div>
<div class="chart-wrapper" :style="chartWrapperStyle">
<div class="chart" :style="chartStyle" ref="chartRef">
<!-- 这里可以放置实际的图表组件 -->
<div class="chart-placeholder">
<div class="chart-title">销售数据图表</div>
<div class="chart-content">
<div
v-for="bar in chartBars"
:key="bar.id"
class="chart-bar"
:style="{ height: bar.height + '%', backgroundColor: bar.color }"
>
<span class="bar-value">{{ bar.value }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="chart-controls">
<button @click="toggleFullscreen" class="control-btn">
{{ isFullscreen ? '退出全屏' : '全屏显示' }}
</button>
<button @click="refreshChart" class="control-btn">
刷新图表
</button>
</div>
</div>
</template>js
import { computed, ref, watch, nextTick } from 'vue';
import { useWindowSize } from '@vant/use';
export default {
setup() {
const { width, height } = useWindowSize();
const chartRef = ref(null);
const isFullscreen = ref(false);
// 计算图表尺寸
const chartWidth = computed(() => {
if (isFullscreen.value) {
return width.value - 40; // 留出边距
}
// 根据窗口宽度计算图表宽度
if (width.value < 768) {
return width.value - 32; // 移动端
} else if (width.value < 1200) {
return Math.min(width.value * 0.8, 800); // 平板和小桌面
} else {
return Math.min(width.value * 0.6, 1000); // 大桌面
}
});
const chartHeight = computed(() => {
if (isFullscreen.value) {
return height.value - 120; // 留出标题和控制按钮空间
}
// 根据图表宽度计算高度,保持合适的宽高比
return Math.min(chartWidth.value * 0.6, 400);
});
// 图表比例
const chartRatio = computed(() => {
const ratio = chartWidth.value / chartHeight.value;
return `${ratio.toFixed(2)}:1`;
});
// 图表容器样式
const chartWrapperStyle = computed(() => ({
width: chartWidth.value + 'px',
height: chartHeight.value + 'px',
margin: '0 auto',
transition: 'all 0.3s ease'
}));
// 图表样式
const chartStyle = computed(() => ({
width: '100%',
height: '100%',
border: '1px solid #e0e0e0',
borderRadius: '8px',
padding: '16px',
backgroundColor: '#fff',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
}));
// 图表数据
const chartBars = ref([
{ id: 1, value: 85, height: 85, color: '#ff6b6b' },
{ id: 2, value: 92, height: 92, color: '#4ecdc4' },
{ id: 3, value: 78, height: 78, color: '#45b7d1' },
{ id: 4, value: 96, height: 96, color: '#f9ca24' },
{ id: 5, value: 73, height: 73, color: '#6c5ce7' },
{ id: 6, value: 88, height: 88, color: '#fd79a8' }
]);
// 切换全屏
const toggleFullscreen = () => {
isFullscreen.value = !isFullscreen.value;
};
// 刷新图表
const refreshChart = () => {
chartBars.value = chartBars.value.map(bar => ({
...bar,
value: Math.floor(Math.random() * 100) + 1,
height: Math.floor(Math.random() * 100) + 1
}));
};
// 监听窗口尺寸变化,重新计算图表
watch([width, height], async () => {
console.log('窗口尺寸变化,重新计算图表尺寸');
// 等待 DOM 更新
await nextTick();
// 这里可以调用图表库的 resize 方法
if (chartRef.value) {
console.log('图表重新渲染');
}
});
// 监听全屏状态变化
watch(isFullscreen, (newValue) => {
if (newValue) {
console.log('进入全屏模式');
document.body.style.overflow = 'hidden';
} else {
console.log('退出全屏模式');
document.body.style.overflow = '';
}
});
return {
width,
height,
chartWidth,
chartHeight,
chartRatio,
chartWrapperStyle,
chartStyle,
chartBars,
chartRef,
isFullscreen,
toggleFullscreen,
refreshChart
};
}
};虚拟滚动优化 📜
根据窗口高度优化虚拟滚动的可见项数量:
html
<template>
<div class="virtual-scroll-demo">
<h3>虚拟滚动演示 📜</h3>
<div class="scroll-info">
<p>窗口高度: {{ height }}px</p>
<p>可见项数: {{ visibleCount }}</p>
<p>总项数: {{ totalItems }}</p>
<p>项目高度: {{ itemHeight }}px</p>
</div>
<div
class="virtual-scroll-container"
:style="containerStyle"
@scroll="handleScroll"
ref="scrollContainer"
>
<div class="virtual-scroll-content" :style="contentStyle">
<div
v-for="item in visibleItems"
:key="item.id"
class="virtual-scroll-item"
:style="getItemStyle(item.index)"
>
<div class="item-content">
<h4>项目 {{ item.id }}</h4>
<p>{{ item.content }}</p>
<small>索引: {{ item.index }}</small>
</div>
</div>
</div>
</div>
<div class="scroll-controls">
<button @click="scrollToTop" class="control-btn">
回到顶部
</button>
<button @click="scrollToBottom" class="control-btn">
滚动到底部
</button>
<button @click="addItems" class="control-btn">
添加更多项目
</button>
</div>
</div>
</template>js
import { computed, ref, watch, nextTick } from 'vue';
import { useWindowSize } from '@vant/use';
export default {
setup() {
const { width, height } = useWindowSize();
const scrollContainer = ref(null);
const scrollTop = ref(0);
const itemHeight = 80; // 每个项目的高度
const totalItems = ref(1000); // 总项目数
// 计算容器高度(基于窗口高度)
const containerHeight = computed(() => {
// 为移动端预留更多空间
if (width.value < 768) {
return Math.min(height.value * 0.6, 400);
} else {
return Math.min(height.value * 0.7, 500);
}
});
// 计算可见项目数量
const visibleCount = computed(() => {
return Math.ceil(containerHeight.value / itemHeight) + 2; // 多渲染2个作为缓冲
});
// 计算开始索引
const startIndex = computed(() => {
return Math.floor(scrollTop.value / itemHeight);
});
// 计算结束索引
const endIndex = computed(() => {
return Math.min(startIndex.value + visibleCount.value, totalItems.value);
});
// 生成可见项目
const visibleItems = computed(() => {
const items = [];
for (let i = startIndex.value; i < endIndex.value; i++) {
items.push({
id: i + 1,
index: i,
content: `这是第 ${i + 1} 个项目的内容。窗口尺寸: ${width.value}x${height.value}`
});
}
return items;
});
// 容器样式
const containerStyle = computed(() => ({
height: containerHeight.value + 'px',
overflow: 'auto',
border: '1px solid #e0e0e0',
borderRadius: '8px',
backgroundColor: '#f9f9f9'
}));
// 内容样式(设置总高度以支持滚动)
const contentStyle = computed(() => ({
height: totalItems.value * itemHeight + 'px',
position: 'relative'
}));
// 获取项目样式
const getItemStyle = (index) => ({
position: 'absolute',
top: index * itemHeight + 'px',
left: '0',
right: '0',
height: itemHeight + 'px',
padding: '12px',
borderBottom: '1px solid #e0e0e0',
backgroundColor: '#fff'
});
// 处理滚动事件
const handleScroll = (event) => {
scrollTop.value = event.target.scrollTop;
};
// 滚动到顶部
const scrollToTop = () => {
if (scrollContainer.value) {
scrollContainer.value.scrollTop = 0;
}
};
// 滚动到底部
const scrollToBottom = () => {
if (scrollContainer.value) {
scrollContainer.value.scrollTop = totalItems.value * itemHeight;
}
};
// 添加更多项目
const addItems = () => {
totalItems.value += 100;
console.log(`添加了100个项目,总数: ${totalItems.value}`);
};
// 监听窗口尺寸变化,重新计算虚拟滚动
watch([width, height], async () => {
console.log('窗口尺寸变化,重新计算虚拟滚动参数');
await nextTick();
// 重新计算可见项目
console.log(`新的可见项目数: ${visibleCount.value}`);
console.log(`新的容器高度: ${containerHeight.value}px`);
});
return {
width,
height,
containerHeight,
visibleCount,
totalItems,
itemHeight,
visibleItems,
containerStyle,
contentStyle,
scrollContainer,
getItemStyle,
handleScroll,
scrollToTop,
scrollToBottom,
addItems
};
}
};性能监控面板 ⚡
创建一个性能监控面板,显示窗口尺寸相关的性能指标:
html
<template>
<div class="performance-monitor">
<h3>性能监控面板 ⚡</h3>
<div class="monitor-grid">
<div class="monitor-card">
<h4>窗口信息 📐</h4>
<div class="metric">
<span class="label">当前尺寸:</span>
<span class="value">{{ width }} x {{ height }}</span>
</div>
<div class="metric">
<span class="label">设备像素比:</span>
<span class="value">{{ devicePixelRatio }}</span>
</div>
<div class="metric">
<span class="label">屏幕尺寸:</span>
<span class="value">{{ screenWidth }} x {{ screenHeight }}</span>
</div>
</div>
<div class="monitor-card">
<h4>性能指标 📊</h4>
<div class="metric">
<span class="label">调整次数:</span>
<span class="value">{{ resizeCount }}</span>
</div>
<div class="metric">
<span class="label">平均间隔:</span>
<span class="value">{{ averageInterval }}ms</span>
</div>
<div class="metric">
<span class="label">最后调整:</span>
<span class="value">{{ lastResizeTime }}</span>
</div>
</div>
<div class="monitor-card">
<h4>布局统计 📱</h4>
<div class="metric">
<span class="label">移动端:</span>
<span class="value">{{ layoutStats.mobile }}次</span>
</div>
<div class="metric">
<span class="label">平板:</span>
<span class="value">{{ layoutStats.tablet }}次</span>
</div>
<div class="metric">
<span class="label">桌面:</span>
<span class="value">{{ layoutStats.desktop }}次</span>
</div>
</div>
</div>
<div class="monitor-actions">
<button @click="resetStats" class="action-btn">
重置统计
</button>
<button @click="exportStats" class="action-btn">
导出数据
</button>
</div>
</div>
</template>js
import { computed, ref, watch } from 'vue';
import { useWindowSize } from '@vant/use';
export default {
setup() {
const { width, height } = useWindowSize();
// 性能统计数据
const resizeCount = ref(0);
const resizeTimes = ref([]);
const layoutStats = ref({
mobile: 0,
tablet: 0,
desktop: 0
});
// 设备信息
const devicePixelRatio = ref(window.devicePixelRatio || 1);
const screenWidth = ref(window.screen.width);
const screenHeight = ref(window.screen.height);
// 计算平均调整间隔
const averageInterval = computed(() => {
if (resizeTimes.value.length < 2) return 0;
const intervals = [];
for (let i = 1; i < resizeTimes.value.length; i++) {
intervals.push(resizeTimes.value[i] - resizeTimes.value[i - 1]);
}
const sum = intervals.reduce((a, b) => a + b, 0);
return Math.round(sum / intervals.length);
});
// 最后调整时间
const lastResizeTime = computed(() => {
if (resizeTimes.value.length === 0) return '无';
const lastTime = resizeTimes.value[resizeTimes.value.length - 1];
return new Date(lastTime).toLocaleTimeString();
});
// 当前设备类型
const currentDeviceType = computed(() => {
if (width.value < 768) return 'mobile';
if (width.value < 1024) return 'tablet';
return 'desktop';
});
// 监听窗口尺寸变化
watch([width, height], () => {
// 更新统计数据
resizeCount.value++;
resizeTimes.value.push(Date.now());
// 限制记录数量,避免内存泄漏
if (resizeTimes.value.length > 100) {
resizeTimes.value = resizeTimes.value.slice(-50);
}
// 更新布局统计
layoutStats.value[currentDeviceType.value]++;
console.log(`窗口调整 #${resizeCount.value}: ${width.value}x${height.value}`);
});
// 重置统计
const resetStats = () => {
resizeCount.value = 0;
resizeTimes.value = [];
layoutStats.value = {
mobile: 0,
tablet: 0,
desktop: 0
};
console.log('统计数据已重置');
};
// 导出统计数据
const exportStats = () => {
const stats = {
windowSize: { width: width.value, height: height.value },
deviceInfo: {
pixelRatio: devicePixelRatio.value,
screenSize: { width: screenWidth.value, height: screenHeight.value }
},
performance: {
resizeCount: resizeCount.value,
averageInterval: averageInterval.value,
layoutStats: layoutStats.value
},
timestamp: new Date().toISOString()
};
console.log('性能统计数据:', stats);
// 可以将数据导出为 JSON 文件
const blob = new Blob([JSON.stringify(stats, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `window-size-stats-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
};
return {
width,
height,
devicePixelRatio,
screenWidth,
screenHeight,
resizeCount,
averageInterval,
lastResizeTime,
layoutStats,
resetStats,
exportStats
};
}
};API 参考 📚
类型定义
ts
function useWindowSize(): {
width: Ref<number>;
height: Ref<number>;
};返回值
| 参数 | 说明 | 类型 |
|---|---|---|
| width | 浏览器窗口的视口宽度,响应窗口大小变化 | Ref<number> |
| height | 浏览器窗口的视口高度,响应窗口大小变化 | Ref<number> |
实际应用场景 🎯
1. 响应式设计
- 断点管理: 根据窗口宽度切换不同的布局断点
- 组件适配: 动态调整组件的显示方式和尺寸
- 导航优化: 在小屏幕上切换到移动端导航
2. 图表可视化
- 图表自适应: 根据容器尺寸动态调整图表大小
- 数据密度: 根据屏幕尺寸调整数据显示密度
- 交互优化: 在不同尺寸下提供不同的交互方式
3. 虚拟滚动
- 可见项计算: 根据容器高度计算虚拟滚动的可见项数
- 性能优化: 动态调整渲染策略以适应不同屏幕
- 内存管理: 根据窗口尺寸优化内存使用
4. 游戏开发
- 画布适配: 动态调整游戏画布尺寸
- UI缩放: 根据屏幕尺寸调整游戏UI元素
- 性能调节: 根据窗口大小调整渲染质量
最佳实践 💡
1. 防抖处理
js
import { debounce } from 'lodash-es';
// 对窗口尺寸变化进行防抖处理
const debouncedResize = debounce(() => {
console.log('窗口尺寸变化处理');
}, 100);
watch([width, height], debouncedResize);2. 断点管理
js
// 定义标准断点
const breakpoints = {
xs: 0,
sm: 576,
md: 768,
lg: 992,
xl: 1200,
xxl: 1400
};
const currentBreakpoint = computed(() => {
const w = width.value;
if (w >= breakpoints.xxl) return 'xxl';
if (w >= breakpoints.xl) return 'xl';
if (w >= breakpoints.lg) return 'lg';
if (w >= breakpoints.md) return 'md';
if (w >= breakpoints.sm) return 'sm';
return 'xs';
});3. 性能优化
js
// 使用 computed 缓存计算结果
const isDesktop = computed(() => width.value >= 1024);
const isMobile = computed(() => width.value < 768);
// 避免在 watch 中执行昂贵操作
watch([width, height], async () => {
await nextTick();
// 执行必要的 DOM 操作
});4. 内存管理
js
// 清理定时器和事件监听器
onUnmounted(() => {
// 清理相关资源
if (resizeTimer) {
clearTimeout(resizeTimer);
}
});调试技巧 🔧
1. 尺寸变化日志
js
watch([width, height], ([newW, newH], [oldW, oldH]) => {
console.log(`尺寸变化: ${oldW}x${oldH} -> ${newW}x${newH}`);
console.log(`变化量: 宽度${newW - oldW}, 高度${newH - oldH}`);
});2. 性能监控
js
let resizeCount = 0;
let lastResizeTime = Date.now();
watch([width, height], () => {
resizeCount++;
const now = Date.now();
const interval = now - lastResizeTime;
console.log(`调整 #${resizeCount}, 间隔: ${interval}ms`);
lastResizeTime = now;
});3. 断点调试
js
// 在开发环境中添加断点信息
if (process.env.NODE_ENV === 'development') {
window.debugWindowSize = {
width,
height,
breakpoint: currentBreakpoint
};
}性能优化 ⚡
1. 减少重新计算
js
// 使用 computed 缓存复杂计算
const layoutConfig = computed(() => {
// 复杂的布局计算逻辑
return calculateLayout(width.value, height.value);
});2. 批量更新
js
// 使用 nextTick 批量处理 DOM 更新
watch([width, height], async () => {
await nextTick();
// 批量更新 DOM
updateLayout();
});3. 条件渲染
js
// 根据屏幕尺寸条件渲染组件
const shouldRenderSidebar = computed(() => width.value >= 768);
const shouldRenderMobileMenu = computed(() => width.value < 768);浏览器兼容性 🌐
useWindowSize 基于标准的 window.innerWidth 和 window.innerHeight API:
- Chrome 1+
- Firefox 1+
- Safari 3+
- Edge 12+
- IE 9+
相关文档 📖
核心概念
- Window.innerWidth - MDN 文档
- Window.innerHeight - MDN 文档
- Resize Observer API - 元素尺寸监听
相关 Hooks
- useElementSize - 元素尺寸监听
- useBreakpoints - 断点管理
- useMediaQuery - 媒体查询
响应式设计
- CSS 媒体查询 - CSS 媒体查询
- Bootstrap 断点 - Bootstrap 断点系统
- Tailwind CSS 响应式 - Tailwind 响应式设计
性能优化
- 防抖和节流 - Lodash 防抖函数
- 虚拟滚动 - Vue 虚拟滚动
- Intersection Observer - 交叉观察器
实际应用
- Element Plus Layout - 布局组件
- Ant Design Vue Grid - 栅格系统
- Vant Layout - 移动端布局
进阶主题
- CSS Container Queries - 容器查询
- Web Components - Web 组件
- PWA 响应式设计 - PWA 响应式设计