diff --git a/.claude/skills/generate-prototype-grid/SKILL.md b/.claude/skills/generate-prototype-grid/SKILL.md index a7c4813..74f1c21 100644 --- a/.claude/skills/generate-prototype-grid/SKILL.md +++ b/.claude/skills/generate-prototype-grid/SKILL.md @@ -319,96 +319,250 @@ prototype/ - 快速链接导航 - 项目概览信息 -## Step 5: 生成文档系统(前端渲染方案) +## Step 5: 生成文档系统(独立页面方案)⭐ -生成 `docs-index.html` 文档首页,使用前端 JavaScript 渲染 Markdown 文件。 +生成 `view-doc.html` 独立文档查看页面,通过 URL 参数传递文档路径。 -### 功能特性 -- 自动扫描 `docs/` 或 `planning-artifacts/` 目录 -- **前端 Markdown 渲染**:使用 markdown-it 库在浏览器中渲染 -- 文档卡片展示,点击后在页面中展开显示内容 -- 响应式设计,支持桌面端和移动端 +### ⭐ 推荐方案:独立页面架构 + +使用独立页面(`view-doc.html?doc=xxx.md`)展示文档,而非在当前页面内展开。 + +### 方案对比 + +| 对比项 | 独立页面方案 ⭐ | 页面内展开方案 | +|--------|----------------|----------------| +| **安全性** | ✅ URL 参数验证,路径遍历防护 | ❌ innerHTML 直接注入,XSS 风险 | +| **性能** | ✅ 页面级缓存,独立加载 | ❌ DOM 操作,主线程阻塞 | +| **可维护性** | ✅ 单一职责,代码简洁 | ❌ 逻辑耦合,状态管理复杂 | +| **用户体验** | ✅ 可分享链接,书签友好 | ❌ 刷新丢失状态 | +| **可扩展性** | ✅ 易于添加新功能 | ❌ 受限于宿主页面 | ### 技术实现 -**1. 引入 markdown-it 库** +**1. view-doc.html 页面结构** ```html - + + + + + + 文档查看器 + + + + + + + + + + +
+
+
+ + 加载文档中... +
+
+
+ + ``` -**2. 文档卡片结构** +**2. 安全性增强:路径验证** +```javascript +// 允许的文档目录白名单 +const ALLOWED_PATHS = [ + '/docs/', + '/planning-artifacts/', + '/' +]; + +// 验证文档路径 +function validateDocPath(docPath) { + // 移除 URL 编码 + const decoded = decodeURIComponent(docPath); + + // 检查路径遍历攻击 + if (decoded.includes('..') || decoded.includes('\\')) { + throw new Error('非法的文档路径'); + } + + // 检查文件扩展名 + if (!decoded.endsWith('.md')) { + throw new Error('仅支持 Markdown 文件'); + } + + // 确保路径在白名单内 + const isValid = ALLOWED_PATHS.some(prefix => decoded.startsWith(prefix)); + if (!isValid) { + throw new Error('文档路径不在允许范围内'); + } + + return decoded; +} +``` + +**3. 性能优化:缓存策略** +```javascript +// 文档缓存(内存级) +const docCache = new Map(); +const CACHE_MAX_SIZE = 20; +const CACHE_TTL = 5 * 60 * 1000; // 5 分钟 + +function getFromCache(key) { + const cached = docCache.get(key); + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + return cached.content; + } + return null; +} + +function setCache(key, content) { + // LRU 淘汰策略 + if (docCache.size >= CACHE_MAX_SIZE) { + const firstKey = docCache.keys().next().value; + docCache.delete(firstKey); + } + docCache.set(key, { + content, + timestamp: Date.now() + }); +} +``` + +**4. 完整加载逻辑** +```javascript +// 从 URL 获取文档路径 +const urlParams = new URLSearchParams(window.location.search); +const docPath = urlParams.get('doc'); + +if (!docPath) { + showError('缺少文档参数'); +} else { + loadDocument(docPath); +} + +async function loadDocument(docPath) { + try { + // 验证路径 + const validatedPath = validateDocPath(docPath); + + // 检查缓存 + const cached = getFromCache(validatedPath); + if (cached) { + renderMarkdown(cached); + return; + } + + // 加载文档 + const response = await fetch(validatedPath); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const markdown = await response.text(); + + // 存入缓存 + setCache(validatedPath, markdown); + + // 渲染 + renderMarkdown(markdown); + + } catch (error) { + showError(error.message); + } +} + +function renderMarkdown(markdown) { + // 使用 marked 解析 + const html = marked.parse(markdown); + + // 安全过滤(DOMPurify 可选,marked 已有基础防护) + const safeHtml = sanitizeHtml(html); + + // 渲染到页面 + document.getElementById('doc-content').innerHTML = safeHtml; + + // 代码高亮 + document.querySelectorAll('pre code').forEach(block => { + hljs.highlightElement(block); + }); + + // 更新标题 + const titleMatch = markdown.match(/^#\s+(.+)$/m); + if (titleMatch) { + document.getElementById('doc-title').textContent = titleMatch[1]; + document.title = titleMatch[1] + ' - 文档'; + } +} +``` + +### 文档链接格式 + +在 `index.html`、`docs-index.html` 和其他页面中,文档链接使用独立页面: + ```html + + + PRD 文档 + + + + + 架构设计 + +
-
-

