Cloudflare 这套东西,很多人还停留在“CDN加速 + DNS解析”的印象里。但如果你把账号下的 Workers、Pages、R2 存储、D1 数据库、KV、Queues 这些服务串起来看,会发现它已经不是一个简单的边缘网络,而是一套能独立支撑完整应用的云原生底座。我在折腾个人项目时,干脆用这整套全家桶搭了一个“Cloudflare OS”——当然,它不是传统意义上跑在虚拟机里的操作系统,而是一个完全跑在 Cloudflare 边缘节点上的个人云平台。
这套体系的本质,是把传统 VPS 上的操作系统、Web 服务器、数据库、对象存储、定时任务、消息队列,全部映射到 Cloudflare 的托管服务上。你不需要买服务器,不需要配 Nginx,不需要手动装 MySQL,所有东西都是以“服务”的形式存在,通过 Wrangler CLI 一键部署。整个过程跑通之后,你会发现运维成本几乎被打到零,而且全球访问速度天然自带 CDN 加成。
这篇文章我会从整体设计思路开始,把每个核心模块的选型逻辑、实际配置过程、踩过的坑和排查方法都写清楚。适合想用 Cloudflare 生态搭建个人项目的人,也适合正在犹豫要不要从传统服务器迁移到边缘平台的人。内容偏实操,我会尽量把每个环节说透,保证你按着走能复现出来。
1. 项目整体设计与思路拆解
1.1 为什么选择 Cloudflare 全家桶而非传统 VPS
先说我自己的真实背景。很早以前我做个人项目,常规套路就是买一台 VPS,装 Ubuntu,配 Nginx,部署 Node.js 应用,再装个 MySQL 或者 SQLite,顺带用 Cron 跑定时任务。这套方案成熟稳定,但有一个绕不开的问题:琐碎的运维工作太多了。系统安全补丁要定期打,Nginx 配置要调,数据库要备份,偶尔还要处理进程挂掉的问题。这些工作本身不难,但非常消耗精力,尤其是你只想快速验证一个想法的时候。
后来我开始尝试 Cloudflare Workers,一发不可收拾。它最吸引我的地方是:你可以不关心服务器在哪、进程怎么保活、流量怎么 load balance,只要把代码推上去,它就在全球 300 多个城市的数据中心帮你跑。这种“丢代码就跑”的体验,和传统服务器完全是两个维度。而当我把 Workers、Pages、R2、D1、KV、Queues 全串起来用之后,我意识到这不就是一个操作系统的等价物吗?
我最终决定用 Cloudflare 全家桶构建个人云平台,核心原因有三点:
第一,冷启动速度和全球节点天然优势。Workers 的冷启动时间通常在毫秒级,而且它运行在离用户最近的数据中心,不需要像传统架构那样,让一个远在硅谷的服务器响应来自国内的请求。对于个人项目的访问体验来说,这是一个肉眼可见的提升。
第二,真正的按量付费,几乎没有闲置成本。我不用再为一个访问量很低的个人应用常年支付一台 VPS 的费用。Workers 免费版每天有 10 万次请求额度,R2 的存储费用也比传统对象存储低得多,D1 有免费额度。对于个人项目来说,很多情况下每月的成本接近零。
第三,类型和生态统一。Cloudflare 的几大服务都是基于 JavaScript/TypeScript 运行时(Workers Runtime),一套代码可以用相同的方式操作 KV、D1、R2、Queues。所有 Binding 的接口风格一致,心智负担比同时维护 Linux 系统命令、MySQL 查询、Redis 命令要小得多。
1.2 整体架构:从“系统层”到“应用层”的映射关系
我把这套平台的架构拆成了几个层级,对应传统操作系统的各个部分:
| 传统操作系统/服务 | Cloudflare 生态对应方案 | 职责说明 |
|---|---|---|
| Linux 进程管理 | Workers / Pages | 运行应用逻辑、处理 HTTP 请求 |
| Web 服务器(Nginx/Apache) | Workers Routes / Pages 自定义域 | 路由分发、静态文件托管 |
| 文件系统(磁盘) | R2 对象存储 | 保存用户上传的图片、视频、附件等 |
| 关系型数据库 | D1(基于 SQLite) | 存储用户信息、业务数据 |
| 键值缓存/会话 | KV | 缓存热点数据、保存 Session |
| 定时任务(Cron) | Workers Cron Triggers | 定期执行数据清理、报表生成等 |
| 消息队列/后台任务 | Queues | 异步处理图片压缩、邮件发送等耗时任务 |
| 配置中心/环境变量 | Workers Secrets / Vars | 管理 API 密钥、环境差异化配置 |
这套架构的核心思路是:把每个传统组件替换为 Cloudflare 的托管服务,并通过 Binding 让这些服务在 Workers 代码中像调用本地函数一样方便。比如你在代码里写env.DB.prepare('SELECT * FROM users'),实际上是在操作远端的 D1 数据库,但你不需要关心连接池、SSL 握手、网络延迟这些问题,Cloudflare 已经把底层基础设施处理好了。
举一个直观的例子。在传统架构中,用户上传头像,你需要先配一个 Nginx 的上传目录,再写一段 Node 代码用fs.writeFile写入磁盘,最后在数据库里记录文件路径。而在 Cloudflare OS 里,你用 R2 的put方法一行代码就能搞定,不需要关心磁盘空间、权限设置、备份策略。这整个从“自己维护磁盘”到“调用对象存储接口”的转变,就是 Cloudflare 全家桶带来的最大价值。
2. 核心基础设施:账号、工具链与运行时环境
2.1 必装工具与账号准备清单
在开始搭建之前,有几样东西是绕不开的。我先列一个最小化清单,然后逐一说明为什么是它们。
- Cloudflare 账号:这是当然的。建议直接注册,不需要预先购买任何套餐,免费版就能覆盖大多数个人项目的需求。
- Node.js 环境:虽然 Workers 并不要求你本地必须装 Node,但 Wrangler CLI 是 Node 包,所以本地需要 Node 16 以上的环境。
- Wrangler CLI:这是整个 Cloudflare OS 的“系统管理命令”,相当于传统系统里的 apt/yum。你用它初始化项目、部署 Workers、管理 D1 数据库、上传 Secrets、查看日志等所有操作。
- Git 与代码编辑器:在本地维护代码的项目文档和版本记录。
安装 Wrangler 非常简单,全局安装也行。我自己习惯用npx每次直接调,避免版本污染:
npm install -g wrangler如果你不想全局装,可以改用:
npx wrangler --version安装完成后,第一步是用wrangler login授权,这个命令会打开浏览器让你登录 Cloudflare 账号,之后 Wrangler 会保存一份 OAuth Token 在本地配置里。
wrangler login提示:如果你在 CI/CD 环境(比如 GitHub Actions)里部署,不方便用 OAuth 登录,建议改用 API Token。在 Cloudflare 后台的“My Profile → API Tokens”页面创建一个具有“Workers Scripts: Edit”权限的 Token,然后通过环境变量
CLOUDFLARE_API_TOKEN传入。
2.2 Worker 运行时的基本约束与语言选型
Cloudflare Workers 不是 Node.js,它是基于 V8 引擎的独立运行时。这意味着你在 Workers 里不能用 Node 的fs、http、crypto等模块,而必须使用 Web 标准 API,比如fetch、Request、Response、crypto.subtle、TextEncoder等。第一次从 Node 迁移过来的人,最容易踩的就是这个坑。
语言选型上,JavaScript 和 TypeScript 都可以。我对新项目的建议是直接用 TypeScript,理由很简单:Workers 的接口类型定义非常完整,在 VSCode 里会有很好的类型提示,尤其是在处理Env接口(描述所有 Bindings 的类型)时,能避免大量拼写错误。而且wrangler init生成的默认模板就支持 TS,不需要额外折腾。
运行时约束方面,有两个数字必须刻在脑子里:
- 请求体大小限制:免费版对请求体的限制是 100 MB,但如果你用 Workers 直接处理上传文件,我建议在应用层加上自己的限制,比如限制到 50 MB,避免不稳定因素。
- CPU 时间限制:Workers 免费版的 CPU 时间限制是每分钟 10 毫秒(付费版是 30 秒)。如果你在请求里做大量计算,比如图像处理、复杂的数据库聚合查询,很容易超时。我的经验是:把耗时任务拆出去,用 Queues 异步处理。
这些约束并不影响个人项目的实现,但它们决定了你要怎么设计方案——换句话说,Cloudflare OS 的“系统元器”不是无限制的 CPU 和磁盘,而是请求数和 CPU 时间。你需要按这个模型去思考问题。
2.3 Wrangler 项目初始化与配置说明
新建一个 Worker 项目非常简单:
npx wrangler init my-cloud-os cd my-cloud-os这个命令会创建一个标准的目录结构,里面有src/index.ts(入口文件)、wrangler.toml(配置文件)、package.json。其中wrangler.toml就是整套系统的“注册表”,所有 Bindings、配置项都在这里声明。
以下是我一个实际项目的wrangler.toml配置示例:
name = "my-cloud-os" main = "src/index.ts" compatibility_date = "2024-09-23" workers_dev = true [[d1_databases]] binding = "DB" database_name = "cloud-os-db" database_id = "your-database-id-here" [[r2_buckets]] binding = "R2" bucket_name = "cloud-os-assets" [[kv_namespaces]] binding = "KV" id = "your-kv-namespace-id" [[queues.producers]] binding = "MY_QUEUE" queue = "my-task-queue" [vars] ENVIRONMENT = "production" API_BASE_URL = "https://api.example.com"这里有几个关键点:
- compatibility_date是必须的配置,它告诉 Cloudflare 你的 Worker 会使用哪个日期的 API 行为。默认会用最新版本,但如果你的代码依赖老 API,建议固定一个日期,避免升级导致行为变化。
- D1 数据库的 database_id是通过
wrangler d1 create创建后返回的,不会是手写的。R2 桶和 KV Namespace 的 id 同理。 - Secrets不应该写在
wrangler.toml里,要用wrangler secret put命令设置,运行时通过env读取。
初始化和配置这块做到位,后续的模块开发才能顺畅。很多人喜欢上来就写代码,结果 Binding 没对上,部署之后各种报错,回头再补配置,反而更费时间。我的习惯是先把所有用到的服务建好、都声明在配置里,再写业务逻辑。
2.4 常用 Wrangler 命令速查
我把平时最常用的 Wrangler 命令整理成一个速查表,方便你开发时对照:
| 命令 | 作用 | 使用频率 |
|---|---|---|
wrangler login | OAuth 登录授权 | 第一次 |
wrangler init <name> | 初始化项目 | 每个新项目一次 |
wrangler dev | 本地开发调试 | 日常 |
wrangler deploy | 部署到 Cloudflare 边缘 | 发布时 |
wrangler d1 create <db-name> | 创建 D1 数据库 | 建库时 |
wrangler d1 execute <db-name> --local --file=./schema.sql | 本地执行 SQL 迁移 | 开发时 |
wrangler d1 execute <db-name> --remote --file=./schema.sql | 生产库执行 SQL 迁移 | 发布时 |
wrangler r2 bucket create <bucket-name> | 创建 R2 桶 | 建桶时 |
wrangler kv namespace create <name> | 创建 KV Namespace | 建 KV 时 |
wrangler secret put <name> | 设置 Secret | 配置环境变量时 |
wrangler tail | 实时查看日志 | 线上调试时 |
wrangler cron get <name> | 查看定时任务触发记录 | 排障时 |
这套命令体系和传统系统管理差不多,熟悉之后效率会很高。
3. 核心模块实现:从零搭建“云端文件系统、数据库、会话与任务调度”
3.1 构建文件存储服务:R2 对象存储接入与应用方式
R2 在 Cloudflare 全家桶里扮演的是“磁盘”的角色。它兼容 S3 API,但有一个让个人开发者激动的好处——没有 egress 流量费。你在传统对象存储里,最怕的是数据往外传的流量费,R2 直接把这项费用免了,对于个人应用来说,这是成本控制上的巨大优势。
我的文件存储模块一般分为三步:创建桶、配置 Binding、在 Worker 里写读写接口。
创建 R2 桶:
wrangler r2 bucket create cloud-os-assets成功后,它会返回桶名,再把它填到wrangler.toml的[[r2_buckets]]配置里,然后我就可以在代码里通过env.R2直接操作了。
下面是我最常用的上传文件的 Worker 代码:
import { WorkerEntrypoint } from '@cloudflare/workers-types'; export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); if (url.pathname === '/upload' && request.method === 'POST') { const formData = await request.formData(); const file = formData.get('file') as File; const fileName = crypto.randomUUID() + '-' + file.name; if (file.size > 50 * 1024 * 1024) { return new Response('文件大小超过 50MB 限制', { status: 400 }); } await env.R2.put(fileName, file); const metadata = await env.R2.head(fileName); return new Response( JSON.stringify({ fileName, size: metadata.size, }), { headers: { 'Content-Type': 'application/json' } } ); } if (url.pathname.startsWith('/file/')) { const key = url.pathname.slice('/file/'.length); const object = await env.R2.get(key); if (!object) { return new Response('File not found', { status: 404 }); } return new Response(object.body); } return new Response('Not found', { status: 404 }); }, };关键点在于,env.R2.put(key, data)可以直接接收来自request.formData()的文件对象,而不需要先读到内存里再转存。R2 在收到数据时直接流转存,不会因为文件大而撑爆 Worker 内存。
实操中有几个细节容易出现坑:
- 文件名规划:不要直接用用户上传的原始文件名,容易遇到重名覆盖和路径穿越问题。我统一用
crypto.randomUUID()生成唯一名称,后缀保留原扩展名。 - 限制文件类型:如果你不想让用户传 exe、html 这类文件,可以在上传时校验
file.type,或者用白名单后缀过滤。例如只允许.jpg|.png|.gif|.webp。 - 大文件分块上传:Workers 免费版请求体上限是 100 MB,所以超过 50 MB 我建议直接拒绝,或者引导用户走 R2 的 presigned URL 直传方式(R2 兼容 S3 的
createPresignedUrl接口)。
R2 本身就是一套“文件系统”,我在这里做的所有操作,和传统系统里mkdir、writeFile的意义是一样的,只是不需要关心磁盘挂载和分区。
3.2 数据库模块:D1 的设计与 SQL 迁移流程
D1 是 Cloudflare 原生的 SQLite 数据库服务。它的定位很清晰:适合中小规模的个人项目,数据量不大,但需要标准 SQL 能力。D1 和传统 SQLite 在语法上没有太大区别,所以如果你熟悉 SQLite,基本零成本上手。
我把 D1 作为整个 Cloudflare OS 的核心数据仓库,用户表、文章表、配置表全放这里。下面是一份建表 SQL 的参考:
CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, nickname TEXT, avatar TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS files ( id TEXT PRIMARY KEY, owner_id INTEGER NOT NULL, file_name TEXT NOT NULL, file_size INTEGER NOT NULL, bucket_key TEXT NOT NULL, uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (owner_id) REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, type TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', payload TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_files_owner ON files(owner_id); CREATE INDEX idx_tasks_status ON tasks(status);如果项目已经有一份 schema,你需要用 D1 的迁移能力。我推荐的做法是:把 SQL 文件放在migrations/目录,按时间命名,比如0001_init.sql、0002_add_posts.sql,然后用命令按顺序执行:
wrangler d1 execute cloud-os-db --remote --file=./migrations/0001_init.sql在 Worker 代码中,D1 的操作方式和 ORM 或数据库驱动略有不同,它提供的是一个基于prepare和bind的链式 API:
const user = await env.DB.prepare( 'SELECT * FROM users WHERE email = ?' ).bind(email).first(); if (!user) { return new Response('用户不存在', { status: 404 }); } const fileList = await env.DB.prepare( 'SELECT * FROM files WHERE owner_id = ? ORDER BY uploaded_at DESC LIMIT 10' ).bind(user.id).all();这里有几个容易踩的坑:
- D1 不支持跨区域主从复制,低延迟但依赖边缘位置。如果你的数据写入压力很大,D1 可能不是最优选择,但对于个人项目来说绰绰有余。
- 不要用
SELECT *频率过高。D1 的按查询计费模式跟扫描的行数有关,尽量只取需要的字段,降低读取开销。 - D1 本地和生产是两套独立数据库。
wrangler d1 execute --local只影响本地模拟环境,--remote才会执行到线上数据库。很多人忘记加--remote,导致本地改完 schema 线上还是旧状态。
个人项目里,D1 搭配 SQL 迁移就是最轻量又足够可靠的数据库方案。我做过一个简单的内容管理后台,几千条数据跑下来,查询速度基本是毫秒级,完全没有性能焦虑。
3.3 会话与会话状态的 KV 解决方案
会话管理是 Web 应用绕不开的一环。在传统方案里,你可能会用 Redis 存 Session,但在 Cloudflare OS 里,KV(Key-Value Store)就是最合适的会话存储。
KV 是一个全球分布式的键值存储,读取延迟很低,适合高频读、低频写的数据。Session 正好满足这个模型:用户登录时写入一次,后续每个请求都要读取校验。
我的做法是用一个随机且长的 Session Token 作为 Key,存用户 ID 和过期时间,并把 Token 种到 Cookie 里:
const sessionId = crypto.randomUUID(); await env.KV.put(sessionId, String(user.id), { expirationTtl: 60 * 60 * 24 * 7 });这样,当用户携带 Cookie 发起请求时,代码只需要一次 KV 查询就能拿到用户身份:
const cookieHeader = request.headers.get('Cookie') || ''; const token = cookieHeader.match(/session=([^;]+)/)?.[1]; if (token) { const userId = await env.KV.get(token); if (userId) { const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(); // 继续业务逻辑 } }KV 的注意点:
- 写入 KV 的 Key 要自带过期时间或主动删除,不然垃圾 Session 会越积越多。用
expirationTtl或expiration参数可以让它自动清理。 - KV 的写入有延迟一致性,刚写入的数据在边缘节点可能不是立刻全局可见。这对 Session 来说问题不大,因为同一个用户通常短时间内在同一区域操作。但我不会用 KV 存金额、库存这类强一致数据。
- KV 的 Value 大小限制是 25 MB,不过我建议只存简单的小对象,别把大 JSON 都塞进去,有读放大问题。
因为 KV 天然是“边缘分布式”,你不需要像传统方案那样,在 Session 失效时去清理 Redis 里的过期 key。KV 自己能搞定。
3.4 定时任务模块:Cron Triggers 与自动化任务设计
个人云平台总会有一些“周期性操作”,比如每天清一次日志、每周汇总一次数据、每月给用户发一封报告邮件。传统做法是 Linux Cron,但在 Cloudflare OS 里对应的是Cron Triggers。
配置方式很简单,在wrangler.toml里声明:
[triggers] crons = ["0 0 * * *", "0 */12 * * *"]然后,在 Worker 代码里监听 Cron 事件:
export default { async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) { if (event.cron === '0 0 * * *') { await runDailyCleanup(env); } else if (event.cron === '0 */12 * * *') { await runDataAggregation(env); } }, };这里有个非常关键的细节:Cron 触发只是给 Worker 一个“信号”,和用户请求是分开的。你可以把定时任务理解为一个不需要前端页面的后台进程,后续如果需要发邮件或处理数据,它和普通fetch请求的代码没有区别。
实践中需要注意:
- Cron 的时区问题:Cron 表达式是基于 UTC 时间计算的,如果你在中国时区,要记得做时区换算。比如北京时间每天凌晨 0 点,对应的 Cron 是
0 0 16 * * *(夏令时还要再加考虑,不过 Cloudflare 的 Cron 只按 UTC 固定执行,所以建议把任何“每天”任务统一成 UTC 来表达)。 - Cron 执行时间窗口和免费版次数限制:免费版的 Cron Trigger 有执行时间限制,如果任务超过几分钟还没跑完,会被强制终止。因此我把重任务都丢给 Queues,而不是直接在 Cron 里同步处理。
- 不要用 Cron 做大量外部 API 调用:Worker 的 CPU 时间限制是硬性的,循环
fetch很多外部接口很可能超时。我一般会把要处理的任务写入 D1 或 Queues,让 Cron 只负责“点亮任务开始”这个动作。
定时任务模块看起来不起眼,但在整个个人云平台里,它承担了“系统守护进程”的角色,很多反人类手动操作都可以自动化掉。
3.5 异步任务队列:Queues 的引入与生产力解放
Queues 是 Cloudflare 全栈中与我个人喜爱度最高的一项。它解决了我在 Worker 里“想跑耗时任务但 CPU 时间不够”的痛点。简单说,它就是一个消息队列:你往队列里塞消息,Cloudflare 会保证消息至少被消费一次,并自动触发消费函数。
先看配置:
[[queues.producers]] binding = "MY_QUEUE" queue = "my-task-queue" [[queues.consumers]] queue = "my-task-queue" max_batch_size = 10 max_retries = 3 dead_letter_queue = "my-dlq"在 Worker 里,你可以这样把任务丢进队列:
await env.MY_QUEUE.send({ taskType: 'sendMail', to: 'user@example.com', payload: { template: 'welcome' }, });任务会异步被消费,消费者函数会自动处理消息:
export default { async queue(batch: MessageBatch<any>, env: Env) { for (const msg of batch.messages) { const { taskType, payload } = msg.body; switch (taskType) { case 'sendMail': await sendMail(payload); break; case 'compressImage': await compressImage(payload); break; } } }, };这里有几个必须掌握的细节:
- 队列消息大小限制是 128KB。如果要传大内容,把值存到 D1 或 R2,再只传 ID 给消息队列。
- 消费失败会自动重试。默认最多重试 3 次,如果三次都失败,消息会进入死信队列(Dead Letter Queue)。死信队列要用一个单独的 Queue 来接收,方便后续人工排查。
- 任务幂等性设计。因为队列保证“至少一次”投递,不保证“只有一次”,所以消费逻辑要千万注意幂等性。比如发送欢迎邮件,你在用户表里加一个
mail_sent_at字段,发送前先检查,避免重复发送。
把耗时操作丢到 Queues 之后,前端接口的响应时间大幅下降,Code 里的await也变得更安心。这个模块相当于传统系统里的 systemd 服务后台任务,非常解放生产力。
4. 应用层路由与核心 API 设计
4.1 用 Hono 构建轻量级 API 路由
Workers 原生路由如果只靠if (url.pathname === '/xxx')堆叠,项目一复杂就很难维护。我的选择是用一个极小且轻量的 Web 框架 ——Hono。它专为边缘运行时设计,API 长得像 Express,但更小更快,体积只有几 KB。
安装 Hono:
npm install hono然后在 Worker 里初始化一个 Hono 应用:
import { Hono } from 'hono'; type Bindings = { DB: D1Database; R2: R2Bucket; KV: KVNamespace; MY_QUEUE: Queue; }; const app = new Hono<{ Bindings: Bindings }>(); app.get('/api/health', (c) => c.json({ status: 'ok' })); app.post('/api/upload', async (c) => { const formData = await c.req.formData(); const file = formData.get('file') as File; // ... 保存 R2 的逻辑 return c.json({ success: true }); }); app.get('/api/files/:id', async (c) => { const id = c.req.param('id'); // ... 查询 D1 + R2 的逻辑 return c.json({ fileId: id }); }); // 对于未匹配的路由 app.notFound((c) => c.json({ error: 'Not Found' }, 404)); export default app;用 Hono 的好处是:
- 类型支持好:可以直接把 Bindings 传给路由,类型安全。
- 中间件生态:比如鉴权中间件,可以用注解形式写在路由前,不用每个接口都手动校验 Session。
- 返回 JSON 极其顺手:
c.json()是每个 API 接口都会用到的功能,代码干净很多。
路由层面设计好了,整个应用层的骨架就出来了。后续只要往 Hono 的大树上加分叉就好。
4.2 基于 D1 + R2 实现一个简单的用户资源管理接口
为了让方案更具体,我拿一个真实场景演示:用户上传头像,并能在列表里看到自己所有的文件。
这里我设计一张files表(前面 schema 里已经建过),每条记录对应 R2 里的一个对象。
上传逻辑:
app.post('/api/files/upload', async (c) => { const user = await authenticateUser(c); // 从 Session 中解析用户 if (!user) return c.json({ error: '未登录' }, 401); const formData = await c.req.formData(); const file = formData.get('file') as File; const bucketKey = `${user.id}/${crypto.randomUUID()}-${file.name}`; await c.env.R2.put(bucketKey, file); await c.env.DB.prepare( 'INSERT INTO files (id, owner_id, file_name, file_size, bucket_key) VALUES (?, ?, ?, ?, ?)' ) .bind(bucketKey, user.id, file.name, file.size, bucketKey) .run(); return c.json({ success: true, fileId: bucketKey }); });获取文件列表:
app.get('/api/files', async (c) => { const user = await authenticateUser(c); if (!user) return c.json({ error: '未登录' }, 401); const { results } = await c.env.DB.prepare( 'SELECT id, file_name, file_size, uploaded_at FROM files WHERE owner_id = ? ORDER BY uploaded_at DESC' ).bind(user.id).all(); return c.json({ files: results }); });实际观察下来,这套逻辑比传统方案清爽得多,没有路径拼接、没有 MIME 映射(R2 默认支持 content-type)、没有磁盘配额校验,数据库和文件存储的 Binding 直接互相对话。
4.3 环境配置与密钥管理
在传统服务器上,环境变量可能是写在.env文件里的,但在 Cloudflare OS 上,正规做法是把环境变量分为两层:
- 非敏感配置(Vars):写在
wrangler.toml的[vars]区块,比如ENVIRONMENT = "production"。它会跟着代码一起提交,适合项目公开配置。 - 敏感配置(Secrets):用
wrangler secret put写入,比如 API Key、数据库 Token。这些值不会出现在源码仓库,也不能通过wrangler.toml查看。
wrangler secret put STRIPE_SECRET_KEY执行后 Wrangler 会提示你输入一个值,输完它就会被加密并绑定到 Worker 运行时,在代码里用env.STRIPE_SECRET_KEY访问。
如果你的项目需要区分本地开发和生产环境,有一个小技巧:本地开发时.dev.vars文件里写本地值,它会被wrangler dev自动读取,但不会影响生产环境。注意.dev.vars不应该提交到 Git,加入.gitignore。
密钥管理做对了,全套系统的安全性就有基本保障。我在一开始就因为图省事把 API Key 写进wrangler.toml,后来推到 GitHub 才发现差点泄露,还好当时用的临时 Key。后来就老实把所有 Secret 都用wrangler secret put管理。
5. 部署流程与线上运维
5.1 本地开发调试:Wrangler Dev 的使用要点
本地开发是每个项目的起步环节。wrangler dev会启动一个本地开发服务器,模拟 Workers 运行时、D1 数据库、R2 存储与 KV。
npx wrangler dev几个值得注意的点:
- 本地数据和线上数据是隔离的。本地默认会用 D1 的本地 SQLite 文件,R2 和 KV 在本地也有本地模拟实现。这意味着你可以在本地放心测试,不影响生产数据。但当你需要排查一个只有线上环境会出现的 Bug 时,用
wrangler dev --remote可以让本地直接连接线上资源。 - 热重载:
wrangler dev默认监听文件变化,改完代码保存就自动重启,这个体验很流畅。 - 模拟绑定:如果项目里注册了 Queue、 D1、R2 等绑定,本地直接就能用,不需要额外配 mock。
5.2 部署到 Global Edge:部署命令与流程
部署是整个链路的最后一步。当你完成了开发、本地测试,直接执行:
npx wrangler deploy部署完成后,Worker 会立即在 Cloudflare 的边缘节点上线。你可以在 Cloudflare 后台的 Workers 页面看到自己的服务名称、路由绑定情况、请求统计和日志。
我个人习惯每次部署前先构建一次 TypeScript 类型检查:
npm run build && npx wrangler deploy这样可以避免线上出现类型错误。
如果你有自定义域名,需要在 Cloudflare 后台把一个域名或子域名绑定到 Worker 上。路径一般是:Workers → 你的 Worker → Settings → Domains & Routes → Add route,填写类似api.example.com/*这样的路由规则,再在 DNS 解析里把api.example.com指向自己的 Worker 的 CNAME 记录,或者在 Workers 后台直接管理 DNS 解析。
部署后立刻需要做的事:
- 用
wrangler tail实时查看请求和错误日志,确认没有抛异常。 - 用
curl打几个核心接口,检查返回状态是否为 200。 - 如果需要,在控制台把 Cron Trigger 的日志和 D1 数据操作情况抽查一遍。
5.3 CI/CD 集成:GitHub Actions 自动化部署
手动wrangler deploy用多了,你会想要更自动化的方案。我把 Cloudflare OS 的部署流程接入了 GitHub Actions,只要 push 到 main 分支就自动触发全线部署。
一个核心的 GitHub Actions 工作流示例:
name: Deploy Worker on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: npm install - run: npm run build - name: Deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} run: npx wrangler deploy在 GitHub 仓库的 Settings → Secrets and variables → Actions 里添加一个CLOUDFLARE_API_TOKEN的 Secret,Token 需要具备 Worker 的编辑权限。这样每次 push 都会自动把最新代码发布到 Cloudflare 边缘。如果同时修改了 D1 数据库的 schema,我会在 Workflow 里加上迁移命令,但要注意确保执行顺序正确,先迁库再部署代码。
CI/CD 做完后,整条线就相当顺畅了。本地写代码 → push → 云端自动部署 → 边缘生效,全程不需要手动登录后台或执行多余的命令。
6. 常见问题与排查技巧实录
6.1 部署时出现的 Runtime 错误与类型不匹配
我遇到的最常见一类问题,是本地测试没问题、部署到线上却报错。这类问题的核心原因往往是代码里用了 Node.js 专属 API,而 Workers 运行时并不支持。
比如,一个新手很容易写出这样的代码:
import fs from 'fs'; const data = fs.readFileSync('/path/to/file', 'utf8');这在 Node 环境下完全没毛病,但部署到 Worker 上直接报错:Cannot find name 'fs'或fs is not defined。解决方式是用 Workers 的 R2、KV 或fetch替代本地文件系统操作。
排查方法:
- 看
wrangler tail输出的堆栈信息,它会明确指出哪一行出了问题。 - 用
npx wrangler dev本地模拟跑一遍,错误信息通常和线上一致。 - 检查代码里有没有引入
node:*模块或非 Web 标准的全局变量。
我的经验是:在 Worker 里写代码,脑子里始终保持一个“沙箱”概念。Worker 的运行时是无服务器的 V8 环境,没有本地文件、没有 socket 直接监听,它的一切能力都是通过 API 暴露出来的。当你习惯了这种模式,这类问题会大幅减少。
6.2 D1 与 R2 的权限/绑定问题
还有一类高频问题,是配置了 D1 或 R2 的 Binding,但代码里访问时发现env.DB是undefined,或者访问时报权限错误。
我做典型排查步骤是:
- 检查
wrangler.toml中的 Binding 名称是否与代码里的env字段完全一致。大小写和命名都是严格匹配的,RD2跟R2是两个东西。 - 检查是否部署到了正确的环境。如果使用了
--env=production之类的多环境部署,配置可能没有同步。 - 查看 Cloudflare 控制台的 Worker Settings 页面,确认 Binding 列表是否出现该服务。如果后台显示没有 Binding,重新部署一次或用
wrangler deploy强制同步。 - 确认 D1 数据库在后台是否确实存在,并且
database_id与你本地wrangler.toml里的一致。
Binding 报错这类问题,90% 都是名称不一致造成的。很多次我顺手把binding = "DB"写成了binding = "database",代码里用env.DB访问,自然就是 undefined。
6.3 KV 和 D1 的数据一致性与本地生产差异
KV 和 D1 在本地模拟环境里的表现,和生产环境有一个很重要的区别:本地环境是单机、同步的,生产环境是分布式、最终一致的。
这意味着你在本地测试时一切正常,但上了生产却发现:刚写入 KV 的数据马上读取却拿不到。这就是因为 KV 的边缘延迟一致性问题。
实操中的解决策略:
- KV 只存短期缓存或 Session,不存关键业务数据,就不用担心强一致问题。
- D1 是强一致的,但它的副本同步也可能会有轻微延迟。如果你在同一请求里刚写入马上查询,建议用同一个 Worker 的调用链路,避免跨区域读到自己没写进去的数据。
- 生产调试时用
wrangler d1 execute --remote查看线上数据,会比你通过 API 查询更直观。
还有一个坑是本地自己有独立的 D1 数据,线上是一个全新的数据库。很多时候本地调试正常,是因为本地库已经被你用wrangler d1 execute --local灌过数据,而线上库是空的。所以上线之后要先确认 schema 是否存在,必要时执行--remote迁移。
6.4 延时、配额限制与免费版额度指北
最后聊聊大家最关心的限额。Cloudflare 免费版对 Workers 的限制:
| 资源项 | 免费额度 |
|---|---|
| 请求次数 | 每天 100,000 次 |
| CPU 时间 | 每天 10 毫秒(免费版) |
| Workers 脚本数量 | 100 个脚本(每个账号) |
| KV 存储 | 1 GB 存储,每日 100,000 次读,1,000 次写 |
| D1 数据库 | 5 GB 数据库,每天 500 万行读 |
| R2 存储 | 10 GB 存储,无出站流量费 |
| Cron Triggers | 免费版支持(执行时间有限) |
对个人项目来说,这些额度相当宽裕。但如果你真碰到配额问题,比如每天 10 万请求不够用,有几个优化方向:
- 用 KV 缓存高频接口:重复请求没必要每次都打到 D1 上。
- 压缩响应体:用
Response的压缩头或预处理,减少出站流量。 - 优化 D1 查询:避免全表扫描,合适的索引能把行读取量大幅降下来。
- 考虑上付费套餐:Workers Paid 是 5 美元/月,CPU 时间提升到 30 秒/天,请求量提升到 1000 万次/天。对正经运营的个人项目很值。
注意:免费版对所有资源都有一个“公平使用”底线,但只要你没有跑大规模爬虫或商业级高并发,个人项目不会有问题。我刚上线的时候每天只有几十个请求,那叫一个浪费。
6.5 日志查看与全链路排错技巧
最后再说排查操作。Worker 的控制台日志不是传统系统里的journalctl那种持续输出,而是按请求粒度记录的。wrangler tail是最有用的命令:
npx wrangler tail它会实时把线上每个请求的URL、status、Error堆栈打印到终端上。
我排查线上问题时,一般这样组合操作:
wrangler tail看实时日志,确认有没有 500 错误或未捕获异常。- 在 Cloudflare 控制台的“Analytics”里看概览数据,比如 CPU 时间、请求数、错误数,判断是持续问题还是偶发问题。
- 如果是数据库/文件存储问题,用
wrangler d1 execute --remote和wrangler r2 object get检查数据是否存在、内容是否符合预期。 - 如果报错信息不够详细,在代码里加
console.log并重新部署,我现在更倾向于通过wrangler dev本地复现。
这套组合拳打下来,基本能定位 95% 的问题。剩余 5% 往往是平台侧的潜在 Bug,我会直接搜索已知问题或提交工单。
个人实操体会
把整套 Cloudflare 生态整合成个人云平台之后,我最大的感受是:你不再需要区分“操作系统”和“云服务”,它们融合在一起了。你写代码时不会去想磁盘还有多少空间、Nginx 要不要重启、数据库备份跑没跑,因为所有这些底层运维都被抽象成了“配置 + API”的形式。
我后来也尝试过把一些独立的小功能拆到不同的 Worker 上,然后用同一个域名路由到它们,效果很理想。尤其是当一个人要维护多个小项目时,每个项目都是独立的 Worker 脚本,互不干扰,部署又极快,这比在一台 Linux 服务器上管理多个 Node 服务要省心得多。
最后分享一个真正的小技巧:如果你要做图片、视频等静态资源的托管,尽量走 R2 + 自定义域名,并配置 Cloudflare 缓存。我一开始把所有资源都直接用 Worker 路由返回,后来发现即使 Worker 很快,但每次请求都会走到 Worker 逻辑,完全没必要。把静态资源改成 R2 直连后,Worker 只负责 API,两边的性能都大幅提升。
如果你正在考虑从传统 VPS 迁移到 Cloudflare 生态,或者想重新整理自己的个人开发架构,我的建议是:别急着把所有东西都塞进去。先从一个小项目开始,比如一个带数据库和文件上传的 API 服务,跑通 Workers + D1 + R2 + KV 这条链路,然后再逐步叠加 Queues、Cron 这些高级能力。等这套流程走顺了,你会发现“云原生优先”的思路真的会改变你写代码的方式。