方案1:使用原生 loading="lazy"(最简单)

<template>
<div class="book-item" v-for="item in list" :key="item.novelID">
<div class="left-line"></div>
<!-- 添加 loading="lazy" -->
<img
:src="item.coverUrl"
:data-src="item.coverUrl"
class="img lazy-img"
alt=""
loading="lazy"
/>
<div class="highlight"></div>
<!-- ... 其他内容 ... -->
</div>
</template>
<style lang="scss" scoped>
.lazy-img {
background-color: #f5f5f5; /* 占位背景色 */
background-image: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
我是用就是这种模式
挺方便快捷的
原生方式最好
方案2:使用 IntersectionObserver API(推荐)
<template>
<div class="book-item" v-for="item in list" :key="item.novelID">
<div class="left-line"></div>
<!-- 使用 data-src 存储真实地址,src 使用占位图 -->
<img
:data-src="item.coverUrl"
:src="placeholder"
class="img lazy-img"
alt=""
ref="lazyImages"
/>
<div class="highlight"></div>
<!-- ... 其他内容 ... -->
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
// 占位图(可以使用base64小图)
const placeholder = ref('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIwIiBoZWlnaHQ9IjMwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PC9zdmc+');
// 图片引用
const lazyImages = ref([]);
// IntersectionObserver 实例
let observer = null;
// 初始化懒加载
const initLazyLoad = () => {
// 如果浏览器不支持 IntersectionObserver,降级处理
if (!('IntersectionObserver' in window)) {
loadAllImages();
return;
}
// 配置观察器
const options = {
root: null, // 视口
rootMargin: '50px', // 提前50px加载
threshold: 0.01 // 只要1%可见就加载
};
observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
loadImage(img);
observer.unobserve(img); // 加载后停止观察
}
});
}, options);
// 开始观察所有图片
setTimeout(() => {
lazyImages.value.forEach(img => {
if (img && img.dataset.src) {
observer.observe(img);
}
});
}, 100);
};
// 加载单张图片
const loadImage = (imgElement) => {
const src = imgElement.dataset.src;
if (!src) return;
// 创建新Image对象预加载
const img = new Image();
img.onload = () => {
// 图片加载成功后设置到元素
imgElement.src = src;
imgElement.classList.remove('lazy-img');
imgElement.classList.add('loaded');
};
img.onerror = () => {
// 加载失败时使用默认图片
imgElement.src = 'https://via.placeholder.com/220x300?text=图片加载失败';
imgElement.classList.remove('lazy-img');
};
img.src = src;
};
// 加载所有图片(降级方案)
const loadAllImages = () => {
lazyImages.value.forEach(img => {
if (img && img.dataset.src) {
img.src = img.dataset.src;
img.classList.remove('lazy-img');
}
});
};
onMounted(() => {
// 等待DOM更新后初始化懒加载
setTimeout(() => {
initLazyLoad();
}, 300);
});
onUnmounted(() => {
// 清理观察器
if (observer) {
observer.disconnect();
}
});
</script>
<style lang="scss" scoped>
.book-item {
position: relative;
.lazy-img {
background-color: #f8f9fa;
background-image: linear-gradient(
90deg,
#f0f0f0 0%,
#f8f8f8 50%,
#f0f0f0 100%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8px;
}
.lazy-img.loaded {
animation: none;
background: none;
}
// 优化 left-line 和 highlight,只在图片加载后显示
.left-line, .highlight {
opacity: 0;
transition: opacity 0.3s;
}
.loaded ~ .left-line,
.loaded ~ .highlight {
opacity: 1;
}
// 图片加载动画
.img {
transition: opacity 0.3s ease;
opacity: 0;
}
.img.loaded {
opacity: 1;
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
</style>
方案3:使用 VueUse 的 useIntersectionObserver(最简洁)
首先安装 VueUse:
npm install @vueuse/core
<template>
<div class="book-item" v-for="item in list" :key="item.novelID">
<div class="left-line"></div>
<!-- 使用 ref 绑定 -->
<img
:data-src="item.coverUrl"
:src="placeholder"
class="img"
alt=""
ref="imageRefs"
/>
<div class="highlight"></div>
<!-- ... 其他内容 ... -->
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useIntersectionObserver } from '@vueuse/core';
const placeholder = ref('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIwIiBoZWlnaHQ9IjMwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PC9zdmc+');
// 图片引用数组
const imageRefs = ref([]);
// 图片懒加载逻辑
const lazyLoadImages = () => {
imageRefs.value.forEach((img, index) => {
if (!img || !img.dataset.src) return;
const { stop } = useIntersectionObserver(
img,
([{ isIntersecting }]) => {
if (isIntersecting) {
// 图片进入视口,开始加载
loadImage(img);
stop(); // 停止观察
}
},
{
rootMargin: '50px',
threshold: 0.01
}
);
});
};
const loadImage = (imgElement) => {
const src = imgElement.dataset.src;
if (!src) return;
const img = new Image();
img.onload = () => {
imgElement.src = src;
imgElement.classList.add('loaded');
};
img.onerror = () => {
imgElement.src = 'https://via.placeholder.com/220x300?text=图片加载失败';
imgElement.classList.add('error');
};
img.src = src;
};
onMounted(() => {
setTimeout(() => {
lazyLoadImages();
}, 300);
});
</script>