docs: 更新 generate-prototype-grid 技能文档,推荐独立页面方案

- 将 Step 5 标题改为"生成文档系统(独立页面方案)"
- 强调使用 view-doc.html 独立页面而非页面内展开
- 添加方案对比表格(独立页面 vs 页面内展开)
- 添加安全性增强说明(路径白名单、扩展名限制、XSS 防护)
- 添加性能优化说明(LRU 缓存、懒加载、CDN 资源)
- 添加最佳实践建议(新标签页、noopener、书签分享)
- 推荐独立页面方案作为最佳实践

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Code
2026-03-29 04:44:13 +00:00
parent 6e9be01a09
commit 2ffd7d3455

View File

@@ -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
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.0.0/dist/markdown-it.min.js"></script>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文档查看器</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/marked@11.0.0/marked.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- 顶部导航 -->
<nav class="bg-white shadow-sm border-b border-gray-200">
<div class="max-w-5xl mx-auto px-4 py-3 flex items-center justify-between">
<div class="flex items-center gap-3">
<i class="fas fa-file-alt text-blue-600 text-xl"></i>
<h1 id="doc-title" class="text-lg font-semibold text-gray-800">加载中...</h1>
</div>
<button onclick="window.close()" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
</nav>
<!-- 文档内容 -->
<main class="max-w-5xl mx-auto px-4 py-8">
<div id="doc-content" class="bg-white rounded-lg shadow-sm p-8 markdown-body">
<div class="flex items-center justify-center py-12">
<i class="fas fa-spinner fa-spin text-blue-600 text-2xl"></i>
<span class="ml-3 text-gray-600">加载文档中...</span>
</div>
</div>
</main>
</body>
</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
<!-- 直接链接到独立页面 -->
<a href="view-doc.html?doc=prd.md" target="_blank">
<i class="fas fa-file-alt"></i> PRD 文档
</a>
<!-- 在新标签页打开 -->
<a href="view-doc.html?doc=architecture.md" target="_blank" rel="noopener">
<i class="fas fa-sitemap"></i> 架构设计
</a>
<!-- 文档卡片 -->
<div class="doc-card">
<div class="doc-header">
<h3>产品需求文档 (PRD)</h3>
<span class="doc-badge">核心文档</span>
</div>
<div class="doc-meta">
<span>1,082 行</span>
<span>40 KB</span>
</div>
<div class="doc-actions">
<button onclick="toggleDoc('prd.md')" class="btn-view">
<i class="fas fa-eye"></i> 查看文档
</button>
</div>
<!-- 文档内容容器(默认隐藏) -->
<div id="doc-prd.md" class="doc-content hidden"></div>
<h3>产品需求文档 (PRD)</h3>
<a href="view-doc.html?doc=prd.md" class="btn-primary">
查看文档 <i class="fas fa-external-link-alt ml-1"></i>
</a>
</div>
```
**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 = '<p class="error">无法加载文档: ' + docFile + '</p>';
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
<!-- 点击展开文档 -->
<button onclick="toggleDoc('prd.md')">
<i class="fas fa-eye"></i> 查看 PRD
</button>
<!-- 或者使用链接 -->
<a href="#" onclick="toggleDoc('prd.md'); return false;">
<i class="fas fa-file-alt"></i> PRD 文档
</a>
```
### ⚠️ 关键设计原则
1. **前端渲染优先**:使用 markdown-it 在浏览器中渲染,不依赖后端 API
2. **直接展示**:点击文档链接后,直接在页面中展开显示内容,不使用弹窗/模态框
3. **渐进加载**:文档内容在需要时才加载,提升页面加载速度
4. **多路径兼容**尝试多个可能的文档路径planning-artifacts、docs、根目录
## Step 6: 生成本地预览服务器
生成 `server.js` 文件,提供静态文件服务。