Conversation
📝 WalkthroughSummary by CodeRabbit
Walkthrough新增 Cloudflare 部署工作流(支持手动与自动触发),并在前端实现请求与渲染缓存机制、后端添加 API 缓存头和优化数据库批量查询逻辑,同时补充部署文档和脚本。 Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Cache as 请求缓存层
participant InflightMap as 并发去重
participant Server as API Server
rect rgba(100, 200, 100, 0.5)
Note over Client,Server: 缓存命中流程
Client->>Cache: fetchJsonWithCache(path, ttl)
activate Cache
Cache->>Cache: 检查 publicCache[path]
alt 缓存有效 (未过期)
Cache->>Client: 返回缓存数据
else 缓存不存在或已过期
Cache->>InflightMap: 检查 inflightRequests[path]
alt 请求已在进行中
InflightMap->>Client: 返回相同 Promise
else 无进行中请求
Cache->>Server: fetch(path)
activate Server
Server-->>Cache: response
deactivate Server
Cache->>Cache: 解析 JSON 并存储<br/>(expiresAt = now + ttl)
Cache->>InflightMap: 移除 inflightRequests[path]
Cache->>Client: 返回数据
end
end
deactivate Cache
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
scripts/deploy-cloudflare.mjs (1)
135-145:⚠️ Potential issue | 🔴 Critical修复 Pages 部署目录与
cwd不一致导致的发布失败。
Line 139仍传入"client/dist",但Line 145已设置cwd: clientRoot。这会把目标目录解析成client/client/dist,在多数环境下会直接导致wrangler pages deploy找不到构建产物。建议修复
runStep("部署 Cloudflare Pages 前端", "npx", [ "wrangler", "pages", "deploy", - "client/dist", + "dist", "--project-name", options.pagesProject, "--branch", options.branch, "--commit-dirty=true", ], { cwd: clientRoot });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/deploy-cloudflare.mjs` around lines 135 - 145, 部署命令传入的目标目录与设置的 cwd 不一致导致查找失败:在 runStep 调用中把传给 wrangler pages deploy 的路径从 "client/dist" 修改为相对于 cwd: either "dist" (since cwd is clientRoot) 或直接传入 clientRoot 的绝对 dist 路径 (e.g. path.join(clientRoot, "dist")), 确保 runStep 的 args 中的目标目录与 { cwd: clientRoot } 对齐(参考 runStep(...) 调用及其 wrangler, pages, deploy 参数)。
🧹 Nitpick comments (1)
server/src/storage/db/d1.ts (1)
725-739:searchPosts仍保留 N+1 查询模式,建议后续优化。此处仍使用
Promise.all+getPostTags(post.id)的方式。考虑到搜索结果通常有limit限制(默认 20),影响可控,但如果后续需要进一步优化性能,可复用getPostTagsMap。♻️ 可选优化方案
async searchPosts(query: string, limit = 20): Promise<PostSummary[]> { // ... 查询逻辑 ... + const tagMap = await this.getPostTagsMap(rows.map((post) => post.id)); - return Promise.all( - rows.map(async (post) => ({ + return rows.map((post) => ({ id: post.id, slug: post.slug, title: post.title, excerpt: post.excerpt || "", coverColor: post.coverColor || "", createdAt: post.createdAt, - tags: await this.getPostTags(post.id), + tags: tagMap.get(post.id) || [], pinned: post.pinned, publishAt: post.publishAt, seriesSlug: post.seriesSlug || null, category: post.category || "", - })) - ); + })); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/storage/db/d1.ts` around lines 725 - 739, 当前实现对每个 post 调用 getPostTags(post.id) 导致 N+1 查询;改为先收集所有 post.id,调用已有的 getPostTagsMap(postIds)(或实现一个批量方法),得到一个以 postId 为键的 tags 映射,然后在原来的 rows.map 映射中直接从 tagsMap[post.id] 读取标签(回退到 [] 或空数组),保留原有字段(id, slug, title, excerpt, coverColor, createdAt, pinned, publishAt, seriesSlug, category)以消除 Promise.all + 每项异步查询的 N+1 问题并提高性能。
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@client/src/lib/api.ts`:
- Around line 6-12: publicCache is unbounded and can leak memory; add a pruning
strategy and expire logic: implement a pruneCache(maxEntries: number) (or simple
LRU) that removes oldest or expired PublicCacheEntry items from publicCache,
call pruneCache() after you insert into publicCache inside fetchJsonWithCache,
and ensure entries are removed when expiresAt < Date.now(); also consider
bounding inflightRequests by deleting its key when the request completes to
avoid retaining promises.
---
Outside diff comments:
In `@scripts/deploy-cloudflare.mjs`:
- Around line 135-145: 部署命令传入的目标目录与设置的 cwd 不一致导致查找失败:在 runStep 调用中把传给 wrangler
pages deploy 的路径从 "client/dist" 修改为相对于 cwd: either "dist" (since cwd is
clientRoot) 或直接传入 clientRoot 的绝对 dist 路径 (e.g. path.join(clientRoot, "dist")),
确保 runStep 的 args 中的目标目录与 { cwd: clientRoot } 对齐(参考 runStep(...) 调用及其 wrangler,
pages, deploy 参数)。
---
Nitpick comments:
In `@server/src/storage/db/d1.ts`:
- Around line 725-739: 当前实现对每个 post 调用 getPostTags(post.id) 导致 N+1 查询;改为先收集所有
post.id,调用已有的 getPostTagsMap(postIds)(或实现一个批量方法),得到一个以 postId 为键的 tags 映射,然后在原来的
rows.map 映射中直接从 tagsMap[post.id] 读取标签(回退到 [] 或空数组),保留原有字段(id, slug, title,
excerpt, coverColor, createdAt, pinned, publishAt, seriesSlug, category)以消除
Promise.all + 每项异步查询的 N+1 问题并提高性能。
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 49b049d7-0ffe-408b-99c6-c8f564faa3d5
📒 Files selected for processing (13)
.github/workflows/deploy-cloudflare.ymlREADME.mdclient/src/lib/api.tsclient/src/lib/markdown.tsclient/src/pages/post.tsxpackage.jsonscripts/deploy-cloudflare.mjsserver/package.jsonserver/src/index.tsserver/src/seeds/seed_test_posts.sqlserver/src/storage/db/d1.tsserver/src/storage/db/postgres.tsserver/src/storage/db/turso.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: ESLint 安全扫描
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: ESLint 安全扫描
🧰 Additional context used
📓 Path-based instructions (3)
client/src/pages/**
⚙️ CodeRabbit configuration file
client/src/pages/**: 页面级组件。审查时请关注: 1. 数据加载和错误处理是否完善 2. SEO 相关(页面标题、meta 标签) 3. 导航和路由是否正确
Files:
client/src/pages/post.tsx
server/src/index.ts
⚙️ CodeRabbit configuration file
server/src/index.ts: Hono Workers API 路由总入口。审查时请关注: 1. JWT 认证中间件是否正确保护管理接口 2. CORS 配置是否安全 3. 请求参数验证
Files:
server/src/index.ts
server/src/storage/**
⚙️ CodeRabbit configuration file
server/src/storage/**: 存储适配器层(数据库 + 对象存储)。审查时请关注: 1. 接口实现是否完整(IDatabase / IObjectStorage) 2. SQL 注入防护(参数化查询) 3. 多后端(D1/Turso/PostgreSQL)行为一致性 4. 错误处理和边界情况
Files:
server/src/storage/db/d1.tsserver/src/storage/db/postgres.tsserver/src/storage/db/turso.ts
🔇 Additional comments (19)
server/package.json (1)
12-12: 本地测试数据脚本配置正确,可直接使用。
--local+src/seeds/seed_test_posts.sql的组合与当前仓库结构一致,便于隔离本地测试数据,不会影响远程迁移流程。package.json (2)
15-15:deploy:client的进程退出码透传实现是加分项。当前写法能在子进程失败时正确让 CI/命令行失败退出,避免“假成功”部署。
19-20: workspace 级别的本地测试种子脚本转发设计合理。根脚本到
monolith-server的转发清晰,便于统一从仓库根目录执行本地初始化。.github/workflows/deploy-cloudflare.yml (1)
71-83: 工作流参数拼装与脚本解析契合度高。
--skip-migrate / --skip-server / --skip-client / --api-base的条件拼装方式与部署脚本当前解析逻辑一致,CI 与本地脚本行为保持统一。README.md (1)
147-177: 部署文档补充完整,防呆说明实用。新增的 Actions 触发方式、必需 Secrets 与本地测试种子约束说明都很清晰,能明显降低部署和数据初始化误操作风险。
server/src/storage/db/d1.ts (2)
57-74: 批量标签查询实现正确,有效消除 N+1 问题。
getPostTagsMap方法通过单次inArray查询获取所有文章标签,逻辑清晰。空数组提前返回的处理也很好。
186-200:getPublishedPosts批量优化实现良好。使用
getPostTagsMap替代Promise.all循环查询,显著减少数据库往返次数。server/src/storage/db/postgres.ts (2)
174-191: PostgreSQL 适配器批量标签查询实现与 D1 保持一致。实现模式与其他适配器统一,确保多后端行为一致性。
221-235:getPublishedPosts批量优化实现正确。client/src/lib/markdown.ts (2)
10-11: 新增 C/C++ 语法高亮支持。注册了完整的 C/C++ 语言别名(
c,h,cpp,c++,cc,cxx,hpp),覆盖常见用法。Also applies to: 28-34
85-85: 代码块语言标签显示逻辑改进。引入
displayLanguage确保即使 Highlight.js 不识别该语言,也能在 UI 上显示用户指定的语言名称,用户体验更好。Also applies to: 116-119
server/src/storage/db/turso.ts (2)
41-58: Turso 适配器批量标签查询实现与其他适配器保持一致。三个数据库适配器(D1、PostgreSQL、Turso)的
getPostTagsMap实现完全一致,确保多后端行为统一。
204-218: 批量标签加载优化实现正确。server/src/index.ts (2)
97-97: 文章列表 API 缓存头设置合理。
max-age=60, s-maxage=300, stale-while-revalidate=600的组合对于公开列表是合适的,平衡了性能和数据新鲜度。
115-152: 注意:缓存与浏览量统计的权衡。该路由同时设置了缓存头(Line 120)并递增浏览量(Line 124)。当 CDN 缓存命中时,后端不会收到请求,浏览量统计会偏低。
这是一个合理的性能与精确度的权衡,但建议在文档中说明此行为。如果需要更精确的统计,可以考虑:
- 前端单独调用浏览量统计 API
- 使用 Cloudflare Analytics 或类似服务
当前实现在性能优先的场景下是可接受的。
client/src/lib/api.ts (2)
14-41: 请求去重与缓存实现整体正确。
inflightRequests防止并发重复请求,设计合理finally确保 inflight 清理,即使出错也能正确处理- 缓存命中时直接返回,避免不必要的网络请求
68-74:fetchPosts和fetchPost使用缓存。60 秒 TTL 与服务端
max-age=60保持一致,客户端和 CDN 缓存策略协调良好。client/src/pages/post.tsx (2)
22-29: 渲染缓存设计合理,使用slug:updatedAt作为缓存键确保内容一致性。当文章更新时,
updatedAt变化会生成新的缓存键,避免显示过期内容。与
api.ts类似,此缓存也会无限增长。考虑到用户在单次会话中浏览的文章数量通常有限,影响较小,但建议保持与api.ts相同的清理策略。
90-113: Markdown 渲染缓存逻辑正确。移除了之前的
setTimeout(..., 10)延迟渲染,改为同步缓存查找,代码更清晰:
- 缓存命中时直接使用,无需重新计算
- 缓存未命中时计算并存储
渲染逻辑从异步变为同步,减少了不必要的 UI 闪烁。
| type PublicCacheEntry<T> = { | ||
| expiresAt: number; | ||
| value: T; | ||
| }; | ||
|
|
||
| const publicCache = new Map<string, PublicCacheEntry<unknown>>(); | ||
| const inflightRequests = new Map<string, Promise<unknown>>(); |
There was a problem hiding this comment.
内存泄漏风险:缓存无上限且无清理机制。
publicCache 会随着访问不同文章而持续增长,在长时间运行的 SPA 中可能导致内存问题。
建议添加简单的 LRU 策略或定期清理过期条目:
🛡️ 可选方案:添加缓存大小限制
const publicCache = new Map<string, PublicCacheEntry<unknown>>();
const inflightRequests = new Map<string, Promise<unknown>>();
+const MAX_CACHE_SIZE = 100;
+
+function pruneCache() {
+ if (publicCache.size > MAX_CACHE_SIZE) {
+ const now = Date.now();
+ // 删除过期条目
+ for (const [key, entry] of publicCache) {
+ if (entry.expiresAt <= now) {
+ publicCache.delete(key);
+ }
+ }
+ // 如果仍超限,删除最早的条目
+ while (publicCache.size > MAX_CACHE_SIZE) {
+ const firstKey = publicCache.keys().next().value;
+ if (firstKey) publicCache.delete(firstKey);
+ }
+ }
+}然后在 fetchJsonWithCache 中缓存后调用 pruneCache()。
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| type PublicCacheEntry<T> = { | |
| expiresAt: number; | |
| value: T; | |
| }; | |
| const publicCache = new Map<string, PublicCacheEntry<unknown>>(); | |
| const inflightRequests = new Map<string, Promise<unknown>>(); | |
| type PublicCacheEntry<T> = { | |
| expiresAt: number; | |
| value: T; | |
| }; | |
| const publicCache = new Map<string, PublicCacheEntry<unknown>>(); | |
| const inflightRequests = new Map<string, Promise<unknown>>(); | |
| const MAX_CACHE_SIZE = 100; | |
| function pruneCache() { | |
| if (publicCache.size > MAX_CACHE_SIZE) { | |
| const now = Date.now(); | |
| // 删除过期条目 | |
| for (const [key, entry] of publicCache) { | |
| if (entry.expiresAt <= now) { | |
| publicCache.delete(key); | |
| } | |
| } | |
| // 如果仍超限,删除最早的条目 | |
| while (publicCache.size > MAX_CACHE_SIZE) { | |
| const firstKey = publicCache.keys().next().value; | |
| if (firstKey) publicCache.delete(firstKey); | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@client/src/lib/api.ts` around lines 6 - 12, publicCache is unbounded and can
leak memory; add a pruning strategy and expire logic: implement a
pruneCache(maxEntries: number) (or simple LRU) that removes oldest or expired
PublicCacheEntry items from publicCache, call pruneCache() after you insert into
publicCache inside fetchJsonWithCache, and ensure entries are removed when
expiresAt < Date.now(); also consider bounding inflightRequests by deleting its
key when the request completes to avoid retaining promises.
Summary
slug + updatedAtto improve repeat navigation without serving stale content/api/postsand/api/posts/:slugso Cloudflare can serve article list/detail responses faster