前端:解决同一张图片由于页面大小不统一导致图片模糊

目录

[一、了解DPR(Device Pixel Ratio)设备像素比](#一、了解DPR(Device Pixel Ratio)设备像素比)

1、基本概念

[2、DPR所造成的问题 --- 图像显示问题](#2、DPR所造成的问题 — 图像显示问题)

二、如何解决DPR所造成的图片模糊

1、如果是背景图

[2、如果是img 元素](#2、如果是img 元素)

3、如果想要动态加载img图片

(1)使用场景

[A. 跨屏协同与窗口拖拽](#A. 跨屏协同与窗口拖拽)

[B. 地图与可视化大屏](#B. 地图与可视化大屏)

[C. 浏览器缩放调试 (Zoom)](#C. 浏览器缩放调试 (Zoom))

[D. 移动端省流量模式](#D. 移动端省流量模式)

(2)核心实现思路

[A. 监听 DPR 的变化](#A. 监听 DPR 的变化)

[B. 动态修改 src 或 srcset (强制重绘)](#B. 动态修改 src 或 srcset (强制重绘))

(3)用vue来写一套解决方案

[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. 动态修改 srcsrcset (强制重绘)

当监听到 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="..." />
  • 强制逻辑

    1. DPR 变了 -> currentDPR 变了 -> key 变了。

    2. 框架(Vue/React)发现 key 变了,会认为这是一个全新的元素,而不是旧元素的更新。

    3. 框架会销毁旧图片,创建新图片

    4. 新图片在初始化的那一刻,会根据最新的 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>
相关推荐
C澒3 小时前
面单打印服务的监控检查事项
前端·后端·安全·运维开发·交通物流
pas1363 小时前
39-mini-vue 实现解析 text 功能
前端·javascript·vue.js
qq_532453533 小时前
使用 GaussianSplats3D 在 Vue 3 中构建交互式 3D 高斯点云查看器
前端·vue.js·3d
Swift社区4 小时前
Flutter 路由系统,对比 RN / Web / iOS 有什么本质不同?
前端·flutter·ios
开发者小天4 小时前
python中计算平均值
开发语言·前端·python
我谈山美,我说你媚4 小时前
qiankun微前端 若依vue2主应用与vue2主应用
前端
雨季6665 小时前
Flutter 三端应用实战:OpenHarmony 简易“动态色盘生成器”交互模式深度解析
开发语言·前端·flutter·ui·交互
雨季6665 小时前
Flutter 三端应用实战:OpenHarmony 简易“可展开任务详情卡片”交互模式深度解析
开发语言·前端·javascript·flutter·ui·交互
东东5165 小时前
基于Web的智慧城市实验室系统设计与实现vue + ssm
java·前端·人工智能·后端·vue·毕业设计·智慧城市