feat: 新增 icons.region 配置项&修改 favicon 加载超时机制&修复去除硬编码
- 新增 icons.region: com | cn 配置项,允许用户选择优先使用国内源或国外源 - com: 优先 gstatic.com,失败回退 gstatic.cn - cn: 优先 gstatic.cn,失败回退 gstatic.com - 修改 favicon 加载超时判断机制 - 自定义 faviconUrl: 5秒超时后显示回退图标 - 自动 favicon: 每次尝试3秒超时,最多等待6秒 - 更新配置文档和默认配置示例 - 去除卡片模板中的url硬编码 Issue: #31
This commit is contained in:
14
README.md
14
README.md
@@ -43,6 +43,20 @@
|
||||
<details>
|
||||
<summary>点击查看/隐藏更新日志</summary>
|
||||
|
||||
### 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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导航';
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,11 +38,11 @@
|
||||
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
|
||||
<img
|
||||
class="favicon-icon"
|
||||
src="https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url={{encodeURIComponent url}}&size=32"
|
||||
src="{{faviconUrl url}}"
|
||||
alt="{{name}} favicon"
|
||||
loading="lazy"
|
||||
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
|
||||
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; var next = this.src.replace('t3.gstatic.com', 't3.gstatic.cn'); if (next !== this.src) { this.src = next; return; } } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');"
|
||||
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');"
|
||||
/>
|
||||
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
|
||||
</div>
|
||||
@@ -56,11 +56,11 @@
|
||||
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
|
||||
<img
|
||||
class="favicon-icon"
|
||||
src="https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url={{encodeURIComponent url}}&size=32"
|
||||
src="{{faviconUrl url}}"
|
||||
alt="{{name}} favicon"
|
||||
loading="lazy"
|
||||
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
|
||||
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; var next = this.src.replace('t3.gstatic.com', 't3.gstatic.cn'); if (next !== this.src) { this.src = next; return; } } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');"
|
||||
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');"
|
||||
/>
|
||||
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
|
||||
</div>
|
||||
@@ -156,11 +156,11 @@
|
||||
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
|
||||
<img
|
||||
class="favicon-icon"
|
||||
src="https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url={{encodeURIComponent url}}&size=32"
|
||||
src="{{faviconUrl url}}"
|
||||
alt="{{name}} favicon"
|
||||
loading="lazy"
|
||||
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
|
||||
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; var next = this.src.replace('t3.gstatic.com', 't3.gstatic.cn'); if (next !== this.src) { this.src = next; return; } } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');"
|
||||
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');"
|
||||
/>
|
||||
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
|
||||
</div>
|
||||
@@ -174,11 +174,11 @@
|
||||
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
|
||||
<img
|
||||
class="favicon-icon"
|
||||
src="https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url={{encodeURIComponent url}}&size=32"
|
||||
src="{{faviconUrl url}}"
|
||||
alt="{{name}} favicon"
|
||||
loading="lazy"
|
||||
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
|
||||
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; var next = this.src.replace('t3.gstatic.com', 't3.gstatic.cn'); if (next !== this.src) { this.src = next; return; } } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');"
|
||||
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');"
|
||||
/>
|
||||
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user