一个跑在 Cloudflare 边缘网络上的社区论坛 — 从数据建模到安卓 / Windows 双端交付
官网:进入
一、技术栈:整站零服务器
云智论坛(yunzhi.dev)没有一台 VPS,没有 Nginx,没有 MySQL 进程要盯。整套系统压在一个 Cloudflare Worker 上,数据落在 D1,文件走 R2,前端由 Static Assets 直接分发。
| 层 | 选型 |
| 计算 | Cloudflare Workers(单 Worker 承载全部 API + 路由) |
| 数据库 | D1(SQLite),wranger migrations 管理表结构 |
| 对象存储 | R2 — 用户上传的图片 / 音频 / APK / 安装包 |
| 前端 | 原生 JS 单页 + Static Assets,无框架、无构建步骤 |
| 消息 | Web Push(自行实现 RFC8291 + RFC8292) |
为什么不用框架?Worker 冷启动 + 打包体积是硬约束。原生 JS 的 app.js 约 5000 行,但首屏只有一个文件,无 hydration、无虚拟 DOM 开销,在低端安卓机上滑动列表比 React SSR 更跟手。
二、功能地图
- 富文本编辑器:标题 / 列表 / 引用 / 代码 / 链接
- 图片、音频、B 站、YouTube、视频直链嵌入
- 上传前 canvas 压缩(最长边 1600px,JPEG 0.85)
- 40 个版块、标签体系、近 7 天趋势榜
- 评论盖楼、作者置顶(单帖上限 1 条)
- 软删除:无子回复的已删评论直接不返回
- 草稿自动保存 + 草稿箱分页检索
- 注册 / 登录 / 会话,per-user 资料缓存
- 关注关系 + 推荐算法(偏好版块加权)
- 私信(D1 原子限流 10 条 / 分钟)
- 站内通知 + Web Push 系统通知
- 8 个社交绑定:官网 / 微信 / X / B站 / YouTube / Reddit / TG / WhatsApp
- 微信二维码弹窗、打赏配置
- 响应式网页 + PWA(可安装、离线壳)
- Android:WebView 壳 2.7MB,原生通知轮询
- Windows:Electron + NSIS 安装包 75MB
- 桌面端托盘未读、关闭到托盘、SPA 内跳转
- 统一版本接口
/api/app/version
- 工具箱:11 个自有 / 关联应用统一入口
- 6 套主题 + 6 种语言(含 RTL 阿拉伯语)
- AI 助手「云小智」:12 个工具、可自主建版块
- API 密钥 + 外部接入 + 公开 MCP 端点
- Web Push 开关、通知偏好
三、八个关键实现细节
私信需要「每用户每分钟 10 条」。先试内存 Map —— 跨 isolate 不共享,12 次测试全部放行。再试 Cloudflare 原生 Rate Limiting binding —— 免费计划下 limit() 恒定返回 success,等于没限。
最终落在 D1 单条原子 SQL,靠 ON CONFLICT ... RETURNING 拿到计数,天然并发安全。实测第 1-10 次通过、第 11 次 429。
Workers 上没有成熟的 Push 库可用(Node 的 web-push 依赖 crypto 原生模块)。于是按 RFC8291 手写 payload 加密(ECDH + HKDF + AES128GCM),按 RFC8292 手写 VAPID ES256 JWT 签名。
收益:用户关掉页面也能收到通知,且不依赖任何第三方推送服务。Mac Safari 与 iOS PWA 实测可用。
关注推荐原来要依次查:版块偏好 → 二度社交 → 已关注 → 候选用户 → 版块统计 → 新用户。六次 D1 往返串起来,慢网下用户看到的是十几秒的转圈。
把五个无依赖查询合并进 Promise.all,只有版块统计依赖候选列表。响应时间从 0.26-0.35s 降到 0.15-0.25s,超时率大幅下降。
首屏推荐结果用 caches.default 缓存 60 秒,key 是 https://follow-rec.internal/<uid>。只有 offset=0 走缓存,翻页不缓存,避免脏数据。
关键是反向失效:关注 / 取关接口里直接 cache.delete 该用户 key,用户操作后不会看到 60 秒前的旧状态。
侧栏渲染函数末尾原本是 catch { hideRightbar(); }。任何一个子区块超时,整个右栏就消失 —— 用户看到的是「侧栏加载失败」,毫无线索。
改成三层防护:① 外层 catch 保留框架,渲染错误卡片 + 重试按钮;② 子区块用 Promise.race 独立超时,只降级自己;③ 两个请求并行发出,超时不叫「失败」而显示「加载较慢」,数据到了自动填充。
Windows 安装包 75MB,安卓 APK 2.7MB,都不适合塞进 Static Assets。Worker 加了一条 /dl/:name 路由,直接从 R2 流式返回。
必须支持 HEAD 和 Range(206)——否则浏览器断点续传、下载器探测都会失效。还有一个坑:路由要在 run_worker_first 和 fetch 入口分发两处都声明,只改一处会被静态资源先吃掉。
「点了选图,选完没反应」排查了两个方向,两个都是真问题,缺一不可:
WebView 侧必须重写 onShowFileChooser,用 params.createIntent() 并显式加 FLAG_GRANT_READ_URI_PERMISSION;网页侧 input[type=file] 必须先挂到 DOM 再 click()(iOS Safari 会静默忽略游离节点)。
安卓端用 WebView 加载 https://yunzhi.dev,Windows 端 Electron 用 win.loadURL() 远程加载。这意味着网页改版不需要重新发版。
客户端只补网页做不到的三件事:系统级通知(轮询未读 + DownloadManager 下载 APK)、托盘常驻与未读徽标、以及在 Electron 里 serviceWorker.ready 永久 pending 时的 Web Push 降级。
四、踩过的坑(真实教训)
| 现象 | 根因 |
| 资料保存后像是没生效 | /api/auth/me 有 per-user 60 秒缓存,写操作后必须主动失效 |
| 移动端弹窗底部按钮点不到 | 发帖 FAB 的 z-index 9999 高于遮罩层 100,整层被压在按钮上 |
| 删掉的评论还留「已删除」 | 列表 SQL 漏了 is_deleted = 0 过滤 |
| AI 老说「版块不存在」 | 报错里没有候选清单,模型只能反复猜;补 list_categories 工具 + 6 级模糊匹配 |
| APK 内点下载链接无反应 | WebView 不处理 apk MIME,必须 setDownloadListener 交给系统 DownloadManager |
| Electron 里推送开关卡死 | navigator.serviceWorker.ready 在 Electron 永远 pending,要先短路判断环境 |
五、这套架构适合谁
- 中小型社区、内部论坛、产品反馈区
- 不想维护服务器、希望全球访问都快
- 需要网页 + App 双形态但预算有限
- 团队小、迭代频繁、要快速试错
- 需要复杂事务、跨表强一致的重业务
- 大量长连接 / 实时推送(WebSocket)
- 全文检索要求高(D1 只有 LIKE / FTS5 有限支持)
- 数据必须落在境内的场景
回头看,这个项目最有价值的部分不是功能数量,而是把「边缘计算 + 低成本 + 多端」这条路线走通了一遍:从 D1 原子限流、自研 Web Push,到 Electron 交叉打包和在 Linux 上打 Windows 安装包。每一块单拎出来都能复用。
评论区
0 条评论 · 评论需审核通过后显示
发表评论