首屏轮播图使用cdn加载webp图片的代码案例

86 阅读4分钟

代码由Doubao-seed-code生成,以下是我的prompt:

  1. 实现一个二次封装的轮播图组件代码开发,并给出使用示例
  2. 组件图片获取存在时效限制,先从CDN返回的图片直接展示,失败返回或者超过2000ms还没有返回的图片,将用户无感知得重试2-3次;后续再次失败,则从自己的远程服务器;再获取不到不重试走默认兜底逻辑
  3. 图片先加载直接进行展示,不需要Promise.all等待所有图片结果加载完成之后,再全部展示;如果有图片暂时没加载出来,显示占位符或者骨架屏
  4. 图片的格式优先webp等高性能格式;如果浏览器不支持webp格式,则选择png/jpg等自输入样式兜底;
  5. 轮播图的组件基于element plus 开发
  6. 使用Ts进行编写

不过体感同一个prompt在同一个模型下生成的代码不一定一致,细节上需要开发人员关注并调整。

特地在prompt里面突出Promise.all,主要还是因为trae生成的代码总是使用Promise.all返回图片,但我希望图片先获得先直接展示。但是如果不在prompt里面添加提示词“不需要Promise.all等待所有图片结果加载完成之后,再全部展示”,模型无法正确理解“图片先加载直接进行展示”的意思。

<template>
  <el-carousel :interval="5000" type="card" height="400px">
    <el-carousel-item v-for="(image, index) in processedImages" :key="index">
      <img
        :src="image.src"
        :alt="`Slide ${index + 1}`"
        class="carousel-image"
      />
    </el-carousel-item>
  </el-carousel>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

interface ImageItem {
  cdn: string
  remote: string
  fallback: string
  suffix: string
}

interface ProcessedImageItem extends ImageItem {
  src: string
}

const props = defineProps({
  images: {
    type: Array as () => ImageItem[],
    required: true
  }
})

const processedImages = ref<ProcessedImageItem[]>([])

// 检测浏览器是否支持 webp 格式
const checkWebpSupport = (): Promise<boolean> => {
  return new Promise((resolve) => {
    const img = new Image()
    img.onload = () => resolve(true)
    img.onerror = () => resolve(false)
    img.src = 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA='
  })
}

// 尝试加载图片,带超时和重试机制
const loadImageWithTimeout = (url: string, timeout: number = 2000): Promise<string> => {
  return new Promise<string>((resolve, reject) => {
    const img = new Image()
    const timer = setTimeout(() => reject(new Error('Image load timeout')), timeout)
    
    img.onload = () => {
      clearTimeout(timer)
      resolve(url)
    }
    
    img.onerror = () => {
      clearTimeout(timer)
      reject(new Error('Image load failed'))
    }
    
    img.src = url
  })
}

// 尝试加载图片,按优先级顺序
const loadImage = async (image: ImageItem): Promise<string> => {
  const supportsWebp = await checkWebpSupport()
  
  // 生成不同格式的图片 URL
  const getUrlByFormat = (url: string): string => {
    if (supportsWebp) {
      return url
    } else {
      // 替换 webp 为 png 或 jpg
      return url.replace('.webp', image.suffix)
    }
  }
  
  // 按优先级尝试加载图片
  const cdnUrl = getUrlByFormat(image.cdn)
  const remoteUrl = getUrlByFormat(image.remote)
  const fallbackUrl = image.fallback
  
  // 尝试 CDN 图片,最多重试 3 次
  const maxRetries = 3
  for (let i = 0; i < maxRetries; i++) {
    try {
      await loadImageWithTimeout(cdnUrl)
      return cdnUrl
    } catch (error) {
      console.log(`Failed to load CDN image (attempt ${i + 1}/${maxRetries}): ${error.message}`)
      // 无感知重试,不抛出错误
    }
  }
  
  // CDN 失败,尝试远程服务器
  try {
    await loadImageWithTimeout(remoteUrl)
    return remoteUrl
  } catch (error) {
    console.log(`Failed to load remote image: ${error.message}`)
  }
  
  // 远程服务器失败,尝试兜底图片
  try {
    await loadImageWithTimeout(fallbackUrl)
    return fallbackUrl
  } catch (error) {
    console.log(`Failed to load fallback image: ${error.message}`)
  }
  
  // 所有尝试都失败,返回默认兜底图片
  return 'https://via.placeholder.com/800x400?text=Image+Not+Available'
}

// 处理所有图片,实现渐进式加载
const processImages = (): void => {
  // 初始化 processedImages,使用占位符
  processedImages.value = props.images.map(image => ({
    ...image,
    src: 'https://via.placeholder.com/800x400?text=Loading...'
  }))
  
  // 并行加载所有图片
  props.images.forEach((image, index) => {
    loadImage(image).then(src => {
      // 图片加载完成后更新对应项
      processedImages.value[index] = {
        ...image,
        src
      }
    })
  })
}

onMounted(() => {
  processImages()
})
</script>

页面使用案例:

