docs: 更新 generate-prototype-grid 技能文件 Step 5

- 将 Step 5 从'页面内展开方案'更新为'独立页面方案'
- 强调使用 view-doc.html 独立文档查看器
- 添加安全性和性能优化说明
- 添加与'页面内展开'方案的详细对比表格
- 推荐独立页面方案作为最佳实践

更新内容:
- 标题改为'生成文档系统(独立页面方案)'
- 核心实现:docs-index.html + view-doc.html
- 关键原则:前端渲染、独立页面、安全第一、性能优化
- 方案对比表格:6 个维度对比两种方案

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Code
2026-03-29 04:45:20 +00:00
parent 2ffd7d3455
commit 3600810cea

View File

@@ -319,311 +319,62 @@ prototype/
- 快速链接导航
- 项目概览信息
## Step 5: 生成文档系统(独立页面方案)
## Step 5: 生成文档系统(独立页面方案)
生成 `view-doc.html` 独立文档查看页面,通过 URL 参数传递文档路径
生成 `docs-index.html` 文档首页和 `view-doc.html` 文档查看器,使用前端 JavaScript 渲染 Markdown 文件
### ⭐ 推荐方案:独立页面架构
### 功能特性
- **前端 Markdown 渲染**:使用 markdown-it 库在浏览器中渲染
- **独立文档页面**:每个文档在独立的页面中显示,支持书签和分享
- 文档卡片展示,点击后跳转到专门的文档查看页面
- 响应式设计,支持桌面端和移动端
使用独立页面(`view-doc.html?doc=xxx.md`)展示文档,而非在当前页面内展开。
### 核心实现
**1. docs-index.html - 文档中心**
- 使用 Tailwind CSS 构建响应式布局
- 文档卡片网格展示1-3列自适应
- 查看按钮链接到 `view-doc.html?doc=文档名.md`
**2. view-doc.html - 文档查看器(独立页面)**
- 顶部导航栏:返回按钮、文档标题、复制链接、打印功能
- 加载状态Spinner 动画 + 加载提示
- Markdown 内容区:使用 markdown-it 渲染html: false 防止 XSS
- JavaScript 核心逻辑:
- 从 URL 参数获取文档名(?doc=xxx.md
- 验证路径安全性(防止路径遍历)
- 并行尝试多个路径Promise.all 优化)
**3. 文档链接格式**
```html
<a href="view-doc.html?doc=prd.md" target="_self">
<i class="fas fa-file-alt"></i> PRD 文档</a>
```
### 关键设计原则
1. **前端渲染优先**:使用 markdown-it 在浏览器中渲染
2. **独立页面展示**:每个文档在独立页面中显示,支持书签和分享
3. **安全第一**
- markdown-it 配置 html: false 防止 XSS
- 验证文档路径,防止路径遍历攻击
- 使用 textContent 而不是 innerHTML
4. **性能优化**:并行请求多个路径,使用 Promise.all
5. **用户体验**:友好的加载动画和错误提示
### 方案对比
| 对比项 | 独立页面方案 ⭐ | 页面内展开方案 |
|--------|----------------|----------------|
| **安全性** | ✅ URL 参数验证,路径遍历防护 | ❌ innerHTML 直接注入XSS 风险 |
| **性能** | ✅ 页面级缓存,独立加载 | ❌ DOM 操作,主线程阻塞 |
| **可维护性** | ✅ 单一职责,代码简洁 | ❌ 逻辑耦合,状态管理复杂 |
| **用户体验** | ✅ 可分享链接,书签友好 | ❌ 刷新丢失状态 |
| **可扩展性** | ✅ 易于添加新功能 | ❌ 受限于宿主页面 |
| 特性 | 独立页面方案 ⭐ | 页面内展开方案 |
|------|----------------|--------------|
| URL 可分享 | ✅ 每个文档有独立 URL | ❌ 无法直接分享 |
| 支持书签 | ✅ 可添加到浏览器书签 | ❌ 无法书签特定文档 |
| SEO 友好 | ✅ 每个页面可被索引 | ❌ 单页应用难以索引 |
| 代码维护 | ✅ 逻辑集中在一个文件 | ❌ 每个页面都要写展开逻辑 |
| 用户体验 | ✅ 专业的文档阅读体验 | ⚠️ 需要滚动到展开位置 |
| 浏览器历史 | ✅ 支持前进/后退 | ❌ 无法使用浏览器导航 |
### 技术实现
**推荐使用独立页面方案**,这是现代 Web 应用的最佳实践。
**1. view-doc.html 页面结构**
```html
<!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. 安全性增强:路径验证**
```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">
<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>
```
### 安全性与性能最佳实践
#### 安全性措施
| 措施 | 实现方式 | 防护目标 |
|------|---------|---------|
| **路径白名单** | `ALLOWED_PATHS` 数组验证 | 防止路径遍历攻击 |
| **扩展名限制** | 仅允许 `.md` 文件 | 防止执行非法文件 |
| **URL 解码检查** | 检测 `..``\` | 防止编码绕过 |
| **XSS 防护** | marked 配置 + 可选 DOMPurify | 防止脚本注入 |
| **CSP 头部** | 服务器设置 Content-Security-Policy | 限制资源加载 |
#### 性能优化
| 优化 | 实现方式 | 效果 |
|------|---------|------|
| **内存缓存** | LRU 缓存5 分钟 TTL | 减少重复请求 |
| **懒加载** | 仅在打开文档时加载 | 减少初始负载 |
| **代码高亮** | 按需 highlight.js | 提升渲染速度 |
| **CDN 资源** | 使用稳定 CDN | 全球加速访问 |
### ⭐ 最佳实践建议
1. **优先使用独立页面**:所有文档查看都应通过 `view-doc.html` 访问
2. **新标签页打开**:使用 `target="_blank"` 保持上下文
3. **添加 noopener**:使用 `rel="noopener"` 防止 window.opener 漏洞
4. **错误处理友好**:显示清晰的错误消息和建议操作
5. **支持书签分享**URL 参数状态可直接分享
### Markdown 样式(保持不变)
```css
.markdown-body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 16px;
line-height: 1.6;
color: #24292f;
}
.markdown-body h1, .markdown-body h2 {
border-bottom: 1px solid #e5e7eb;
padding-bottom: 0.5em;
margin-bottom: 1em;
}
.markdown-body h1 { font-size: 2em; }
.markdown-body h2 { font-size: 1.5em; }
.markdown-body h3 { font-size: 1.25em; }
.markdown-body pre {
background: #1f2937;
color: #e5e7eb;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
}
.markdown-body code {
background: #f3f4f6;
padding: 0.2em 0.4em;
border-radius: 0.25rem;
font-size: 0.9em;
}
.markdown-body table {
border-collapse: collapse;
width: 100%;
}
.markdown-body table th, .markdown-body table td {
border: 1px solid #e5e7eb;
padding: 0.5rem;
}
.markdown-body table tr:nth-child(even) {
background: #f9fafb;
}
.markdown-body blockquote {
border-left: 4px solid #3b82f6;
padding-left: 1rem;
color: #6b7280;
}
.markdown-body a {
color: #2563eb;
}
.markdown-body img {
max-width: 100%;
}
```
## Step 6: 生成本地预览服务器
生成 `server.js` 文件,提供静态文件服务。