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:
374
tests/test-mermaid-fallback.html
Normal file
374
tests/test-mermaid-fallback.html
Normal 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>
|
||||
249
tests/test-mermaid-fallback.php
Normal file
249
tests/test-mermaid-fallback.php
Normal file
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
/**
|
||||
* Mermaid 库加载失败降级处理测试
|
||||
*
|
||||
* 测试场景:
|
||||
* 1. 主 CDN 加载失败时,自动尝试备用 CDN
|
||||
* 2. 所有 CDN 都失败时,显示友好的错误提示
|
||||
* 3. 备用 CDN 加载成功后,正常初始化渲染引擎
|
||||
*/
|
||||
|
||||
// 加载 WordPress 环境
|
||||
require_once dirname(__FILE__) . '/../../../wp-load.php';
|
||||
|
||||
// 加载主题函数
|
||||
require_once get_template_directory() . '/functions.php';
|
||||
|
||||
/**
|
||||
* 测试 1: 验证备用 CDN URL 列表
|
||||
*/
|
||||
function test_fallback_urls() {
|
||||
echo "测试 1: 验证备用 CDN URL 列表\n";
|
||||
echo str_repeat('-', 50) . "\n";
|
||||
|
||||
$fallback_urls = argon_get_mermaid_fallback_urls();
|
||||
|
||||
// 验证返回的是数组
|
||||
if (!is_array($fallback_urls)) {
|
||||
echo "❌ 失败: 返回值不是数组\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证至少有 3 个备用 URL
|
||||
if (count($fallback_urls) < 3) {
|
||||
echo "❌ 失败: 备用 URL 数量少于 3 个\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证每个 URL 都是有效的
|
||||
foreach ($fallback_urls as $index => $url) {
|
||||
if (empty($url)) {
|
||||
echo "❌ 失败: URL #{$index} 为空\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证 URL 格式
|
||||
if (!preg_match('/^https?:\/\/.+\.js$/', $url) && !preg_match('/\/mermaid\.min\.js$/', $url)) {
|
||||
echo "❌ 失败: URL #{$index} 格式无效: {$url}\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
echo "✓ URL #{$index}: {$url}\n";
|
||||
}
|
||||
|
||||
echo "✅ 通过: 备用 CDN URL 列表验证成功\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 2: 验证降级处理脚本生成
|
||||
*/
|
||||
function test_fallback_script_generation() {
|
||||
echo "测试 2: 验证降级处理脚本生成\n";
|
||||
echo str_repeat('-', 50) . "\n";
|
||||
|
||||
// 启用 Mermaid
|
||||
update_option('argon_mermaid_enabled', true);
|
||||
|
||||
// 创建一个包含 Mermaid 代码块的测试文章
|
||||
global $post;
|
||||
$post = (object) [
|
||||
'ID' => 1,
|
||||
'post_content' => '<div class="mermaid">flowchart TD\nA-->B</div>'
|
||||
];
|
||||
|
||||
// 捕获输出
|
||||
ob_start();
|
||||
argon_add_mermaid_fallback_script();
|
||||
$output = ob_get_clean();
|
||||
|
||||
// 验证输出包含必要的脚本
|
||||
$checks = [
|
||||
'argonMermaidLoadFallback' => '降级处理函数',
|
||||
'loadMermaidWithFallback' => '递归加载函数',
|
||||
'showGlobalError' => '错误提示函数',
|
||||
'fallbackUrls' => '备用 URL 列表',
|
||||
'script.onerror' => '错误处理',
|
||||
'script.onload' => '加载成功处理'
|
||||
];
|
||||
|
||||
$all_passed = true;
|
||||
foreach ($checks as $keyword => $description) {
|
||||
if (strpos($output, $keyword) !== false) {
|
||||
echo "✓ 包含 {$description}\n";
|
||||
} else {
|
||||
echo "❌ 缺少 {$description}\n";
|
||||
$all_passed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($all_passed) {
|
||||
echo "✅ 通过: 降级处理脚本生成正确\n\n";
|
||||
return true;
|
||||
} else {
|
||||
echo "❌ 失败: 降级处理脚本不完整\n\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 3: 验证 onerror 属性添加
|
||||
*/
|
||||
function test_onerror_attribute() {
|
||||
echo "测试 3: 验证 onerror 属性添加\n";
|
||||
echo str_repeat('-', 50) . "\n";
|
||||
|
||||
// 模拟脚本标签
|
||||
$original_tag = '<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>';
|
||||
|
||||
// 调用函数添加属性
|
||||
$modified_tag = argon_add_mermaid_async_attribute($original_tag, 'mermaid');
|
||||
|
||||
// 验证包含 async 属性
|
||||
if (strpos($modified_tag, 'async') === false) {
|
||||
echo "❌ 失败: 缺少 async 属性\n";
|
||||
return false;
|
||||
}
|
||||
echo "✓ 包含 async 属性\n";
|
||||
|
||||
// 验证包含 onerror 属性
|
||||
if (strpos($modified_tag, 'onerror') === false) {
|
||||
echo "❌ 失败: 缺少 onerror 属性\n";
|
||||
return false;
|
||||
}
|
||||
echo "✓ 包含 onerror 属性\n";
|
||||
|
||||
// 验证 onerror 调用正确的函数
|
||||
if (strpos($modified_tag, 'argonMermaidLoadFallback()') === false) {
|
||||
echo "❌ 失败: onerror 函数名不正确\n";
|
||||
return false;
|
||||
}
|
||||
echo "✓ onerror 调用正确的函数\n";
|
||||
|
||||
echo "修改后的标签: {$modified_tag}\n";
|
||||
echo "✅ 通过: onerror 属性添加正确\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 4: 验证非 Mermaid 脚本不受影响
|
||||
*/
|
||||
function test_other_scripts_unaffected() {
|
||||
echo "测试 4: 验证非 Mermaid 脚本不受影响\n";
|
||||
echo str_repeat('-', 50) . "\n";
|
||||
|
||||
// 模拟其他脚本标签
|
||||
$original_tag = '<script src="https://example.com/other-script.js"></script>';
|
||||
|
||||
// 调用函数
|
||||
$modified_tag = argon_add_mermaid_async_attribute($original_tag, 'other-script');
|
||||
|
||||
// 验证标签未被修改
|
||||
if ($original_tag === $modified_tag) {
|
||||
echo "✓ 非 Mermaid 脚本未被修改\n";
|
||||
echo "✅ 通过: 其他脚本不受影响\n\n";
|
||||
return true;
|
||||
} else {
|
||||
echo "❌ 失败: 非 Mermaid 脚本被错误修改\n";
|
||||
echo "原始: {$original_tag}\n";
|
||||
echo "修改: {$modified_tag}\n\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 5: 验证 JSON 编码的备用 URL
|
||||
*/
|
||||
function test_json_encoded_urls() {
|
||||
echo "测试 5: 验证 JSON 编码的备用 URL\n";
|
||||
echo str_repeat('-', 50) . "\n";
|
||||
|
||||
$fallback_urls = argon_get_mermaid_fallback_urls();
|
||||
$json = json_encode($fallback_urls);
|
||||
|
||||
// 验证 JSON 编码成功
|
||||
if ($json === false) {
|
||||
echo "❌ 失败: JSON 编码失败\n";
|
||||
return false;
|
||||
}
|
||||
echo "✓ JSON 编码成功\n";
|
||||
|
||||
// 验证可以解码回数组
|
||||
$decoded = json_decode($json, true);
|
||||
if (!is_array($decoded)) {
|
||||
echo "❌ 失败: JSON 解码失败\n";
|
||||
return false;
|
||||
}
|
||||
echo "✓ JSON 解码成功\n";
|
||||
|
||||
// 验证解码后的数组与原数组一致
|
||||
if ($decoded !== $fallback_urls) {
|
||||
echo "❌ 失败: 解码后的数组与原数组不一致\n";
|
||||
return false;
|
||||
}
|
||||
echo "✓ 解码后的数组与原数组一致\n";
|
||||
|
||||
echo "JSON: {$json}\n";
|
||||
echo "✅ 通过: JSON 编码验证成功\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
// 运行所有测试
|
||||
echo "\n";
|
||||
echo "=================================================\n";
|
||||
echo "Mermaid 库加载失败降级处理测试\n";
|
||||
echo "=================================================\n\n";
|
||||
|
||||
$tests = [
|
||||
'test_fallback_urls',
|
||||
'test_fallback_script_generation',
|
||||
'test_onerror_attribute',
|
||||
'test_other_scripts_unaffected',
|
||||
'test_json_encoded_urls'
|
||||
];
|
||||
|
||||
$passed = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($tests as $test) {
|
||||
if ($test()) {
|
||||
$passed++;
|
||||
} else {
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// 输出测试总结
|
||||
echo "=================================================\n";
|
||||
echo "测试总结\n";
|
||||
echo "=================================================\n";
|
||||
echo "通过: {$passed} 个测试\n";
|
||||
echo "失败: {$failed} 个测试\n";
|
||||
|
||||
if ($failed === 0) {
|
||||
echo "\n✅ 所有测试通过!\n";
|
||||
exit(0);
|
||||
} else {
|
||||
echo "\n❌ 部分测试失败,请检查实现。\n";
|
||||
exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user