Skip to content

Instantly share code, notes, and snippets.

@lhlyu
Last active March 2, 2025 14:05
获取网站logo
/**
* 域名提取选项
*/
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