Last active
March 2, 2025 14:05
获取网站logo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 域名提取选项 | |
*/ | |
interface DomainOptions { | |
/** | |
* 是否移除域名中的 'www.' 前缀 | |
* @默认值 true | |
*/ | |
stripWWW?: boolean; | |
} | |
/** | |
* 网站图标获取选项 | |
*/ | |
interface FaviconOptions { | |
/** | |
* 每个图标请求的超时时间(毫秒) | |
* @默认值 1000 | |
*/ | |
timeout?: number; | |
/** | |
* 自定义备选图标URL列表 | |
* 如果提供,将使用自定义列表替代默认列表 | |
*/ | |
fallbacks?: string[]; | |
} | |
/** | |
* 网站图标相关错误类型 | |
*/ | |
class FaviconError extends Error { | |
constructor(message: string, public readonly cause?: Error) { | |
super(message); | |
this.name = 'FaviconError'; | |
} | |
} | |
/** | |
* 从URL字符串中提取域名 | |
* @param url - 要提取域名的URL | |
* @param options - 域名提取选项 | |
* @returns 提取后的域名 | |
*/ | |
export function getDomain(url: string, options: DomainOptions = { stripWWW: true }): string { | |
try { | |
const parsed = new URL(url); | |
return options.stripWWW ? parsed.hostname.replace(/^www\./, '') : parsed.hostname; | |
} catch { | |
// 对非法URL的兼容处理 | |
const domain = url.replace(/^(https?:\/\/)?(www\.)?/, '').split('/')[0]; | |
return options.stripWWW ? domain.replace(/^www\./, '') : domain; | |
} | |
} | |
/** | |
* 默认备选图标URL生成器 | |
*/ | |
const DEFAULT_FALLBACKS = (domain: string): string[] => [ | |
`https://${domain}/favicon.ico`, | |
`https://${domain}/logo.svg`, | |
`https://${domain}/logo.png`, | |
`https://${domain}/apple-touch-icon.png`, | |
`https://${domain}/apple-touch-icon-precomposed.png`, | |
`https://www.google.com/s2/favicons?domain=${domain}&sz=64`, | |
`https://icons.duckduckgo.com/ip3/${domain}.ico`, | |
]; | |
/** | |
* 创建超时拒绝的Promise | |
*/ | |
function createTimeout(ms: number): Promise<never> { | |
return new Promise((_, reject) => { | |
setTimeout(() => reject(new FaviconError(`操作在${ms}毫秒后超时`)), ms); | |
}); | |
} | |
/** | |
* 带超时的图片加载方法 | |
* @throws {FaviconError} | |
*/ | |
async function loadImageWithTimeout(src: string, timeout: number): Promise<void> { | |
if (typeof Image !== 'undefined') { | |
// 浏览器环境 | |
await Promise.race([ | |
new Promise((resolve, reject) => { | |
const img = new Image(); | |
img.onload = () => resolve(); | |
img.onerror = () => reject(new FaviconError('图片加载失败')); | |
img.src = src; | |
}), | |
createTimeout(timeout) | |
]); | |
} else { | |
// Node.js/Bun 环境 | |
const controller = new AbortController(); | |
const timeoutId = setTimeout(() => controller.abort(), timeout); | |
try { | |
const response = await fetch(src, { | |
method: 'HEAD', | |
signal: controller.signal | |
}); | |
clearTimeout(timeoutId); | |
if (!response.ok) { | |
throw new FaviconError(`无效的状态码: ${response.status}`); | |
} | |
// 检查内容类型是否为图片 | |
const contentType = response.headers.get('Content-Type'); | |
if (!contentType?.startsWith('image/')) { | |
throw new FaviconError(`无效的内容类型: ${contentType}`); | |
} | |
} catch (error) { | |
clearTimeout(timeoutId); | |
throw new FaviconError( | |
'请求失败', | |
error instanceof Error ? error : undefined | |
); | |
} | |
} | |
} | |
/** | |
* 获取网站的图标URL | |
* @param url - 目标网站URL | |
* @param options - 图标获取选项 | |
* @returns 图标URL或null(未找到时) | |
*/ | |
export async function getFavicon( | |
url: string, | |
options: FaviconOptions = {} | |
): Promise<string | null> { | |
if (!url) { | |
throw new FaviconError('URL不能为空'); | |
} | |
const timeout = options.timeout ?? 1000; | |
const domain = getDomain(url); | |
const fallbacks = options.fallbacks ?? DEFAULT_FALLBACKS(domain); | |
for (const source of fallbacks) { | |
try { | |
await loadImageWithTimeout(source, timeout); | |
return source; | |
} catch (e) { | |
// console.error(`${source}: ${e}`); | |
// 继续尝试下一个备选URL | |
continue; | |
} | |
} | |
return null; | |
} | |
// 使用示例 | |
const url = await getFavicon('https://www.deepseek.com'); | |
console.log(url); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment