- 作者:老汪软件技巧
- 发表时间:2024-11-20 21:05
- 浏览量:
前端开发中,图片懒加载是常见的优化措施之一。在使用 vue3 开发项目里,借助 Element Plus ,只需要给 添加上 loading="lazy" 或是 lazy 属性,就可以很方便地实现图片的懒加载。但如果离开了第三方框架,我们又该如何实现图片的懒加载呢?接下去就说说我的实现方案。
前期准备
我准备了几张图片放在项目的 public\imgs 目录中。页面组件代码如下,就是将几张图片简单地依次竖向排列:
此时打开页面效果如下,可以看到 6 张图片是同时请求的:
原生方案
原生
标签有个 loading 属性,默认值为 eager,意为立即加载图片,而不管该图片是否在可视视口之内。我们可以将 loading 的值改为 lazy,就可以实现图片的延迟加载,直到它和视口接近到一个计算得到的距离(由浏览器定义)。
现在给代码片段一中的
标签添加上 loading="lazy":
<img class="img" :src="item" alt="" loading="lazy" />
此时再次加载页面,可以看到初始时浏览器只请求了 4 张图片,只有当页面滚到一定位置时,才会去请求 5.jpg 和 6.jpg:
请注意,并非所有浏览器都支持这种原生方案,其可用性如下:
而且原生方案也无法在图片加载之前添加上一个默认的占位图片。
自定义实现
我们可以自定义一个 vue 指令 v-lazy ,结合滚动监听来实现我们自己的懒加载方案。
封装全局指令
为了方便维护,我们可以在项目的 src 目录下新建 directives 文件夹用于管理自定义指令,里面新建 index.ts 和具体指令的定义。比如 lazy 指令我就定义在 lazy.ts 中:
// src\directives\lazy.ts
import type { App } from 'vue'
export default function (app: App ) {
app.directive('lazy', {
// 具体实现
})
}
然后在 index.ts 中引入:
// src\directives\index.ts
import lazyDirective from './lazy'
import type { App } from 'vue'
export default function (app: App) {
lazyDirective(app)
}
之后在 main.ts 引入 directives\index.ts 并将 app 传入执行即可:
// src\main.ts
import { createApp } from 'vue'
import App from './App.vue'
import lazyDirective from './directives'
const app = createApp(App)
lazyDirective(app)
app.mount('#app')
接下去就是对指令的具体实现了。
具体实现
先给代码片段一中的
标签添加上 v-lazy 并赋值 item,即各个图片的路径地址,同时可以删除 :src="item":
<img class="img" v-lazy="item" />
指令定义
在指令的 mounted 生命周期函数里, 将所有添加了 v-lazy 指令的图片元素,都对应地往一个准备好的数组 list 中加入一个对象,该对象有 2 个属性:
interface IListItem {
el: any
src: string
}
// 添加了 v-lazy 指令的图片信息,加载后就会被添加进 list
let list: IListItem[] = []
export default function (app: App ) {
app.directive('lazy', {
mounted(el, binding) {
const item = {
el,
src: binding.value
}
list.push(item)
// 立即处理一次,主要作用于页面加载一开始就显示的图片
handleImg(item)
},
unmounted(el) {
list = list.filter(item => item.el !== el)
}
})
}
在 unmounted 中,即绑定了指令的图片元素的父组件卸载后,比如切换到别的页面时,就将该图片元素对应的对象从 list 中剔除。
位置判断
至于 handleImg() 方法,则是用于判断绑定了指令的图片是否出现在了视口范围内,从而需要加载:
function handleImg(item: IListItem) {
// 默认占位图片
item.el.src = '/imgs/0.jpg'
const rect = item.el.getBoundingClientRect()
// 图片位于视口下方
const isBottom = rect.top >= viewHeight
// 图片位于视口上方
const isTop = rect.bottom <= 0
// 图片位于视口内
if (!isBottom && !isTop) {
item.el.src = item.src
// 处理后删除
list = list.filter(imgItem => imgItem.src !== item.src)
}
}
一开始时先让绑定了指令的图片的地址指向一张默认的占位图,即 0.jpg,图片为黑色背景,显示金色 2025 字样。
然后通过 getBoundingClientRect() 获取图片元素相对于视口的位置, 和 rect.bottom 的示意如下,图片引用至 mdn:
viewHeight 为视口高度,通过 window.innerHeight 获取:
// 获取视口高度
const viewHeight = window.innerHeight
如果判断结果为图片元素出现在了视口范围内,则将图片的 src 赋值为原本的图片地址,替换掉占位图。接着将该图片对应的对象从 list 中剔除,避免重复处理。
滚动监听
handleImg() 的调用则主要是在发生滚动的时候,为了防止过于频繁地触发,我使用了从 underscore 引入的防抖函数:
import _ from 'underscore'
// 监听滚动
window.addEventListener('scroll', _.debounce(handleImgList, 50))
// 处理图片数组
function handleImgList() {
list.forEach(item => {
handleImg(item)
})
}
一旦页面发生滚动,则对 list 数组中的各个对象都执行一次 handleImg(),以判断对应的图片是否需要正确显示。
效果展示
现在,一开始时只会加载位于视口内的图片 1.jpg 和 2.jpg 以及占位图 0.jpg。当滚动页面时,再适时加载后续图片。图片加载过程中显示占位图,等所有图片都加载完毕,向上滚动也不会再有多余的处理: