Skip to main content

目录结构

双通道架构

Qingyu’Blog 的核心设计是双通道架构——同一份代码支持两种运行模式: 云端模式下,请求分两条路径落地:

前端架构

前台 (app.js)

  • 真实路径路由:无 hash,基于 history.pushState;file:// 本地打开自动退化为 #/ 内部路由
  • Markdown 渲染:内置解析器,支持代码高亮、TOC 生成
  • 评论系统:云端 D1 / 静态 localStorage,支持嵌套回复;重复发送拦截(后端 409 + 前端透传原因)
  • 留言板:/guestbook 双分区(留言 / 优化方案),复用评论管道
  • 文章加密:PBKDF2 + AES-GCM 端到端加密
  • 搜索:实时匹配标题/标签/摘要,结果展示关键字所在完整句子并高亮
  • 主题与强调色:深色/浅色两档(qingyu.theme)× 4 种强调色 terra(赭橙,默认)/ indigo(黛蓝)/ bamboo(竹青)/ dusk(凝夜紫),存 qingyu.accent;桌面为图标按钮 + 色板弹层,手机端为原生下拉
  • 背景动画:首页春夏秋冬素描粒子动画,桌面默认开启、触屏默认关闭,开关存 qingyu.bgAnim;脚本按需加载
  • 多语言:5 种语言,自动识别浏览器语言(桌面 🌐 图标弹层 / 手机原生下拉),选择存 blog.locale
  • AI 文章摘要:文章页一键生成内容摘要(单篇缓存 30 天),AI 不可用自动隐藏入口
  • 全站音乐播放器:右下角悬浮音符按钮(缩进窗口外露出一点,悬停/点击滑出)→ 弹出面板(曲目信息 / 进度 / 播放控制 / 音量 / 播放列表);自动连播、记忆上次曲目与进度、音量持久化;无音乐隐藏、后台路由收起(独立模块 music-player.js,通过 qy:route 事件同步路由)

首屏弱网兜底(v2.10.93 / v2.10.97)

首屏关键资源(config / posts / i18n / app / style)在 index.html 中显式 preload,不再靠脚本探测器猜时机:
实测排查结论:站点「打开慢」的主因是线路丢包,不是项目代码。首屏兜底是为了在丢包时优雅降级,而非掩盖性能问题。

资源按需加载

首屏只加载 config.min.js、posts.min.js、i18n.min.js、app.min.js 与 style.min.css,其余重资源全部延后:

后台 (admin.js)

完整的 SPA 管理界面(懒加载 admin.min.js / admin.min.css):
后台编辑器已提供分类、发布状态、作者、SEO 与文章加密 UI;预览分享链接、版本恢复、导入导出与静态站导出均在后台完成。

后端架构

API 核心 (api-core.js)

所有 API 逻辑集中在单个文件中,被两处复用:
  1. Cloudflare Pages Functions:functions/api/ 下的路由文件
  2. Cloudflare Workers:worker.js 的路由分发

Workers 入口 (worker.js)

