fix(logo): fetch 预检 + 全局缓存根除 favicon 加载控制台噪音
- 用 fetch(no-cors) 代替直接 img.src,网络层失败(DNS/连接/超时)不再涌入控制台报错 - 全局跨组件 URL 探测缓存 + 24h 死链 TTL,列表滚动不重复探测 - 过滤 *.github.com 无效子域名(如 epeli.github.com),从源头减少无效请求 - 添加 referrerpolicy=no-referrer,减少跨域策略噪音
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user