feat: 实现 Mermaid 库加载失败的降级处理机制

- 添加多 CDN 备选方案(jsdelivr、unpkg、本地镜像)
- 实现递归加载逻辑,主 CDN 失败时自动尝试备用 CDN
- 添加 onerror 事件处理,捕获库加载失败
- 所有 CDN 失败时显示友好的错误提示
- 在错误提示中保留原始代码供用户查看
- 添加详细的控制台日志输出
- 创建 PHP 和 HTML 测试文件验证功能
- 暴露 MermaidRenderer 到全局作用域供降级处理使用

Requirements: 1.4, 2.3, 7.1, 7.2, 7.3, 7.4, 7.5
This commit is contained in:
2026-01-23 23:12:05 +08:00
parent 43b695bd66
commit 1d5899ce7e
8 changed files with 1962 additions and 3 deletions

View File

@@ -0,0 +1,374 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mermaid 库加载失败降级处理测试</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.test-section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 {
color: #333;
border-bottom: 2px solid #5e72e4;
padding-bottom: 10px;
}
h2 {
color: #5e72e4;
margin-top: 0;
}
.mermaid {
background: #f8f9fa;
padding: 20px;
border-radius: 4px;
margin: 10px 0;
}
.mermaid-error-container {
background: #fff5f5;
border: 1px solid #fc8181;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.mermaid-error-header {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.mermaid-error-icon {
font-size: 24px;
margin-right: 10px;
}
.mermaid-error-title {
font-size: 18px;
font-weight: bold;
color: #c53030;
}
.mermaid-error-body {
margin-bottom: 15px;
}
.mermaid-error-type {
font-weight: bold;
color: #742a2a;
margin: 5px 0;
}
.mermaid-error-message {
color: #742a2a;
margin: 5px 0;
}
.mermaid-error-code {
margin-top: 10px;
}
.mermaid-error-code summary {
cursor: pointer;
color: #5e72e4;
font-weight: bold;
}
.mermaid-error-code pre {
background: #2d3748;
color: #e2e8f0;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
margin-top: 10px;
}
.test-log {
background: #2d3748;
color: #e2e8f0;
padding: 15px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 14px;
max-height: 300px;
overflow-y: auto;
margin-top: 10px;
}
.test-log div {
margin: 5px 0;
}
.log-info {
color: #63b3ed;
}
.log-warn {
color: #f6ad55;
}
.log-error {
color: #fc8181;
}
.log-success {
color: #68d391;
}
.test-controls {
margin: 20px 0;
}
.test-controls button {
background: #5e72e4;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
font-size: 14px;
}
.test-controls button:hover {
background: #4c63d2;
}
.test-controls button:disabled {
background: #cbd5e0;
cursor: not-allowed;
}
</style>
</head>
<body>
<h1>🧪 Mermaid 库加载失败降级处理测试</h1>
<div class="test-section">
<h2>测试说明</h2>
<p>本测试页面用于验证 Mermaid 库加载失败时的降级处理机制。</p>
<ul>
<li><strong>测试 1</strong>: 主 CDN 加载失败,自动尝试备用 CDN</li>
<li><strong>测试 2</strong>: 所有 CDN 都失败,显示友好的错误提示</li>
<li><strong>测试 3</strong>: 备用 CDN 加载成功,正常渲染图表</li>
</ul>
</div>
<div class="test-section">
<h2>测试控制</h2>
<div class="test-controls">
<button onclick="testFailedMainCDN()">测试 1: 主 CDN 失败</button>
<button onclick="testAllCDNsFailed()">测试 2: 所有 CDN 失败</button>
<button onclick="testSuccessfulFallback()">测试 3: 备用 CDN 成功</button>
<button onclick="clearLog()">清空日志</button>
</div>
<div id="testLog" class="test-log"></div>
</div>
<div class="test-section">
<h2>测试图表</h2>
<div class="mermaid">
flowchart TD
A[开始] --> B{主 CDN 加载}
B -->|成功| C[渲染图表]
B -->|失败| D[尝试备用 CDN 1]
D -->|成功| C
D -->|失败| E[尝试备用 CDN 2]
E -->|成功| C
E -->|失败| F[显示错误提示]
</div>
</div>
<script>
// 模拟降级处理脚本
(function() {
'use strict';
// 备用 CDN URL 列表
const fallbackUrls = [
'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js',
'https://unpkg.com/mermaid@10/dist/mermaid.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.0.0/mermaid.min.js'
];
let loadAttempted = false;
// 日志函数
function log(message, type = 'info') {
const logDiv = document.getElementById('testLog');
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-${type}`;
logEntry.textContent = `[${timestamp}] ${message}`;
logDiv.appendChild(logEntry);
logDiv.scrollTop = logDiv.scrollHeight;
// 同时输出到控制台
console.log(`[Argon Mermaid] ${message}`);
}
/**
* 尝试从备用 CDN 加载 Mermaid 库
*/
window.argonMermaidLoadFallback = function() {
// 避免重复调用
if (loadAttempted) {
return;
}
loadAttempted = true;
log('主 CDN 加载失败,尝试备用 CDN', 'warn');
// 尝试加载备用 CDN
loadMermaidWithFallback(fallbackUrls, 0);
};
/**
* 递归加载备用 CDN
* @param {Array} urls - CDN URL 列表
* @param {number} index - 当前尝试的索引
*/
function loadMermaidWithFallback(urls, index) {
// 如果所有 CDN 都失败了
if (index >= urls.length) {
log('所有 CDN 加载失败', 'error');
showGlobalError();
return;
}
const url = urls[index];
log(`尝试从备用 CDN 加载: ${url}`, 'info');
// 创建 script 标签
const script = document.createElement('script');
script.src = url;
script.async = true;
// 加载失败,尝试下一个 CDN
script.onerror = function() {
log(`CDN ${url} 加载失败`, 'warn');
loadMermaidWithFallback(urls, index + 1);
};
// 加载成功,初始化 Mermaid
script.onload = function() {
log(`成功从备用 CDN 加载: ${url}`, 'success');
// 初始化 Mermaid
if (typeof window.mermaid !== 'undefined') {
window.mermaid.initialize({
startOnLoad: true,
theme: 'default'
});
log('Mermaid 初始化成功', 'success');
}
};
// 添加到页面
document.head.appendChild(script);
}
/**
* 显示全局错误提示
*/
function showGlobalError() {
// 查找所有 Mermaid 代码块
const selectors = [
'div.mermaid',
'pre code.language-mermaid',
'pre[data-lang="mermaid"]',
'code.mermaid'
];
let blocks = [];
selectors.forEach(function(selector) {
const elements = document.querySelectorAll(selector);
elements.forEach(function(element) {
if (!blocks.includes(element)) {
blocks.push(element);
}
});
});
log(`找到 ${blocks.length} 个 Mermaid 代码块,显示错误提示`, 'info');
// 在每个代码块位置显示错误提示
blocks.forEach(function(block) {
const errorContainer = document.createElement('div');
errorContainer.className = 'mermaid-error-container';
errorContainer.innerHTML = `
<div class="mermaid-error-header">
<span class="mermaid-error-icon">⚠️</span>
<span class="mermaid-error-title">Mermaid 库加载失败</span>
</div>
<div class="mermaid-error-body">
<p class="mermaid-error-type">错误类型: 网络错误</p>
<p class="mermaid-error-message">无法从任何 CDN 加载 Mermaid 库,请检查网络连接或联系管理员。</p>
</div>
<details class="mermaid-error-code">
<summary>查看原始代码</summary>
<pre><code class="language-mermaid">${escapeHtml(block.textContent)}</code></pre>
</details>
`;
block.parentNode.replaceChild(errorContainer, block);
});
}
/**
* HTML 转义
* @param {string} text - 要转义的文本
* @returns {string} 转义后的文本
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 测试函数
window.testFailedMainCDN = function() {
log('=== 开始测试 1: 主 CDN 失败 ===', 'info');
loadAttempted = false;
argonMermaidLoadFallback();
};
window.testAllCDNsFailed = function() {
log('=== 开始测试 2: 所有 CDN 失败 ===', 'info');
loadAttempted = false;
// 使用无效的 URL 列表
loadMermaidWithFallback(['https://invalid-cdn-1.com/mermaid.js', 'https://invalid-cdn-2.com/mermaid.js'], 0);
};
window.testSuccessfulFallback = function() {
log('=== 开始测试 3: 备用 CDN 成功 ===', 'info');
loadAttempted = false;
// 直接加载有效的 CDN
loadMermaidWithFallback(fallbackUrls, 0);
};
window.clearLog = function() {
document.getElementById('testLog').innerHTML = '';
log('日志已清空', 'info');
};
// 页面加载完成后的初始化
log('测试页面加载完成', 'success');
log('点击上方按钮开始测试', 'info');
})();
</script>
</body>
</html>