diff --git a/.kiro/specs/pjax-lazyload-fix/code-review-summary.md b/.kiro/specs/pjax-lazyload-fix/code-review-summary.md new file mode 100644 index 0000000..f66b07d --- /dev/null +++ b/.kiro/specs/pjax-lazyload-fix/code-review-summary.md @@ -0,0 +1,692 @@ +# PJAX & Lazyload 代码审查总结 + +## 审查日期 +2026-01-25 + +## 审查范围 +- `argontheme.js` - 主题核心 JavaScript 文件 (6709 行) +- `style.css` - 主题样式文件 +- PJAX 和 Lazyload 相关功能模块 + +## 代码质量评估 + +### ✅ 优点 + +1. **模块化结构清晰** + - 代码按功能模块组织,使用注释分隔 + - 性能优化模块独立引入 + - 资源清理函数集中管理 + +2. **错误处理完善** + - 关键函数都有 try-catch 包裹 + - 提供降级方案(IntersectionObserver → 滚动监听) + - 第三方库缺失时有空实现保护 + +3. **性能优化到位** + - 使用节流函数优化滚动事件 + - 使用 requestAnimationFrame 优化动画 + - DOM 缓存系统减少重复查询 + - 批量渲染 Mermaid 图表 + +4. **兼容性考虑周全** + - Polyfill 确保第三方库存在 + - jQuery easing 函数补充 + - 多种验证码类型支持 + +### ⚠️ 需要改进的地方 + +1. **JSDoc 注释不完整** + - 大部分函数缺少 JSDoc 注释 + - 参数类型和返回值未标注 + - 函数用途说明不够详细 + +2. **全局变量较多** + - `argonConfig`, `translation`, `pjaxLoading` 等全局变量 + - 建议使用命名空间封装 + +3. **代码风格不统一** + - 部分使用 `var`,部分使用 `let/const` + - 字符串引号混用(单引号/双引号) + +## 关键函数文档 + +### Cookie 操作 + +```javascript +/** + * 设置 Cookie + * @param {string} cname - Cookie 名称 + * @param {string} cvalue - Cookie 值 + * @param {number} exdays - 过期天数 + * @returns {void} + */ +function setCookie(cname, cvalue, exdays) + +/** + * 获取 Cookie + * @param {string} cname - Cookie 名称 + * @returns {string} Cookie 值,不存在则返回空字符串 + */ +function getCookie(cname) +``` + +### 多语言支持 + +```javascript +/** + * 翻译文本 + * @param {string} text - 要翻译的文本 + * @returns {string} 翻译后的文本,如果没有翻译则返回原文 + */ +function __(text) +``` + +### 搜索功能 + +```javascript +/** + * 搜索文章 + * @param {string} word - 搜索关键词 + * @returns {void} + */ +function searchPosts(word) +``` + +### 瀑布流布局 + +```javascript +/** + * 初始化瀑布流布局 + * 根据配置和屏幕宽度计算列数,动态调整文章卡片位置 + * @returns {void} + */ +function waterflowInit() +``` + +### 图片懒加载 + +```javascript +/** + * 初始化图片懒加载 + * 优先使用 IntersectionObserver,不支持时降级到滚动监听 + * @returns {void} + */ +function lazyloadInit() + +/** + * 优化的图片加载函数 + * 使用 requestAnimationFrame 优化性能 + * @param {HTMLImageElement} img - 图片元素 + * @param {string} effect - 加载效果类型 ('fadeIn' 或 'slideDown') + * @returns {void} + */ +function loadImageOptimized(img, effect) + +/** + * 应用加载效果 + * @param {HTMLImageElement} img - 图片元素 + * @param {string} effect - 加载效果类型 ('fadeIn' 或 'slideDown') + * @returns {void} + */ +function applyLoadEffectOptimized(img, effect) + +/** + * 懒加载降级方案(滚动监听) + * 当浏览器不支持 IntersectionObserver 时使用 + * @param {NodeList} images - 图片元素列表 + * @param {string} effect - 加载效果类型 + * @param {number} threshold - 提前加载的阈值(像素) + * @returns {void} + */ +function lazyloadFallback(images, effect, threshold) + +/** + * 立即加载所有图片 + * 当懒加载被禁用时调用 + * @returns {void} + */ +function loadAllImagesImmediately() +``` + +### PJAX 资源清理 + +```javascript +/** + * 清理 Lazyload Observer + * 断开连接并置空引用,防止内存泄漏 + * @returns {void} + */ +function cleanupLazyloadObserver() + +/** + * 清理 Zoomify 实例 + * 销毁所有图片放大实例 + * @returns {void} + */ +function cleanupZoomifyInstances() + +/** + * 清理 Tippy 实例 + * 销毁所有 Tooltip 实例 + * @returns {void} + */ +function cleanupTippyInstances() + +/** + * 清理 Mermaid 实例 + * 清理已渲染的图表记录和相关资源 + * @returns {void} + */ +function cleanupMermaidInstances() + +/** + * 清理动态样式 + * 只清理标记为 data-dynamic="true" 的样式 + * @returns {void} + */ +function cleanupDynamicStyles() + +/** + * 清理动态脚本 + * 只清理标记为 data-dynamic="true" 的脚本 + * @returns {void} + */ +function cleanupDynamicScripts() + +/** + * 清理事件监听器 + * 清理 Mermaid 相关的事件监听器 + * @returns {void} + */ +function cleanupEventListeners() + +/** + * 清理所有 PJAX 资源 + * 在 pjax:beforeReplace 事件中调用 + * 统一清理 Observer、第三方库实例、动态标签等 + * @returns {void} + */ +function cleanupPjaxResources() +``` + +### 脚本执行 + +```javascript +/** + * 执行单个脚本 + * 创建新的 script 元素并执行 + * @param {HTMLScriptElement} oldScript - 原始脚本元素 + * @returns {boolean} 是否执行成功 + */ +function executeScript(oldScript) + +/** + * 执行容器内的所有内联脚本 + * 按 DOM 顺序依次执行,错误隔离 + * @param {HTMLElement} container - 容器元素 + * @returns {Object} 执行结果统计 {total, success, failed} + */ +function executeInlineScripts(container) +``` + +### 评论功能 + +```javascript +/** + * 显示回复框 + * @param {number} commentID - 评论ID + * @returns {void} + */ +function reply(commentID) + +/** + * 取消回复 + * @returns {void} + */ +function cancelReply() + +/** + * 编辑评论 + * @param {number} commentID - 评论ID + * @returns {void} + */ +function edit(commentID) + +/** + * 取消编辑 + * @param {boolean} clear - 是否清空输入框 + * @returns {void} + */ +function cancelEdit(clear) + +/** + * 发送评论 + * 验证表单,发送 AJAX 请求,处理响应 + * @returns {void} + */ +function postComment() + +/** + * 编辑评论 + * 验证表单,发送 AJAX 请求,更新评论内容 + * @returns {void} + */ +function editComment() + +/** + * 切换评论置顶状态 + * @param {number} commentID - 评论ID + * @param {boolean} pinned - 当前是否已置顶 + * @returns {void} + */ +function toogleCommentPin(commentID, pinned) + +/** + * 删除评论 + * @param {number} commentID - 评论ID + * @returns {void} + */ +function deleteComment(commentID) +``` + +### 工具函数 + +```javascript +/** + * 折叠过长评论 + * @returns {void} + */ +function foldLongComments() + +/** + * 生成评论文字头像 + * @param {HTMLImageElement} img - 头像图片元素 + * @returns {void} + */ +function generateCommentTextAvatar(img) + +/** + * 刷新评论文字头像 + * @returns {void} + */ +function refreshCommentTextAvatar() + +/** + * 根据 Hash 定位到页面元素 + * @param {string} hash - Hash 值(如 #comment-123) + * @param {number} durtion - 滚动动画时长 + * @param {string} easing - 缓动函数名称 + * @returns {void} + */ +function gotoHash(hash, durtion, easing = 'easeOutExpo') + +/** + * 从 URL 中提取 Hash + * @param {string} url - URL 字符串 + * @returns {string} Hash 值 + */ +function getHash(url) +``` + +### 颜色转换工具 + +```javascript +/** + * RGB 转 HSL + * @param {number} R - 红色值 (0-255) + * @param {number} G - 绿色值 (0-255) + * @param {number} B - 蓝色值 (0-255) + * @returns {Object} {H, S, L} + */ +function rgb2hsl(R, G, B) + +/** + * HSL 转 RGB + * @param {number} h - 色相 (0-360) + * @param {number} s - 饱和度 (0-100) + * @param {number} l - 亮度 (0-100) + * @returns {Object} {R, G, B} + */ +function hsl2rgb(h, s, l) + +/** + * RGB 转 HEX + * @param {number} r - 红色值 (0-255) + * @param {number} g - 绿色值 (0-255) + * @param {number} b - 蓝色值 (0-255) + * @returns {string} HEX 颜色值(如 #FF0000) + */ +function rgb2hex(r, g, b) + +/** + * HEX 转 RGB + * @param {string} hex - HEX 颜色值(如 #FF0000) + * @returns {Object} {R, G, B} + */ +function hex2rgb(hex) + +/** + * RGB 转灰度值 + * @param {number} R - 红色值 (0-255) + * @param {number} G - 绿色值 (0-255) + * @param {number} B - 蓝色值 (0-255) + * @returns {number} 灰度值 (0-255) + */ +function rgb2gray(R, G, B) +``` + +## 代码风格检查 + +### 符合规范 ✅ + +1. **缩进**:使用 Tab 缩进 +2. **注释**:使用 `// ==========` 分隔大区块 +3. **函数命名**:使用 camelCase +4. **错误处理**:关键函数有 try-catch + +### 需要改进 ⚠️ + +1. **变量声明** + - 部分使用 `var`(如 `var translation`, `var zoomifyInstances`) + - 建议统一使用 `let/const` + +2. **字符串引号** + - 混用单引号和双引号 + - 建议统一使用单引号 + +3. **比较运算符** + - 部分使用 `==` + - 建议统一使用 `===` + +4. **JSDoc 注释** + - 大部分函数缺少 JSDoc + - 建议为所有公共函数添加 JSDoc + +## 性能优化亮点 + +1. **事件节流** + ```javascript + const throttledChangeToolbarTransparency = argonEventManager ? + argonEventManager.throttle(changeToolbarTransparency, 16) : + changeToolbarTransparency; + document.addEventListener("scroll", throttledChangeToolbarTransparency, {passive: true}); + ``` + +2. **requestAnimationFrame 优化** + ```javascript + function loadImageOptimized(img, effect) { + requestAnimationFrame(() => { + img.src = src; + // ... + }); + } + ``` + +3. **DOM 缓存** + ```javascript + let $bannerContainer = $("#banner_container"); + let $content = $("#content"); + ``` + +4. **批量处理** + ```javascript + // 批量渲染 Mermaid 图表 + const blocks = detectMermaidBlocks(); + blocks.forEach((block, index) => { + renderMermaidChart(block, index); + }); + ``` + +## 安全性检查 + +### ✅ 良好实践 + +1. **XSS 防护** + - 使用 `textContent` 而非 `innerHTML`(部分场景) + - 评论内容经过服务端验证 + +2. **CSRF 防护** + - 使用 `argon_nonce` 验证 + - AJAX 请求包含 nonce + +3. **输入验证** + - 邮箱格式验证 + - URL 格式验证 + - 验证码验证 + +### ⚠️ 需要注意 + +1. **innerHTML 使用** + - 部分场景直接使用 `innerHTML` 插入 HTML + - 建议确保内容已经过滤 + +2. **eval 风险** + - 使用 `document.execCommand` 和动态脚本执行 + - 已有错误处理,但需注意安全性 + +## 兼容性检查 + +### ✅ 良好支持 + +1. **IntersectionObserver 降级** + ```javascript + if ('IntersectionObserver' in window) { + initWithObserver(images); + } else { + initWithScrollListener(images); + } + ``` + +2. **第三方库缺失保护** + ```javascript + if (typeof window.Prism === 'undefined') { + window.Prism = { + highlightAll: function() {}, + highlightElement: function() {} + }; + } + ``` + +3. **jQuery easing 补充** + ```javascript + if (typeof $.easing.easeOutCirc === 'undefined') { + $.easing.easeOutCirc = function(x) { + return Math.sqrt(1 - Math.pow(x - 1, 2)); + }; + } + ``` + +## 内存管理 + +### ✅ 良好实践 + +1. **Observer 清理** + ```javascript + function cleanupLazyloadObserver() { + if (lazyloadObserver) { + lazyloadObserver.disconnect(); + lazyloadObserver = null; + } + } + ``` + +2. **实例销毁** + ```javascript + zoomifyInstances.forEach(instance => { + if (instance && typeof instance.destroy === 'function') { + instance.destroy(); + } + }); + zoomifyInstances = []; + ``` + +3. **事件监听器清理** + ```javascript + function cleanupEventListeners() { + // 清理 Mermaid 相关的事件监听器 + document.querySelectorAll('.mermaid-container').forEach(container => { + // 移除事件监听器 + }); + } + ``` + +## 测试建议 + +### 单元测试 + +1. **Cookie 操作** + - 测试 setCookie 和 getCookie + - 测试过期时间处理 + +2. **颜色转换** + - 测试 RGB/HSL/HEX 互转 + - 测试边界值 + +3. **资源清理** + - 测试 Observer 清理 + - 测试实例销毁 + +### 集成测试 + +1. **PJAX 流程** + - 测试页面切换 + - 测试资源清理 + - 测试脚本执行 + +2. **懒加载** + - 测试 IntersectionObserver + - 测试降级方案 + - 测试加载效果 + +3. **评论功能** + - 测试发送评论 + - 测试回复评论 + - 测试编辑评论 + +### 性能测试 + +1. **内存泄漏检测** + - 多次 PJAX 切换后检查内存 + - 检查 Observer 是否正确清理 + +2. **渲染性能** + - 测试瀑布流布局性能 + - 测试 Mermaid 批量渲染 + +3. **滚动性能** + - 测试节流函数效果 + - 测试懒加载性能 + +## 改进建议 + +### 高优先级 + +1. **添加 JSDoc 注释** + - 为所有公共函数添加 JSDoc + - 标注参数类型和返回值 + - 添加使用示例 + +2. **统一代码风格** + - 将 `var` 改为 `let/const` + - 统一使用单引号 + - 统一使用 `===` + +3. **完善错误处理** + - 为所有 AJAX 请求添加错误处理 + - 记录详细的错误日志 + - 提供用户友好的错误提示 + +### 中优先级 + +1. **封装全局变量** + - 使用命名空间封装 + - 减少全局变量污染 + +2. **优化函数长度** + - 拆分过长的函数 + - 提取重复代码 + +3. **添加类型检查** + - 使用 JSDoc 类型注解 + - 考虑引入 TypeScript + +### 低优先级 + +1. **代码分割** + - 按功能模块分割文件 + - 使用模块化加载 + +2. **添加单元测试** + - 为工具函数添加测试 + - 提高代码覆盖率 + +3. **性能监控** + - 添加性能指标收集 + - 监控内存使用情况 + +## 总结 + +### 整体评价 + +代码质量良好,功能完善,性能优化到位。主要优点包括: +- 模块化结构清晰 +- 错误处理完善 +- 性能优化充分 +- 兼容性考虑周全 + +主要需要改进的地方: +- JSDoc 注释不完整 +- 代码风格不统一 +- 全局变量较多 + +### 下一步行动 + +1. ✅ 完成代码审查文档 +2. 📝 为关键函数添加 JSDoc 注释 +3. 🔧 统一代码风格(var → let/const) +4. 🧪 添加单元测试 +5. 📊 性能测试和优化 + +## 附录:代码风格规范 + +### 变量声明 +```javascript +// ✅ 推荐 +const MAX_COUNT = 100; +let currentCount = 0; + +// ❌ 不推荐 +var MAX_COUNT = 100; +var currentCount = 0; +``` + +### 字符串 +```javascript +// ✅ 推荐 +const message = 'Hello World'; + +// ❌ 不推荐 +const message = "Hello World"; +``` + +### 比较运算符 +```javascript +// ✅ 推荐 +if (value === 0) { } + +// ❌ 不推荐 +if (value == 0) { } +``` + +### JSDoc 注释 +```javascript +/** + * 函数说明 + * @param {string} param1 - 参数1说明 + * @param {number} param2 - 参数2说明 + * @returns {boolean} 返回值说明 + */ +function exampleFunction(param1, param2) { + // 函数实现 +} +``` diff --git a/.kiro/specs/pjax-lazyload-fix/code-style-checklist.md b/.kiro/specs/pjax-lazyload-fix/code-style-checklist.md new file mode 100644 index 0000000..fcdccbe --- /dev/null +++ b/.kiro/specs/pjax-lazyload-fix/code-style-checklist.md @@ -0,0 +1,526 @@ +# 代码风格检查清单 + +## 检查日期 +2026-01-25 + +## 检查范围 +- `argontheme.js` - 主题核心 JavaScript 文件 + +## 代码风格规范 + +### 1. 变量声明 ⚠️ + +#### 规范 +- 优先使用 `const` 声明不会重新赋值的变量 +- 使用 `let` 声明会重新赋值的变量 +- 避免使用 `var`(除非需要函数作用域或全局变量) + +#### 当前状态 +```javascript +// ❌ 需要改进 +var translation = {}; +var zoomifyInstances = []; +var lazyloadObserver = null; +var pjaxLoading = false; +var headroom = null; + +// ✅ 推荐写法 +const translation = {}; +let zoomifyInstances = []; +let lazyloadObserver = null; +let pjaxLoading = false; +let headroom = null; +``` + +#### 检查结果 +- ❌ 发现 20+ 处使用 `var` 声明变量 +- 建议:将所有 `var` 改为 `let` 或 `const` + +--- + +### 2. 字符串引号 ⚠️ + +#### 规范 +- 优先使用单引号 `'` +- 字符串中包含单引号时使用双引号 `"` +- 模板字符串使用反引号 `` ` `` + +#### 当前状态 +```javascript +// ❌ 混用单引号和双引号 +let message = "Hello World"; +let name = 'John'; + +// ✅ 推荐写法 +let message = 'Hello World'; +let name = 'John'; +``` + +#### 检查结果 +- ⚠️ 部分代码混用单引号和双引号 +- 建议:统一使用单引号 + +--- + +### 3. 比较运算符 ⚠️ + +#### 规范 +- 使用严格相等 `===` 和 `!==` +- 避免使用 `==` 和 `!=` + +#### 当前状态 +```javascript +// ❌ 需要改进 +if (value == 0) { } +if (text != "") { } + +// ✅ 推荐写法 +if (value === 0) { } +if (text !== '') { } +``` + +#### 检查结果 +- ⚠️ 发现 50+ 处使用 `==` 或 `!=` +- 建议:全部改为 `===` 或 `!==` + +--- + +### 4. 分号使用 ✅ + +#### 规范 +- 语句末尾必须有分号 `;` + +#### 当前状态 +```javascript +// ✅ 符合规范 +let value = 10; +function test() { } +``` + +#### 检查结果 +- ✅ 所有语句末尾都有分号 +- 符合规范 + +--- + +### 5. 缩进 ✅ + +#### 规范 +- 使用 Tab 缩进 +- 保持一致的缩进层级 + +#### 当前状态 +```javascript +// ✅ 符合规范 +function test() { + if (condition) { + doSomething(); + } +} +``` + +#### 检查结果 +- ✅ 使用 Tab 缩进 +- ✅ 缩进层级一致 +- 符合规范 + +--- + +### 6. 函数命名 ✅ + +#### 规范 +- 使用 camelCase 命名 +- 函数名应该是动词或动词短语 +- 布尔值返回函数以 `is`、`has`、`can` 等开头 + +#### 当前状态 +```javascript +// ✅ 符合规范 +function setCookie() { } +function getCookie() { } +function waterflowInit() { } +function lazyloadInit() { } +``` + +#### 检查结果 +- ✅ 函数命名符合 camelCase +- ✅ 命名清晰明确 +- 符合规范 + +--- + +### 7. 变量命名 ✅ + +#### 规范 +- 使用 camelCase 命名 +- 常量使用 UPPER_SNAKE_CASE +- jQuery 对象以 `$` 开头 + +#### 当前状态 +```javascript +// ✅ 符合规范 +let currentCount = 0; +const MAX_COUNT = 100; +let $element = $('#test'); +``` + +#### 检查结果 +- ✅ 变量命名符合 camelCase +- ✅ jQuery 对象以 `$` 开头 +- 符合规范 + +--- + +### 8. 注释规范 ⚠️ + +#### 规范 +- 使用 JSDoc 注释函数 +- 大区块使用 `// ==========` 分隔 +- 小区块使用 `// ----------` 分隔 +- 单行注释使用 `//` + +#### 当前状态 +```javascript +// ✅ 区块注释符合规范 +// ========================================================================== +// 性能优化模块引入 +// ========================================================================== + +// ❌ 缺少 JSDoc 注释 +function setCookie(cname, cvalue, exdays) { + // 函数实现 +} + +// ✅ 推荐写法 +/** + * 设置 Cookie + * @param {string} cname - Cookie 名称 + * @param {string} cvalue - Cookie 值 + * @param {number} exdays - 过期天数 + * @returns {void} + */ +function setCookie(cname, cvalue, exdays) { + // 函数实现 +} +``` + +#### 检查结果 +- ✅ 区块注释符合规范 +- ❌ 大部分函数缺少 JSDoc 注释 +- 建议:为所有公共函数添加 JSDoc 注释 + +--- + +### 9. 空格使用 ✅ + +#### 规范 +- 关键字后有空格(if, for, while, function 等) +- 运算符前后有空格 +- 逗号后有空格 +- 对象字面量冒号后有空格 + +#### 当前状态 +```javascript +// ✅ 符合规范 +if (condition) { } +for (let i = 0; i < 10; i++) { } +let obj = {key: 'value'}; +let arr = [1, 2, 3]; +``` + +#### 检查结果 +- ✅ 空格使用符合规范 +- 符合规范 + +--- + +### 10. 代码块 ✅ + +#### 规范 +- 始终使用花括号 `{}` +- 左花括号不换行 +- 右花括号单独一行 + +#### 当前状态 +```javascript +// ✅ 符合规范 +if (condition) { + doSomething(); +} + +// ❌ 不推荐 +if (condition) doSomething(); +``` + +#### 检查结果 +- ✅ 代码块使用符合规范 +- 符合规范 + +--- + +### 11. 错误处理 ✅ + +#### 规范 +- 关键函数使用 try-catch +- 记录错误日志 +- 提供降级方案 + +#### 当前状态 +```javascript +// ✅ 符合规范 +function cleanupZoomifyInstances() { + if (zoomifyInstances && zoomifyInstances.length > 0) { + zoomifyInstances.forEach(instance => { + try { + if (instance && typeof instance.destroy === 'function') { + instance.destroy(); + } + } catch(e) { + ArgonDebug.warn('Failed to destroy Zoomify instance:', e); + } + }); + } +} +``` + +#### 检查结果 +- ✅ 关键函数有错误处理 +- ✅ 记录错误日志 +- ✅ 提供降级方案 +- 符合规范 + +--- + +### 12. 函数长度 ⚠️ + +#### 规范 +- 单个函数不超过 100 行 +- 复杂函数应该拆分 + +#### 当前状态 +```javascript +// ⚠️ 部分函数过长 +function postComment() { + // 200+ 行代码 +} +``` + +#### 检查结果 +- ⚠️ 发现 5+ 个函数超过 100 行 +- 建议:拆分复杂函数,提取重复代码 + +--- + +### 13. 全局变量 ⚠️ + +#### 规范 +- 尽量减少全局变量 +- 使用命名空间封装 +- 必要的全局变量添加注释说明 + +#### 当前状态 +```javascript +// ⚠️ 全局变量较多 +var argonConfig = {}; +var translation = {}; +var pjaxLoading = false; +var headroom = null; +var zoomifyInstances = []; +var lazyloadObserver = null; +``` + +#### 检查结果 +- ⚠️ 发现 20+ 个全局变量 +- 建议:使用命名空间封装,减少全局变量污染 + +--- + +### 14. 性能优化 ✅ + +#### 规范 +- 使用节流/防抖优化事件 +- 使用 requestAnimationFrame 优化动画 +- 缓存 DOM 查询结果 + +#### 当前状态 +```javascript +// ✅ 符合规范 +const throttledChangeToolbarTransparency = argonEventManager ? + argonEventManager.throttle(changeToolbarTransparency, 16) : + changeToolbarTransparency; +document.addEventListener("scroll", throttledChangeToolbarTransparency, {passive: true}); + +// ✅ 使用 requestAnimationFrame +function loadImageOptimized(img, effect) { + requestAnimationFrame(() => { + img.src = src; + }); +} + +// ✅ 缓存 DOM 查询 +let $bannerContainer = $("#banner_container"); +let $content = $("#content"); +``` + +#### 检查结果 +- ✅ 使用节流函数优化滚动事件 +- ✅ 使用 requestAnimationFrame 优化动画 +- ✅ 缓存 DOM 查询结果 +- 符合规范 + +--- + +## 总体评分 + +| 项目 | 状态 | 评分 | +|------|------|------| +| 变量声明 | ⚠️ 需要改进 | 6/10 | +| 字符串引号 | ⚠️ 需要改进 | 7/10 | +| 比较运算符 | ⚠️ 需要改进 | 6/10 | +| 分号使用 | ✅ 符合规范 | 10/10 | +| 缩进 | ✅ 符合规范 | 10/10 | +| 函数命名 | ✅ 符合规范 | 10/10 | +| 变量命名 | ✅ 符合规范 | 10/10 | +| 注释规范 | ⚠️ 需要改进 | 5/10 | +| 空格使用 | ✅ 符合规范 | 10/10 | +| 代码块 | ✅ 符合规范 | 10/10 | +| 错误处理 | ✅ 符合规范 | 9/10 | +| 函数长度 | ⚠️ 需要改进 | 7/10 | +| 全局变量 | ⚠️ 需要改进 | 6/10 | +| 性能优化 | ✅ 符合规范 | 9/10 | + +**总体评分:8.2/10** + +--- + +## 改进优先级 + +### 高优先级 🔴 + +1. **添加 JSDoc 注释** + - 影响:代码可维护性 + - 工作量:中等 + - 建议:为所有公共函数添加 JSDoc + +2. **统一比较运算符** + - 影响:代码质量和安全性 + - 工作量:小 + - 建议:全局替换 `==` 为 `===` + +3. **统一变量声明** + - 影响:代码现代化 + - 工作量:小 + - 建议:将 `var` 改为 `let/const` + +### 中优先级 🟡 + +4. **统一字符串引号** + - 影响:代码一致性 + - 工作量:小 + - 建议:统一使用单引号 + +5. **拆分长函数** + - 影响:代码可读性 + - 工作量:中等 + - 建议:拆分超过 100 行的函数 + +6. **减少全局变量** + - 影响:代码组织性 + - 工作量:大 + - 建议:使用命名空间封装 + +### 低优先级 🟢 + +7. **代码分割** + - 影响:代码组织性 + - 工作量:大 + - 建议:按功能模块分割文件 + +--- + +## 自动化工具建议 + +### ESLint 配置 + +```json +{ + "rules": { + "no-var": "error", + "prefer-const": "error", + "eqeqeq": ["error", "always"], + "quotes": ["error", "single"], + "semi": ["error", "always"], + "require-jsdoc": ["warn", { + "require": { + "FunctionDeclaration": true, + "MethodDefinition": true, + "ClassDeclaration": true + } + }] + } +} +``` + +### Prettier 配置 + +```json +{ + "useTabs": true, + "tabWidth": 4, + "semi": true, + "singleQuote": true, + "trailingComma": "none" +} +``` + +--- + +## 检查清单 + +### 代码提交前检查 + +- [ ] 所有新函数都有 JSDoc 注释 +- [ ] 使用 `let/const` 而非 `var` +- [ ] 使用 `===` 而非 `==` +- [ ] 使用单引号而非双引号 +- [ ] 语句末尾有分号 +- [ ] 使用 Tab 缩进 +- [ ] 关键函数有错误处理 +- [ ] 性能敏感代码已优化 +- [ ] 没有 console.log 调试代码 +- [ ] 代码通过 ESLint 检查 + +### 代码审查检查 + +- [ ] 函数命名清晰明确 +- [ ] 变量命名符合规范 +- [ ] 注释充分且准确 +- [ ] 没有重复代码 +- [ ] 函数长度合理 +- [ ] 全局变量使用合理 +- [ ] 错误处理完善 +- [ ] 性能优化到位 +- [ ] 兼容性考虑周全 +- [ ] 安全性没有问题 + +--- + +## 下一步行动 + +1. ✅ 完成代码风格检查清单 +2. 📝 为关键函数添加 JSDoc 注释 +3. 🔧 统一代码风格(var → let/const, == → ===) +4. 🧪 配置 ESLint 和 Prettier +5. 📊 运行自动化检查工具 +6. 🔍 进行代码审查 +7. ✨ 提交代码改进 + +--- + +## 参考资料 + +- [Argon 主题代码规范](code-style.md) +- [JavaScript 代码规范](https://github.com/airbnb/javascript) +- [JSDoc 使用指南](https://jsdoc.app/) +- [ESLint 规则](https://eslint.org/docs/rules/) +- [Prettier 配置](https://prettier.io/docs/en/options.html) diff --git a/.kiro/specs/pjax-lazyload-fix/design.md b/.kiro/specs/pjax-lazyload-fix/design.md new file mode 100644 index 0000000..79a992f --- /dev/null +++ b/.kiro/specs/pjax-lazyload-fix/design.md @@ -0,0 +1,947 @@ +# Design Document: PJAX 和 Lazyload 功能修复 + +## Overview + +本设计旨在修复 Argon WordPress 主题中 PJAX 页面无刷新跳转和 Lazyload 图片懒加载功能的问题。当前实现存在以下核心问题: + +1. **资源泄漏**:页面切换时 Observer 和第三方库实例未正确清理 +2. **脚本执行失败**:新页面的内联脚本未执行 +3. **样式错误**:动态样式未清理导致样式冲突 +4. **Mermaid 初始化时序问题**:清除缓存后首次加载报语法错误 + +设计方案采用完整的生命周期管理模式,确保资源在正确的时机创建和销毁,同时提供降级方案保证兼容性。 + +## Architecture + +### 核心架构原则 + +1. **生命周期驱动**:所有资源管理基于 PJAX 生命周期事件 +2. **集中清理**:统一的资源清理函数,避免遗漏 +3. **错误隔离**:每个模块独立的错误处理,互不影响 +4. **降级支持**:为不支持的特性提供降级方案 + +### 架构图 + +``` +PJAX Lifecycle +├── pjax:click (开始) +│ └── 设置加载状态 +├── pjax:beforeReplace (清理阶段) +│ ├── cleanupPjaxResources() +│ │ ├── 清理 Lazyload Observer +│ │ ├── 销毁 Zoomify 实例 +│ │ ├── 销毁 Tippy 实例 +│ │ ├── 清理 Mermaid 实例 +│ │ └── 移除动态 style/script +│ └── 更新 UI 状态 +├── pjax:complete (初始化阶段) +│ ├── 执行内联脚本 +│ ├── 初始化功能模块 +│ │ ├── waterflowInit() +│ │ ├── lazyloadInit() +│ │ ├── zoomifyInit() +│ │ ├── highlightJsRender() +│ │ └── ... (其他模块) +│ └── 恢复滚动位置 +└── pjax:end (收尾阶段) + ├── resetMobileCatalog() + └── resetGT4Captcha() +``` + + +## Components and Interfaces + +### 1. Resource Cleanup Manager (资源清理管理器) + +**职责**:统一管理所有资源的清理 + +**接口**: +```javascript +/** + * 清理 PJAX 页面切换前的所有资源 + * @returns {void} + */ +function cleanupPjaxResources() { + cleanupLazyloadObserver(); + cleanupZoomifyInstances(); + cleanupTippyInstances(); + cleanupMermaidInstances(); + cleanupDynamicStyles(); + cleanupDynamicScripts(); + cleanupEventListeners(); +} + +/** + * 清理 Lazyload Observer + * @returns {void} + */ +function cleanupLazyloadObserver() { + if (lazyloadObserver) { + lazyloadObserver.disconnect(); + lazyloadObserver = null; + } +} + +/** + * 清理 Zoomify 实例 + * @returns {void} + */ +function cleanupZoomifyInstances() { + if (zoomifyInstances && zoomifyInstances.length > 0) { + zoomifyInstances.forEach(instance => { + try { + if (instance && typeof instance.destroy === 'function') { + instance.destroy(); + } + } catch(e) { + ArgonDebug.warn('Failed to destroy Zoomify instance:', e); + } + }); + zoomifyInstances = []; + } + $('img.zoomify-initialized').removeClass('zoomify-initialized'); +} +``` + +### 2. Script Executor (脚本执行器) + +**职责**:提取并执行新页面中的内联脚本 + +**接口**: +```javascript +/** + * 执行新页面中的内联脚本 + * @param {HTMLElement} container - 新页面的容器元素 + * @returns {void} + */ +function executeInlineScripts(container) { + const scripts = container.querySelectorAll('script'); + scripts.forEach(script => { + if (!script.src) { // 只执行内联脚本 + try { + executeScript(script); + } catch(e) { + ArgonDebug.error('Script execution failed:', e); + } + } + }); +} + +/** + * 执行单个脚本 + * @param {HTMLScriptElement} script - 脚本元素 + * @returns {void} + */ +function executeScript(script) { + const newScript = document.createElement('script'); + newScript.textContent = script.textContent; + // 复制属性 + Array.from(script.attributes).forEach(attr => { + newScript.setAttribute(attr.name, attr.value); + }); + document.head.appendChild(newScript); + document.head.removeChild(newScript); +} +``` + + +### 3. Lazyload Manager (懒加载管理器) + +**职责**:管理图片懒加载的完整生命周期 + +**接口**: +```javascript +/** + * 初始化懒加载 + * @returns {void} + */ +function lazyloadInit() { + // 清理旧的 Observer + cleanupLazyloadObserver(); + + // 检查是否启用 + if (argonConfig.lazyload === false) { + loadAllImagesImmediately(); + return; + } + + const images = document.querySelectorAll('img.lazyload[data-src]'); + if (images.length === 0) return; + + // 使用 IntersectionObserver 或降级方案 + if ('IntersectionObserver' in window) { + initWithObserver(images); + } else { + initWithScrollListener(images); + } +} + +/** + * 使用 IntersectionObserver 初始化 + * @param {NodeList} images - 图片元素列表 + * @returns {void} + */ +function initWithObserver(images) { + const threshold = parseInt(argonConfig.lazyload_threshold) || 800; + lazyloadObserver = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + loadImage(entry.target); + lazyloadObserver.unobserve(entry.target); + } + }); + }, { rootMargin: `${threshold}px 0px` }); + + images.forEach(img => lazyloadObserver.observe(img)); +} + +/** + * 加载单张图片 + * @param {HTMLImageElement} img - 图片元素 + * @returns {void} + */ +function loadImage(img) { + const src = img.getAttribute('data-src'); + if (!src) return; + + const tempImg = new Image(); + tempImg.onload = () => { + img.src = src; + img.removeAttribute('data-src'); + img.classList.remove('lazyload'); + applyLoadEffect(img); + }; + tempImg.onerror = () => { + // 降级:直接设置 src + img.src = src; + img.removeAttribute('data-src'); + img.classList.remove('lazyload'); + }; + tempImg.src = src; +} +``` + +### 4. Mermaid Renderer (Mermaid 渲染器) + +**职责**:管理 Mermaid 图表的渲染和生命周期 + +**接口**: +```javascript +/** + * 初始化 Mermaid + * @returns {Promise} 初始化是否成功 + */ +async function initMermaid() { + // 等待 Mermaid 库加载 + if (typeof window.mermaid === 'undefined') { + await waitForMermaid(); + } + + // 检查 API 可用性 + if (!checkMermaidAPI()) { + return false; + } + + // 配置 Mermaid + const theme = getMermaidTheme(); + window.mermaid.initialize({ + startOnLoad: false, + theme: theme, + securityLevel: 'loose' + }); + + return true; +} + +/** + * 等待 Mermaid 库加载 + * @param {number} timeout - 超时时间(毫秒) + * @returns {Promise} + */ +function waitForMermaid(timeout = 5000) { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + const checkInterval = setInterval(() => { + if (typeof window.mermaid !== 'undefined' && + typeof window.mermaid.render === 'function') { + clearInterval(checkInterval); + resolve(); + } else if (Date.now() - startTime > timeout) { + clearInterval(checkInterval); + reject(new Error('Mermaid load timeout')); + } + }, 100); + }); +} + +/** + * 渲染所有 Mermaid 图表 + * @returns {void} + */ +function renderAllMermaidCharts() { + const blocks = detectMermaidBlocks(); + blocks.forEach((block, index) => { + renderMermaidChart(block, index); + }); +} + +/** + * 渲染单个图表 + * @param {HTMLElement} element - 代码块元素 + * @param {number} index - 图表索引 + * @returns {void} + */ +async function renderMermaidChart(element, index) { + const chartId = `mermaid-chart-${Date.now()}-${index}`; + const code = extractMermaidCode(element); + + try { + const result = await window.mermaid.render(`mermaid-svg-${chartId}`, code); + const container = createMermaidContainer(chartId, result.svg, code); + element.parentNode.replaceChild(container, element); + } catch(error) { + // 尝试降级方案 + if (await tryLegacyMermaidAPI(element, code, chartId)) { + return; + } + // 显示错误 + showMermaidError(element, error, code); + } +} +``` + + +### 5. Style Manager (样式管理器) + +**职责**:管理动态样式的添加和清理 + +**接口**: +```javascript +/** + * 清理动态添加的样式 + * @returns {void} + */ +function cleanupDynamicStyles() { + // 只清理标记为动态的样式 + document.querySelectorAll('style[data-dynamic="true"]').forEach(style => { + style.remove(); + }); +} + +/** + * 应用新页面的样式 + * @param {HTMLElement} container - 新页面容器 + * @returns {void} + */ +function applyNewPageStyles(container) { + const styles = container.querySelectorAll('style'); + styles.forEach(style => { + const newStyle = style.cloneNode(true); + newStyle.setAttribute('data-dynamic', 'true'); + document.head.appendChild(newStyle); + }); +} +``` + +### 6. Event Manager (事件管理器) + +**职责**:管理事件监听器的绑定和清理 + +**接口**: +```javascript +/** + * 事件监听器注册表 + */ +const eventRegistry = new Map(); + +/** + * 注册事件监听器 + * @param {HTMLElement} element - 目标元素 + * @param {string} event - 事件名称 + * @param {Function} handler - 处理函数 + * @param {Object} options - 选项 + * @returns {void} + */ +function registerEventListener(element, event, handler, options = {}) { + element.addEventListener(event, handler, options); + + // 记录到注册表 + if (!eventRegistry.has(element)) { + eventRegistry.set(element, []); + } + eventRegistry.get(element).push({ event, handler, options }); +} + +/** + * 清理所有注册的事件监听器 + * @returns {void} + */ +function cleanupEventListeners() { + eventRegistry.forEach((listeners, element) => { + listeners.forEach(({ event, handler, options }) => { + element.removeEventListener(event, handler, options); + }); + }); + eventRegistry.clear(); +} + +/** + * 使用事件委托绑定 + * @param {string} selector - 选择器 + * @param {string} event - 事件名称 + * @param {Function} handler - 处理函数 + * @returns {void} + */ +function delegateEvent(selector, event, handler) { + $(document).on(event, selector, handler); +} +``` + +## Data Models + +### ResourceState (资源状态) + +```javascript +/** + * 资源状态枚举 + */ +const ResourceState = { + UNINITIALIZED: 'uninitialized', // 未初始化 + INITIALIZING: 'initializing', // 初始化中 + READY: 'ready', // 就绪 + CLEANING: 'cleaning', // 清理中 + CLEANED: 'cleaned' // 已清理 +}; + +/** + * 资源管理器状态 + */ +class ResourceManager { + constructor() { + this.state = ResourceState.UNINITIALIZED; + this.resources = { + lazyloadObserver: null, + zoomifyInstances: [], + tippyInstances: [], + mermaidInstances: [], + dynamicStyles: [], + dynamicScripts: [], + eventListeners: [] + }; + } + + /** + * 清理所有资源 + */ + cleanup() { + this.state = ResourceState.CLEANING; + // 清理逻辑 + this.state = ResourceState.CLEANED; + } + + /** + * 初始化资源 + */ + initialize() { + this.state = ResourceState.INITIALIZING; + // 初始化逻辑 + this.state = ResourceState.READY; + } +} +``` + +### LazyloadConfig (懒加载配置) + +```javascript +/** + * 懒加载配置 + */ +class LazyloadConfig { + constructor() { + this.enabled = true; // 是否启用 + this.effect = 'fadeIn'; // 加载效果 + this.threshold = 800; // 提前加载阈值(像素) + this.useObserver = true; // 是否使用 Observer + } + + /** + * 从全局配置加载 + */ + loadFromGlobal() { + this.enabled = argonConfig.lazyload !== false; + this.effect = argonConfig.lazyload_effect || 'fadeIn'; + this.threshold = parseInt(argonConfig.lazyload_threshold) || 800; + this.useObserver = 'IntersectionObserver' in window; + } +} +``` + +### MermaidState (Mermaid 状态) + +```javascript +/** + * Mermaid 渲染状态 + */ +class MermaidState { + constructor() { + this.initialized = false; // 是否已初始化 + this.libraryLoaded = false; // 库是否加载 + this.theme = 'default'; // 当前主题 + this.renderedCharts = new Set(); // 已渲染的图表 + } + + /** + * 检查是否可以渲染 + */ + canRender() { + return this.initialized && this.libraryLoaded; + } + + /** + * 标记图表已渲染 + */ + markRendered(chartId) { + this.renderedCharts.add(chartId); + } + + /** + * 清理状态 + */ + cleanup() { + this.renderedCharts.clear(); + } +} +``` + + +## Correctness Properties + +### 什么是 Correctness Properties? + +属性(Property)是一个关于系统行为的形式化陈述,它应该在所有有效执行中保持为真。属性是人类可读规范和机器可验证正确性保证之间的桥梁。通过属性测试,我们可以验证系统在各种输入下的行为是否符合预期。 + +### Property Reflection(属性反思) + +在编写属性之前,我们需要识别并消除冗余的属性: + +**识别的冗余项**: +1. 属性 8.3 (Mermaid 渲染失败显示错误) 与属性 3.5 重复 +2. 属性 10.1 (IntersectionObserver 降级) 与属性 8.2 重复 +3. 多个清理相关的属性可以合并为一个综合的资源清理属性 + +**合并策略**: +- 将所有资源清理验证合并为一个综合属性 +- 将所有降级方案验证合并为一个兼容性属性 +- 保留独立的功能性属性(如脚本执行、图片加载等) + +### Core Properties(核心属性) + +#### Property 1: 资源清理完整性 +*For any* PJAX 页面切换,当触发 pjax:beforeReplace 事件时,所有旧页面资源(Observer、第三方库实例、动态标签)都应该被完全清理,且相关引用应该被置为 null 或清空。 + +**Validates: Requirements 1.1, 1.2, 1.3, 1.4, 2.3** + +#### Property 2: Observer 生命周期管理 +*For any* Lazyload 初始化操作,如果存在旧的 Observer 实例,则必须先调用 disconnect() 并置空引用,然后才能创建新的 Observer 实例。 + +**Validates: Requirements 2.1, 2.2** + +#### Property 3: 图片加载后清理 +*For any* 被 Lazyload 监听的图片,当图片加载完成(成功或失败)后,Observer 应该取消对该图片的监听,避免重复处理。 + +**Validates: Requirements 2.4, 2.5** + +#### Property 4: 功能模块错误隔离 +*For any* 功能模块初始化过程,如果某个模块抛出错误,该错误应该被捕获并记录,且不应该阻止其他模块的初始化。 + +**Validates: Requirements 1.6, 8.1** + +#### Property 5: 脚本执行顺序 +*For any* 新页面包含的内联脚本集合,这些脚本应该按照它们在 DOM 中出现的顺序依次执行。 + +**Validates: Requirements 4.3** + +#### Property 6: 脚本错误隔离 +*For any* 内联脚本执行过程,如果某个脚本抛出错误,该错误应该被捕获,且不应该阻止后续脚本的执行。 + +**Validates: Requirements 4.4** + +#### Property 7: Mermaid 库加载等待 +*For any* Mermaid 初始化操作,系统应该检查 mermaid.render 方法是否存在,如果不存在则等待库加载或使用降级方案。 + +**Validates: Requirements 3.1, 3.2** + +#### Property 8: Mermaid 渲染降级 +*For any* Mermaid 图表渲染失败的情况,系统应该尝试使用旧版 API(mermaidAPI.render 或 mermaid.init),如果所有方案都失败,则显示友好的错误提示并保留原始代码。 + +**Validates: Requirements 3.3, 3.5** + +#### Property 9: 动态样式清理选择性 +*For any* 页面切换操作,系统应该只清理标记为动态的 style 标签,保留主题核心样式。 + +**Validates: Requirements 5.1, 5.2** + +#### Property 10: 事件监听器清理 +*For any* 注册到 eventRegistry 的事件监听器,在页面切换前都应该被正确移除。 + +**Validates: Requirements 6.1** + +#### Property 11: 节流函数限制频率 +*For any* 使用节流函数包装的事件处理器,在指定时间窗口内,无论触发多少次事件,处理函数的实际执行次数都不应该超过 1 次。 + +**Validates: Requirements 7.1** + +#### Property 12: IntersectionObserver 降级 +*For any* 浏览器环境,如果不支持 IntersectionObserver,Lazyload 应该自动使用滚动监听降级方案。 + +**Validates: Requirements 8.2, 10.1** + +#### Property 13: 第三方库缺失保护 +*For any* 第三方库(Zoomify、Tippy、Mermaid),如果库未加载,系统应该提供空实现或跳过相关功能,不应该抛出未捕获的错误。 + +**Validates: Requirements 8.5** + +#### Property 14: PJAX 加载失败降级 +*For any* PJAX 加载失败的情况,系统应该回退到传统的页面跳转方式。 + +**Validates: Requirements 8.4** + +#### Property 15: 懒加载禁用时立即加载 +*For any* 图片元素,当懒加载功能被禁用时,所有图片应该立即加载,不应该创建 Observer 实例。 + +**Validates: Requirements 2.6** + + +## Error Handling + +### 错误处理策略 + +1. **模块级错误隔离** + - 每个功能模块的初始化都包裹在 try-catch 中 + - 错误不应该传播到其他模块 + - 记录详细的错误信息用于调试 + +2. **降级方案** + - IntersectionObserver 不支持 → 滚动监听 + - Mermaid.render 失败 → 旧版 API → init 方法 → 显示错误 + - PJAX 失败 → 传统页面跳转 + - 第三方库缺失 → 空实现或跳过功能 + +3. **用户友好的错误提示** + - Mermaid 渲染失败显示可折叠的错误信息 + - 保留原始代码供用户查看 + - 提供错误类型分类(语法错误、渲染错误等) + +### 错误处理实现 + +```javascript +/** + * 安全执行函数(带错误处理) + * @param {Function} fn - 要执行的函数 + * @param {string} moduleName - 模块名称 + * @returns {boolean} 是否执行成功 + */ +function safeExecute(fn, moduleName) { + try { + fn(); + ArgonDebug.log(`${moduleName} initialized successfully`); + return true; + } catch(error) { + ArgonDebug.error(`${moduleName} initialization failed:`, error); + return false; + } +} + +/** + * PJAX complete 事件处理(带错误隔离) + */ +$(document).on('pjax:complete', function() { + pjaxLoading = false; + NProgress.inc(); + + // 每个模块独立的错误处理 + safeExecute(() => waterflowInit(), 'Waterflow'); + safeExecute(() => lazyloadInit(), 'Lazyload'); + safeExecute(() => zoomifyInit(), 'Zoomify'); + safeExecute(() => highlightJsRender(), 'HighlightJS'); + safeExecute(() => panguInit(), 'Pangu'); + safeExecute(() => clampInit(), 'Clamp'); + safeExecute(() => tippyInit(), 'Tippy'); + safeExecute(() => renderAllMermaidCharts(), 'Mermaid'); + + // 恢复滚动位置 + if (pjaxScrollTop > 0) { + $('body,html').scrollTop(pjaxScrollTop); + pjaxScrollTop = 0; + } + + NProgress.done(); +}); +``` + +### 降级方案实现 + +```javascript +/** + * Lazyload 降级方案 + */ +function initWithScrollListener(images) { + const loadedImages = new Set(); + + function checkImagesInView() { + const viewportHeight = window.innerHeight; + const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; + + images.forEach(img => { + if (loadedImages.has(img)) return; + + const rect = img.getBoundingClientRect(); + const threshold = parseInt(argonConfig.lazyload_threshold) || 800; + + if (rect.top < viewportHeight + threshold && rect.bottom > -threshold) { + loadImage(img); + loadedImages.add(img); + } + }); + } + + // 使用节流优化性能 + const throttledCheck = argonEventManager ? + argonEventManager.throttle(checkImagesInView, 100) : + checkImagesInView; + + window.addEventListener('scroll', throttledCheck, {passive: true}); + window.addEventListener('resize', throttledCheck, {passive: true}); + + checkImagesInView(); +} + +/** + * Mermaid 降级方案 + */ +async function tryLegacyMermaidAPI(element, code, chartId) { + // 尝试 Mermaid 8.x API + if (typeof window.mermaidAPI !== 'undefined' && + typeof window.mermaidAPI.render === 'function') { + try { + window.mermaidAPI.render(`mermaid-svg-${chartId}`, code, (svgCode) => { + const container = createMermaidContainer(chartId, svgCode, code); + element.parentNode.replaceChild(container, element); + }); + return true; + } catch(e) { + ArgonDebug.warn('Legacy API failed:', e); + } + } + + // 尝试 init 方法 + if (typeof window.mermaid.init === 'function') { + try { + const container = document.createElement('div'); + container.className = 'mermaid'; + container.textContent = code; + element.parentNode.replaceChild(container, element); + window.mermaid.init(undefined, container); + return true; + } catch(e) { + ArgonDebug.warn('Init method failed:', e); + } + } + + return false; +} +``` + +## Testing Strategy + +### 测试方法 + +本项目采用**双重测试策略**:单元测试 + 属性测试 + +1. **单元测试**:验证具体示例、边缘情况和错误条件 +2. **属性测试**:验证通用属性在各种输入下的正确性 + +两种测试方法互补,共同保证代码质量: +- 单元测试捕获具体的 bug +- 属性测试验证通用的正确性 + +### 属性测试配置 + +**测试库选择**:使用 `fast-check` (JavaScript 的属性测试库) + +**配置要求**: +- 每个属性测试至少运行 100 次迭代 +- 每个测试必须引用对应的设计文档属性 +- 标签格式:`Feature: pjax-lazyload-fix, Property N: [property_text]` + +### 测试用例示例 + +#### 单元测试示例 + +```javascript +describe('Resource Cleanup', () => { + it('should disconnect lazyload observer on cleanup', () => { + // 创建 Observer + lazyloadInit(); + expect(lazyloadObserver).not.toBeNull(); + + // 清理 + cleanupLazyloadObserver(); + + // 验证 + expect(lazyloadObserver).toBeNull(); + }); + + it('should handle missing Mermaid library gracefully', () => { + // 移除 Mermaid + const originalMermaid = window.mermaid; + delete window.mermaid; + + // 尝试渲染 + expect(() => renderAllMermaidCharts()).not.toThrow(); + + // 恢复 + window.mermaid = originalMermaid; + }); +}); +``` + +#### 属性测试示例 + +```javascript +const fc = require('fast-check'); + +describe('Property Tests', () => { + /** + * Feature: pjax-lazyload-fix, Property 1: 资源清理完整性 + * For any PJAX 页面切换,所有旧页面资源都应该被完全清理 + */ + it('Property 1: Resource cleanup completeness', () => { + fc.assert( + fc.property( + fc.array(fc.string()), // 随机页面内容 + (pageContent) => { + // 创建资源 + lazyloadInit(); + createZoomifyInstances(); + createTippyInstances(); + + // 记录资源状态 + const hasObserver = lazyloadObserver !== null; + const hasZoomify = zoomifyInstances.length > 0; + const hasTippy = document.querySelectorAll('[data-tippy-root]').length > 0; + + // 清理 + cleanupPjaxResources(); + + // 验证所有资源都被清理 + expect(lazyloadObserver).toBeNull(); + expect(zoomifyInstances).toHaveLength(0); + expect(document.querySelectorAll('[data-tippy-root]')).toHaveLength(0); + + return true; + } + ), + { numRuns: 100 } + ); + }); + + /** + * Feature: pjax-lazyload-fix, Property 4: 功能模块错误隔离 + * For any 功能模块初始化,某个模块错误不应该阻止其他模块 + */ + it('Property 4: Module error isolation', () => { + fc.assert( + fc.property( + fc.integer(0, 10), // 随机选择失败的模块索引 + (failIndex) => { + const modules = [ + { name: 'waterflow', fn: waterflowInit }, + { name: 'lazyload', fn: lazyloadInit }, + { name: 'zoomify', fn: zoomifyInit }, + { name: 'highlight', fn: highlightJsRender } + ]; + + // 让指定模块抛出错误 + const originalFn = modules[failIndex % modules.length].fn; + modules[failIndex % modules.length].fn = () => { + throw new Error('Test error'); + }; + + // 初始化所有模块 + const results = modules.map(m => safeExecute(m.fn, m.name)); + + // 恢复原函数 + modules[failIndex % modules.length].fn = originalFn; + + // 验证:失败的模块返回 false,其他模块不受影响 + const failedCount = results.filter(r => !r).length; + expect(failedCount).toBe(1); + + return true; + } + ), + { numRuns: 100 } + ); + }); + + /** + * Feature: pjax-lazyload-fix, Property 5: 脚本执行顺序 + * For any 内联脚本集合,应该按 DOM 顺序执行 + */ + it('Property 5: Script execution order', () => { + fc.assert( + fc.property( + fc.array(fc.string(), 1, 10), // 随机脚本内容 + (scriptContents) => { + // 创建测试容器 + const container = document.createElement('div'); + const executionOrder = []; + + // 创建脚本标签 + scriptContents.forEach((content, index) => { + const script = document.createElement('script'); + script.textContent = `window.testOrder.push(${index});`; + container.appendChild(script); + }); + + // 执行脚本 + window.testOrder = []; + executeInlineScripts(container); + + // 验证执行顺序 + const expectedOrder = scriptContents.map((_, i) => i); + expect(window.testOrder).toEqual(expectedOrder); + + return true; + } + ), + { numRuns: 100 } + ); + }); +}); +``` + +### 测试覆盖目标 + +- **单元测试覆盖率**:> 80% +- **属性测试**:覆盖所有 15 个核心属性 +- **集成测试**:覆盖完整的 PJAX 生命周期 +- **边缘情况**:Mermaid 缓存清除、浏览器兼容性 + +### 测试执行 + +```bash +# 运行所有测试 +npm test + +# 运行单元测试 +npm run test:unit + +# 运行属性测试 +npm run test:property + +# 生成覆盖率报告 +npm run test:coverage +``` + diff --git a/.kiro/specs/pjax-lazyload-fix/jsdoc-templates.md b/.kiro/specs/pjax-lazyload-fix/jsdoc-templates.md new file mode 100644 index 0000000..c9e8341 --- /dev/null +++ b/.kiro/specs/pjax-lazyload-fix/jsdoc-templates.md @@ -0,0 +1,659 @@ +# JSDoc 注释模板 + +本文档提供了 argontheme.js 中关键函数的 JSDoc 注释模板,可以直接复制使用。 + +## Cookie 操作 + +```javascript +/** + * 设置 Cookie + * @param {string} cname - Cookie 名称 + * @param {string} cvalue - Cookie 值 + * @param {number} exdays - 过期天数 + * @returns {void} + * @example + * setCookie('theme', 'dark', 365); + */ +function setCookie(cname, cvalue, exdays) { } + +/** + * 获取 Cookie + * @param {string} cname - Cookie 名称 + * @returns {string} Cookie 值,不存在则返回空字符串 + * @example + * const theme = getCookie('theme'); + */ +function getCookie(cname) { } +``` + +## 多语言支持 + +```javascript +/** + * 翻译文本 + * 根据当前语言设置返回对应的翻译文本 + * @param {string} text - 要翻译的文本(中文) + * @returns {string} 翻译后的文本,如果没有翻译则返回原文 + * @example + * const translated = __('发送成功'); + */ +function __(text) { } +``` + +## 搜索功能 + +```javascript +/** + * 搜索文章 + * 使用 PJAX 方式跳转到搜索结果页面 + * @param {string} word - 搜索关键词 + * @returns {void} + * @example + * searchPosts('JavaScript'); + */ +function searchPosts(word) { } +``` + +## 瀑布流布局 + +```javascript +/** + * 初始化瀑布流布局 + * 根据配置和屏幕宽度计算列数,动态调整文章卡片位置 + * 支持 1/2/3 列布局,响应式自适应 + * @returns {void} + * @example + * waterflowInit(); + * // 窗口大小改变时重新初始化 + * $(window).resize(waterflowInit); + */ +function waterflowInit() { } +``` + +## 图片懒加载 + +```javascript +/** + * 初始化图片懒加载 + * 优先使用 IntersectionObserver API,不支持时降级到滚动监听 + * 清理旧的 Observer 实例,避免重复初始化 + * @returns {void} + * @example + * lazyloadInit(); + */ +function lazyloadInit() { } + +/** + * 优化的图片加载函数 + * 使用 requestAnimationFrame 优化性能,避免布局抖动 + * 支持 fadeIn 和 slideDown 两种加载效果 + * @param {HTMLImageElement} img - 图片元素 + * @param {string} effect - 加载效果类型 ('fadeIn' 或 'slideDown') + * @returns {void} + * @example + * loadImageOptimized(imgElement, 'fadeIn'); + */ +function loadImageOptimized(img, effect) { } + +/** + * 应用加载效果 + * 为图片添加淡入或滑入动画效果 + * @param {HTMLImageElement} img - 图片元素 + * @param {string} effect - 加载效果类型 ('fadeIn' 或 'slideDown') + * @returns {void} + * @example + * applyLoadEffectOptimized(imgElement, 'fadeIn'); + */ +function applyLoadEffectOptimized(img, effect) { } + +/** + * 懒加载降级方案(滚动监听) + * 当浏览器不支持 IntersectionObserver 时使用 + * 使用节流函数优化滚动事件性能 + * @param {NodeList} images - 图片元素列表 + * @param {string} effect - 加载效果类型 + * @param {number} threshold - 提前加载的阈值(像素) + * @returns {void} + * @example + * lazyloadFallback(images, 'fadeIn', 800); + */ +function lazyloadFallback(images, effect, threshold) { } + +/** + * 立即加载所有图片 + * 当懒加载被禁用时调用,不应用任何加载效果 + * @returns {void} + * @example + * loadAllImagesImmediately(); + */ +function loadAllImagesImmediately() { } +``` + +## PJAX 资源清理 + +```javascript +/** + * 清理 Lazyload Observer + * 断开 IntersectionObserver 连接并置空引用,防止内存泄漏 + * @returns {void} + * @example + * cleanupLazyloadObserver(); + */ +function cleanupLazyloadObserver() { } + +/** + * 清理 Zoomify 实例 + * 销毁所有图片放大实例,移除相关 CSS 类 + * @returns {void} + * @example + * cleanupZoomifyInstances(); + */ +function cleanupZoomifyInstances() { } + +/** + * 清理 Tippy 实例 + * 销毁所有 Tooltip 实例,释放相关资源 + * @returns {void} + * @example + * cleanupTippyInstances(); + */ +function cleanupTippyInstances() { } + +/** + * 清理 Mermaid 实例 + * 清理已渲染的图表记录和相关资源 + * 移除图表容器的事件监听器 + * @returns {void} + * @example + * cleanupMermaidInstances(); + */ +function cleanupMermaidInstances() { } + +/** + * 清理动态样式 + * 只清理标记为 data-dynamic="true" 的 style 标签 + * 保留主题核心样式 + * @returns {void} + * @example + * cleanupDynamicStyles(); + */ +function cleanupDynamicStyles() { } + +/** + * 清理动态脚本 + * 只清理标记为 data-dynamic="true" 的 script 标签 + * @returns {void} + * @example + * cleanupDynamicScripts(); + */ +function cleanupDynamicScripts() { } + +/** + * 清理事件监听器 + * 清理 Mermaid 相关的事件监听器 + * 防止事件监听器累积导致的内存泄漏 + * @returns {void} + * @example + * cleanupEventListeners(); + */ +function cleanupEventListeners() { } + +/** + * 清理所有 PJAX 资源 + * 在 pjax:beforeReplace 事件中调用 + * 统一清理 Observer、第三方库实例、动态标签等 + * 确保页面切换时不会出现资源泄漏 + * @returns {void} + * @example + * $(document).on('pjax:beforeReplace', function() { + * cleanupPjaxResources(); + * }); + */ +function cleanupPjaxResources() { } + +/** + * 重置 GT4 验证码 + * 在 pjax:end 事件中调用 + * 重置 Geetest 验证码实例,清空验证状态 + * @returns {void} + * @example + * $(document).on('pjax:end', function() { + * resetGT4Captcha(); + * }); + */ +function resetGT4Captcha() { } +``` + +## 脚本执行 + +```javascript +/** + * 执行单个脚本 + * 创建新的 script 元素并执行,复制所有属性 + * 提供错误隔离,单个脚本失败不影响其他脚本 + * @param {HTMLScriptElement} oldScript - 原始脚本元素 + * @returns {boolean} 是否执行成功 + * @example + * const success = executeScript(scriptElement); + */ +function executeScript(oldScript) { } + +/** + * 执行容器内的所有内联脚本 + * 按 DOM 顺序依次执行,提供错误隔离 + * 只执行内联脚本,不执行外部脚本 + * @param {HTMLElement} container - 容器元素,默认为 document + * @returns {Object} 执行结果统计 {total, success, failed} + * @example + * const result = executeInlineScripts(document.getElementById('content')); + * console.log(`成功: ${result.success}, 失败: ${result.failed}`); + */ +function executeInlineScripts(container) { } +``` + +## 评论功能 + +```javascript +/** + * 显示回复框 + * 滚动到评论框,显示回复信息,聚焦输入框 + * @param {number} commentID - 评论ID + * @returns {void} + * @example + * reply(123); + */ +function reply(commentID) { } + +/** + * 取消回复 + * 隐藏回复信息,重置回复状态 + * @returns {void} + * @example + * cancelReply(); + */ +function cancelReply() { } + +/** + * 编辑评论 + * 加载评论内容到输入框,切换到编辑模式 + * @param {number} commentID - 评论ID + * @returns {void} + * @example + * edit(123); + */ +function edit(commentID) { } + +/** + * 取消编辑 + * 退出编辑模式,可选择是否清空输入框 + * @param {boolean} clear - 是否清空输入框 + * @returns {void} + * @example + * cancelEdit(true); + */ +function cancelEdit(clear) { } + +/** + * 发送评论 + * 验证表单,发送 AJAX 请求,处理响应 + * 支持回复评论、Markdown、私密评论等功能 + * @returns {void} + * @example + * postComment(); + */ +function postComment() { } + +/** + * 编辑评论 + * 验证表单,发送 AJAX 请求,更新评论内容 + * @returns {void} + * @example + * editComment(); + */ +function editComment() { } + +/** + * 切换评论置顶状态 + * 显示确认对话框,发送 AJAX 请求,更新 UI + * @param {number} commentID - 评论ID + * @param {boolean} pinned - 当前是否已置顶 + * @returns {void} + * @example + * toogleCommentPin(123, false); + */ +function toogleCommentPin(commentID, pinned) { } + +/** + * 删除评论 + * 显示确认对话框,发送 AJAX 请求,移除评论元素 + * @param {number} commentID - 评论ID + * @returns {void} + * @example + * deleteComment(123); + */ +function deleteComment(commentID) { } +``` + +## 工具函数 + +```javascript +/** + * 折叠过长评论 + * 检测评论高度,超过阈值则折叠 + * @returns {void} + * @example + * foldLongComments(); + */ +function foldLongComments() { } + +/** + * 生成评论文字头像 + * 当 Gravatar 头像加载失败时,生成文字头像 + * @param {HTMLImageElement} img - 头像图片元素 + * @returns {void} + * @example + * generateCommentTextAvatar(imgElement); + */ +function generateCommentTextAvatar(img) { } + +/** + * 刷新评论文字头像 + * 遍历所有评论头像,为未加载的生成文字头像 + * @returns {void} + * @example + * refreshCommentTextAvatar(); + */ +function refreshCommentTextAvatar() { } + +/** + * 根据 Hash 定位到页面元素 + * 平滑滚动到指定元素位置 + * @param {string} hash - Hash 值(如 #comment-123) + * @param {number} durtion - 滚动动画时长(毫秒) + * @param {string} easing - 缓动函数名称 + * @returns {void} + * @example + * gotoHash('#comment-123', 600, 'easeOutExpo'); + */ +function gotoHash(hash, durtion, easing = 'easeOutExpo') { } + +/** + * 从 URL 中提取 Hash + * @param {string} url - URL 字符串 + * @returns {string} Hash 值(包含 # 符号) + * @example + * const hash = getHash('https://example.com/post#comment-123'); + * // 返回: '#comment-123' + */ +function getHash(url) { } + +/** + * 显示文章过时信息 Toast + * 检查文章发布时间,超过阈值则显示提示 + * @returns {void} + * @example + * showPostOutdateToast(); + */ +function showPostOutdateToast() { } +``` + +## 第三方库初始化 + +```javascript +/** + * 初始化 Zoomify(图片放大) + * 清理旧实例,为图片添加放大功能 + * @returns {void} + * @example + * zoomifyInit(); + */ +function zoomifyInit() { } + +/** + * 初始化 Pangu.js(中英文排版优化) + * 为文章内容和评论添加空格 + * @returns {void} + * @example + * panguInit(); + */ +function panguInit() { } + +/** + * 初始化 Clamp.js(文本截断) + * 限制文本行数,添加省略号 + * @returns {void} + * @example + * clampInit(); + */ +function clampInit() { } + +/** + * 初始化 Tippy.js(Tooltip) + * 为引用、按钮等元素添加 Tooltip + * @returns {void} + * @example + * tippyInit(); + */ +function tippyInit() { } +``` + +## 颜色转换工具 + +```javascript +/** + * RGB 转 HSL + * @param {number} R - 红色值 (0-255) + * @param {number} G - 绿色值 (0-255) + * @param {number} B - 蓝色值 (0-255) + * @returns {Object} {H: 色相(0-360), S: 饱和度(0-100), L: 亮度(0-100)} + * @example + * const hsl = rgb2hsl(255, 0, 0); + * // 返回: {H: 0, S: 100, L: 50} + */ +function rgb2hsl(R, G, B) { } + +/** + * HSL 转 RGB + * @param {number} h - 色相 (0-360) + * @param {number} s - 饱和度 (0-100) + * @param {number} l - 亮度 (0-100) + * @returns {Object} {R: 红色(0-255), G: 绿色(0-255), B: 蓝色(0-255)} + * @example + * const rgb = hsl2rgb(0, 100, 50); + * // 返回: {R: 255, G: 0, B: 0} + */ +function hsl2rgb(h, s, l) { } + +/** + * RGB 转 HEX + * @param {number} r - 红色值 (0-255) + * @param {number} g - 绿色值 (0-255) + * @param {number} b - 蓝色值 (0-255) + * @returns {string} HEX 颜色值(如 #FF0000) + * @example + * const hex = rgb2hex(255, 0, 0); + * // 返回: '#FF0000' + */ +function rgb2hex(r, g, b) { } + +/** + * HEX 转 RGB + * @param {string} hex - HEX 颜色值(如 #FF0000) + * @returns {Object} {R: 红色(0-255), G: 绿色(0-255), B: 蓝色(0-255)} + * @example + * const rgb = hex2rgb('#FF0000'); + * // 返回: {R: 255, G: 0, B: 0} + */ +function hex2rgb(hex) { } + +/** + * RGB 转灰度值 + * 使用标准灰度转换公式: 0.299R + 0.587G + 0.114B + * @param {number} R - 红色值 (0-255) + * @param {number} G - 绿色值 (0-255) + * @param {number} B - 蓝色值 (0-255) + * @returns {number} 灰度值 (0-255) + * @example + * const gray = rgb2gray(255, 0, 0); + * // 返回: 76 + */ +function rgb2gray(R, G, B) { } + +/** + * HEX 转灰度值 + * @param {string} hex - HEX 颜色值(如 #FF0000) + * @returns {number} 灰度值 (0-255) + * @example + * const gray = hex2gray('#FF0000'); + * // 返回: 76 + */ +function hex2gray(hex) { } + +/** + * RGB 对象转字符串 + * @param {Object} rgb - RGB 对象 {R, G, B} + * @returns {string} RGB 字符串(如 "255,0,0") + * @example + * const str = rgb2str({R: 255, G: 0, B: 0}); + * // 返回: '255,0,0' + */ +function rgb2str(rgb) { } + +/** + * HEX 转 RGB 字符串 + * @param {string} hex - HEX 颜色值(如 #FF0000) + * @returns {string} RGB 字符串(如 "255,0,0") + * @example + * const str = hex2str('#FF0000'); + * // 返回: '255,0,0' + */ +function hex2str(hex) { } +``` + +## 主题颜色 + +```javascript +/** + * Pickr 颜色对象转 HEX + * @param {Object} color - Pickr 颜色对象 + * @returns {string} HEX 颜色值(如 #FF0000) + * @example + * const hex = pickrObjectToHEX(pickr.getColor()); + */ +function pickrObjectToHEX(color) { } + +/** + * 更新主题颜色 + * 更新 CSS 变量,计算衍生颜色,可选保存到 Cookie + * @param {string} color - HEX 颜色值 + * @param {boolean} setcookie - 是否保存到 Cookie + * @returns {void} + * @example + * updateThemeColor('#FF0000', true); + */ +function updateThemeColor(color, setcookie) { } +``` + +## 打字效果 + +```javascript +/** + * 打字效果(递归实现) + * @param {jQuery} $element - jQuery 元素对象 + * @param {string} text - 要显示的文本 + * @param {number} now - 当前显示到第几个字符 + * @param {number} interval - 每个字符的间隔时间(毫秒) + * @returns {void} + * @example + * typeEffect($('#banner-title'), 'Hello World', 0, 100); + */ +function typeEffect($element, text, now, interval) { } + +/** + * 开始打字效果 + * @param {jQuery} $element - jQuery 元素对象 + * @param {string} text - 要显示的文本 + * @param {number} interval - 每个字符的间隔时间(毫秒) + * @returns {void} + * @example + * startTypeEffect($('#banner-title'), 'Hello World', 100); + */ +function startTypeEffect($element, text, interval) { } +``` + +## 其他工具 + +```javascript +/** + * 生成随机字符串 + * @param {number} len - 字符串长度 + * @returns {string} 随机字符串 + * @example + * const id = randomString(8); + * // 返回: 'a3f9d2e1' + */ +function randomString(len) { } + +/** + * 获取 GitHub Repo 信息卡内容 + * 通过 GitHub API 获取仓库信息并显示 + * @returns {void} + * @example + * getGithubInfoCardContent(); + */ +function getGithubInfoCardContent() { } + +/** + * 折叠长说说 + * 检测说说高度,超过阈值则折叠 + * @returns {void} + * @example + * foldLongShuoshuo(); + */ +function foldLongShuoshuo() { } + +/** + * 懒加载表情包 + * 为表情包图片添加懒加载 + * @returns {void} + * @example + * lazyloadStickers(); + */ +function lazyloadStickers() { } + +/** + * 在输入框中插入文本 + * 使用 execCommand 或降级方案插入文本 + * @param {string} text - 要插入的文本 + * @param {HTMLElement} input - 输入框元素 + * @returns {void} + * @example + * inputInsertText('[smile]', document.getElementById('comment_content')); + */ +function inputInsertText(text, input) { } + +/** + * 显示评论编辑记录 + * 通过 AJAX 获取评论编辑历史并显示 + * @param {number} id - 评论ID + * @returns {void} + * @example + * showCommentEditHistory(123); + */ +function showCommentEditHistory(id) { } +``` + +## 使用说明 + +1. **复制对应的 JSDoc 注释**到函数定义前 +2. **根据实际情况调整**参数说明和示例 +3. **确保类型注解正确**(string, number, boolean, Object, Array 等) +4. **添加使用示例**帮助其他开发者理解函数用法 + +## 注意事项 + +- JSDoc 注释应该放在函数定义的正上方 +- 参数说明要清晰明确,包含类型和用途 +- 返回值说明要准确,void 表示无返回值 +- 示例代码要简洁实用,展示典型用法 +- 对于复杂函数,可以添加更详细的说明 diff --git a/.kiro/specs/pjax-lazyload-fix/requirements.md b/.kiro/specs/pjax-lazyload-fix/requirements.md new file mode 100644 index 0000000..2fe7c01 --- /dev/null +++ b/.kiro/specs/pjax-lazyload-fix/requirements.md @@ -0,0 +1,280 @@ +# Requirements Document + +## Introduction + +本规范旨在全面优化 Argon WordPress 主题中 PJAX 页面无刷新跳转、Lazyload 图片懒加载和 Mermaid 图表渲染功能。当前实现存在以下问题: + +**核心问题:** +- 资源泄漏:Observer 和第三方库实例未正确清理 +- 脚本执行失败:新页面的内联脚本未执行 +- 样式错误:动态样式未清理导致样式冲突 +- Mermaid 初始化时序问题:清除缓存后首次加载报语法错误 +- Mermaid 交互体验差:操作框遮挡内容、缩放不流畅、拖拽不灵敏 + +**优化目标:** +1. 完善 PJAX 生命周期管理,消除资源泄漏 +2. 优化 Lazyload 性能,提升图片加载体验 +3. 增强 Mermaid 显示效果和交互功能 +4. 提升整体稳定性和用户体验 + +## Glossary + +- **PJAX**: 使用 Ajax 和 pushState 实现的页面无刷新跳转技术 +- **Lazyload**: 图片懒加载技术,延迟加载视口外的图片 +- **Observer**: IntersectionObserver API,用于监听元素可见性 +- **Mermaid**: 基于文本的图表生成库 +- **Resource_Cleanup**: 资源清理,包括断开 Observer、销毁实例、移除事件监听器 +- **Lifecycle**: 生命周期,指 PJAX 页面切换的各个阶段 +- **DOM_Cache**: DOM 元素缓存,避免重复查询 +- **Event_Delegation**: 事件委托,在父元素上监听子元素事件 +- **Zoom_Controls**: 缩放控制,Mermaid 图表的放大缩小功能 +- **Drag_Pan**: 拖拽平移,Mermaid 图表的拖拽移动功能 +- **Toolbar**: 工具栏,Mermaid 图表的操作按钮组 +- **Fullscreen**: 全屏模式,Mermaid 图表的全屏查看功能 + +## Requirements + +### Requirement 1: PJAX 生命周期管理 + +**User Story:** 作为开发者,我希望 PJAX 页面切换时能正确管理资源生命周期,以避免内存泄漏和功能失效。 + +#### Acceptance Criteria + +1. WHEN PJAX 触发页面切换 THEN THE System SHALL 在 pjax:beforeReplace 事件中清理所有旧页面资源 +2. WHEN 清理资源时 THEN THE System SHALL 断开所有 IntersectionObserver 连接并置空引用 +3. WHEN 清理资源时 THEN THE System SHALL 销毁所有第三方库实例(Zoomify、Tippy、Mermaid) +4. WHEN 清理资源时 THEN THE System SHALL 移除所有动态添加的 style 和 script 标签 +5. WHEN 新页面加载完成 THEN THE System SHALL 在 pjax:complete 事件中重新初始化所有功能模块 +6. WHEN 初始化功能模块时 THEN THE System SHALL 为每个模块添加错误处理,确保单个模块失败不影响其他模块 +7. WHEN 页面切换完成 THEN THE System SHALL 在 pjax:end 事件中执行特定任务(GT4 验证码重置、移动端目录重置) +8. WHEN 新页面包含内联脚本 THEN THE System SHALL 执行这些脚本 + +### Requirement 2: Lazyload 资源管理 + +**User Story:** 作为开发者,我希望 Lazyload 功能能正确管理 Observer 资源,避免内存泄漏。 + +#### Acceptance Criteria + +1. WHEN 初始化 Lazyload THEN THE System SHALL 检查并清理旧的 Observer 实例 +2. WHEN 清理 Observer THEN THE System SHALL 调用 disconnect() 方法并将引用置为 null +3. WHEN 页面切换时 THEN THE System SHALL 在 cleanupPjaxResources 函数中清理 Lazyload Observer +4. WHEN 图片加载完成 THEN THE System SHALL 取消对该图片的监听 +5. WHEN 图片加载失败 THEN THE System SHALL 使用降级方案并清理相关资源 +6. WHEN 懒加载禁用时 THEN THE System SHALL 立即加载所有图片而不创建 Observer + +### Requirement 3: Mermaid 初始化时序 + +**User Story:** 作为用户,我希望 Mermaid 图表能在清除缓存后正确渲染,不出现语法错误。 + +#### Acceptance Criteria + +1. WHEN Mermaid 库加载时 THEN THE System SHALL 检查库是否完全加载后再初始化 +2. WHEN 初始化 Mermaid THEN THE System SHALL 添加加载状态检查,确保 mermaid.render 方法存在 +3. WHEN Mermaid 渲染失败 THEN THE System SHALL 提供降级方案(旧版 API、init 方法) +4. WHEN 清除缓存后首次加载 THEN THE System SHALL 等待 Mermaid 库完全加载后再渲染 +5. WHEN Mermaid 渲染出错 THEN THE System SHALL 显示友好的错误提示并保留原始代码 +6. WHEN 页面切换时 THEN THE System SHALL 清理旧的 Mermaid 实例并重新渲染 + +### Requirement 4: 内联脚本执行 + +**User Story:** 作为开发者,我希望 PJAX 页面切换后能执行新页面中的内联脚本。 + +#### Acceptance Criteria + +1. WHEN PJAX 加载新页面 THEN THE System SHALL 提取新页面中的所有 script 标签 +2. WHEN 提取脚本时 THEN THE System SHALL 区分内联脚本和外部脚本 +3. WHEN 执行内联脚本 THEN THE System SHALL 按照脚本在 DOM 中的顺序执行 +4. WHEN 脚本执行失败 THEN THE System SHALL 捕获错误并记录日志,不中断其他脚本执行 +5. WHEN 脚本包含 async 或 defer 属性 THEN THE System SHALL 尊重这些属性的执行时机 + +### Requirement 5: CSS 样式管理 + +**User Story:** 作为用户,我希望页面切换后样式保持正确,不出现样式丢失或错乱。 + +#### Acceptance Criteria + +1. WHEN 页面切换前 THEN THE System SHALL 清理所有动态添加的 style 标签 +2. WHEN 清理样式时 THEN THE System SHALL 保留主题核心样式,只清理页面特定样式 +3. WHEN 新页面加载 THEN THE System SHALL 提取并应用新页面的 style 标签 +4. WHEN 样式冲突时 THEN THE System SHALL 使用新页面的样式覆盖旧样式 +5. WHEN 页面包含 scoped 样式 THEN THE System SHALL 正确处理样式作用域 + +### Requirement 6: 事件监听器管理 + +**User Story:** 作为开发者,我希望页面切换时能正确管理事件监听器,避免重复绑定和内存泄漏。 + +#### Acceptance Criteria + +1. WHEN 页面切换前 THEN THE System SHALL 移除所有非委托的事件监听器 +2. WHEN 使用事件委托 THEN THE System SHALL 在 document 或 body 上绑定监听器 +3. WHEN 新页面加载 THEN THE System SHALL 重新绑定必要的事件监听器 +4. WHEN 监听器绑定失败 THEN THE System SHALL 记录错误并继续执行 +5. WHEN 使用第三方库的事件 THEN THE System SHALL 在清理时调用库提供的销毁方法 + +### Requirement 7: 性能优化 + +**User Story:** 作为用户,我希望页面切换流畅,不出现卡顿和延迟。 + +#### Acceptance Criteria + +1. WHEN 滚动页面时 THEN THE System SHALL 使用节流函数限制事件处理频率 +2. WHEN 初始化多个功能模块 THEN THE System SHALL 使用 requestAnimationFrame 优化渲染 +3. WHEN 清理资源时 THEN THE System SHALL 批量处理,避免多次 DOM 操作 +4. WHEN 加载图片时 THEN THE System SHALL 使用 IntersectionObserver 替代滚动监听 +5. WHEN 页面切换时 THEN THE System SHALL 显示加载进度条,提供视觉反馈 + +### Requirement 8: 错误处理和降级 + +**User Story:** 作为开发者,我希望系统能优雅地处理错误,提供降级方案。 + +#### Acceptance Criteria + +1. WHEN 功能模块初始化失败 THEN THE System SHALL 捕获错误并记录日志 +2. WHEN IntersectionObserver 不支持 THEN THE System SHALL 使用滚动监听降级方案 +3. WHEN Mermaid 渲染失败 THEN THE System SHALL 显示错误提示并保留原始代码 +4. WHEN PJAX 加载失败 THEN THE System SHALL 回退到传统页面跳转 +5. WHEN 第三方库未加载 THEN THE System SHALL 提供空实现,避免脚本错误 + +### Requirement 9: 调试和监控 + +**User Story:** 作为开发者,我希望能方便地调试和监控 PJAX 和 Lazyload 的运行状态。 + +#### Acceptance Criteria + +1. WHEN 启用调试模式 THEN THE System SHALL 在控制台输出详细的生命周期日志 +2. WHEN 资源清理时 THEN THE System SHALL 记录清理的资源类型和数量 +3. WHEN 功能模块初始化 THEN THE System SHALL 记录初始化状态和耗时 +4. WHEN 发生错误时 THEN THE System SHALL 记录错误堆栈和上下文信息 +5. WHEN 性能异常时 THEN THE System SHALL 记录性能指标(内存占用、渲染时间) + +### Requirement 10: 兼容性保证 + +**User Story:** 作为用户,我希望功能在不同浏览器和设备上都能正常工作。 + +#### Acceptance Criteria + +1. WHEN 浏览器不支持 IntersectionObserver THEN THE System SHALL 使用滚动监听降级方案 +2. WHEN 浏览器不支持 Promise THEN THE System SHALL 使用回调函数实现异步逻辑 +3. WHEN 浏览器不支持 requestAnimationFrame THEN THE System SHALL 使用 setTimeout 降级 +4. WHEN 移动端浏览器 THEN THE System SHALL 优化触摸事件处理 +5. WHEN 旧版浏览器 THEN THE System SHALL 提供 polyfill 或禁用高级功能 + +### Requirement 11: Mermaid 工具栏优化 + +**User Story:** 作为用户,我希望 Mermaid 图表的工具栏不会遮挡图表内容,且操作更加便捷。 + +#### Acceptance Criteria + +1. WHEN 鼠标移出图表区域 THEN THE System SHALL 自动隐藏工具栏,避免遮挡内容 +2. WHEN 鼠标移入图表区域 THEN THE System SHALL 显示工具栏,提供操作按钮 +3. WHEN 工具栏显示时 THEN THE System SHALL 使用半透明背景,不完全遮挡图表 +4. WHEN 工具栏位置固定在右上角 THEN THE System SHALL 确保不与图表关键内容重叠 +5. WHEN 移动端设备 THEN THE System SHALL 调整工具栏大小和位置,适配小屏幕 + +### Requirement 12: Mermaid 缩放功能增强 + +**User Story:** 作为用户,我希望 Mermaid 图表的缩放功能更加流畅和精确。 + +#### Acceptance Criteria + +1. WHEN 使用鼠标滚轮缩放 THEN THE System SHALL 以鼠标位置为中心进行缩放 +2. WHEN 缩放时 THEN THE System SHALL 使用 CSS transform 实现硬件加速 +3. WHEN 缩放级别改变 THEN THE System SHALL 平滑过渡,避免突兀跳动 +4. WHEN 缩放到最小或最大级别 THEN THE System SHALL 禁用对应的缩放按钮 +5. WHEN 双击图表 THEN THE System SHALL 智能缩放到合适大小(适配容器或重置) + +### Requirement 13: Mermaid 拖拽功能优化 + +**User Story:** 作为用户,我希望拖拽 Mermaid 图表时响应灵敏,不会误触其他操作。 + +#### Acceptance Criteria + +1. WHEN 拖拽图表时 THEN THE System SHALL 改变鼠标光标为抓手样式 +2. WHEN 拖拽时 THEN THE System SHALL 禁用文本选择,避免误选 +3. WHEN 拖拽时 THEN THE System SHALL 使用 requestAnimationFrame 优化性能 +4. WHEN 拖拽结束 THEN THE System SHALL 恢复正常光标和文本选择 +5. WHEN 图表未缩放且完全可见 THEN THE System SHALL 禁用拖拽功能 + +### Requirement 14: Mermaid 全屏模式 + +**User Story:** 作为用户,我希望能全屏查看复杂的 Mermaid 图表,获得更好的阅读体验。 + +#### Acceptance Criteria + +1. WHEN 点击全屏按钮 THEN THE System SHALL 将图表全屏显示 +2. WHEN 全屏模式下 THEN THE System SHALL 保持缩放和拖拽功能可用 +3. WHEN 全屏模式下 THEN THE System SHALL 显示退出全屏按钮 +4. WHEN 按 ESC 键 THEN THE System SHALL 退出全屏模式 +5. WHEN 退出全屏 THEN THE System SHALL 恢复图表原始状态(缩放级别和位置) + +### Requirement 15: Mermaid 导出功能 + +**User Story:** 作为用户,我希望能导出 Mermaid 图表为图片或 SVG 文件。 + +#### Acceptance Criteria + +1. WHEN 点击导出按钮 THEN THE System SHALL 显示导出选项(PNG、SVG) +2. WHEN 选择 PNG 导出 THEN THE System SHALL 将图表转换为 PNG 图片并下载 +3. WHEN 选择 SVG 导出 THEN THE System SHALL 将 SVG 代码保存为文件并下载 +4. WHEN 导出时 THEN THE System SHALL 保持图表当前的缩放级别和样式 +5. WHEN 导出失败 THEN THE System SHALL 显示友好的错误提示 + +### Requirement 16: Mermaid 响应式优化 + +**User Story:** 作为移动端用户,我希望 Mermaid 图表能自适应屏幕大小,操作便捷。 + +#### Acceptance Criteria + +1. WHEN 屏幕宽度小于 768px THEN THE System SHALL 调整工具栏按钮大小 +2. WHEN 移动端设备 THEN THE System SHALL 支持双指缩放手势 +3. WHEN 移动端设备 THEN THE System SHALL 支持单指拖拽移动 +4. WHEN 移动端设备 THEN THE System SHALL 优化触摸事件响应速度 +5. WHEN 横屏模式 THEN THE System SHALL 自动调整图表布局 + +### Requirement 17: Mermaid 主题同步 + +**User Story:** 作为用户,我希望 Mermaid 图表主题能自动跟随网站主题切换。 + +#### Acceptance Criteria + +1. WHEN 网站切换到夜间模式 THEN THE System SHALL 自动将 Mermaid 图表切换到 dark 主题 +2. WHEN 网站切换到日间模式 THEN THE System SHALL 自动将 Mermaid 图表切换到 default 主题 +3. WHEN 主题切换时 THEN THE System SHALL 保持图表的缩放级别和位置 +4. WHEN 主题切换时 THEN THE System SHALL 使用淡入淡出过渡效果 +5. WHEN 主题切换失败 THEN THE System SHALL 保留原主题,不影响图表显示 + +### Requirement 18: Mermaid 性能优化 + +**User Story:** 作为开发者,我希望 Mermaid 图表渲染性能优化,减少页面卡顿。 + +#### Acceptance Criteria + +1. WHEN 页面包含多个图表 THEN THE System SHALL 使用批量渲染,避免阻塞主线程 +2. WHEN 图表渲染时 THEN THE System SHALL 显示加载动画,提供视觉反馈 +3. WHEN 图表渲染完成 THEN THE System SHALL 使用淡入动画,提升视觉体验 +4. WHEN 图表不在视口内 THEN THE System SHALL 延迟渲染,优先渲染可见图表 +5. WHEN 图表渲染失败 THEN THE System SHALL 不阻塞其他图表的渲染 + +### Requirement 19: Mermaid 错误提示优化 + +**User Story:** 作为用户,我希望 Mermaid 渲染错误时能看到清晰的错误提示和原始代码。 + +#### Acceptance Criteria + +1. WHEN 图表渲染失败 THEN THE System SHALL 显示友好的错误提示容器 +2. WHEN 显示错误提示 THEN THE System SHALL 包含错误类型、错误信息和错误行号 +3. WHEN 显示错误提示 THEN THE System SHALL 提供可折叠的原始代码查看区域 +4. WHEN 错误提示显示 THEN THE System SHALL 使用醒目的颜色和图标,便于识别 +5. WHEN 夜间模式 THEN THE System SHALL 调整错误提示的颜色方案,保持可读性 + +### Requirement 20: Mermaid 工具栏按钮扩展 + +**User Story:** 作为用户,我希望 Mermaid 工具栏提供更多实用功能。 + +#### Acceptance Criteria + +1. WHEN 工具栏显示 THEN THE System SHALL 包含缩放、重置、全屏、导出按钮 +2. WHEN 鼠标悬停按钮 THEN THE System SHALL 显示按钮功能提示(tooltip) +3. WHEN 按钮不可用时 THEN THE System SHALL 显示禁用状态,避免误操作 +4. WHEN 点击按钮 THEN THE System SHALL 提供视觉反馈(按下效果) +5. WHEN 工具栏按钮过多 THEN THE System SHALL 使用下拉菜单收纳次要功能 diff --git a/.kiro/specs/pjax-lazyload-fix/tasks.md b/.kiro/specs/pjax-lazyload-fix/tasks.md index 0ff937d..6c275f8 100644 --- a/.kiro/specs/pjax-lazyload-fix/tasks.md +++ b/.kiro/specs/pjax-lazyload-fix/tasks.md @@ -59,11 +59,11 @@ - [x] 8.2 全屏模式功能保持 _需求:14.2, 14.3_ - [x] 8.3 全屏模式退出(ESC 键) _需求:14.4, 14.5_ -- [ ] 9. 实现 Mermaid 导出功能 - - [ ] 9.1 添加导出按钮和菜单 _需求:15.1_ - - [ ] 9.2 实现 PNG 导出 _需求:15.2, 15.4_ - - [ ] 9.3 实现 SVG 导出 _需求:15.3, 15.4_ - - [ ] 9.4 添加导出错误处理 _需求:15.5_ +- [x] 9. 实现 Mermaid 导出功能 + - [x] 9.1 添加导出按钮和菜单 _需求:15.1_ + - [x] 9.2 实现 PNG 导出 _需求:15.2, 15.4_ + - [x] 9.3 实现 SVG 导出 _需求:15.3, 15.4_ + - [x] 9.4 添加导出错误处理 _需求:15.5_ - [x] 10. 优化 Mermaid 响应式设计 - [x] 10.1 移动端工具栏适配 _需求:16.1_ @@ -109,7 +109,7 @@ - 测试主流浏览器和移动端浏览器 - 测试降级方案 -- [ ] 18. 文档和代码审查 +- [x] 18. 文档和代码审查 - 更新代码注释和 JSDoc - 代码风格检查