<template>
  <div class="app-container">
    <h1>轮播图组件测试</h1>
    <carousel-component :images="images" />
  </div>
</template>

<script setup lang="ts">
import CarouselComponent from './components/CarouselComponent.vue'

interface ImageItem {
  cdn: string
  remote: string
  fallback: string
  suffix: string
}

const images: ImageItem[] = [
  {
    cdn: 'https://cdn.example.com/image1.webp',
    remote: 'https://example.com/image1.webp',
    fallback: 'https://example.com/default1.jpg',
    suffix: '.png'
  },
  {
    cdn: 'https://cdn.example.com/image2.webp',
    remote: 'https://example.com/image2.webp',
    fallback: 'https://example.com/default2.jpg',
    suffix: '.png'
  },
  {
    cdn: 'https://cdn.example.com/image3.webp',
    remote: 'https://example.com/image3.webp',
    fallback: 'https://example.com/default3.jpg',
    suffix: '.png'
  }
]
</script>

知识点解析:

Q1:如何识别到浏览器支持webp格式

A1: 主流的方法有:

  1. 加载一张最基础的base64的webp图片,如果加载成功则说明浏览器支持webp格式,如果加载失败就说明不支持,最基础也是最常用的方法
const checkWebpSupport = (): Promise<boolean> => {
  return new Promise((resolve) => {
    const img = new Image()
    img.onload = () => resolve(true)
    img.onerror = () => resolve(false)
    img.src = 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA='
  })
}

//正式使用时获取promise的结果
const supportsWebp = await checkWebpSupport()
  
// 生成不同格式的图片 URL
const getUrlByFormat = (url: string): string => {
  if (supportsWebp) {
    return url
  } else {
    // 替换 webp 为 png 或 jpg
    return url.replace('.webp', image.suffix)   
  }
}
  1. 建立一张canvas,导成webp的格式,如果能正常导出,说明支持webp;如果失败到处说明不支持,需要建立dom节点
function isSupportWebPSync() {
  const canvas = document.createElement('canvas');
  // 尝试把 canvas 导出成 webP 格式
  const dataURL = canvas.toDataURL('image/webp');
  // 如果返回的字符串包含 webp 说明支持
  return dataURL.indexOf('data:image/webp') === 0;
}

如果浏览器支持 WebP,会返回:

data:image/webp;base64,...

不支持,浏览器会自动降级成 png,返回:

data:image/png;base64,...

3. navigator.mediaCapabilities提供查询功能,但是navigator这个API本身只在部分浏览器有效

Q: 如何实现加载CDN失败则重试2-3次;再次失败则走到本地服务器;依然失败则走到兜底方案?

A:使用Promise抛出错误,trycatch捕获错误的方式

// 尝试加载图片,带超时和重试机制
const loadImageWithTimeout = (url: string, timeout: number = 2000): Promise<string> => {
  return new Promise<string>((resolve, reject) => {
    const img = new Image()
    //添加setTimeout进行时间限制
    const timer = setTimeout(() => reject(new Error('Image load timeout')), timeout)
    
    img.onload = () => {
      //及时加载则清除定时器,并且返回url
      clearTimeout(timer)
      resolve(url)
    }
    
    img.onerror = () => {
      //加载失败则清除定时器,并且抛出错误,用于跟后续的trycatch进行联动
      clearTimeout(timer)
      reject(new Error('Image load failed'))
    }
    
    img.src = url
  })
}
// 按优先级尝试加载图片
  const cdnUrl = getUrlByFormat(image.cdn)
  const remoteUrl = getUrlByFormat(image.remote)
  const fallbackUrl = image.fallback
  
  // 尝试 CDN 图片,最多重试 3 次
  const maxRetries = 3
  for (let i = 0; i < maxRetries; i++) {
    try {
      await loadImageWithTimeout(cdnUrl)
      //如果cdnUrl获取数据,则直接抛出结果,不会再执行到后面远程服务器以及兜底的逻辑
      return cdnUrl
    } catch (error) {
     //弹出错误,并且继续循环,直到都失败,继续执行后续远程服务器的逻辑
      console.log(`Failed to load CDN image (attempt ${i + 1}/${maxRetries}): ${error.message}`)
      // 无感知重试,不抛出错误
    }
  }
  
  // CDN 失败,尝试远程服务器
  try {
    await loadImageWithTimeout(remoteUrl)
    return remoteUrl
  } catch (error) {
    console.log(`Failed to load remote image: ${error.message}`)
  }
  
  // 远程服务器失败,尝试兜底图片
  try {
    await loadImageWithTimeout(fallbackUrl)
    return fallbackUrl
  } catch (error) {
    console.log(`Failed to load fallback image: ${error.message}`)
  }
  
  // 所有尝试都失败,返回默认兜底图片
  return 'https://via.placeholder.com/800x400?text=Image+Not+Available'
  1. CDN加载机制

image.png

远程CDN文件有两种链接地址:

(1)写死的通用库地址,比如vue3.6的地址

(2)需要根据文件名动态生成的地址,比如图片包含content-hash名的文件