先上結論:給 <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.valueif (!fallbackSrc) returnel.addEventListener('error', function onErr() {el.src = fallbackSrcel.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?: stringfallback?: stringsize?: numberradius?: number | stringfit?: '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 || defaultFallbackconst target = e.target as HTMLImageElementtarget.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