Vue3 零基础每日笔记(075):图片懒加载与骨架屏

一、前言

图片是大头资源:商品列表 200 张图,用户首屏只看得到 3 张,剩下 197 张的下载纯属浪费。本篇三连招:图片懒加载 (进视口才加载)、骨架屏 (加载前的占位动画)、加载失败兜底,全部可复制。

二、懒加载的三种姿势

方案 原理 适用
原生 loading="lazy" 浏览器自动,一行搞定 ★ 简单场景首选
自定义指令 v-lazy IntersectionObserver 监听进视口 需要自定义加载态/渐显动画
IntersectionObserver 手写 指令的底层 理解原理 / 特殊需求

原生方案演示:

html 复制代码
<!-- 浏览器自动判断:进视口附近才发请求 -->
<img src="big.jpg" loading="lazy" alt="商品图" />

能用原生的先用原生;需要"加载前灰底 + 加载后渐显 + 失败兜底"完整体验,就上自定义指令。

三、自定义指令 v-lazy:完整实现

034 篇的自定义指令 + IntersectionObserver 组合:

ts 复制代码
// src/directives/lazy.ts
import type { Directive } from 'vue'

const observerMap = new WeakMap<Element, IntersectionObserver>()   // 便于清理

export const vLazy: Directive<HTMLImageElement, string> = {
  mounted(el, binding) {
    // 1. 真实地址暂存,src 先放占位图(1px 透明图,不发网络请求)
    const realSrc = binding.value
    el.src =
      'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciLz4='
    el.style.backgroundColor = 'var(--el-fill-color-light)'   // 灰底占位

    // 2. 进视口才真正加载
    const io = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (!entry.isIntersecting) return
        el.src = realSrc                       // ★ 进入视口:换真实地址
        el.onload = () => el.style.opacity = '1'   // 加载完渐显
        el.style.transition = 'opacity 0.3s'
        el.style.opacity = '0'
        io.unobserve(el)                       // 只需要触发一次
        observerMap.delete(el)
      })
    }, { rootMargin: '200px' })                // ★ 提前 200px 预加载,滚动更顺滑
    io.observe(el)
    observerMap.set(el, io)
  },

  // 3. 组件卸载时断开监听(防内存泄漏,037 篇同款约定)
  unmounted(el) {
    observerMap.get(el)?.disconnect()
    observerMap.delete(el)
  }
}
ts 复制代码
// src/main.ts
import { vLazy } from '@/directives/lazy'
app.directive('lazy', vLazy)
vue 复制代码
<!-- 组件里:src 属性换成 v-lazy 指令 -->
<img
  v-for="g in goodsList"
  :key="g.id"
  v-lazy="g.imgUrl"
  style="width: 100%; aspect-ratio: 4 / 3; object-fit: cover"
/>

四、骨架屏:el-skeleton 占位

图片加载慢的"等待期"用骨架屏安抚用户(比转圈体验好:提前告知内容形状):

vue 复制代码
<template>
  <!-- is-loading 为 true 时显示骨架,false 显示真实内容 -->
  <el-skeleton :loading="loading" animated :count="3">
    <template #template>
      <div style="display: flex; gap: 12px; margin-bottom: 12px">
        <el-skeleton-item variant="image" style="width: 80px; height: 60px" />
        <div style="flex: 1">
          <el-skeleton-item variant="text" style="width: 60%" />
          <el-skeleton-item variant="text" style="width: 40%; margin-top: 8px" />
        </div>
      </div>
    </template>

    <!-- #default:真实内容 -->
    <template #default>
      <div v-for="g in goodsList" :key="g.id" style="display: flex; gap: 12px; margin-bottom: 12px">
        <img v-lazy="g.imgUrl" style="width: 80px; height: 60px; object-fit: cover" />
        <div>
          <p>{{ g.name }}</p>
          <p>¥{{ g.price }}</p>
        </div>
      </div>
    </template>
  </el-skeleton>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getGoodsList } from '@/api/goods'
import type { GoodsItem } from '@/api/goods'

const loading = ref(true)
const goodsList = ref<GoodsItem[]>([])

onMounted(async () => {
  const res = await getGoodsList()   // 062 篇的 api
  goodsList.value = res.data.list
  loading.value = false              // ★ 数据到了才关骨架
})
</script>

【截图位置:弱网下先看到灰色骨架动画 → 数据到达后骨架替换为真实列表】

五、失败兜底:加载失败显示占位图

ts 复制代码
// 指令里补失败处理(挂在 el 上即可)
el.onerror = () => {
  el.src = '/img/fallback.png'   // 本地默认占位图(logo 灰图)
  el.style.opacity = '1'
}

原则:每张图都有"加载中"(骨架/灰底)和"加载失败"(占位图)两个归宿,用户永远不会看到"裂图"图标。

六、踩坑记录

  1. 占位图写成大图:占位图本身几 MB 就本末倒置------用 1px 透明 base64 或纯灰底色
  2. 忘了 unobserve:一直监听,列表长时 observer 堆积------加载后立即 unobserve + 移除引用
  3. unmounted 不清理:路由切换后旧元素还被监听(内存泄漏)------WeakMap + disconnect(037 篇事件清理同款)
  4. 图片没有固定尺寸:懒加载后图片撑开布局,滚动位置跳动(CLS)------固定宽高或 aspect-ratio 占位
  5. 骨架屏一直转:接口报错时 loading 忘了关------try/finally 里关 loading
  6. rootMargin 不加:进入视口才加载,滚动快时总慢半拍------提前 200~500px 预加载

七、今日小结

  • 三种姿势:原生 loading="lazy" 优先;完整体验用 v-lazy 指令
  • 指令核心:IntersectionObserver 进视口换 src + rootMargin 提前量 + unmounted 清理
  • 骨架屏:el-skeleton :loading 切换骨架/内容,形状贴近真实内容
  • 双兜底:加载中(灰底/骨架)+ 失败(占位图),消灭裂图

下篇预告

搜索框每敲一个字发一次请求、提交按钮狂点发 N 个请求------服务端和用户都在崩溃。下一篇 076 防抖节流在 Vue3 中的正确使用:038 篇封装的工具终于大规模实战。

相关推荐
IT_Octopus1 小时前
【零基础入门 LLM 开发 · Day 11】:ChatOpenAI vs init_chat_model——一行换供应商
java·前端·javascript
柚yuzumi1 小时前
React 19 Hooks 入门:函数组件的四大法宝 (useState / useEffect / useRef / useContext)
前端·javascript·架构
会博通·代码搬运工1 小时前
家装数字化交付笔记:图纸归档、工地采集与档案系统对接的工程实践
笔记·数字化转型·档案管理系统·业务档案系统·家装数字化
King禅2 小时前
JavaScript 调试实战指南:从 console.log 到 DevTools 深度运用
javascript
全栈项目管理程序猿2 小时前
ArcGIS JS 基础教程(28):图层渲染顺序管理
前端·javascript
Code_Solitude2 小时前
高数第一章函数笔记(原稿手写+AI识别优化)
人工智能·笔记
扶风ff2 小时前
练题簿在线免费刷题:创建个人任务,把刷题、听题和模考目标放到每天的学习中
开发语言·javascript·学习·小程序
asong2 小时前
没有源代码,AI 也能拆解软件:这个开源项目已有 3.7 万 Star
javascript·后端·github
用户1733598075372 小时前
纯前端 PDF 压平避坑指南:压平后表单字段变了?
前端·javascript·vue.js