From 3473aaebd7e4d23df0782789ec7fd04e12186f1a Mon Sep 17 00:00:00 2001 From: rbetree Date: Sat, 3 Jan 2026 16:43:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20icons.region=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=A1=B9&=E4=BF=AE=E6=94=B9=20favicon=20?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E8=B6=85=E6=97=B6=E6=9C=BA=E5=88=B6&?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8E=BB=E9=99=A4=E7=A1=AC=E7=BC=96=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 icons.region: com | cn 配置项,允许用户选择优先使用国内源或国外源 - com: 优先 gstatic.com,失败回退 gstatic.cn - cn: 优先 gstatic.cn,失败回退 gstatic.com - 修改 favicon 加载超时判断机制 - 自定义 faviconUrl: 5秒超时后显示回退图标 - 自动 favicon: 每次尝试3秒超时,最多等待6秒 - 更新配置文档和默认配置示例 - 去除卡片模板中的url硬编码 Issue: #31 --- README.md | 14 +++++++++ config/README.md | 6 +++- config/_default/site.yml | 8 ++++- src/generator.js | 2 ++ src/helpers/utils.js | 46 +++++++++++++++++++++++++++- src/script.js | 48 ++++++++++++++++++++++++++++-- templates/components/site-card.hbs | 16 +++++----- 7 files changed, 127 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 40e0007..0b66a76 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,20 @@
点击查看/隐藏更新日志 +### 2026/01/03 + +关联 Issue:[#31](https://github.com/rbetree/menav/issues/31) + +**1. favicon 加载优化** + +- 新增 `icons.region: com | cn` 配置项,允许用户选择优先使用国内源或国外源 + - `com`(默认):优先 gstatic.com,失败回退 gstatic.cn + - `cn`:优先 gstatic.cn,失败回退 gstatic.com +- 修改 favicon 加载超时判断机制 + - 自定义 faviconUrl:5秒超时后显示回退图标 + - 自动 favicon:每次尝试3秒超时,最多等待6秒 + - 避免网络慢时长时间显示加载动画 + ### 2026/01/02 关联 Issue:[#30](https://github.com/rbetree/menav/issues/30) diff --git a/config/README.md b/config/README.md index 94b086a..482bf75 100644 --- a/config/README.md +++ b/config/README.md @@ -106,6 +106,10 @@ MeNav 配置系统采用“完全替换”策略(不合并),按以下优 - `icons.mode: favicon | manual` - `favicon`:会请求第三方服务(Google)获取站点 favicon,失败自动回退到 Font Awesome 图标 - `manual`:完全使用手动 Font Awesome 图标,不发起外部请求(适合内网/离线/隐私敏感场景) + - `icons.region: com | cn`(默认 `com`) + - `com`:优先使用 `gstatic.com`(国际版),失败后回退到 `gstatic.cn`(中国版) + - `cn`:优先使用 `gstatic.cn`(中国版),失败后回退到 `gstatic.com`(国际版) + - 说明:如果你在中国大陆且访问 gstatic.com 较慢,建议设置为 `cn` 以提升图标加载速度 - 站点级覆盖(可选,写在 `pages/*.yml` 的每个 `sites[]` 节点上): - `faviconUrl`:为单个站点指定图标链接(可远程或本地相对路径;本地建议以 `assets/` 开头,构建会复制到 `dist/` 同路径),优先级最高 - `forceIconMode: favicon | manual`:强制该站点使用指定模式(不设置则跟随全局 `icons.mode`) @@ -116,7 +120,7 @@ MeNav 配置系统采用“完全替换”策略(不合并),按以下优 - name: "Ant Design" url: "https://ant.design/" icon: "fas fa-th" - forceIconMode: manual # 强制使用手动图标,绕过 favicon 默认“地球”图标 + forceIconMode: manual # 强制使用手动图标,绕过 favicon 默认"地球"图标 - name: "Example" url: "https://example.com/" faviconUrl: "https://example.com/favicon.png" # 单站点自定义 favicon diff --git a/config/_default/site.yml b/config/_default/site.yml index 36e3b46..319c526 100644 --- a/config/_default/site.yml +++ b/config/_default/site.yml @@ -12,9 +12,15 @@ icons: # 站点卡片图标模式: # - favicon:自动根据 URL 加载站点 favicon(失败时回退到 Font Awesome 图标) # - manual:始终使用手动指定的 Font Awesome 图标(不发起外部请求) - # 隐私提示:启用 favicon 模式会请求第三方服务以获取图标,可能将站点 URL 发送给服务商(详见 README“隐私说明”)。 + # 隐私提示:启用 favicon 模式会请求第三方服务以获取图标,可能将站点 URL 发送给服务商(详见 README"隐私说明")。 mode: favicon # 可选: favicon | manual(默认 favicon) + # favicon 服务区域选择(仅在 mode: favicon 时生效): + # - com:优先使用 gstatic.com(国际版),失败后回退到 gstatic.cn(中国版) + # - cn:优先使用 gstatic.cn(中国版),失败后回退到 gstatic.com(国际版) + # 说明:如果你在中国大陆且访问 gstatic.com 较慢,建议设置为 cn 以提升图标加载速度 + region: cn # 可选: com | cn(默认 com) + # 字体设置:全站基础字体 # - source: css | google | system # - css: 通过 cssUrl 引入第三方字体 CSS diff --git a/src/generator.js b/src/generator.js index 004263e..e01c6a5 100644 --- a/src/generator.js +++ b/src/generator.js @@ -307,6 +307,8 @@ function ensureConfigDefaults(config) { result.icons = result.icons || {}; // icons.mode: manual | favicon, 默认 favicon result.icons.mode = result.icons.mode || 'favicon'; + // icons.region: com | cn, 默认 com(优先使用 gstatic.com,失败后回退到 gstatic.cn) + result.icons.region = result.icons.region || 'com'; // 站点基本信息默认值 result.site.title = result.site.title || 'MeNav导航'; diff --git a/src/helpers/utils.js b/src/helpers/utils.js index 47ca280..e9a6ffe 100644 --- a/src/helpers/utils.js +++ b/src/helpers/utils.js @@ -194,6 +194,48 @@ function add(a, b) { return numA + numB; } +/** + * 根据 icons.region 配置生成 favicon URL + * @param {string} url 站点 URL + * @param {Object} options Handlebars options 对象 + * @returns {string} favicon URL + * @example {{faviconUrl url}} + */ +function faviconUrl(url, options) { + if (!url) return ''; + + const region = options.data.root.icons?.region || 'com'; + const domain = region === 'cn' ? 't3.gstatic.cn' : 't3.gstatic.com'; + + try { + const encodedUrl = encodeURIComponent(String(url)); + return `https://${domain}/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodedUrl}&size=32`; + } catch (e) { + return ''; + } +} + +/** + * 根据 icons.region 配置生成 favicon 回退 URL + * @param {string} url 站点 URL + * @param {Object} options Handlebars options 对象 + * @returns {string} favicon 回退 URL + * @example {{faviconFallbackUrl url}} + */ +function faviconFallbackUrl(url, options) { + if (!url) return ''; + + const region = options.data.root.icons?.region || 'com'; + const domain = region === 'cn' ? 't3.gstatic.com' : 't3.gstatic.cn'; + + try { + const encodedUrl = encodeURIComponent(String(url)); + return `https://${domain}/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodedUrl}&size=32`; + } catch (e) { + return ''; + } +} + // 导出所有工具类助手函数 module.exports = { slice, @@ -205,5 +247,7 @@ module.exports = { pick, keys, encodeURIComponent: encodeURIComponentHelper, - add + add, + faviconUrl, + faviconFallbackUrl }; diff --git a/src/script.js b/src/script.js index 8d08b7a..535f4ea 100644 --- a/src/script.js +++ b/src/script.js @@ -392,14 +392,17 @@ window.MeNav = { contentWrapper.appendChild(descEl); let iconsMode = 'favicon'; + let iconsRegion = 'com'; try { const cfg = window.MeNav && typeof window.MeNav.getConfig === 'function' ? window.MeNav.getConfig() : null; iconsMode = (cfg && (cfg.data?.icons?.mode || cfg.icons?.mode)) || 'favicon'; + iconsRegion = (cfg && (cfg.data?.icons?.region || cfg.icons?.region)) || 'com'; } catch (e) { iconsMode = 'favicon'; + iconsRegion = 'com'; } const shouldUseCustomFavicon = Boolean(siteFaviconUrl); @@ -422,11 +425,23 @@ window.MeNav = { favicon.src = siteFaviconUrl; favicon.alt = `${siteName} favicon`; favicon.loading = 'lazy'; + + // 超时处理:5秒后如果还没加载成功,显示回退图标 + let loadTimeout = setTimeout(() => { + if (!favicon.classList.contains('loaded')) { + favicon.classList.add('error'); + placeholder.classList.add('hidden'); + fallback.classList.add('visible'); + } + }, 5000); + favicon.addEventListener('load', () => { + clearTimeout(loadTimeout); favicon.classList.add('loaded'); placeholder.classList.add('hidden'); }); favicon.addEventListener('error', () => { + clearTimeout(loadTimeout); favicon.classList.add('error'); placeholder.classList.add('hidden'); fallback.classList.add('visible'); @@ -437,8 +452,13 @@ window.MeNav = { iconContainer.appendChild(fallback); iconWrapper.appendChild(iconContainer); } else if (effectiveIconsMode === 'favicon' && siteUrl && /^https?:\/\//i.test(siteUrl)) { - const faviconUrlPrimary = `https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(siteUrl)}&size=32`; - const faviconUrlFallback = `https://t3.gstatic.cn/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(siteUrl)}&size=32`; + // 根据 icons.region 配置决定优先使用哪个域名 + const faviconUrlPrimary = iconsRegion === 'cn' + ? `https://t3.gstatic.cn/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(siteUrl)}&size=32` + : `https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(siteUrl)}&size=32`; + const faviconUrlFallback = iconsRegion === 'cn' + ? `https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(siteUrl)}&size=32` + : `https://t3.gstatic.cn/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(siteUrl)}&size=32`; const iconContainer = document.createElement('div'); iconContainer.className = 'icon-container'; @@ -457,14 +477,38 @@ window.MeNav = { favicon.alt = `${siteName} favicon`; favicon.loading = 'lazy'; let faviconFallbackTried = false; + let loadTimeout = null; + + // 超时处理:3秒后如果还没加载成功,尝试回退 URL 或显示 Font Awesome 图标 + const startTimeout = () => { + if (loadTimeout) clearTimeout(loadTimeout); + loadTimeout = setTimeout(() => { + if (!favicon.classList.contains('loaded')) { + if (!faviconFallbackTried) { + faviconFallbackTried = true; + favicon.src = faviconUrlFallback; + startTimeout(); // 为 fallback URL 也设置超时 + } else { + favicon.classList.add('error'); + placeholder.classList.add('hidden'); + fallback.classList.add('visible'); + } + } + }, 3000); + }; + startTimeout(); + favicon.addEventListener('load', () => { + if (loadTimeout) clearTimeout(loadTimeout); favicon.classList.add('loaded'); placeholder.classList.add('hidden'); }); favicon.addEventListener('error', () => { + if (loadTimeout) clearTimeout(loadTimeout); if (!faviconFallbackTried) { faviconFallbackTried = true; favicon.src = faviconUrlFallback; + startTimeout(); // 为 fallback URL 也设置超时 return; } diff --git a/templates/components/site-card.hbs b/templates/components/site-card.hbs index 52c3435..10931ae 100644 --- a/templates/components/site-card.hbs +++ b/templates/components/site-card.hbs @@ -38,11 +38,11 @@ {{name}} favicon @@ -56,11 +56,11 @@ {{name}} favicon @@ -156,11 +156,11 @@ {{name}} favicon @@ -174,11 +174,11 @@ {{name}} favicon