渡劫code 的整个后端跑在 Cloudflare 边缘网络上,没有一台传统服务器。这篇文章把 Workers / KV / D1 / R2 / Durable Objects 五种原语的分工讲清楚:它们各自解决什么问题、彼此如何协作,以及我们在真实项目里的绑定方式。选型结论先行——无状态走 Worker,配置与限流走 KV,关系数据走 D1,大文件走 R2,需要单点协调的有状态逻辑走 DO。

为什么是全边缘架构

个人项目最贵的成本不是机器,是运维时间。传统 VPS 方案要操心系统更新、备份、证书续期、数据库主从,还要防着国内访问绕路。Cloudflare 的方案把这一切换成了「绑定即用」:请求在离用户最近的 PoP 被执行,存储与计算同厂原生打通,免费额度足够单人项目跑到上市。国内可达性则由 三端同步架构 兜底——静态镜像走 Gitee Pages,边缘 API 由 Workers 直服,不做地域跳转。

五种原语各司其职

原语数据模型一致性渡劫code 中的用途
Workers无状态函数—全部 API 入口(Hono 路由)、鉴权中间件
KVKey-Value最终一致(约 60s 全球生效)限流计数、特性开关、IndexNow key
D1SQLite 关系库强一致(单库单写点)评论、点赞、浏览、账号数据
R2对象存储强一致用户上传的文件与项目快照
Durable Objects单例有状态对象强一致(串行)WebSocket 房间、Shell 会话

wrangler.toml:一次声明所有绑定

所有原语通过 wrangler.toml 声明式绑定,Worker 代码里通过 env 访问,不需要任何 SDK 客户端初始化:

name = "dujiecone"
main = "worker/index.ts"
compatibility_date = "2026-10-01"

[[kv_namespaces]]
binding = "CACHE"
id = "<kv_namespace_id>"

[[d1_databases]]
binding = "BLOG_DB"
database_name = "blog"
database_id = "<d1_database_id>"

[[r2_buckets]]
binding = "ASSETS"
bucket_name = "dujiecode-assets"

[[durable_objects.bindings]]
name = "SHELL_ROOM"
class_name = "ShellRoom"

[[migrations]]
tag = "v1"
new_classes = ["ShellRoom"]

DO 的 class 必须出现在 [[migrations]] 里才会被实例化,这是最容易踩的一步——漏配的表现为运行时 No such class。

Workers:无状态入口层

Worker 本身不存任何数据,只做路由、鉴权、编排。我们用 Hono 组织路由,统一信封 {code: 0, data} 返回(契约细节见评论系统实战):

import { Hono } from 'hono';

const app = new Hono<{ Bindings: Env }>();

app.get('/api/blog/stats', async (c) => {
  const slugs = (c.req.query('slugs') || '').split(',').filter(Boolean);
  const db = c.env.BLOG_DB;
  // D1 prepared statement + batch,单次往返
  const stmts = slugs.map((s) =>
    db.prepare('SELECT slug, views, likes FROM stats WHERE slug = ?').bind(s));
  const rows = await db.batch(stmts);
  return c.json({ code: 0, data: rows.map((r) => r.results).flat() });
});

KV:最终一致的配置与限流桶

KV 的定位是读多写少、可容忍秒级延迟的数据。限流计数器是典型场景:评论接口按访客指纹计数,expirationTtl 到点自动清零,不用自己写过期逻辑:

async function rateLimit(env: Env, key: string, limit = 2, windowSec = 60) {
  const k = `rl:${key}`;
  const used = Number((await env.CACHE.get(k)) || '0');
  if (used >= limit) return false;
  await env.CACHE.put(k, String(used + 1), { expirationTtl: windowSec });
  return true;
}

注意 KV 的最终一致性:写入后最长约 60 秒才全球生效,且同一 key 的并发写可能互相覆盖。所以精确计数(浏览量、点赞数)必须放 D1,KV 只做「粗略限流」这类丢几次也无所谓的场景。

D1:关系型数据的家

凡是需要事务、唯一约束、复杂查询的数据都进 D1。评论系统就是活样本:UNIQUE(slug, visitor) 保证 UV 级浏览统计不重复,UNIQUE(visitor, comment_id) 保证一人一赞,楼中楼靠 parent_id 自关联。写入走 batch() 把多条语句合成一次往返,且任一条失败整体回滚——相当于隐式事务。更完整的 Schema 与坑位见 D1 边缘 SQLite 实战。

R2:S3 兼容的对象存储

R2 对个人项目最大的吸引力是零出站流量费,且 API 与 S3 兼容,老代码改个 endpoint 就能迁。在 Worker 里直接走绑定,连签名都不用:

// 上传:POST /api/files  body: multipart/form-data
app.post('/api/files', async (c) => {
  const key = `uploads/${crypto.randomUUID()}`;
  await c.env.ASSETS.put(key, c.req.body(), {
    httpMetadata: { contentType: c.req.header('content-type') || 'application/octet-stream' },
  });
  return c.json({ code: 0, data: { key } }, 201);
});

Durable Objects:有状态协调者

DO 是 Cloudflare 最被低估的原语:每个对象是全局唯一的单例,拥有自己的存储,请求被串行化执行,天然就是分布式锁。渡劫code 的实时通道——WebSocket 房间与 Shell 会话——全部由 DO 承载,每个房间一个对象实例:

export class ShellRoom {
  private sessions: Map<WebSocket, string> = new Map();

  async fetch(req: Request) {
    const [client, server] = Object.values(new WebSocketPair());
    this.sessions.set(server, crypto.randomUUID());
    server.accept();
    return new Response(null, { status: 101, webSocket: client });
  }
}

配合 hibernation API,空闲房间不烧 CPU 时间;连接恢复时从 storage 重建状态。这套设计的完整取舍见 AI 编程工作台架构。

部署与调试:wrangler 日常

本地开发用 wrangler dev,它会在本地模拟全部绑定:KV 落到本地存储、D1 起本地 SQLite、DO 起本地实例,断网也能写代码。怀疑绑定在边缘上的行为不一致时,加 --remote 直连真实环境复现;线上排错靠 wrangler tail 实时看日志,用 --status error 只保留错误:

npx wrangler dev                    # 本地全绑定模拟
npx wrangler dev --remote           # 本地代码 + 真实边缘绑定
npx wrangler tail --status error    # 实时错误日志
npx wrangler deploy                 # 发布(生产由 Actions 执行)

发布一律走 GitHub Actions 而非本地 wrangler deploy,保证每次上线都有 commit、日志与回滚点可回溯——部署纪律见三端同步进阶。另外 compatibility_date 不要频繁改动:它决定运行时行为版本,升级前先在预览部署里跑一遍测试。

决策速查表

问题答案
用户会话放哪?短周期 JWT + DO/KV 存储,不放 D1
浏览量精确统计?D1 UPSERT,KV 会丢写
接口限流?KV 计数器 + expirationTtl
大文件上传?R2,Worker 只做预签名/中转
多人实时协作?DO WebSocket,单房间单实例

小结

全边缘架构的本质是把「状态」按一致性和延迟要求分层:越强一致、越靠近数据库的层越贵也越该少用。Workers 负责编排,KV/D1/R2 各管一段,DO 收拾所有「必须只有一个」的场景。五件套用对了,个人项目也能拥有生产级的全球架构——而且月账单是 0。