Vue:检测图片加载状态,按需显示骨架屏问题求助
问题解决:图片加载完成前骨架屏提前消失
问题背景
需求是:页面需等待数据获取完成且对应卡片图片加载完成后,才显示真实卡片内容;否则显示<ItemCardSkeleton />骨架屏。同时支持部分图片先加载完成时,对应卡片显示内容,其余卡片维持骨架屏直至加载完成。
当前实现的问题:页面刷新时,仅在数据请求pending阶段显示骨架屏,数据请求结束后,哪怕图片还在加载,骨架屏就直接消失,不符合预期。
核心问题分析
- 原方案用单个
isLoaded布尔值记录所有卡片的加载状态,只要有一个图片加载完成,isLoaded就变为true,父组件直接切换显示所有真实卡片,忽略了其他未加载完成的图片。 - 初始时
isLoaded为undefined,isLoaded === false的判断不成立,数据请求结束后直接跳过骨架屏逻辑。
解决思路
将单个全局的加载状态,改为每个卡片独立的加载状态,通过记录每个recipe的id对应的加载状态,实现卡片级别的加载控制:
- 父组件维护一个对象,存储每个recipe id对应的图片加载状态;
- 数据加载完成后,初始化所有卡片的加载状态为
false; - 子组件图片加载完成(或失败)时,通知父组件更新对应id的加载状态;
- 父组件针对每个卡片,根据其独立的加载状态决定显示骨架屏还是真实内容。
具体代码修改
父组件(ItemSwiper.vue)
<template> <Swiper class="swiper" :breakpoints="swiperOptions.breakpoints" :pagination="{ clickable: true, }" :loop="true" :modules="swiperOptions.modules"> <!-- 数据请求中:显示批量骨架屏 --> <template v-if="storeRecipes.pending"> <SwiperSlide class="swiper__slide" v-for="item in 3" :key="item"> <ItemCardSkeleton /> </SwiperSlide> </template> <!-- 数据已加载:逐个判断卡片加载状态 --> <template v-else> <template v-for="recipe in storeRecipes.data" :key="recipe.id"> <SwiperSlide class="swiper__slide"> <!-- 对应卡片未加载完成时显示骨架屏 --> <ItemCardSkeleton v-if="!cardLoadedStates[recipe.id]" /> <!-- 加载完成后显示真实卡片 --> <ItemCard v-else :data="recipe" @update:is-loaded="(isLoaded) => updateCardLoaded(recipe.id, isLoaded)" /> </SwiperSlide> <div class="swiper-custom-pagination"></div> </template> </template> </Swiper> </template> <script setup> import { onMounted, ref, watch } from 'vue'; import { Swiper, SwiperSlide } from 'swiper/vue'; import { Pagination } from 'swiper/modules'; import 'swiper/css/free-mode'; import 'swiper/css/pagination'; import 'swiper/css'; import { useStoreRecipes } from '@/stores/recipes/storeRecipes.js'; import ItemCard from '@/components/ItemCard.vue'; import ItemCardSkeleton from '@/components/SkeletonLoaders/ItemCardSkeleton.vue'; const storeRecipes = useStoreRecipes(); // 用对象存储每个卡片的加载状态,key为recipe的id const cardLoadedStates = ref({}); onMounted(() => { storeRecipes.loadRecipes(); }); // 监听recipes数据变化,初始化每个卡片的加载状态为false watch(() => storeRecipes.data, (newData) => { if (newData.length) { const initialStates = {}; newData.forEach(recipe => { initialStates[recipe.id] = false; }); cardLoadedStates.value = initialStates; } }, { immediate: true }); // 更新对应id卡片的加载状态 const updateCardLoaded = (recipeId, isLoaded) => { cardLoadedStates.value[recipeId] = isLoaded; }; </script>
子组件(ItemCard.vue)
<template> <div class="card"> <div class="card__item"> <img class="card__image" @load="handleImageLoad" @error="handleImageLoad" <!-- 加载失败也标记为已加载,避免骨架屏一直显示 --> :src="getSrc('.jpg')" :alt="data.alt"/> <div class="card__content"> <h2 class="card__title">{{ data.title }}</h2> <p class="card__text">{{ data.text }}</p> <router-link class="card__link" :to="{ name: 'Home' }"> View more </router-link> </div> </div> </div> </template> <script setup> const props = defineProps(['data']); const emit = defineEmits(['update:is-loaded']); const getSrc = ext => { return new URL( `../assets/images/recipe/${props.data.image}${ext}`, import.meta.url ).href; }; // 图片加载完成/失败时通知父组件 const handleImageLoad = () => { emit('update:is-loaded', true); }; </script>
效果说明
- 数据请求阶段:显示3个骨架屏;
- 数据加载完成后:每个卡片初始显示骨架屏,对应图片加载完成后自动切换为真实卡片;
- 图片加载失败时:自动切换为真实卡片,避免骨架屏一直显示;
- 完全符合“部分加载完就显示对应卡片,其余维持骨架屏”的需求。
内容的提问来源于stack exchange,提问作者Michael
相关产品推荐
相关产品推荐

