📅 我们继续 50 个小项目挑战!—— RandomImageGenerator组件

仓库地址:https://github.com/SunACong/50-vue-projects

项目预览地址:https://50-vue-projects.vercel.app/


使用 Vue 3 的组合式 API (<script setup>) 和 Tailwind CSS 创建一个动态的随机图片流展示页面。这个组件会从 Lorem Picsum 服务获取不同尺寸的随机图片,并在图片加载失败时自动重试,非常适合用作作品集、博客或设计类网站的背景或内容填充。


🎯 应用目标

  • 创建一个包含多张随机图片的网格布局
  • 每张图片从 Lorem Picsum 服务获取,尺寸随机
  • 图片加载失败时自动尝试加载新的随机图片
  • 使用 Vue 3 的响应式系统管理图片数据
  • 实现页面加载时自动初始化图片流

🔧 技术实现点

技术点 描述
Vue 3 <script setup> 使用 ref, onMounted
ref images 数组存储所有图片信息(URL、宽、高)
onMounted 组件挂载完成后调用 generateImages 初始化图片
v-for 遍历 images 数组,渲染图片元素
:key 为每个图片项提供唯一的 key(使用 index
:style 动态绑定图片的 widthheight 样式
@error 事件 监听图片加载失败,调用 handleImageError 重新加载
Math.random() 生成随机数,用于:
  • 图片尺寸
  • URL 查询参数(防止缓存)
  • 重试时生成新图片 |
    | Tailwind CSS flex-wrap / max-w-6xl / object-cover | 创建响应式网格容器和图片样式 |

📚 核心数据与状态

1. 图片数据 (images)

这是一个响应式数组,使用 ref 包裹。每个数组项是一个对象,包含:

  • url: 图片的完整 URL
  • width: 图片的宽度(像素)
  • height: 图片的高度(像素)

2. 配置常量

常量 作用
baseURL 'https://picsum.photos' Lorem Picsum 服务的基础 URL
rows 5 网格的行数
columns 3 网格的列数
totalImages rows * columns = 15 总共需要生成的图片数量

3. 辅助函数

函数 作用
getRandomSize() 返回一个介于 300 到 309 像素之间的随机整数(Math.floor(Math.random() * 10) + 300
generateImages() 生成 rows * columns 张图片的数据,并赋值给 images.value
handleImageError(index) 当索引为 index 的图片加载失败时,为其生成一张新的随机图片

🖌️ 组件实现

🎨 模板结构 <template>

<template>
    <div class="font-roboto flex min-h-screen flex-col items-center justify-center bg-white p-4">
        <h1 class="title my-6 text-center text-3xl font-bold">Random Image Feed</h1>
        <div class="container mx-auto flex max-w-6xl flex-wrap items-center justify-center">
            <div v-for="(image, index) in images" :key="index" class="m-3">
                <img
                    :src="image.url"
                    :alt="'Random image ' + (index + 1)"
                    class="h-full w-full max-w-full object-cover"
                    :style="{ width: image.width + 'px', height: image.height + 'px' }"
                    @error="handleImageError(index)" />
            </div>
        </div>
    </div>
</template>

模板结构解析:

  1. 外层容器 (div)

    • flex min-h-screen flex-col items-center justify-center:使用 Flexbox 将整个内容在视口中垂直居中。flex-col 表示子元素垂直排列。
    • bg-white:白色背景。
    • p-4:整体内边距。
    • font-roboto:使用 Roboto 字体(需引入)。
  2. 标题 (h1)

    • text-3xl:大号字体。
    • font-bold:粗体。
    • text-center:居中对齐。
    • my-6:上下外边距。
  3. 图片容器 (div.container)

    • flex max-w-6xl flex-wrap:创建一个最大宽度为 6xl (80rem ≈ 1280px) 的 Flex 容器,并允许子元素换行 (flex-wrap)。
    • mx-auto:水平居中。
    • items-center justify-center:子元素在交叉轴(垂直)和主轴(水平,换行后)上都居中。
    • 这个容器内的 div 元素(每个包含一张图片)会根据屏幕宽度自动排列成多行。
  4. 单个图片项 (divimg)

    • v-for="(image, index) in images":遍历 images 数组。
    • :key="index":为每个列表项提供唯一的 key。注意:如果 images 数组顺序会变或项目会被删除/插入,使用 index 作为 key 可能不是最佳实践。但在此例中,数组长度固定且顺序不变,index 是可接受的。
    • class="m-3":每个图片项周围有 0.75rem 的外边距,形成网格间隙。
    • <img> 元素
      • :src="image.url":绑定图片 URL。
      • :alt="...":动态生成 alt 文本,提升可访问性。
      • class="h-full w-full max-w-full object-cover"
        • h-full / w-full:尝试填满父容器 div
        • max-w-full:防止图片宽度超过其容器。
        • object-cover关键! 确保图片按比例缩放并覆盖整个容器,常用于图片网格,避免留白。
      • :style="{ width: image.width + 'px', height: image.height + 'px' }"关键! 使用内联样式精确设置图片的宽度和高度(以像素为单位)。这是实现不同尺寸图片网格的核心。Tailwind 的 w-* / h-* 类无法直接处理变量。
      • @error="handleImageError(index)":监听图片加载失败事件,将当前图片的 index 传递给处理函数。

💻 脚本逻辑 <script setup>

<script setup>
    import { ref, onMounted } from 'vue'

    const images = ref([])
    const baseURL = 'https://picsum.photos'
    const rows = 5
    const columns = 3

    const getRandomSize = () => Math.floor(Math.random() * 10) + 300

    const generateImages = () => {
        const newImages = []
        for (let i = 0; i < rows * columns; i++) {
            const width = getRandomSize()
            const height = getRandomSize()
            // 用随机 query 避免缓存重复
            const url = `${baseURL}/${width}/${height}?random=${Math.random()}`
            newImages.push({ url, width, height })
        }
        images.value = newImages
    }

    const handleImageError = (index) => {
        const width = getRandomSize()
        const height = getRandomSize()
        images.value[index] = {
            ...images.value[index],
            url: `${baseURL}/${width}/${height}?random=${Math.random()}`,
            width,
            height,
        }
    }

    onMounted(generateImages)
</script>

脚本逻辑详解:

  • getRandomSize() 函数

    • Math.random() 生成 [0, 1) 的随机小数。
    • * 10 得到 [0, 10)
    • Math.floor() 向下取整,得到 [0, 9] 的整数。
    • + 300 得到 [300, 309] 的整数。确保图片不会太小。
  • generateImages() 函数

    • 创建一个空数组 newImages
    • 使用 for 循环生成 rows * columns (15) 张图片。
    • 为每张图片生成随机的 widthheight
    • 构造 URL:${baseURL}/${width}/${height}?random=${Math.random()}
      • /width/height 是 Picsum 的标准格式。
      • ?random=... 查询参数是关键!它确保即使 widthheight 相同,浏览器也会认为是不同的 URL,从而避免从缓存加载完全相同的图片。这对于展示“随机”图片流至关重要。
    • 将包含 url, width, height 的对象推入 newImages
    • 最后将 newImages 赋值给 images.value,触发视图更新。
  • handleImageError(index) 函数

    • 当某张图片加载失败(如网络问题、Picsum 服务暂时不可用)时,该函数被调用。
    • 传入 index 参数,知道是哪一张图片出错。
    • 生成新的 widthheight
    • 构造一个新的、带有新随机查询参数的 URL。
    • 使用数组索引赋值 images.value[index] = { ... } 更新该特定图片的数据。
      • ...images.value[index] 展开原对象(虽然这里只有 url, width, height 会被覆盖,但保留了结构)。
      • 覆盖 url, width, height 为新值。
    • 由于 images 是响应式的,这个更新会立即触发视图重新渲染该图片,尝试加载新的随机图片。
  • onMounted(generateImages)

    • 使用 onMounted 生命周期钩子,在组件挂载到 DOM 后立即调用 generateImages 函数,初始化图片流。

🎨 Tailwind CSS 样式重点

类名 作用
font-roboto 使用 Roboto 字体
min-h-screen 最小高度为视口高度
flex / flex-col / items-center / justify-center Flexbox 布局,垂直居中
bg-white 白色背景
p-4 内边距
text-3xl / text-center / font-bold / my-6 标题样式
max-w-6xl 最大宽度 (80rem ≈ 1280px)
flex-wrap 允许子元素换行
mx-auto 水平居中
m-3 外边距 (0.75rem),形成网格间隙
w-full / h-full / max-w-full / object-cover 图片样式,确保按比例覆盖容器
:style 动态设置精确的像素尺寸

⚠️ 注意事项与潜在问题

  1. index 作为 key:虽然在此例中(固定长度、不改变顺序)是安全的,但通常建议使用唯一 ID 作为 key。如果未来功能扩展(如删除、插入、排序图片),使用 index 会导致性能问题和状态混乱。
  2. 图片加载性能:一次性加载 15 张图片(即使尺寸不大)可能会对网络和页面性能造成压力,尤其是在移动设备或慢速网络上。可以考虑:
    • 懒加载 (Lazy Loading):使用 loading="lazy" 属性或 Intersection Observer API,只在图片进入视口时才加载。
    • 分页或无限滚动:不要一次性加载所有图片。
  3. Picsum 服务依赖:应用完全依赖于外部服务 picsum.photos。如果该服务宕机或更改 API,你的图片流将无法显示。handleImageError 只能处理单次失败,如果服务大面积不可用,用户会看到多次闪烁的重试。
  4. 随机性getRandomSize() 产生的尺寸范围 (300-309px) 非常窄,图片尺寸差异不大。可以扩大范围(如 200-600)以获得更丰富的视觉效果。
  5. 响应式flex-wrap 提供了基本的响应式,但可以进一步优化不同屏幕尺寸下的 rows/columnsmax-w-*

📁 常量定义 + 组件路由

constants/index.js 添加组件预览常量:

{
        id: 48,
        title: 'RandomImageGenerator',
        image: 'https://50projects50days.com/img/projects-img/48-random-image-feed.png',
        link: 'RandomImageGenerator',
    },

router/index.js 中添加路由选项:

{
        path: '/RandomImageGenerator',
        name: 'RandomImageGenerator',
        component: () => import('@/projects/RandomImageGenerator.vue'),
    },

🏁 总结

项目很好地展示了如何利用外部 API(Lorem Picsum)快速搭建视觉内容,并通过 @error 事件和响应式数据更新来提升用户体验的健壮性。通过结合 Vue 3 的响应式特性、生命周期管理、事件处理和 Tailwind CSS 的实用类,我们实现了图片的动态生成、精确尺寸控制和错误恢复机制。 happy coding! ✨

你可以扩展以下功能:

  • 添加加载状态:在图片加载完成前显示骨架屏 (Skeleton Screen) 或加载动画。
  • 实现懒加载:提升初始加载性能。
  • 增加交互:点击图片放大预览,或添加喜欢/收藏功能。
  • 搜索或筛选:允许用户通过关键词搜索 Picsum 图片(Picsum 支持 ?grayscale 等参数)。
  • 动态调整网格:提供按钮让用户选择每行显示的图片数量。
  • 错误边界:如果 handleImageError 也失败(如网络完全断开),可以显示一个通用的“加载失败”占位符。

👉 下一篇,我们将完成TodoList组件,一个视觉舒适的TODO UI设计。🚀

感谢阅读,欢迎点赞、收藏和分享 😊

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