fix(logo): fetch 预检 + 全局缓存根除 favicon 加载控制台噪音
CI / Backend tests (push) Canceled after 0s
CI / Frontend build (push) Canceled after 0s

- 用 fetch(no-cors) 代替直接 img.src,网络层失败(DNS/连接/超时)不再涌入控制台报错
- 全局跨组件 URL 探测缓存 + 24h 死链 TTL,列表滚动不重复探测
- 过滤 *.github.com 无效子域名(如 epeli.github.com),从源头减少无效请求
- 添加 referrerpolicy=no-referrer,减少跨域策略噪音
This commit is contained in:
yinjun.chen1
2026-09-21 22:19:55 +08:00
parent a688adeffa
commit b0ac90ca9e
+54 -3
View File
@@ -57,11 +57,15 @@ const style = computed(() => {
// - 其他主页 → 域名根路径 /favicon.ico
// 加载失败一律回退首字母徽标
const imgOk = ref(false)
const actualSrc = ref('')
const faviconUrl = computed(() => {
const hp = props.homepage
if (!hp) return ''
try {
const u = new URL(hp)
// 过滤明显无效的 GitHub 子域名(如 epeli.github.com,正确应为 *.github.io)
if (u.hostname.endsWith('.github.com') && u.hostname !== 'www.github.com') return ''
if (u.hostname === 'github.com' || u.hostname === 'www.github.com') {
const m = hp.match(/^https?:\/\/(?:www\.)?github\.com\/([^/?#]+)\/[^/?#]+/)
if (m) return `https://github.com/${m[1]}.png`
@@ -71,15 +75,62 @@ const faviconUrl = computed(() => {
return ''
}
})
watch(faviconUrl, () => { imgOk.value = false })
// ===== 共享 URL 探测缓存(跨组件全局)=====
// 策略:用 fetch(no-cors) 预检代替直接 img.src。
// fetch 的网络层失败(DNS/连接/超时)不会涌入控制台红色报错;
// 同 URL 所有组件实例共用一个 Promise,避免重复并发。
// 404 等 no-cors 下 opaque resolve,由 img @error 兜底(少量且可控)。
const _urlCache = new Map() // url -> { ok, expires }
const _urlPending = new Map() // url -> Promise<boolean>
const TTL = 24 * 3600 * 1000 // 失败缓存 24h,避免反复探测已知的死链
const PROBE_TIMEOUT = 8000 // 探测超时 8s
function getUrlCache(url) {
const entry = _urlCache.get(url)
if (!entry) return null
if (entry.expires < Date.now()) { _urlCache.delete(url); return null }
return entry.ok
}
async function probeUrl(url) {
const cached = getUrlCache(url)
if (cached !== null) return cached
if (_urlPending.has(url)) return _urlPending.get(url)
const p = (async () => {
try {
const ctrl = new AbortController()
const t = setTimeout(() => ctrl.abort(), PROBE_TIMEOUT)
await fetch(url, { mode: 'no-cors', signal: ctrl.signal, referrerPolicy: 'no-referrer' })
clearTimeout(t)
_urlCache.set(url, { ok: true, expires: Date.now() + TTL })
return true
} catch {
_urlCache.set(url, { ok: false, expires: Date.now() + TTL })
return false
}
})()
_urlPending.set(url, p)
try { return await p } finally { _urlPending.delete(url) }
}
watch([() => inView.value, () => faviconUrl.value], async ([v, url]) => {
if (!v || !url) { actualSrc.value = ''; imgOk.value = false; return }
const ok = await probeUrl(url)
if (ok) actualSrc.value = url
else { actualSrc.value = ''; imgOk.value = false }
}, { immediate: true })
function onImgOk() { imgOk.value = true }
function onImgErr() { imgOk.value = false }
</script>
<template>
<span ref="rootEl" class="pkg-logo" :style="style" :title="name">
<!-- 进入视口后才挂载 img 发起 favicon 请求;加载成功后显示,失败回退字母徽标 -->
<img v-if="faviconUrl && inView" class="fav" :class="{ on: imgOk }" :src="faviconUrl" alt="" @load="onImgOk" @error="onImgErr" />
<!-- fetch 预检通过后才挂载 img;加载成功后显示,失败回退字母徽标 -->
<img v-if="actualSrc" class="fav" :class="{ on: imgOk }" :src="actualSrc" alt="" referrerpolicy="no-referrer" @load="onImgOk" @error="onImgErr" />
<span v-show="!imgOk" class="lt">{{ letter }}</span>
</span>
</template>