vue3封装Hooks 实现图片懒加载(保姆级教程)

一、背景

在现代 Web 开发领域,图片懒加载的重要性不容小觑。

首先,它对页面加载速度有着极大提升作用。当页面包含大量图片时,若不采用懒加载,所有图片会在页面初始化时一同请求加载,这会导致加载时间过长。而懒加载仅在图片进入浏览器可视区域时才触发加载,大大减少了初始加载的数据量,使关键资源(如 HTML、CSS 等)能优先高效加载,让用户更快看到页面布局并进行交互。

其次,能有效节省用户流量。对于移动用户或流量有限的情况,懒加载按需加载图片,避免了加载那些用户可能根本不会看到的图片,防止不必要的流量消耗。

二、图片懒加载原理概述

简单解释什么是图片懒加载。即图片在进入浏览器的可视区域时才进行加载,而不是在页面初始化时就全部加载所有图片。

可以用一些简单的示意图或者类比的方式(比如把页面想象成一个画展,只有当观众走到画作前才会仔细去看这幅画,也就是才加载这幅画的细节,类似图片进入可视区才加载)来帮助读者更好地理解懒加载的概念。

三、图片懒加载模块展示

  一、图片懒加载核心逻辑(useLazyLoad.js)        

import { reactive, onBeforeUnmount } from 'vue'

export default function useLazyLoad() {
  const state = reactive({
    observers: new Map(),
    imageStates: new Map(),
  })

  const observe = (imgElement) => {
    if (!imgElement || state.observers.has(imgElement)) return

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          loadImage(imgElement)
          unobserve(imgElement)
        }
      },
      {
        rootMargin: '200px 0px',
        threshold: 0.01,
      },
    )

    observer.observe(imgElement)
    state.observers.set(imgElement, observer)
    state.imageStates.set(imgElement, 'pending')
  }

  const loadImage = (imgElement) => {
    state.imageStates.set(imgElement, 'loading')

    const tempImg = new Image()
    tempImg.src = imgElement.dataset.src

    tempImg.onload = () => {
      // 延迟 3 秒后再显示图片
      setTimeout(() => {
        imgElement.src = imgElement.dataset.src
        state.imageStates.set(imgElement, 'loaded')
      }, 500)
    }

    tempImg.onerror = () => {
      // 错误处理也可以添加延迟,这里直接显示错误图片
      imgElement.src = '/error.jpg'
      state.imageStates.set(imgElement, 'error')
    }
  }

  const unobserve = (imgElement) => {
    if (!imgElement) return
    state.observers.get(imgElement)?.unobserve(imgElement)
    state.observers.delete(imgElement)
  }

  const getImageState = (imgElement) => {
    return imgElement ? state.imageStates.get(imgElement) || 'pending' : 'pending'
  }

  onBeforeUnmount(() => {
    state.observers.forEach((observer) => observer.disconnect())
    state.observers.clear()
    state.imageStates.clear()
  })

  return { observe, unobserve, getImageState }
}

  1. 状态管理部分

const state = reactive({
  observers: new Map(),
  imageStates: new Map(),
})
  • reactive 函数:在 Vue 3 中,reactive 用于创建响应式对象。这里我们创建了一个 state 对象,它包含两个 Map 实例。Map 是一种键值对的数据结构,相比于普通的对象,Map 可以使用任意类型作为键,并且能更方便地进行增删改查操作。
  • observers:这个 Map 用于存储每个图片元素对应的 IntersectionObserver 实例。通过图片元素作为键,我们可以方便地管理和操作每个图片的观察器。
  • imageStates:该 Map 用于存储每个图片元素的加载状态,状态值可以是 pending(待加载)、loading(正在加载)、loaded(已加载)、error(加载错误)。这样做的好处是可以方便地跟踪每个图片的加载进度,从而在 UI 上做出相应的展示。

   2、observe 函数

const observe = (imgElement) => {
  if (!imgElement || state.observers.has(imgElement)) return

  const observer = new IntersectionObserver(
    ([entry]) => {
      if (entry.isIntersecting) {
        loadImage(imgElement)
        unobserve(imgElement)
      }
    },
    {
      rootMargin: '200px 0px',
      threshold: 0.01,
    },
  )

  observer.observe(imgElement)
  state.observers.set(imgElement, observer)
  state.imageStates.set(imgElement, 'pending')
}
  • 参数检查:首先检查传入的 imgElement 是否存在,以及该元素是否已经有对应的观察器。如果不满足条件,则直接返回,避免重复操作。
  • IntersectionObserver:这是浏览器提供的一个 API,用于监听目标元素与视口(或指定的根元素)的交叉状态。
    • 回调函数:当目标元素与视口的交叉状态发生变化时,会触发回调函数。这里我们只关注元素是否进入视口(entry.isIntersecting 为 true),如果进入视口,则调用 loadImage 函数加载图片,并调用 unobserve 函数停止对该元素的观察,以节省性能。
    • 配置选项
      • rootMargin:设置为 '200px 0px' 表示在目标元素距离视口顶部或底部还有 200px 时就开始加载图片,这样可以提前加载图片,提升用户体验。
      • threshold:设置为 0.01 表示当目标元素与视口的交叉比例达到 0.01 时就触发回调函数,确保能及时发现元素进入视口。
  • 状态设置:将观察器存储到 observers 中,并将图片的初始状态设置为 pending

