
本文介绍如何设计一个真正通用的 Vue 3 composable(useRecipe),使其既能接收组件 props 中的单个 recipe 对象,也能直接消费 Pinia store 中响应式数据,统一处理图片路径生成与加载状态管理。
本文介绍如何设计一个真正通用的 vue 3 composable(`userecipe`),使其既能接收组件 props 中的单个 recipe 对象,也能直接消费 pinia store 中响应式数据,统一处理图片路径生成与加载状态管理。
在 Vue 3 项目中,将重复逻辑(如图片预加载、路径解析)抽离为 composable 是最佳实践。但若直接将 props.data 或 store.data 硬编码传入,会导致 composable 失去通用性——正如原始问题所示:ItemCard 依赖 props.data,而 ViewRecipeDetail 依赖 storeRecipe.data,二者结构相似(均为单个 recipe 对象),却因来源不同难以共用同一逻辑。
关键在于:composable 应对“数据源”保持抽象,而非绑定具体变量名或响应式来源。解决方案是利用 Vue 的 toValue 工具函数 + watchEffect 自动追踪响应式变化:
-
toValue(data)可安全解包ref、computed或普通对象,避免类型判断; -
watchEffect在data(无论来自 props 还是 store)变更时自动触发重新加载; - 返回
src(加载完成后的 URL)和isLoaded(布尔状态),语义清晰且无需手动调用getSrc()。
以下是优化后的通用 composable 实现:
// composables/useRecipe.js
import { ref, toValue, watchEffect } from 'vue'
export function useRecipe(data, ext = '.jpg') {
const isLoaded = ref(false)
const src = ref(null)
const loadImage = () => {
isLoaded.value = false
const imageKey = toValue(data)?.image
if (!imageKey) return
try {
const url = new URL(
`../assets/images/content/recipe/${imageKey}${ext}`,
import.meta.url
).href
const img = new Image()
img.onload = () => {
src.value = url
isLoaded.value = true
}
img.onerror = () => {
console.warn(`Failed to load recipe image: ${url}`)
src.value = null
isLoaded.value = false
}
img.src = url
} catch (err) {
console.error('Invalid image path:', err)
src.value = null
isLoaded.value = false
}
}
// 自动响应 data 变更(props 更新、store 切换等)
watchEffect(loadImage)
return {
src,
isLoaded
}
}✅ 使用方式完全一致,来源无关:
立即学习“前端免费学习笔记(深入)”;
<!-- ItemCard.vue -->
<script setup>
import { useRecipe } from '@/composables/useRecipe'
const props = defineProps(['data'])
const { src, isLoaded } = useRecipe(props.data, '.jpg')
</script>
<template>
<img class="card__image" :src="src" :alt="data.alt" v-if="isLoaded" />
<div v-else class="skeleton-loader" /> <!-- 加载中占位符 -->
</template><!-- ViewRecipeDetail.vue -->
<script setup>
import { useStoreRecipe } from '@/store/storeRecipe'
import { useRecipe } from '@/composables/useRecipe'
const storeRecipe = useStoreRecipe()
const { src, isLoaded } = useRecipe(storeRecipe.data, '.jpg')
</script>
<template>
<img class="card__image" :src="src" :alt="storeRecipe.data?.alt" v-if="isLoaded" />
<div v-else class="skeleton-loader" />
<div class="card__content">
<h2>{{ storeRecipe.data?.title }}</h2>
<p>{{ storeRecipe.data?.short_description }}</p>
</div>
</template>⚠️ 注意事项:
-
data必须是响应式对象(ref/computed/store属性)或普通对象;若为null/undefined,toValue安全返回null,不会报错; -
ext参数支持自定义后缀(如.webp),默认.jpg; - 添加了
onerror回调与try/catch,增强健壮性; -
v-if="isLoaded"配合骨架屏,确保视觉一致性; - 不再需要
onMounted手动触发——watchEffect在首次渲染和后续更新时均自动执行。
通过此设计,useRecipe 成为真正“通用”的组合式函数:它不关心数据来自 props、store、computed 还是 ref,只关注“当前有效 recipe 数据是否存在 image 字段”。这正是 Vue 3 响应式系统与组合式 API 赋予开发者的核心能力——逻辑复用,而非模板复制。


















