airi 项目中的 VueUse useDevicePixelRatio 深入指南:响应式追踪 DPR 与高分屏渲染实践
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
本篇技术指南围绕 VueUse 的useDevicePixelRatiocomposable 展开,讲解如何在 Vue 3 / Nuxt 3 应用中响应式追踪window.devicePixelRatio(DPR)的变化,并解释其基于matchMedia的实现原理。结合当前仓库 airi 中 MMD 渲染、画布绘制等高分屏适配场景,读者将掌握 DPR 追踪的完整用法、组件式调用方式,以及在高清渲染管线中的实际落地技巧。
什么是useDevicePixelRatio
useDevicePixelRatio是 VueUse 中归类于Sensors(传感器)类别的一个 composable,它的功能是:
Reactively track
window.devicePixelRatio—— 响应式追踪浏览器窗口的物理像素与 CSS 像素之比。
在 airi 仓库中,这个 composable 被记录在技能库 vueuse-functions/SKILL.md 的函数清单中(Sensors 分类,Invocation 规则为AUTO),作为 Vue 3 / Nuxt 项目开发时优先选用的现成工具:遇到与设备像素比、视口密度相关的需求时,应先检查是否可用该 composable 实现,而不是手写自定义代码。
为什么需要追踪 DPR
window.devicePixelRatio表示设备物理像素与 CSS 像素的比值:
- 普通屏幕上该值通常为
1; - Retina / HiDPI 屏幕上通常为
2(或更高,如部分 Android 旗舰机的3); - 当用户在浏览器中执行Ctrl/Cmd + 缩放页面时,该值同样会发生变化(缩放 125% 时约为
1.25)。
DPR 直接影响一切需要"按物理像素绘制"的场景:<canvas>高清绘制、WebGL 渲染器分辨率(如 three.js 的renderer.setPixelRatio)、图片清晰度选择、SVG/位图资源切换等。如果忽视 DPR,高分屏上会出现模糊、锯齿或资源浪费。
追踪 DPR 的难点
window.devicePixelRatio的一个关键特性是:浏览器没有为它提供任何变更事件(event listener)。你无法像监听resize那样直接监听 DPR 变化。这正是useDevicePixelRatio存在的意义——它通过以下方式解决:
NOTE: there is no event listener for
window.devicePixelRatiochange. So this function usesTesting media queries programmatically (window.matchMedia)applying the same mechanism as described in this example.
即:程序化测试媒体查询(window.matchMedia),采用与 MDN 官方 "Monitoring screen resolution or zoom level changes" 示例相同的机制。核心思路是:
- 通过
window.matchMedia()查询一组针对不同 DPR 的媒体查询(如(resolution: 1dppx)、(resolution: 2dppx)、(resolution: 3dppx)等); - 为这些
MediaQueryList注册change事件监听器; - 当任何一条媒体查询的匹配状态发生翻转(即 DPR 跨越了某个档位),说明 DPR 已变化,此时重新读取
window.devicePixelRatio并更新响应式状态。
由于缩放 / 切换屏幕 / 拖动窗口到不同 DPR 的显示器时,分辨率媒体查询会随之变化,这一机制可以可靠地感知 DPR 的变更。
基本用法
函数式调用(Composition API)
在<script setup>中直接调用即可:
import { useDevicePixelRatio } from '@vueuse/core' const { pixelRatio } = useDevicePixelRatio()pixelRatio是一个响应式的 ref,读取其.value(在模板中直接使用)即可获得当前 DPR:
<script setup lang="ts"> import { useDevicePixelRatio } from '@vueuse/core' const { pixelRatio } = useDevicePixelRatio() </script> <template> <p>当前设备像素比:{{ pixelRatio }}</p> </template>组件式调用(Component Usage)
VueUse 为useDevicePixelRatio同时提供了配套的<UseDevicePixelRatio>渲染组件,通过作用域插槽(scoped slot)暴露pixelRatio:
<template> <UseDevicePixelRatio v-slot="{ pixelRatio }"> Pixel Ratio: {{ pixelRatio }} </UseDevicePixelRatio> </template>两种方式返回的pixelRatio都是响应式的,DPR 变化时视图会自动更新,无需手动刷新。
类型声明与返回值解析
参考文档给出了完整的类型签名,下面逐项拆解其含义:
export interface UseDevicePixelRatioOptions extends ConfigurableWindow {} export interface UseDevicePixelRatioReturn { pixelRatio: Readonly<ShallowRef<number>> stop: WatchStopHandle } /** * Reactively track `window.devicePixelRatio`. * * @see https://vueuse.org/useDevicePixelRatio * * @__NO_SIDE_EFFECTS__ */ export declare function useDevicePixelRatio( options?: UseDevicePixelRatioOptions, ): UseDevicePixelRatioReturnUseDevicePixelRatioOptions
选项类型继承自 VueUse 的ConfigurableWindow,即包含一个可选的window字段:
interface ConfigurableWindow { window?: Window }window(可选):指定要绑定的window对象。默认使用当前全局window。该参数的主要用途有两个:- SSR 场景下注入自定义窗口实现(如
jsdom或测试环境模拟对象),避免在服务端访问不存在的window; - 多窗口 / iframe 场景下指定目标窗口(例如在 Electron 多窗口渲染器或跨 iframe 的 UI 中追踪特定窗口的 DPR)。
- SSR 场景下注入自定义窗口实现(如
调用方式:
const { pixelRatio } = useDevicePixelRatio({ window: someIframe.contentWindow })不传任何选项时直接调用useDevicePixelRatio()即可,默认使用window。
UseDevicePixelRatioReturn返回值
| 返回值 | 类型 | 说明 |
|---|---|---|
pixelRatio | Readonly<ShallowRef<number>> | 当前设备像素比。只读的浅层 ref(ShallowRef),DPR 变化时自动更新;只读保证你无法意外改写该值 |
stop | WatchStopHandle | 停止追踪的句柄。调用stop()后会解除内部注册的matchMediachange 监听器,停止后续更新 |
典型的手动停止场景:
const { pixelRatio, stop } = useDevicePixelRatio() // 某些情况下主动停止追踪 stop()由于该 composable 被标记为@__NO_SIDE_EFFECTS__,它本身不产生全局副作用,停止监听后不会留下残留事件处理器,适合在组件卸载或功能生命周期结束时调用stop()做清理。
在 airi 仓库中的高分屏渲染实践
useDevicePixelRatio的价值在真实的渲染管线中体现得最为充分。airi 仓库中虽然没有直接 import 该 composable(多数地方使用原生window.devicePixelRatio快速取值),但多处代码展示了相同的 DPR 适配思路,可以直接映射到useDevicePixelRatio的响应式用法上。
1. three.js / MMD 渲染器的像素比设置
在 packages/stage-ui-mmd/src/components/scenes/MMD.vue 中,渲染器通过 DPR 与用户渲染缩放系数共同决定输出分辨率:
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2) * renderScale.value)要点:
Math.min(window.devicePixelRatio, 2)将 DPR 封顶在2,避免在超高 DPR 设备(如3)上因分辨率过高造成 GPU 压力过大、帧率骤降;- 再乘以
renderScale.value(用户可调的渲染质量系数),实现"清晰度 × 性能"的平衡; - 该文件在
watch(renderScale, ...)中重新调用setPixelRatio(L528-L531),说明当影响渲染分辨率的参数变化时需要重新设置像素比——这正是useDevicePixelRatio的响应式特性可以顺理成章接入的地方:
import { useDevicePixelRatio } from '@vueuse/core' const { pixelRatio } = useDevicePixelRatio() watchEffect(() => { renderer.setPixelRatio(Math.min(pixelRatio.value, 2) * renderScale.value) })这样当用户缩放页面或将窗口拖到 DPR 不同的显示器时,渲染分辨率会自动跟随,无需手动监听。
2. 画布高分屏绘制
在 packages/stage-pages/src/pages/devtools/beat-sync.vue 中,画布绘制前会按 DPR 校准物理尺寸:
const dpr = window.devicePixelRatio || 1 const { clientWidth, clientHeight } = canvas if (canvas.width !== clientWidth * dpr || canvas.height !== clientHeight * dpr) { // 按 dpr 重设画布物理尺寸,避免高分屏模糊 }这正是经典的高分屏 canvas 适配模式:将画布的width/height(物理像素)设为 CSS 尺寸乘以 DPR,绘制时再按 DPR 缩放坐标系。useDevicePixelRatio可以将这里的dpr变成响应式值,画布尺寸重算逻辑放入watch(pixelRatio, ...)即可实现缩放时自动重绘。
3. 渲染质量的持久化配置
在 packages/stage-ui-three/src/stores/model-store.ts 中,默认渲染缩放直接取自 DPR:
const renderScale = useLocalStorage('settings/stage-ui-three/renderScale', Math.min(window.devicePixelRatio, 2))这里展示了项目中的通用约定:默认渲染质量与 DPR 挂钩,但封顶为2。如果你的代码需要在初始化时获取一次 DPR(如设置持久化默认值),使用useDevicePixelRatio()的pixelRatio初始值即可,同时还能在之后保持响应式:
const { pixelRatio } = useDevicePixelRatio() const renderScale = useLocalStorage('settings/stage-ui-three/renderScale', () => Math.min(pixelRatio.value, 2))适用场景与选择建议
结合useDevicePixelRatio的特性和 airi 项目实践,推荐在以下场景使用它:
| 场景 | 说明 | 推荐方式 |
|---|---|---|
| Canvas 2D 高清绘制 | 画布物理尺寸 = CSS 尺寸 × DPR,缩放时重绘 | 函数式 +watch(pixelRatio) |
| WebGL / three.js 渲染 | renderer.setPixelRatio()随 DPR 更新 | 函数式 +watchEffect |
| 模板中展示/切换 DPR 相关 UI | 如"当前缩放比例"提示条、画质档位自动切换 | 组件式<UseDevicePixelRatio> |
| 需要手动控制生命周期 | 明确管理监听器启停 | 函数式,手动调用stop() |
| SSR / 测试环境 | 注入自定义window | 传{ window }选项 |
与相关 VueUse 函数的关系
useDevicePixelRatio属于 VueUse Sensors 分类,与它常搭配使用的还有:
useWindowSize:响应式窗口尺寸,与 DPR 组合可完整描述视口物理像素(物理宽 = width × DPR);useMediaQuery/useBreakpoints:同样是基于matchMedia的响应式封装,useDevicePixelRatio在底层实现上与它们共享"媒体查询即响应式信号"的机制;useSSRWidth:在 SSR 场景下设置全局视口宽度,供依赖视口的 composable 在服务端渲染时使用,useDevicePixelRatio在 SSR 下也需要类似的外部窗口注入策略。
从技能库 SKILL.md 的说明来看,这些函数在 Vue 3 / Nuxt 3 项目中均标记为AUTO(自动优先使用),意味着遇到对应需求时应当优先选择 VueUse 现成实现,以保持代码简洁、可维护且高性能。
注意事项
- SSR 兼容性:
useDevicePixelRatio依赖浏览器window。在服务端渲染(Nuxt 的setup阶段)调用时,需要传入模拟的window对象(ConfigurableWindow),或确保只在客户端组件中调用。 - 性能开销极小:底层只是注册若干
matchMedia的change监听,DPR 不变时不产生额外计算;配合ShallowRef(浅层 ref)返回值,避免了对number这类原始值的多余深度响应开销。 stop的语义:返回值中的stop是WatchStopHandle,调用后 DPR 将停止更新。在组合式函数内部使用时,交给tryOnScopeDispose或在onUnmounted中调用是更稳妥的清理方式。- DPR 与缩放的区别:DPR 变化可能来自显示器切换、窗口跨屏拖动或浏览器页面缩放,
useDevicePixelRatio统一以最终window.devicePixelRatio为准,无需区分来源。
总结
useDevicePixelRatio用最少的代码解决了"浏览器不提供 DPR 变更事件"这一平台限制:通过window.matchMedia程序化探测分辨率档位变化,配合 Vue 的响应式系统,把 DPR 变成可持续追踪的信号。在 airi 这类重度依赖高分屏渲染的桌面/Web 应用中,无论是 three.js 渲染器的setPixelRatio、canvas 的高清绘制,还是渲染质量默认值推导,都能借助它在"清晰度"与"性能"之间做出响应式的动态取舍。掌握函数式与组件式两种调用形态,理解其ConfigurableWindow选项与stop生命周期语义,即可在自己的 Vue 3 / Nuxt 3 项目中直接落地这套高分屏适配方案。
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考