3、loadImage 函数

const loadImage = (imgElement) => {
  state.imageStates.set(imgElement, 'loading')

  const tempImg = new Image()
  tempImg.src = imgElement.dataset.src

  tempImg.onload = () => {
    // 延迟 3 秒后再显示图片
    setTimeout(() => {
      imgElement.src = imgElement.dataset.src
      state.imageStates.set(imgElement, 'loaded')
    }, 500)
  }

  tempImg.onerror = () => {
    // 错误处理也可以添加延迟,这里直接显示错误图片
    imgElement.src = '/error.jpg'
    state.imageStates.set(imgElement, 'error')
  }
}
  • 状态更新:将图片的状态设置为 loading,表示开始加载图片。
  • 临时图片对象:创建一个新的 Image 对象 tempImg,并将其 src 属性设置为图片元素的 data-src 属性值。这样做的目的是在图片真正显示之前先进行预加载,避免在页面上直接显示时出现闪烁。
  • 加载成功处理:当 tempImg 加载成功时,会触发 onload 事件。这里使用 setTimeout 函数延迟 500 毫秒后再将图片元素的 src 属性设置为 data-src 的值,并将图片状态设置为 loaded。延迟显示可以模拟一些加载效果,提升用户体验。
  • 加载失败处理:当 tempImg 加载失败时,会触发 onerror 事件。此时将图片元素的 src 属性设置为错误图片的路径 /error.jpg,并将图片状态设置为 error

4、unobserve 函数

const unobserve = (imgElement) => {
  if (!imgElement) return
  state.observers.get(imgElement)?.unobserve(imgElement)
  state.observers.delete(imgElement)
}
  • 该函数用于停止对指定图片元素的观察。首先检查元素是否存在,如果存在,则获取对应的观察器并调用其 unobserve 方法停止观察,然后从 observers 中删除该元素的记录。

5、getImageState 函数

const getImageState = (imgElement) => {
  return imgElement ? state.imageStates.get(imgElement) || 'pending' : 'pending'
}
  • 该函数用于获取指定图片元素的加载状态。如果元素存在,则从 imageStates 中获取其状态,如果不存在则返回 pending

6、资源清理

onBeforeUnmount(() => {
  state.observers.forEach((observer) => observer.disconnect())
  state.observers.clear()
  state.imageStates.clear()
})
  • onBeforeUnmount 是 Vue 3 的生命周期钩子,在组件卸载之前执行。这里我们遍历 observers 中的所有观察器,调用其 disconnect 方法停止观察,然后清空 observers 和 imageStates,释放资源,避免内存泄漏。

二、懒加载图片组件(LazyImage.vue

<template>
  <div class="lazy-container">
    <img
      ref="imgEl"
      :data-src="dataSrc"
      :alt="alt"
      :class="status"
    />

    <transition name="fade">
      <div
        v-show="showPlaceholder"
        class="loading-overlay"
      >
        <div class="spinner"></div>
        <span class="loading-text">加载中...</span>
      </div>
    </transition>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import useLazyLoad from '@/hooks/useLazyLoad';

const props = defineProps({
  dataSrc: String,
  alt: String
});

const imgEl = ref(null);
const { observe, unobserve, getImageState } = useLazyLoad();

console.log('Actual image dataSrc:', props.dataSrc);

const status = computed(() => {
  const state = getImageState(imgEl.value);
  console.log('Current image status:', state);
  return state;
});
const showPlaceholder = computed(() => status.value === 'loading');

onMounted(() => {
  console.log('Component mounted, observing image');
  if (imgEl.value) {
    observe(imgEl.value);
  }
});

onBeforeUnmount(() => {
  console.log('Component unmounting, unobserving image');
  if (imgEl.value) {
    unobserve(imgEl.value);
  }
});
</script>

<style scoped>
.lazy-container {
  position: relative;
  width: 100%;
  height: 100%;
}

img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.6s ease;
}
.placeholder-img {
  width: 100%; /* 或者根据需要调整大小 */
  height: auto;
  object-fit: cover; /* 保持图片比例 */
}
img.loaded {
  opacity: 1;
}

