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:
@@ -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` 文件,提供静态文件服务。
|
||||
|
||||
Reference in New Issue
Block a user