ByteNoteByteNote

字节笔记本

2026年8月28日

Cloudflare R2 + D1 做 OTA 更新

API中转
¥120

做桌面端或移动端壳应用时,发一版安装包成本不低。Electron、React Native(尤其是走 CodePush / 热更新那一类流程)都希望「改完 JS 或静态资源,用户下次启动就能拿到」,不必再走一遍商店审核。Cloudflare 的 R2 存文件、D1 存版本元数据,再加一个 Worker 当检查更新的 API,整条链路可以压到几乎零固定成本,延迟也还行。

下面按「存什么、查什么、客户端怎么接」拆一遍,方便你直接落到项目里。

这条链路各自干什么

组件角色
R2放更新包(zip、jsbundle、asar 差量包等),对象存储,按量计费,出站走 Cloudflare 网络
D1存版本表:渠道、版本号、文件 key、校验和、是否强制更新、发布时间
Workers对外暴露 GET /update/check,读 D1、比对客户端版本,返回下载 URL 或「已是最新」

R2 本身没有「按版本查询」的语义,所以版本真相放在 D1;Worker 只做只读查询和签名/鉴权(如果需要),别把写库权限散到客户端。

官方入口可以先看 R2D1Workers

D1 里建议怎么建表

一个够用的最小表大致是:

sql
CREATE TABLE app_releases (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  app_id TEXT NOT NULL,          -- 例如 com.example.app
  channel TEXT NOT NULL DEFAULT 'stable',  -- stable / beta
  platform TEXT NOT NULL,        -- electron | ios | android
  version TEXT NOT NULL,         -- semver,如 1.4.2
  build INTEGER NOT NULL,        -- 单调递增的内部号,便于比较
  object_key TEXT NOT NULL,      -- R2 里的 key
  sha256 TEXT NOT NULL,
  force_update INTEGER NOT NULL DEFAULT 0,
  notes TEXT,
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX idx_releases_lookup
  ON app_releases (app_id, channel, platform, build DESC);

客户端上报:app_idchannelplatform、当前 build(或 version)。Worker 查该渠道下 build 更大的最新一条;没有就返回 upToDate: true

R2 的对象命名

建议 key 带版本,避免覆盖历史包:

releases/{app_id}/{platform}/{channel}/{version}/update.zip

上传可以用 Wrangler:

bash
npx wrangler r2 object put my-ota-bucket \
  --file ./dist/update.zip \
  --key releases/com.example.app/electron/stable/1.4.2/update.zip

然后在 D1 插入对应行(object_keysha256build)。发版脚本里「上传 → 写库」做成原子步骤:上传成功后再 insert;失败不要写库。

Worker:检查更新 API 示例

绑定 R2 与 D1 后,核心逻辑可以长这样(示意):

js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname !== "/update/check") {
      return new Response("not found", { status: 404 });
    }

    const appId = url.searchParams.get("app_id");
    const channel = url.searchParams.get("channel") || "stable";
    const platform = url.searchParams.get("platform");
    const build = Number(url.searchParams.get("build") || 0);

    if (!appId || !platform) {
      return Response.json({ error: "missing params" }, { status: 400 });
    }

    const row = await env.DB.prepare(
      `SELECT version, build, object_key, sha256, force_update, notes
       FROM app_releases
       WHERE app_id = ? AND channel = ? AND platform = ? AND build > ?
       ORDER BY build DESC LIMIT 1`
    )
      .bind(appId, channel, platform, build)
      .first();

    if (!row) {
      return Response.json({ upToDate: true });
    }

    // 公开读:用自定义域名或 R2 公开桶;私有桶则签发短时 URL
    const downloadUrl = `${env.R2_PUBLIC_BASE}/${row.object_key}`;

    return Response.json({
      upToDate: false,
      version: row.version,
      build: row.build,
      sha256: row.sha256,
      forceUpdate: !!row.force_update,
      notes: row.notes,
      url: downloadUrl,
    });
  },
};

wrangler.toml 里挂上:

toml
[[d1_databases]]
binding = "DB"
database_name = "ota-meta"
database_id = "<your-d1-id>"

[[r2_buckets]]
binding = "ASSETS"
bucket_name = "my-ota-bucket"

如果更新包不能公开,用 R2 预签名 URL 或 Worker 里 env.ASSETS.get() 再流式返回,并限制 Referer / 带一次性 token。

客户端怎么接(Electron / RN)

两端套路一样:启动或定时调 GET /update/check,有新版本就下载 → 校验 sha256 → 替换本地资源 → 重启或热切换。

Electron 常见做法:

  1. 主进程在 ready 后请求检查接口。
  2. 下载到 userData 临时目录,校验哈希。
  3. 解压到 app.getPath('userData')/updates/<build>/,下次启动从该目录加载渲染资源;或配合 electron-updater 只把「元数据源」换成你的 Worker(完整安装包仍可走 GitHub Releases,热更新走 R2)。

React Native

  • 若只更 JS bundle:把 jsbundle 放 R2,检查接口返回 URL,用自家小模块下载后 CodePush-式替换(注意 iOS/Android 对热更新的政策差异)。
  • 若更原生层:这条链只适合资源/JS;完整 IPA/APK 仍走商店或企业分发。

无论哪端,强制更新时客户端应阻塞进入主界面,只留「去更新」;非强制可提示后继续用旧版。

发版流水线建议

  1. CI 打出 update.zip,计算 sha256,build 取 CI 流水号或 git 计数。
  2. 上传 R2 → INSERT D1(同一 job,失败回滚:删掉刚传的 object)。
  3. 用 curl 打一遍检查接口,确认新 build 能返回。
  4. 灰度:先写 channel=beta,内部验完再插一条 stable(或改 channel)。
bash
curl -s "https://ota.example.com/update/check?app_id=com.example.app&platform=electron&channel=stable&build=100"

成本与注意点

  • R2 存储和 A 类操作很便宜,适合放历史包;记得定期清掉极旧 build,避免桶无限涨。
  • D1 按行读写,版本表很小,几乎可忽略。
  • Worker 免费额度对「启动时检查一次」通常够用;高频轮询就加缓存头(Cache-Control)或 Durable Object 限流。
  • 安全:检查接口可以匿名,但写 D1 / 上传 R2 必须只在 CI(API Token),不要把账号密钥打进客户端。
  • 完整性:客户端务必校验 sha256,不要只信 HTTPS。
  • Electron 若更新主进程二进制,热更新解决不了,仍需整包安装程序。

小结

R2 管文件、D1 管「哪个版本该推给谁」、Worker 管查询。对独立开发者做 Electron 壳或 RN 热更新资源,这条组合足够简单,也容易和现有 CI 接上。先跑通「一个 app、一个渠道、手动上传 + 手写一条 D1」,再把上传和 insert 丢进 CI,比一上来上完整差量协议要稳。

分享: