目录
[一、了解DPR(Device Pixel Ratio)设备像素比](#一、了解DPR(Device Pixel Ratio)设备像素比)
[2、DPR所造成的问题 --- 图像显示问题](#2、DPR所造成的问题 — 图像显示问题)
[2、如果是img 元素](#2、如果是img 元素)
[A. 跨屏协同与窗口拖拽](#A. 跨屏协同与窗口拖拽)
[B. 地图与可视化大屏](#B. 地图与可视化大屏)
[C. 浏览器缩放调试 (Zoom)](#C. 浏览器缩放调试 (Zoom))
[D. 移动端省流量模式](#D. 移动端省流量模式)
[A. 监听 DPR 的变化](#A. 监听 DPR 的变化)
[B. 动态修改 src 或 srcset (强制重绘)](#B. 动态修改 src 或 srcset (强制重绘))
[A. 描述](#A. 描述)
[B. 实现](#B. 实现)
一、了解DPR(Device Pixel Ratio)设备像素比
1、基本概念
DPR = 物理像素 (设备的像素)/ 逻辑像素(css中设置的px像素)
你可以在浏览器中输入window.devicePixelRatio来查看当前的DPR。
javascript
// 获取当前设备的DPR
const dpr = window.devicePixelRatio;
console.log(`设备像素比: ${dpr}`);
2、DPR所造成的问题 --- 图像显示问题
比如:我们在css中设置了100px宽度的图片,但是设备像素比为2的话,实际需要的是200px宽度的图片,这个时候图片只能被放大,就会导致图片模糊。
html
<img src="image-100x100.png" width="100" height="100">
二、如何解决DPR所造成的图片模糊
1、如果是背景图
我们在css选择器中使用background来设置,但是-webkit-image-set有兼容性问题
css
.container{
backgaround: -webkit-image-set(
url(./imgs/iamge-1x.png) 1x,
url(./imgs/iamge-2x.png) 2x,
url(./imgs/iamge-3x.png) 3x,
url(./imgs/iamge-4x.png) 4x
)
no-repeat;
background-size: 100% auto;
}
2、如果是 img 元素
可以用<img>标签的srcset属性,但是只能刷新首次加载,不能动态变化(就是不能随着浏览器放大缩小而变化)。
html
<img
src="image-1x.jpg"
srcset="image-1x.jpg 1x, image-2x.jpg 2x, image-3x.jpg 3x"
alt="响应式图片"
>
3、如果想要动态加载img图片
(1)使用场景
首先你得想清楚,什么情况下使用它:
A. 跨屏协同与窗口拖拽
场景:专业设计师或程序员通常有多个显示器(一个笔记本 2x 屏,一个外接 1x 屏)
痛点:当用户把 Chrome 窗口从高清屏拖到普通屏时,如果图片不降级,会浪费显存;反之,从普通屏拖到高清屏,图片会瞬间变得模糊。动态更新能保证图片始终"丝滑"。
B. 地图与可视化大屏
场景:Echarts 图表、Google Maps 或 3D 渲染页面。
痛点:这些场景对像素对齐(Pixel Perfection)要求极高。DPR 变化会导致文字发虚、线条断裂。
C. 浏览器缩放调试 (Zoom)
场景 :用户使用 Ctrl + 放大网页时,浏览器的 devicePixelRatio 实际上是会随之改变的。
痛点:如果图片不随缩放动态加载更高清的版本,放大后的网页图片会充满锯齿感。
D. 移动端省流量模式
场景 :在弱网下,通过 JS 动态将全局"逻辑 DPR"降为 1,强制所有图片加载 1x 版本,即使在 Retina 屏上也能大幅提升首屏速度。
(2)核心实现思路
A. 监听 DPR 的变化
浏览器没有 on-dpr-change 这种直接的事件,但我们可以利用 window.matchMedia 来"曲线救国"。
思路:创建一个监听器,监听当前 DPR 范围的媒体查询。当 DPR 跨越阈值时,触发回调。
javascript
const monitorDPR = (callback) => {
const dpr = window.devicePixelRatio;
// 监听一个极其微小的变化范围,一旦偏离当前值即触发
const mqString = `(resolution: ${dpr}dppx)`;
window.matchMedia(mqString).addEventListener("change", () => {
callback();
monitorDPR(callback); // 递归监听下一次变化
}, { once: true });
};
B. 动态修改 src 或 srcset (强制重绘)
当监听到 DPR 变化后,你需要让 <img> 标签感知到变化。
Key 值的妙用 :在 React/Vue 等框架中,最简单的办法是给图片组件加一个由 dpr 组成的 key。当 dpr 变化时,key 变了,框架会销毁旧图片并创建新图片,强制浏览器重新匹配 srcset。
手动替换 URL :如果是原生 JS,可以遍历图片,在图片 URL 后面加一个随机时间戳,或者修改 srcset 属性。
(3)用vue来写一套解决方案
A. 描述
我们可以把这个过程想象成一个安保报警系统, 需要有监控 、通知 和执行三个重要环节。
a. Media Query 站岗(监控环节)
含义:利用浏览器的媒体查询(Media Query)来充当"哨兵",时刻盯着屏幕的像素比是否发生了变化。
-
为什么需要它? 浏览器没有
onDPRChange这种直接的事件。但是,媒体查询可以感知resolution(分辨率)。 -
具体做法 :通过 JavaScript 的
window.matchMedia方法,设置一个符合当前 DPR 的条件。 -
哨兵逻辑 :一旦用户把窗口从 Retina 屏拖到普通屏,或者缩放了页面,原本满足的媒体查询条件就会失效,触发一个
change事件。这就像哨兵发现情况不对,立刻"吹哨"报警。
b. 状态管理 广播(通知环节)
含义:当"哨兵"发现 DPR 变了,需要把这个新消息告诉页面上所有的图片组件。
-
为什么需要它? 页面上可能有几十张图片,你不能一个一个去手动修改。
-
具体做法 :使用前端框架的状态管理工具(如 Vue 的
ref/reactive或 React 的useState/Context)。 -
广播逻辑 :将最新的
window.devicePixelRatio存储在一个全局变量中。一旦这个变量更新,所有依赖这个变量的组件都会收到通知,准备迎接"新身份"。
c. Key 机制 强制 DOM 更新(执行环节)
含义 :这是最关键的一步。通过改变组件的 key 属性,强行让浏览器"重新做人"。
-
为什么需要它? 即使你修改了
img标签的srcset,浏览器有时会为了省事(优化性能),觉得"反正图片已经显示在那了,就不重新下载了",导致画面依然模糊。 -
具体做法 :在框架中给
<img>标签绑定一个key,这个key包含当前的 DPR 值。- 例如:
<img :key="currentDPR" :src="..." />
- 例如:
-
强制逻辑:
-
DPR 变了 ->
currentDPR变了 ->key变了。 -
框架(Vue/React)发现
key变了,会认为这是一个全新的元素,而不是旧元素的更新。 -
框架会销毁旧图片,创建新图片。
-
新图片在初始化的那一刻,会根据最新的 DPR 去解析
srcset,从而精准地拉取最清晰的那张图。
-
B. 实现
将监听逻辑封装成一个自定义 Hook(Composable),然后利用 Vue 的 key 属性来"欺骗"浏览器重绘图片。
a. 第一步:封装监听 DPR 的 Hook
我们先写一个名为 useDPR 的函数,它负责"站岗"和"广播"。
使用的是三重保障策略:
(1)轮询检测(主要)--- 每 200ms 检查,最可靠
(2)媒体查询监听(辅助)--- 事件驱动,性能更好
(3)事件监听(补充)--- 在特定场景下快速响应
javascript
// useDPR.js
import { ref, onMounted, onUnmounted } from "vue";
export function useDPR() {
const dpr = ref(window.devicePixelRatio || 1);
let pollTimer = null;
let lastDPR = window.devicePixelRatio || 1;
let mediaQueries = [];
// 检查并更新 DPR
const checkAndUpdateDPR = () => {
const currentDPR = window.devicePixelRatio || 1;
if (currentDPR !== lastDPR) {
lastDPR = currentDPR;
dpr.value = currentDPR;
console.log('DPR 变化:', currentDPR);
}
};
// 轮询检测 DPR 变化(最可靠的方式)
const startPolling = () => {
// 每 200ms 检查一次,平衡性能和响应速度
pollTimer = setInterval(() => {
checkAndUpdateDPR();
}, 200);
};
// 设置媒体查询监听(作为辅助检测)
const setupMediaQueries = () => {
// 清理旧的媒体查询
mediaQueries.forEach(({ media, handler, remove }) => {
if (remove) remove();
});
mediaQueries = [];
// 监听常见的 DPR 值
const possibleDPRs = [1, 1.5, 2, 2.5, 3, 4];
possibleDPRs.forEach((targetDPR) => {
const mqString = `(resolution: ${targetDPR}dppx)`;
const media = window.matchMedia(mqString);
const handler = () => {
checkAndUpdateDPR();
};
if (media.addEventListener) {
media.addEventListener("change", handler);
mediaQueries.push({
media,
handler,
remove: () => media.removeEventListener("change", handler)
});
} else if (media.addListener) {
// 兼容旧浏览器
media.addListener(handler);
mediaQueries.push({
media,
handler,
remove: () => media.removeListener(handler)
});
}
});
};
// 处理窗口事件
const handleResize = () => {
// 延迟检查,等待浏览器完成 DPR 更新
setTimeout(checkAndUpdateDPR, 100);
};
const handleOrientationChange = () => {
setTimeout(checkAndUpdateDPR, 200);
};
onMounted(() => {
setupMediaQueries();
startPolling();
// 监听窗口缩放和方向变化
window.addEventListener("resize", handleResize);
window.addEventListener("orientationchange", handleOrientationChange);
});
onUnmounted(() => {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
mediaQueries.forEach(({ remove }) => {
if (remove) remove();
});
mediaQueries = [];
window.removeEventListener("resize", handleResize);
window.removeEventListener("orientationchange", handleOrientationChange);
});
return { dpr };
}
b. 第二步:在组件中使用 Key 机制
在主组件中,我们把这个 随时变化 DPR 状态绑定在<img>标签上。
html
<template>
<div class="container">
<h3>当前屏幕 DPR: {{ dpr }}</h3>
<p>当前图片: {{ currentImageName }}</p>
<img
:key="`img-${Math.round(dpr)}-${imageKey}`"
:src="imageSrc"
alt="响应式图片"
@load="onImageLoad"
@error="onImageError"
/>
</div>
</template>
<script setup>
import { computed, watch, ref } from "vue";
import { useDPR } from "@/hook/useDPR.js";
// 导入图片资源
import icon1x from "@/assets/images/icon-1x.png";
import icon2x from "@/assets/images/icon-2x.png";
import icon3x from "@/assets/images/icon-3x.png";
const { dpr } = useDPR();
// 图片映射
const imageMap = {
1: { src: icon1x, name: 'icon-1x.png' },
2: { src: icon2x, name: 'icon-2x.png' },
3: { src: icon3x, name: 'icon-3x.png' },
};
// 根据 DPR 值动态选择图片源
const imageSrc = computed(() => {
const currentDPR = Math.round(dpr.value);
// 根据 DPR 值选择最合适的图片
if (currentDPR >= 3) {
return imageMap[3].src;
} else if (currentDPR >= 2) {
return imageMap[2].src;
} else {
return imageMap[1].src;
}
});
// 当前图片名称(用于显示)
const currentImageName = computed(() => {
const currentDPR = Math.round(dpr.value);
if (currentDPR >= 3) {
return imageMap[3].name;
} else if (currentDPR >= 2) {
return imageMap[2].name;
} else {
return imageMap[1].name;
}
});
// 用于强制重新渲染的 key
const imageKey = ref(0);
// 监听 DPR 变化,强制更新图片
watch(dpr, (newDPR, oldDPR) => {
const newDPRRounded = Math.round(newDPR);
const oldDPRRounded = Math.round(oldDPR);
if (newDPRRounded !== oldDPRRounded) {
console.log(`DPR 从 ${oldDPR} (${oldDPRRounded}) 变化到 ${newDPR} (${newDPRRounded})`);
console.log('切换图片:', currentImageName.value, imageSrc.value);
// 更新 key 强制重新渲染图片元素
imageKey.value++;
}
}, { immediate: true });
const onImageLoad = (event) => {
console.log('图片加载完成:', {
DPR: dpr.value,
imageName: currentImageName.value,
src: event.target.src
});
};
const onImageError = (event) => {
console.error('图片加载失败:', {
DPR: dpr.value,
imageName: currentImageName.value,
src: event.target.src
});
};
</script>
<style scoped>
img {
width: 300px; /* 逻辑宽度固定,物理像素随 DPR 变化 */
height: auto;
}
</style>