自定义组件的props怎么设计才更灵活?

Mr.爱娜 阅读 17

我写了一个用户头像组件,但不确定props该怎么设计才能兼顾默认头像和自定义头像地址。现在传了url就用url,没传就用默认图,但感觉这样扩展性不好,比如以后还要加尺寸、形状等选项。

目前是这么写的:

<template>
  <img :src="avatarUrl || '/default-avatar.png'" alt="avatar" />
</template>

<script>
export default {
  props: ['avatarUrl']
}
</script>
我来解答 赞 3 收藏
二维码
手机扫码查看
1 条解答
设计师艺童
首先你要明白,组件设计的灵活性本质上是把可变的部分抽出来,让调用者能按需覆盖,而默认行为要足够智能。你现在的写法其实能用,但确实有点“硬编码”了,比如尺寸、形状、加载失败的兜底逻辑这些都还没考虑进去,后面加起来会很别扭。

我建议你分三层来设计 props:

第一层是核心内容,就是头像来源,这个不要只传一个字符串,而是用对象形式,这样以后加字段不冲突
第二层是展示控制,比如尺寸、形状、是否圆形这些,用简单类型就行
第三层是事件和回调,比如加载失败的时候要不要重试、加载完成要不要通知父组件

具体来改写你的组件:

先看 props 怎么写:

props: {
// 头像配置对象,优先级最高,可以覆盖其他 props
avatar: {
type: [String, Object],
default: ''
},
// 兼容旧写法,如果直接传字符串,当作 url 处理
src: {
type: String,
default: ''
},
// 尺寸,支持 px 数值或带单位的字符串,比如 40 或 '40px' 或 '2rem'
size: {
type: [Number, String],
default: 40
},
// 形状:circle 圆形,square 方形,rounded 圆角矩形
shape: {
type: String,
default: 'circle'
},
// 加载失败时的备用图(可选,不传就用默认图)
fallbackUrl: {
type: String,
default: ''
},
// 是否在加载失败时自动重试
retry: {
type: Boolean,
default: false
}
}


然后 template 里用计算属性把所有逻辑理清楚,别让模板里全是三元表达式,看着累:

computed: {
finalAvatarUrl() {
// 优先用 avatar 对象里的 url 字段(如果传的是对象)
if (typeof this.avatar === 'object' && this.avatar.url) {
return this.avatar.url
}
// 其次用 src prop(兼容旧写法)
if (this.src) {
return this.src
}
// 最后用默认图
return '/default-avatar.png'
},
finalFallbackUrl() {
// 如果没传 fallbackUrl,就用默认图兜底
return this.fallbackUrl || '/default-avatar.png'
},
// 尺寸处理:统一转成 '40px' 这种形式
sizeStyle() {
const val = this.size
if (typeof val === 'number') {
return ${val}px
}
if (typeof val === 'string' && !isNaN(Number(val))) {
return ${val}px
}
return val // 已经带单位了,直接用
},
// 形状对应的类名
shapeClass() {
const map = {
circle: 'avatar--circle',
square: 'avatar--square',
rounded: 'avatar--rounded'
}
return map[this.shape] || 'avatar--circle'
}
}


template 就清爽多了:

<img
:src="finalAvatarUrl"
:alt="alt || 'avatar'"
:class="['avatar', shapeClass]"
:style="{ width: sizeStyle, height: sizeStyle }"
@error="handleError"
@load="handleLoad"
/>


再加几个方法处理错误和加载事件:

methods: {
handleError(e) {
// 如果允许重试,先加个简单防抖重试
if (this.retry && !this.retryCount) {
this.retryCount = 1
setTimeout(() => {
e.target.src = this.finalFallbackUrl
}, 200)
return
}

// 不重试或者重试也失败,就换 fallbackUrl
e.target.src = this.finalFallbackUrl

// 如果需要通知父组件,可以抛个事件
this.$emit('error', e)
},
handleLoad(e) {
this.$emit('load', e)
}
}


最后注意加个 alt 属性支持,别忘了:

props: {
alt: {
type: String,
default: ''
}
}


这样设计的好处是:
调用的时候可以很灵活,比如直接传字符串
也可以高级点传对象
后面再想加新功能,比如懒加载、加载骨架屏,直接在 computed 或 methods 里加逻辑就行,不用改调用方式

你可能会说这样 props 定义有点多,但实际开发中,宁可多写几个可选 prop,也别在组件内部 hardcode 一堆逻辑,否则别人用你组件的时候,发现“怎么这个尺寸改不了?”“怎么不能圆角?”“加载失败没兜底?”,那才是真麻烦。

另外建议给这个组件加个文档注释,比如 JSDoc,写清楚每个 prop 的用途和默认值,自己半年后再看也能秒懂,别人接手也不抓狂。
点赞 3
2026-02-23 20:42