img.placeholder-img {
  opacity: 1; /* 占位图始终显示 */
}

.loading-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.9);
}

.spinner {
  width: 32px;
  height: 32px;
  border: 3px solid rgba(0, 0, 0, 0.1);
  border-radius: 50%;
  border-top-color: #3498db;
  animation: spin 0.8s linear infinite;
}

.loading-text {
  margin-top: 12px;
  color: #666;
  font-size: 0.9em;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
</style>
1. 模板部分
<template>
  <div class="lazy-container">
    <img
      ref="imgEl"
      :data-src="dataSrc"
      :alt="alt"
      :class="status"
    />

    <transition name="fade">
      <div
        v-show="showPlaceholder"
        class="loading-overlay"
      >
        <div class="spinner"></div>
        <span class="loading-text">加载中...</span>
      </div>
    </transition>
  </div>
</template>
  • 图片元素:使用 img 标签显示图片,通过 ref 绑定到 imgEl,方便在脚本中访问。data-src 属性用于存储图片的真实地址,alt 属性用于提供图片的替代文本,class 属性根据图片的加载状态动态添加类名。
  • 过渡组件:使用 transition 组件包裹加载提示层,name 属性设置为 fade,实现淡入淡出的过渡效果。v-show 指令根据 showPlaceholder 的值决定是否显示加载提示层。
2. 脚本部分
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import useLazyLoad from '@/hooks/useLazyLoad';

const props = defineProps({
  dataSrc: String,
  alt: String
});

const imgEl = ref(null);
const { observe, unobserve, getImageState } = useLazyLoad();

console.log('Actual image dataSrc:', props.dataSrc);

const status = computed(() => {
  const state = getImageState(imgEl.value);
  console.log('Current image status:', state);
  return state;
});
const showPlaceholder = computed(() => status.value === 'loading');

onMounted(() => {
  console.log('Component mounted, observing image');
  if (imgEl.value) {
    observe(imgEl.value);
  }
});

onBeforeUnmount(() => {
  console.log('Component unmounting, unobserving image');
  if (imgEl.value) {
    unobserve(imgEl.value);
  }
});
  • 导入和定义:导入必要的 Vue 3 API 和 useLazyLoad 函数,定义组件的 props,包括 dataSrc 和 alt
  • 响应式引用:使用 ref 创建 imgEl 引用,用于获取图片元素。
  • 状态计算
    • status:使用 computed 计算属性获取图片的加载状态,并在控制台输出状态信息。
    • showPlaceholder:根据 status 的值判断是否显示加载提示层。
  • 生命周期钩子
    • onMounted:在组件挂载后,调用 observe 函数开始观察图片元素。
    • onBeforeUnmount:在组件卸载前,调用 unobserve 函数停止观察图片元素。
3. 样式部分
.lazy-container {
  position: relative;
  width: 100%;
  height: 100%;
}

img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.6s ease;
}
.placeholder-img {
  width: 100%; /* 或者根据需要调整大小 */
  height: auto;
  object-fit: cover; /* 保持图片比例 */
}
img.loaded {
  opacity: 1;
}

img.placeholder-img {
  opacity: 1; /* 占位图始终显示 */
}

.loading-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.9);
}

.spinner {
  width: 32px;
  height: 32px;
  border: 3px solid rgba(0, 0, 0, 0.1);
  border-radius: 50%;
  border-top-color: #3498db;
  animation: spin 0.8s linear infinite;
}

