阅读视图

发现新文章,点击刷新页面。

Vue使用<Suspense/>实现图片加载组件

什么是Suspense?

SuspenseVue的内置组件,能让你管理组件加载时的等待、出错和最终渲染逻辑。

主要配合异步组件async/await 版的 setup() 使用。

它提供两个插槽:

  • default(默认插槽):要渲染的异步内容
  • fallback:等待异步内容加载时显示的兜底内容

话不多说,上代码:

第一步,实现图片异步组件

<template>
    <img 
    :src="props.src" 
    :alt="props.alt" 
    :style="{ 
      width: setWidth, 
      height: setHeight, 
      borderRadius: rounded, 
      objectFit: 'cover'
    }"
    />
</template>
// =================================== 此处为计算属性逻辑 ================================
<script setup lang="ts">
import { computed, ref } from "vue";

// 图片形状可选项
type ShapeOption = "circle" | "square" | "roundRect";

interface ILazyImage {
  src: string;
  width:string|number;
  height:string|number;
  delay?:number;//延迟执行?
  timeout?:number;//超时时间?
  shape?:ShapeOption;//图片形状
  alt?:string;
}

const props = defineProps<ILazyImage>();

//计算超时
const timeoutSet = computed(() => {
    let time = Number(props.timeout) || 5;
    return time * 1000;
});

//计算延迟
const delayTime = computed(() => {
    let time = Number(props.delay) || 0.1;
    return time * 1000;
});


//计算宽度
const setWidth = computed(() => {
    const val = parseFloat(props.width)
    if(!val) return '100px'
    return val  + "px";
});

//计算高度
const setHeight = computed(() => {
    const val = parseFloat(props.Height)
    if(!val) return '100px'
    return val  + "px";
});

// 计算圆角
const rounded = computed(() => {
    switch (props.shape) {
        case "circle":
            return "50%";
        case "square":
            return "0";
        case "roundRect":
            return "10px";
        default:
            return "10px";
    }
});

//========================实现异步加载图片==================================

const loadImage = (src: string) => {
    return new Promise((resolve, reject) => {
    // 创建一个AbortController,用于取消请求
        const controller = new AbortController();
        const signal = controller.signal;

        const timeoutId = setTimeout(() => {
        // 超时时取消请求
            controller.abort();
            reject(new Error("图片加载超时"));
        }, timeoutSet.value);

        setTimeout(() => {
            const image = new Image();
            image.src = src;
            image.onload = () => {
                clearTimeout(timeoutId);
                resolve(src);
            };
            image.onerror = () => {
                clearTimeout(timeoutId);
                reject(new Error("图片加载失败"));
            };
        }, delayTime.value);
    });
};

// 执行图片加载(async/await 触发 Suspense 等待)
await loadImage(props.src);

</script>

第二步:实现懒加载组件

  • default(默认插槽):要渲染的异步内容
  • fallback:等待异步内容加载时显示的兜底内容
  • 使用v-bind()绑定属性

<Suspense></Suspense>中直接使用异步组件即可,<template #fallback></template>则是加载时显示

<template>
    <div>
        <Suspense>
            <template #default>
                <div class="image">
                    <Image v-bind="{ ...props }"></Image>
                </div>
            </template>
            <template #fallback>
                <div class="skeleton"></div>
            </template>
        </Suspense>
    </div>
</template>

<script setup lang="ts">
import { computed } from "vue";
import Image from "./Image.vue";
// 图片形状可选项
type ShapeOption = "circle" | "square" | "roundRect";

interface ILazyImage {
  src: string;
  width:string|number;
  height:string|number;
  delay?:number;//延迟执行?
  timeout?:number;//超时时间?
  shape?:ShapeOption;//图片形状
  alt?:string;
}

const props = defineProps<ILazyImage>();

//计算超时
const timeoutSet = computed(() => {
    let time = Number(props.timeout) || 5;
    return time * 1000;
});

//计算延迟
const delayTime = computed(() => {
    let time = Number(props.delay) || 0.1;
    return time * 1000;
});


//计算宽度
const setWidth = computed(() => {
    const val = parseFloat(props.width)
    if(!val) return '100px'
    return val  + "px";
});

//计算高度
const setHeight = computed(() => {
    const val = parseFloat(props.Height)
    if(!val) return '100px'
    return val  + "px";
});

// 计算骨架屏动画:遮罩宽度(取容器宽度的 40%) 
const setSeletonWidth = computed(() => { 
    // 提取宽度数值(兼容 px 单位) 
    const widthNum = parseFloat(setWidth.value) || 100; 
    return `${widthNum * 0.4}px`;
}); 


// 骨架屏动画起始位置(从容器左侧外 30% 开始)
const setSeletonStartRight = computed(() => { 
    const widthNum = parseFloat(setWidth.value) || 100; 
    return `-${widthNum * 1.3}px`;  // 起始在容器左侧外
});

// 骨架屏动画结束位置(到容器右侧外 30%) 
const setSeletonEndRight = computed(() => { 
    const widthNum = parseFloat(setWidth.value) || 100; return `${widthNum * 1.3}px`;
    // 结束在容器右侧外
});

// 计算圆角
const rounded = computed(() => {
    switch (props.shape) {
        case "circle":
            return "50%";
        case "square":
            return "0";
        case "roundRect":
            return "10px";
        default:
            return "10px";
    }
});
</script>

<style scoped>
.image {
    overflow: hidden;
}

.skeleton {
    width: v-bind(setWidth);
    height: v-bind(setHeight);
    background-color: #dfe4ea;
    overflow: hidden;
    position: relative;

    border-radius: v-bind(rounded);
}

.skeleton::before {
    content: "";
    display: block;
    background-color: #ced6e0;
    box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
    position: absolute;
    top: 0;
    right: v-bind(setSeletonStartRight);
    width: v-bind(setSeletonWidth);
    height: v-bind(setHeight);
    transform: skewX(-30deg);
    animation: ping 1s infinite ease-out;
    filter: blur(10px);
}

@keyframes ping {
    from {
        right: v-bind(setSeletonStartRight);
    }

    to {
        right: v-bind(setSeletonEndRight);
    }
}
</style>

补充

  • 代码内有些重复的内容,可使用另外的ts来声明或实现。

  • 其中可能有些变量或实现过程有误,此文章仅供参考。

❌