先上结论:给 <img>
绑定 @error
,在回调里将 src
切到默认头像,并断开二次触发,配合 new URL(..., import.meta.url).href
解析静态资源路径,可靠、可维护。
场景与目标
- 登录用户有头像 URL,但可能 404/跨域/失效
- 希望头像加载失败时自动展示本地默认图
- 方案要易用、无副作用、可复用
方案一:在组件内用 @error
兜底(最轻量)
核心点:
@error="setDefaultAvatar"
捕获加载错误event.target.src = defaultAvatar
切换默认图event.target.onerror = null
防止死循环- 用
new URL(..., import.meta.url).href
解析本地静态资源,避免相对路径坑
示例(节选自 header.vue):
<template>
<div class="user-info">
<!-- 未登录 -->
<a href="javascript:;" class="loginBtn" @click="goLogin('/index')" v-if="!isLogin()">
<img src="../../assets/logo/yonghutu.png" alt="" class="user-avatar" @error="setDefaultAvatar">
</a>
<span class="user-name" v-if="!isLogin()" @click="goLogin('/index')">登录</span>
<!-- 已登录 -->
<a href="javascript:;" class="loginBtn" @click="router.push('/index')" v-if="isLogin()">
<img :src="avatar" alt="" class="user-avatar" @error="setDefaultAvatar">
</a>
<span class="user-name" v-if="isLogin()" @click="router.push('/index')">{{ userName }}</span>
</div>
</template>
<script setup>
import { ref } from 'vue'
const defaultAvatar = new URL('../../assets/logo/yonghutu.png', import.meta.url).href
const avatar = ref(defaultAvatar)
function setDefaultAvatar(event) {
try {
if (event && event.target) {
event.target.src = defaultAvatar
// 防止 onerror 死循环
event.target.onerror = null
}
} catch (_) {}
}
</script>
为什么用 new URL
?
- 构建工具(Vite/Rollup)会静态分析并正确处理资源(hash、输出目录)
- 避免相对路径在不同目录/构建模式下失效
注意点:
- 一定要在回调里置空
onerror
,否则默认图异常也会循环触发 - 默认图建议放
src/assets
,构建时会被正确打包
方案二:抽成全局指令(全站任意 img 一把梭)
当全局大量使用图片兜底时推荐。一次注册,哪里需要哪里用。
指令定义(例如 src/directives/imgFallback.js
):
export default {
mounted(el, binding) {
const fallbackSrc = binding.value
if (!fallbackSrc) return
el.addEventListener('error', function onErr() {
el.src = fallbackSrc
el.removeEventListener('error', onErr) // 防止死循环
})
},
}
在入口注册(main.js
):
import { createApp } from 'vue'
import App from './App.vue'
import imgFallback from './directives/imgFallback'
const app = createApp(App)
app.directive('img-fallback', imgFallback)
app.mount('#app')
使用:
<img :src="user.avatar" v-img-fallback="defaultAvatar" alt="">
搭配 new URL
:
const defaultAvatar = new URL('@/assets/logo/yonghutu.png', import.meta.url).href
优点:
- 一次注册,全局可用
- 模板更干净,不用每次都写
@error
可选增强:封装组件 <AvatarImg />
当你想统一尺寸、圆角、占位 skeleton、裁剪模式时,用组件最舒服。
<!-- components/AvatarImg.vue -->
<template>
<img
:src="currentSrc"
:alt="alt"
:style="style"
@error="onErr"
loading="lazy"
decoding="async"
/>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Props {
src?: string
fallback?: string
size?: number
radius?: number | string
fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
alt?: string
}
const props = withDefaults(defineProps<Props>(), {
size: 28,
radius: '50%',
fit: 'cover',
alt: 'avatar',
})
const defaultFallback = new URL('@/assets/logo/yonghutu.png', import.meta.url).href
const currentSrc = ref(props.src || defaultFallback)
function onErr(e: Event) {
currentSrc.value = props.fallback || defaultFallback
const target = e.target as HTMLImageElement
target.onerror = null
}
const style = computed(() => ({
width: `${props.size}px`,
height: `${props.size}px`,
borderRadius: typeof props.radius === 'number' ? `${props.radius}px` : props.radius,
objectFit: props.fit,
}))
</script>
使用:
<AvatarImg :src="user.avatar" :fallback="defaultAvatar" :size="32" />
踩坑提示(别跳)
- 防死循环:兜底里务必
el.onerror = null
或移除监听 - 跨域:远程头像若无 CORS,不能
canvas
操作;兜底不受影响 - Token 鉴权:需要带 Header 的图片建议走后端代理;兜底依然有效
- CLS 问题:给
<img>
固定width/height
或用包裹容器固定尺寸,避免布局抖动 - SSR/静态导出:
new URL(..., import.meta.url)
在 Vite/SSR 里均可用,避免硬编码路径 - 性能:加上
loading="lazy" decoding="async"
,滚动页面更顺畅
结论
- 局部用法:
@error + setDefaultAvatar
,最简单 - 全局用法:自定义指令
v-img-fallback
- 高级用法:封装
<AvatarImg />
统一样式与行为
参考
- Vue 官方文档(指令与事件处理)
https://vuejs.org/guide/essentials/template-syntax.html
- Vite 资源处理
https://vitejs.dev/guide/assets.html