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