worker.js 负责三件事:
  1. API 路由分发:把 /api/* 请求交给 api-core.js 或 _lib/ 中的处理函数
  2. 静态资源服务与 SPA 回退:静态文件命中 404、请求方法为 GET/HEAD 且路径不含扩展名时,回退为 index.html(由前端按 pathname 渲染);带扩展名的真实文件不会回退
  3. 未知 /api/* 一律 JSON 404:/api 与 /api/ 开头且未命中任何路由的请求返回 {"ok":false,"error":"Not Found"},绝不回退 HTML,避免调用方拿到 HTML 再去 JSON.parse 时报出难懂的解析错误

Pages 侧兜底 (functions/api/[[path]].js)

Pages 部署下 public/_redirects 末尾的 SPA 回退规则 /* /index.html 200 会把「没有对应函数文件」的 /api/* 路径兜成 HTML 200。catch-all 动态路由 [[path]].js 因此承担 Pages 侧兜底:
  • 已知的具体路由(KNOWN_ROUTES,与 functions/api/** 一一对应)显式放行,不吞真实接口
  • 其余未知 /api/* 返回 JSON 404(带 hint 说明)
  • OPTIONS 预检直接回 CORS 头,避免浏览器把「无 ACAO 的 404」报成 CORS 错误

缓存策略

Feed / Sitemap 动态端点:云端模式下 /feed.xml、/sitemap.xml 与 /api/feed.xml、/api/sitemap.xml 均实时从 D1 当前文章生成,文章增删改查后自动更新,不再需要手工维护 public/feed.xml / public/sitemap.xml。D1 空表且尚未配置过管理员时,readPosts() 会回退到静态 posts.js 示例文章;一旦写过 admin_auth(视为作者已接管),空表就返回空,避免删光文章后示例内容「复活」。
按标签清缓存:只读响应带 Cache-Tag(posts / feed / sitemap / post:<id> / stats:<id>)。写操作后由 purgeTags() 调用 Cloudflare Cache Purge API 按标签清除,让「发布即生效」。需要 CF_ZONE_ID + 具备 Zone·Cache Purge 权限的 Token(工作流会把它写成 Worker 的 CF_API_TOKEN);未配置时不清缓存,依赖 s-maxage 自然过期,访客侧最长延迟 60 秒。
管理端缓存穿透:GET /api/posts 带 s-maxage=60 边缘缓存,删除/发布文章后管理后台会以 api/posts?_=<时间戳> 请求穿透缓存立即取到最新列表(配合前台内存同步,删除无需刷新网页即可全线生效)。

KV 的真实用途

KV(绑定名 BLOG)只用于限流计数、去重标记与 AI 缓存,不再存文章、feed 或 sitemap 缓存:
KV 未绑定时频控会静默失效(服务端打印告警);AI 的全站每日额度计数采用 fail-closed,KV 不可用时直接拒绝生成。

音乐(R2 直传)

音乐走浏览器直传 R2 通道:Worker 只签发预签名 PUT URL,音频文件不经过 Worker(规避请求体上限与带宽计费),元数据存 D1 music 表。删除时 Worker 侧签名向 R2 发起 DELETE 再删 D1 行。 目标桶由 musicStorage() 决定:R2_BUCKET 与 R2_PUBLIC_BASE 都非空时写音乐桶,否则回退到 R2_MEDIA_BUCKET + R2_MEDIA_PUBLIC_BASE。详见音乐。

媒体图片(R2 直传)

后台媒体库的图片上传与音乐同款:POST /api/media/upload-url 签发预签名 PUT URL,浏览器直传 R2 媒体专用桶(R2_MEDIA_BUCKET),D1 media 表只存元数据(url 为 R2 公开地址)。删除时若 url 属于本站媒体公开域名则先删 R2 对象再删 D1 行;接口仅接受 http/https 链接(不接受 base64 内嵌)。详见媒体。

静态资源缓存与安全响应头

worker.js 会给所有静态响应统一注入安全响应头,并按路径类型设置缓存:
Workers 部署下的已知限制:Workers 的静态资源会在 Worker 代码之前被平台直接返回,因此上表中 ?v= 资源的强缓存规则实际未生效,线上返回的是 public, must-revalidate, max-age=0。让缓存逻辑先执行需要 run_worker_first = true,该字段要求 wrangler ≥ 3.94(wrangler 4.x 在本项目部署会失败),因此暂无法修复,已在主仓库 wrangler.workers.toml 与守卫测试中写明原因。
安全响应头清单(securityHeaders(),API 响应与静态响应共用):X-Content-Type-Options: nosniff、Referrer-Policy: strict-origin-when-cross-origin、X-Frame-Options: SAMEORIGIN、Cross-Origin-Opener-Policy: same-origin、Content-Security-Policy(含 object-src 'none'、frame-ancestors 'none'、form-action 'self')。
Pages 纯静态路径下 HTML 由 Pages 直接返回、不经过任何函数,因此同样的安全头在 public/_headers 中重复声明了一份(见部署到 Cloudflare Workers)。缓存与安全的完整说明见安全概览。

资源发现

站点为主流 AI/Agent 抓取提供了三个发现入口: public/index.html 中通过 <link rel="ard"> 与 <link rel="ai-catalog"> 声明;public/_headers 为这两个 JSON 设置了 Access-Control-Allow-Origin: * 与 1 小时缓存。