产品需求文档 (PRD)

- 核心文档 -
-
- 1,082 行 - 40 KB -
-
- -
- - +

产品需求文档 (PRD)

+ + 查看文档 +
``` -**3. JavaScript 渲染逻辑** -```javascript -// 初始化 markdown-it -const md = window.markdownit({ - html: true, - linkify: true, - typographer: true, - breaks: true -}); +### 安全性与性能最佳实践 -// 切换文档显示 -function toggleDoc(docFile) { - const container = document.getElementById('doc-' + docFile); - const isHidden = container.classList.contains('hidden'); +#### 安全性措施 - if (isHidden) { - // 加载并渲染文档 - loadDoc(docFile, container); - } else { - // 隐藏文档 - container.classList.add('hidden'); - } -} +| 措施 | 实现方式 | 防护目标 | +|------|---------|---------| +| **路径白名单** | `ALLOWED_PATHS` 数组验证 | 防止路径遍历攻击 | +| **扩展名限制** | 仅允许 `.md` 文件 | 防止执行非法文件 | +| **URL 解码检查** | 检测 `..` 和 `\` | 防止编码绕过 | +| **XSS 防护** | marked 配置 + 可选 DOMPurify | 防止脚本注入 | +| **CSP 头部** | 服务器设置 Content-Security-Policy | 限制资源加载 | -// 加载文档 -async function loadDoc(docFile, container) { - // 尝试多个路径 - const paths = ['/planning-artifacts/' + docFile, '/docs/' + docFile, '/' + docFile]; +#### 性能优化 - for (const path of paths) { - try { - const response = await fetch(path); - if (response.ok) { - const markdown = await response.text(); - const html = md.render(markdown); - container.innerHTML = html; - container.classList.remove('hidden'); - return; - } - } catch (err) { - continue; - } - } +| 优化 | 实现方式 | 效果 | +|------|---------|------| +| **内存缓存** | LRU 缓存,5 分钟 TTL | 减少重复请求 | +| **懒加载** | 仅在打开文档时加载 | 减少初始负载 | +| **代码高亮** | 按需 highlight.js | 提升渲染速度 | +| **CDN 资源** | 使用稳定 CDN | 全球加速访问 | - // 所有路径都失败 - container.innerHTML = '

无法加载文档: ' + docFile + '

'; - container.classList.remove('hidden'); -} -``` +### ⭐ 最佳实践建议 + +1. **优先使用独立页面**:所有文档查看都应通过 `view-doc.html` 访问 +2. **新标签页打开**:使用 `target="_blank"` 保持上下文 +3. **添加 noopener**:使用 `rel="noopener"` 防止 window.opener 漏洞 +4. **错误处理友好**:显示清晰的错误消息和建议操作 +5. **支持书签分享**:URL 参数状态可直接分享 + +### Markdown 样式(保持不变) -**4. Markdown 内容样式** ```css .markdown-body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; @@ -470,30 +624,6 @@ async function loadDoc(docFile, container) { max-width: 100%; } ``` - -### 文档链接格式 - -在 `index.html` 和其他页面中,文档链接使用以下格式: - -```html - - - - - - PRD 文档 - -``` - -### ⚠️ 关键设计原则 - -1. **前端渲染优先**:使用 markdown-it 在浏览器中渲染,不依赖后端 API -2. **直接展示**:点击文档链接后,直接在页面中展开显示内容,不使用弹窗/模态框 -3. **渐进加载**:文档内容在需要时才加载,提升页面加载速度 -4. **多路径兼容**:尝试多个可能的文档路径(planning-artifacts、docs、根目录) - ## Step 6: 生成本地预览服务器 生成 `server.js` 文件,提供静态文件服务。