Vue3 图片加载失败回退为默认图:最简、健壮的两种实现(含完整代码)

发布于:2025-08-30 ⋅ 阅读:(15) ⋅ 点赞:(0)

先上结论:给 <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

网站公告

今日签到

点亮在社区的每一天
去签到