.loading-text {
  margin-top: 12px;
  color: #666;
  font-size: 0.9em;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
  • 容器样式lazy-container 设置为相对定位,方便子元素进行绝对定位。
  • 图片样式:初始状态下图片的 opacity 为 0,不可见。当图片加载完成后,添加 loaded 类,将 opacity 设置为 1,实现淡入效果。
  • 加载提示层样式loading-overlay 覆盖在图片上方,使用 flex 布局居中显示加载图标和文字。
  • 加载图标样式spinner 通过边框和动画实现

三、图片列表组件(ImageList.vue

   

<template>
  <div class="image-grid">
    <div
      v-for="(img, index) in images"
      :key="img.id"
      class="image-item"
    >
      <LazyImage
        :data-src="img.url"
        :alt="`图片 ${index + 1}`"
      />
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import LazyImage from './LazyImage.vue'

// 模拟图片数据
const images = ref([
  {id:1,url:'https://img.51miz.com/Element/00/73/12/40/f12ed593_E731240_093fc95a.jpg'},
  {id:2,url:'https://ts1.cn.mm.bing.net/th/id/R-C.ac133896b2735871271f35272484cfbf?rik=k0a86ZhyanNt9g&riu=http%3a%2f%2fn.sinaimg.cn%2fsinacn10114%2f600%2fw1920h1080%2f20190518%2f1372-hwzkfpv2143245.jpg&ehk=DU3HSmHLIjdJx1fj6fhR1FLcDsewKDsoKdyrELfab%2bg%3d&risl=&pid=ImgRaw&r=0'},
  // ...更多图片数据
])
</script>

<style>
.image-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 1.5rem;
  padding: 2rem;
  max-width: 1200px;
  margin: 0 auto;
  @media (max-width: 768px) {
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 1rem;
  }
}

.image-item {
  aspect-ratio: 16/9;
  background: #f8f8f8;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  transition: transform 0.3s ease;
}

.image-item:hover {
  transform: translateY(-4px);
}
</style>
1. 模板部分
<template>
  <div class="image-grid">
    <div
      v-for="(img, index) in images"
      :key="img.id"
      class="image-item"
    >
      <LazyImage
        :data-src="img.url"
        :alt="`图片 ${index + 1}`"
      />
    </div>
  </div>
</template>
  • 容器结构:使用 image-grid 作为容器,内部通过 v-for 循环渲染每个 image-item
  • key 值:使用 img.id 作为 key,确保列表渲染的高效性和唯一性。
  • 懒加载组件:每个图片项使用自定义的 LazyImage 组件,并传递 data-src(真实图片地址)和 alt(替代文本)属性。
2. 脚本部分
import { ref } from 'vue'
import LazyImage from './LazyImage.vue'

const images = ref([
  {id:1,url:'https://img.51miz.com/Element/00/73/12/40/f12ed593_E731240_093fc95a.jpg'},
  // ...更多图片数据
])
  • 依赖导入:引入 ref 用于创建响应式数据,导入 LazyImage 组件。
  • 图片数据:使用 ref 定义 images 数组,包含多个图片对象(id 和 url)。这里模拟了真实图片数据,实际项目中可以通过 API 动态获取。
3. 样式部分
.image-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 1.5rem;
  padding: 2rem;
  max-width: 1200px;
  margin: 0 auto;
  @media (max-width: 768px) {
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 1rem;
  }
}

.image-item {
  aspect-ratio: 16/9;
  background: #f8f8f8;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  transition: transform 0.3s ease;
}

.image-item:hover {
  transform: translateY(-4px);
}
  • 网格布局
    • grid-template-columns:使用 repeat(auto-fill, minmax(300px, 1fr)) 创建自适应列数的网格布局,每列最小宽度为 300px,最大宽度为可用空间的 100%。
    • 响应式设计:通过媒体查询,当屏幕宽度小于 768px 时,调整列宽为 240px,间距为 1rem,提升移动端体验。
  • 图片项样式
    • aspect-ratio:设置为 16/9,保持图片容器的宽高比。
    • 背景色与阴影:使用浅灰色背景(#f8f8f8)和轻微阴影(box-shadow),提升视觉层次感。
    • 悬停动画:当鼠标悬停时,图片项向上移动 4px(transform: translateY(-4px)),增强交互反馈。

四、设计亮点

  1. 响应式布局

    • 使用 CSS Grid 实现灵活的图片排列,自动适应不同屏幕尺寸。
    • 媒体查询确保移动端和桌面端的显示效果一致。
  2. 懒加载集成

    • 通过 LazyImage 组件实现图片的懒加载,减少初始请求数量,提升页面加载速度。
  3. 视觉优化

    • 统一的图片容器样式(圆角、阴影)提升整体美观度。
    • 悬停动画增强用户交互体验。
  4. 性能考虑

    • 图片数据通过 ref 响应式管理,确保数据变化时 UI 及时更新。
    • 使用 img.id 作为 key 值,优化列表渲染性能。

五、实际应用场景

  • 图片库或作品集展示:适合展示大量图片,支持响应式布局和懒加载。
  • 电商商品列表:通过统一的图片样式和交互效果,提升商品展示的专业性。
  • 内容社区:支持用户上传图片,懒加载和响应式设计确保页面流畅性。

六、未来需要优化的地方

  1. 占位图升级:当前占位图较简单,可采用模糊预览图或低分辨率图片作为占位,在图片加载过程中提供更好预览,优化用户等待体验。
  2. 性能监控完善:添加加载时间统计、请求成功率等监控指标,分析性能瓶颈,针对性优化加载策略,如调整 rootMargin 和 threshold 参数。
  3. 服务端协作:结合图片 CDN 的响应式图片服务,根据设备屏幕和网络情况返回合适尺寸与格式图片;采用 WebP 格式,进一步减小图片体积,提升加载速度。
  4. 错误处理增强:目前仅显示固定错误图片,可增加重试机制,多次尝试加载失败后再显示错误信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值