问题场景
页面顶部的导航栏右侧有一组操作图标(收藏、历史、动态、消息等),每个图标都带有文字标签。当用户使用 Ctrl+滚轮缩放页面时,设计上要求在超过 100% 缩放时自动隐藏这些文字标签,只保留图标,以免布局错乱。
现有方案的隐藏阈值不稳定------有时缩放 110% 才触发,有时 90% 就触发了。
根本需求是 精准 100% 触发:100% 缩放时正常显示,一旦超过 100% 立即隐藏。
尝试过的方案
方案一:DPR 比率
js
// ❌ 不可靠
const checkZoom = () => {
const currentDPR = window.devicePixelRatio
const ratio = currentDPR / initialDPR
isZoomed.value = ratio >= threshold
}
致命问题:
-
devicePixelRatio在缩放时存在过渡期乱值,不稳定 -
初始基线
initialDPR的捕获时机不可控,页面加载时如果浏览器已处于缩放状态,基线就偏了 -
不同显示器(Retina / 普通屏 / 系统缩放)的 DPR 基数不同,无法通用
-
resize事件触发时 DPR 尚未稳定,导致比值忽高忽低
方案二:visualViewport.scale
js
// ❌ 桌面端无效
const scale = window.visualViewport.scale
visualViewport.scale 只反映移动端的双指捏合缩放,桌面端 Ctrl+滚轮缩放时该值恒为 1,完全无法检测。
通用解决方案:outerWidth / innerWidth 比率
js
const zoomRatio = window.outerWidth / window.innerWidth
const shouldHide = zoomRatio > 1.0
原理
Ctrl+滚轮缩放时:
-
window.innerWidth会随页面缩放而变化(放大→数值变小,缩小→数值变大) -
window.outerWidth保持不变(浏览器窗口的物理宽度不变)
两者的比值 outerWidth / innerWidth 就近似等于当前缩放倍数:
| 缩放 | innerWidth | outerWidth | 比值 |
|---|---|---|---|
| 100% | 1440 | 1440 | 1.0 |
| 110% | 1309 | 1440 | 1.1 |
| 90% | 1600 | 1440 | 0.9 |
比值 > 1.0 说明页面被放大,触发隐藏逻辑。
前端框架集成示例
Vue 3 组合式 API
vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const ZOOM_THRESHOLD = 1.0
const isZoomed = ref(false)
// 防抖函数
const debounce = (fn, delay) => {
let timer = null
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
}
// 核心检测逻辑
const checkZoom = () => {
const ratio = window.outerWidth / window.innerWidth
isZoomed.value = ratio > ZOOM_THRESHOLD
}
// 防抖版本
const debouncedCheckZoom = debounce(checkZoom, 100)
// 事件绑定
const events = ['resize', 'wheel', 'keydown', 'visualViewport.resize']
onMounted(() => {
events.forEach(event => {
window.addEventListener(event, debouncedCheckZoom)
})
checkZoom() // 初始检测
})
onUnmounted(() => {
events.forEach(event => {
window.removeEventListener(event, debouncedCheckZoom)
})
})
</script>
<template>
<div class="nav-icons">
<div class="nav-item">
<Icon />
<span v-if="!isZoomed" class="label">收藏</span>
</div>
<!-- 更多图标... -->
</div>
</template>
React Hooks
tsx
import { useState, useEffect, useCallback } from 'react'
const ZOOM_THRESHOLD = 1.0
const useZoomDetection = () => {
const [isZoomed, setIsZoomed] = useState(false)
const checkZoom = useCallback(() => {
const ratio = window.outerWidth / window.innerWidth
setIsZoomed(ratio > ZOOM_THRESHOLD)
}, [])
useEffect(() => {
let timer: number | null = null
const handler = () => {
clearTimeout(timer!)
timer = setTimeout(checkZoom, 100)
}
const events = ['resize', 'wheel', 'keydown']
events.forEach(evt => window.addEventListener(evt, handler))
checkZoom()
return () => {
events.forEach(evt => window.removeEventListener(evt, handler))
clearTimeout(timer!)
}
}, [checkZoom])
return isZoomed
}
// 在组件中使用
const NavIcons = () => {
const isZoomed = useZoomDetection()
return (
<div className="nav-icons">
<div className="nav-item">
<Icon />
{!isZoomed && <span className="label">收藏</span>}
</div>
</div>
)
}
关键设计要点
1. 无需初始基线
直接读取实时比值,没有基线捕获时机的问题,彻底避免 DPR 方案中的基线漂移。
2. 统一防抖
所有事件(resize、wheel、keydown、visualViewport.resize)都走同一个防抖函数,延迟 100ms,避免在缩放过渡期读取不稳定的中间值。
3. 严格阈值判断
使用 > 而非 >=:100% 时比值 = 1.0,不触发;超过 1.0 才触发。
阈值配置
js
// 可配置的阈值
const ZOOM_THRESHOLD = 1.0 // 100%
// 按需调整:1.05 表示 105% 以上才触发隐藏
跨端注意
-
上述方案基于桌面端 Ctrl+滚轮缩放场景设计
-
移动端双指缩放可用
visualViewport.scale检测,与桌面端分开处理 -
本方案无需
visualViewportAPI,兼容性更好
相关 API
| API | 用途 | 桌面端缩放 | 移动端缩放 |
|---|---|---|---|
window.outerWidth |
浏览器窗口物理宽度 | 不变 | 不变 |
window.innerWidth |
视口 CSS 像素宽度 | 缩放时变化 | 缩放时变化 |
window.devicePixelRatio |
物理像素与 CSS 像素比 | 变化不稳定 | 变化不稳定 |
visualViewport.scale |
视口缩放比例 | 恒为 1 | 双指缩放变化 |
总结
outerWidth / innerWidth 比率法是检测桌面端浏览器缩放最可靠的方式,无基线漂移、无过渡期乱值、跨显示器通用。配合防抖处理,可实现精准的 100% 阈值触发。