我用个人项目这几年有个很深的感触:业务逻辑本身往往不难,真正消耗精力的全是那些“围绕在代码旁边的事”。服务器要不要续费、证书是不是快过期了、配置又改崩了、半夜被报警叫醒去查日志……有一段时间我甚至觉得,独立开发者的宿命就是半个运维。直到我把“服务”这个概念从一台机器上彻底挪开,用 Cloudflare 的全家桶重新组装了一遍自己的业务,还顺手给这套方法起了个代号叫 cloudflare-os。
我说的 os 不是传统意义的操作系统,而是一个组合出来的“边界系统”:用 Workers 跑逻辑,用 Pages 托管前端,用 D1 存关系数据,用 KV 做高速缓存,用 R2 放文件,再用一套统一的路由规则把它们全部挂载到同一个域名下。整个过程不需要买服务器、不需要手动维护镜像、不需要配 Nginx,甚至 SSL 证书和 DDOS 防护都不用关心。听起来像不是有点耳熟?很多人觉得这不就是个 Serverless 网站吗,能有多特别。但当你真的把所有服务都抽象成“OS 里的进程、磁盘、内存”之后,开发体验是会被抬到另一个高度的。
这篇文章是我从零把 cloudflare-os 这套方法落地之后的实操笔记。适合那种手头有几个小项目、不想再伺候物理机器的开发者,也适合想用低成本跑一个小型生产环境的小团队。我会把设计思路、组件选型、完整搭建过程、踩过的坑全部拆开给你看,你照着走就能把之前的 VPS 项目迁过来。
1. 项目整体设计与思路拆解
1.1 cloudflare-os 究竟是一个什么东西
很多人听到 Cloudflare 的第一反应是 CDN,第二反应是“那是不是只能加速静态页面”。早几年确实是这样,但现在已经完全变了。现在的 Cloudflare 更像是一个覆盖全球 300 多个城市的分布式运行时,你上传一段代码,它会被部署到距离用户最近的数据中心里执行,用户访问你的域名时,背后不是一台固定的服务器,而是一整套分布式的边缘计算网络。
cloudflare-os 做的事情,是把这个分布式网络按照操作系统的逻辑重新组织一遍:
- Workers 就是 CPU 里的计算进程,代码跑在它里面。
- Pages 就是文件系统里的常规目录,承载前端静态文件和构建产物。
- KV 是内存缓存层,适合高频读、低频写。
- D1 是磁盘上的关系数据库,事务、索引、SQL 全都能用。
- R2 是对象存储,类似外接硬盘。
- Tunnel 是网络接口,把本地运行的开发服务安全地暴露到公网。
这样的抽象有个非常大的好处:以往你要搞一台服务器,要担心的东西是 CPU 够不够、磁盘满没满、内核有没有漏洞、SSH 是不是又被扫了。而在 cloudflare-os 里,单个组件可以被单独替换和横向扩展,你不需要关心硬件,只需要关心你的代码和配置。所以我的看法是,它不是某个具体的软件安装包,而是一种资源组织方式。
1.2 为什么我放弃了传统的 VPS 和容器方案
我做技术选型的时候,很少只看“能不能跑通”。我更喜欢先算一笔账:我要花多少精力去维护这件事。
以前我有一台 2 核 4G 的 VPS,只要项目开始稳定运转,运维工作就自动冒出来了。首先是系统更新,半个月不补丁心里就发慌;其次是安全加固,改 SSH 端口、装 Fail2ban、配 firewalld;再往后是体检日志,发现磁盘占用要清理、发现进程异常要排查。最让我崩溃的一次是某个深夜,数据库突然把磁盘写满,服务直接挂掉,我在被窝里爬起来翻监控。后来我就想,我需要的不是一个“可以控制一切的服务器”,而是一个“能让我专心写业务逻辑的地方”。
Cloudflare 免费版和 Workers 免费计划组合起来能承受的流量,对小项目来讲完全够用。更关键的是,分布在边缘节点上的代码天然具备容灾和负载均衡,你不需要考虑单点故障。对比传统 VPS,节省的不只是那几十块钱月租,而是每周两三次的“例行维护”时间。对我这种个人开发者和几个人的小团队来说,时间是比机器更值钱的资源。
1.3 核心组件选型一览
第一次接触 Cloudflare 页面的时候,你会发现产品非常多,容易脸盲。我在这儿先整理一张我在 cloudflare-os 里的组件分工表,后面章节的实操都是基于这套分工来做的:
| 组件 | 在 OS 中的类比 | 主要用途 | 选用理由 |
|---|---|---|---|
| Workers | 进程 | 跑 API、业务逻辑、中间件 | 部署简单,按请求计费,天然边缘执行 |
| Pages | 文件目录 | 托管前端 SPA、静态站点 | 支持 Git 集成,自动构建,自带 CDN |
| KV | 内存缓存 | 配置信息、临时会话、高频读取数据 | 全球分布,读取低延迟 |
| D1 | 关系型数据库 | 用户表、订单表、任何需要事务的数据 | 兼容 SQLite,免费额度内足够用 |
| R2 | 对象存储 | 图片、文件、备份数据 | 零出口流量费,和 S3 类似 |
| Tunnel | 网络接口 | 让本地服务通过 Cloudflare 节点对外访问 | 不暴露本机端口,自带加密和防护 |
| Access | 访问控制网关 | 给你的后台页面加统一登录认证 | 不需要自己写复杂的鉴权逻辑 |
| Rules | 路由规则 | 把不同路径分配到不同服务 | 一个域名下统一编排所有子服务 |
这张表基本上就是整个架构的地图。不要一次性把这些全塞进一个项目里,而是按需引入。项目第一版我只用了 Workers、Pages、D1 和 KV,后来才加了 R2 和 Tunnel。
2. 核心细节解析与实操要点
2.1 计算与静态资源分离:为什么要把 Workders 和 Pages 拆开
我在设计接口时有一个习惯,动态和静态永远分开。Pages 管静态资源,Workers 管动态逻辑,两者通过路由挂到同一个域名之下。为什么要这么拆分?最重要的原因是部署节奏不同。前端可能一天要改好几次样式,后端接口则要尽量保持稳定。如果它们混在一个服务里,每次前端改动都得连带把整个 Worker 重新部署一次,风险隔离做不好。
Pages 的构建机制绑定 GitHub 或 GitLab,你每次 push 代码,云端都会自动拉取、安装依赖、构建、部署。前端开发流程简直可以简化成“合并分支即发布”。Workers 的部署则完全走命令行,提交代码后执行wrangler deploy src/index.ts,服务就更新了。这样前端和后端各自有各自的发布节奏,互不阻塞。
路由层面我非常推荐用子域名而不是路径来区分。比如:
myapp.com指向 Pages,承载首页和前端资源。api.myapp.com指向 Worker,承载所有后端接口。files.myapp.com指向另一个 Worker 并绑定 R2,承载上传和文件访问。admin.myapp.com指向 Pages,并且在前面加一道 Access 登录认证。
这种方式在 Cloudflare 的仪表盘里配起来很快。点击对应资源,选“自定义域”,输入子域名,然后等几秒钟 SSL 证书自动签发。相比以前要手动配置 DNS 记录 + Caddy/Nginx 反代,这里已经把流程压缩到近乎“填表即生效”。
2.2 D1 和 KV:数据层里两种存储的边界在哪里
数据是任何系统的底座,cloudflare-os 里最容易被混淆的就是 KV 和 D1。KV 是“全局键值存储”,它背后的传播模型是最终一致性。也就是说,你在一条边缘节点写入的数据,不会立刻同步到全球所有节点,可能需要一两秒才能让其他节点看到。KV 特别适合那些“多读少写、偶尔一致也没关系”的数据,比如网站的主题配置、功能开关、菜单列表。
D1 则完全不同。它基于 SQLite,进行一次查询时会去主库拿数据,所以它是强一致的。你写入一条记录,立刻就能查出来,而且支持多行事务、外键、索引这些传统数据库的能力。代价是 D1 的查询速度比 KV 要慢一些,毕竟 SQLite 引擎的解析和索引查询都需要时间。
我一般通过这几个维度来决策:
- 数据会被频繁修改吗?会,就用 D1。
- 数据需要跨表关联吗?需要,就用 D1。
- 数据很少变化,但每秒钟要被读几千次?用 KV。
- 数据能容忍几秒钟的迟到吗?能,用 KV;不能,用 D1。
我曾经做过一个 JSON 接口,原本放 D1,后来查了日志平均响应 30ms。我把数据迁到 KV 之后,响应降到了 10ms 以内。但反过来有个项目,我直接用 KV 存了用户订单,结果用户刚下单就去查详情,偶尔出现查不到,因为 KV 还没同步。这就是没分清边界的反面教材。现在 cloudflare-os 里的通用做法是:“写操作走 D1,读多改少的聚合数据再主动同步一份到 KV”。这样两条数据链路的优势都能利用起来。
2.3 身份认证:一个较轻量但完整的 Token 方案
边缘计算环境下,服务是多节点并行的,没有任何一台机器能保存“会话状态”。所以你千万不能按照传统思路在进程内存里存 Session。cloudflare-os 里我最常用的认证方案有两种:
第一种适合个人项目或内部接口,直接在 Worker 环境变量里存一个静态 token,每个请求进来先比对Authorization头。这个办法零依赖、执行起来飞快,适合做 cron 定时任务用的内部触发接口,或者你只想挡住绝大多少无关调用。
第二种适合真实用户体系,使用 JWT。用户登录后签发一个带签名的 token,之后的每次请求 Worker 只需要验签,不需要查询会话表。关键代码如下:
function createAuthMiddleware(env: Env) { return async (request: Request): Promise<Response | null> => { const header = request.headers.get("Authorization"); if (!header || !header.startsWith("Bearer ")) { return new Response("Unauthorized", { status: 401 }); } const token = header.slice(7); // 用 Web Crypto 做 HMAC 验签 const encoder = new TextEncoder(); const keyBytes = encoder.encode(env.AUTH_SECRET); const cryptoKey = await crypto.subtle.importKey( "raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["verify"] ); // 解析 payload const parts = token.split("."); const valid = await crypto.subtle.verify( "HMAC", cryptoKey, // 这里的 signature 需要 base64url 解码 base64UrlDecode(parts[2]), encoder.encode(`${parts[0]}.${parts[1]}`) ); return valid ? null : new Response("Unauthorized", { status: 401 }); }; }这里的关键点是:验签用的是标准 Web Crypto API,不需要引入任何第三方 JWT 库,Workers 原生支持。你可以把这种 middleware 放到 worker 请求处理的最前面,凡是需要鉴权的接口统一过一层校验。如果某个页面是后台管理界面,也可以直接配上 Cloudflare Access 在边缘网关层做拦截,那样你的业务代码里连鉴权都不用写了。
2.4 路由与域名:一个域名调度全部子服务
我经常把 cloudflare-os 比作一个“城市交通系统”,域名就是主干道,路由规则就是交通指示牌。Cloudflare 的路由能力可以精确到“哪个域名 + 哪个路径”匹配到哪个 Worker 或 Pages 项目。比如:
routes = [ { pattern = "api.example.com/*", zone_name = "example.com" }, { pattern = "files.example.com/*", zone_name = "example.com" }, ]这里的pattern支持通配符,也能写详细的路径,比如api.example.com/admin/*。当用户访问api.example.com时,Cloudflare 会在边缘层直接把请求转发给你指定的 Worker。没有 Nginx,没有负载均衡器,没有端口监听,所有调度全部由 Cloudflare 的全局代理执行。
你甚至可以在同一个域名的不同路径上挂不同的 Worker。有些版本不支持重叠路由,但大部分情况只要路径前缀不冲突就直接可用。要注意的一点是,路由规则在“Workers Routes”里面配置时,如果同时有一个 Pages 项目也匹配了这个域名,那么两者之间会存在竞争优先级,标准和坑我会在第 4 章详细说。总之先记住一个原则:域名路径规划在项目开始时就做好,别等到服务多了再改,否则 DNS 缓存和 Worker 预热过程会让你排查起来很崩溃。
3. 实操过程与核心环节实现
3.1 初始化项目与 Wrangler 配置
首先要做的就是安装官方命令行工具 Wrangler。它是操作整个 cloudflare-os 的“终端入口”,所有的部署、日志、数据库配置都通过它完成。
npm install -g wrangler wrangler login登录之后运行wrangler whoami确认账号已经关联。接下来创建项目:
npm create cloudflare@latest cloudflare-os -- --type workers --lang ts cd cloudflare-os这个命令会生成一个 TypeScript 类型的 Workers 项目模板,自带wrangler.toml配置文件和最小入口文件。我的完整配置文件长这样:
name = "cloudflare-os" main = "src/index.ts" compatibility_date = "2024-11-01" workers_dev = true [[d1_databases]] binding = "DB" database_name = "os_db" database_id = "你没有创建的id可以先留空" [[kv_namespaces]] binding = "KV_STORE" id = "留空,首次绑定后再填" routes = [ { pattern = "api.example.com/*", zone_name = "example.com" } ]可以看到d1_databases和kv_namespaces的 ID 都还没确定。你需要先创建对应的资源:
wrangler d1 create os_db wrangler kv namespace create KV_STORE命令会把生成的 id 输出在终端里,你复制回填到wrangler.toml中即可。这段是在项目里非常有价值的一步,因为很多教程只让你在 Dashboard 界面点,点完之后没有把 ID 填到本地配置,导致部署完全失败。
3.2 编写核心 Worker:认证、接口和数据库访问
现在来写真正跑在边缘逻辑。为了方便拆解,我设计一个最小可用的“笔记接口系统”,包含三个接口:健康检查、创建笔记、列出笔记。
export interface Env { DB: D1Database; KV_STORE: KVNamespace; AUTH_SECRET: string; } export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); const authMiddleware = createAuthMiddleware(env); const authResult = authMiddleware(request); if (authResult !== null) return authResult; if (url.pathname === "/health") { return Response.json({ status: "ok" }); } if (url.pathname === "/notes" && request.method === "GET") { const result = await env.DB.prepare( "SELECT * FROM notes ORDER BY created_at DESC LIMIT 20" ).all(); return Response.json(result.results); } if (url.pathname === "/notes" && request.method === "POST") { const body = await request.json<{ content: string }>(); const result = await env.DB.prepare( "INSERT INTO notes (content, created_at) VALUES (?, ?)" ) .bind(body.content, new Date().toISOString()) .run(); return Response.json({ id: result.meta.last_row_id }); } return Response.json({ error: "Not Found" }, { status: 404 }); }, };这里面有几个细节值得单独说:
authMiddleware会普通所有请求,所以健康检查也被保护了。如果你的健康检查需要被外部监控系统访问,可以单独放一个不做认证的路径,或者把 AUTH_SECRET 设成监控系统相同的 token。- D1 的查询语法就是 SQLite 的语法,几乎零学习成本。
?是绑定参数的占位符,用.bind()传入值,在边缘环境下这也能有效规避 SQL 注入。 - 返回 JSON 我没有手动设置
Content-Type,直接用的Response.json(),省掉一截样板代码。
写完之后本地运行一下:
wrangler dev --remote wrangler d1 execute os_db --remote --file=./schema.sqlschema.sql里就是建表语句,我这里创建一个notes表:
CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, created_at TEXT NOT NULL );本地验证通过后,直接部署:
wrangler deploy命令输出里会给出workers.dev下的临时域名,以及你自定义路由的最终地址。这时候去访问api.example.com/notes,如果返回 401,说明认证中间件起作用了;带上 Header 之后就能正常读写数据。
3.3 部署 Pages 前端并联调路由
后端能跑了,前端也得配上。我用 Vite 搭一个极简的页面,负责展示笔记列表。
npm create vite@latest frontend -- --template react-ts cd frontend npm install npm run build构建完成之后,最直接的方式是登录 Cloudflare Dashboard,在 Pages 里“创建项目”,然后选择“直接上传资产”,把你dist目录上传上去。不过我更推荐直接把仓库连到 GitHub,这样每次 push 后 Pages 自动构建,省到人工上传。
上传完成后为 Pages 项目绑定一个自定义域名,比如example.com。此时你的主页应该可以正常访问了。接着让前端代码请求 API 时带上 token。为了避免把 token 暴露在浏览器里面,可以把这个复用的 token 放在一个只存在于 Worker 端的环境变量里,前端代码通过一个代理接口来取。
更常见的做法是让 Worker 直接作为 API 网关,前端页面通过AUTHORIZATION请求头把用户登录态的 token 带上。具体贴代码就不展开了,核心思路是:Pages 只负责静态资源渲染,所有动态请求都打到api.example.com这个 Worker 上,跨域问题通过 Worker 里的 CORS 中间件解决。
一个简单 CORS 中间件可以这样写:
export function corsHeaders(request: Request): Record<string, string> { const origin = request.headers.get("Origin") || "*"; return { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", }; }在 Worker 的 fetch 处理逻辑里遇到 OPTIONS 请求直接返回 204。前端就可以在example.com的页面安全地调用api.example.com的接口了,不需要在网关层做任何额外代理。
3.4 打通本地联调与自动化部署
项目还没结束,工程化到这一步,接下来要考虑的是“怎么让团队协作和部署变成流水线”。wrangler dev --local会在本地用 Miniflare 模拟一套完整的运行时,包括 D1、KV 和 Workers 的绑定关系。你不用担心改了代码会影响线上,本地模拟器就是最佳的联调环境。
我推荐在项目根目录放一个scripts/init-db.sh,用来执行迁移:
#!/bin/bash wrangler d1 execute os_db --remote --file=./schema.sql自动化发布我会用一个 GitHub Actions 工作流。每次 push 到 main 分支,自动跑测试、构建、部署:
name: Deploy cloudflare-os 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 Worker run: npx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}这里的关键是要在 Cloudflare 后台生成一个 API Token,并添加到 GitHub Secrets 中。Actions 部署时就不用每次手工登录,速度大概只需要二三十秒。这套流程下来,主分支更新代码等于自动发布,我一度觉得我的“操作系统”已经开始自我维护了。
4. 常见问题与排查技巧实录
4.1 免费额度边界:别把免费套餐当作无限资源
cloudflare-os 用起来很爽,但免费额度必须心里有数。我刚开始的时候,因为一个小脚本疯狂刷新 KV 导致额度超限,整个 Worker 被暂停来一上午。Cloudflare 给的免费额度大约是:
| 资源 | 免费额度(大概值) | 我的建议 |
|---|---|---|
| Workers 请求 | 每天 10 万次 | 对个人项目足够,但别跑无人值守的高频轮询 |
| KV 读写 | 读 10 万次/天,写 1 千次/天 | 写入量是最大瓶颈,批量写要注意 |
| D1 读行数 | 每天 500 万行 | 查询条件要带 LIMIT,避免全表扫描 |
| D1 写行数 | 每天 10 万行 | 事务里不要做无谓的 UPDATE |
| R2 存储 | 10GB 存储 | 图片类应用很容易超,需定期清理 |
| Pages 构建 | 每月 500 次 | 不要频繁触发无意义的重构 |
我的建议是:进 Dashboard 设置用量提醒,或者每周手动看一次用量曲线。万万不能等到被限流了才发现某个接口遍历了全表。
4.2 KV 写入后立刻读到旧数据怎么办
有一次我做了个“公告开关”,管理端写入 KV 后,立马调接口验证,结果发现页面还是旧状态。排查了很久才发现 KV 是全球分布式存储,写入的是某个边缘节点,而读请求被路由到了另一个节点,那边还没有同步完成。想解决就要分场景:
- 如果只是标题、状态这种可以容忍秒级延迟的数据,直接在响应头加上
Cache-Control: no-cache,让页面不要在浏览器端缓存。 - 如果这个数据必须强一致,那就改存 D1,或者写入时顺带在请求里拿到 KV 写入确认,等待几十毫秒后再返回。
- 如果你的业务对一致性要求极高,那么从一开始就不该考虑 KV,就像我前面讲的用 D1。
后面我再做类似的“开关”类功能,直接写 D1,没有再出现这个问题。
4.3 路由绑定和自定义域名里的坑汇总
workers_dev = true时,每个 Worker 都会有一个xxx.workers.dev的免费子域名。但如果你想用自己的域名,必须保证域名已经在 Cloudflare 接入。接入通常要求你的域名 DNS 服务器改成 Cloudflare 提供的两个地址。改完之后,解析记录通常会保留原来的值,但由 Cloudflare 作为权威 DNS,这个变更可能需要一两个小时生效。
另一个我踩过的坑是:给 Pages 项目绑定自定义域时,只有在 DNS 选择“CNAME”别名的时候才支持根域名,老用户有时候默认生成的是 A 记录,需要手动改成 CNAME。如果绑定后一直显示待验证,不用太慌,等半个小时再看,可能是 DNS 传播没完成。
最后是路由优先级问题:如果你同时给example.com/notes/*创建了一个 Worker 路由,又给 Pages 绑定example.com作为自定义域,两者可能存在冲突。Cloudflare 对 Pages 自定义域的解析优先级有时候会高于 Workers Routes。操作上我的建议是尽量用子域名区分,避免路径级路由和 Pages 自定义域打架。
4.4 冷启动与响应延迟的实测优化
很多人对 Serverless 的抱怨是冷启动耗时。Workers 本身冷启动控制得比较好,通常只有几毫秒到几十毫秒,远好于普通的云函数。但如果你在代码里引用了体积巨大的 npm 包,那冷启动时依赖解析和模块加载都会被拖慢。
我实测过几个优化点,效果非常明显:
- 部署时加
--minify压缩代码体积。 - 把
node_modules中不用的模块删干净,能用原生 API 的情况下不要引入第三方库。 - 尽量把“配置”放在环境变量或 KV 里,不要在模块初始化时做大量计算。
- 响应时能缓存就缓存,用
caches.default把重复请求的响应直接打回去。
const cache = caches.default; const cached = await cache.match(request.url); if (cached) return cached; // 业务逻辑 const response = new Response(body, { headers: { "Cache-Control": "max-age=300" } }); await cache.put(request.url, response.clone());加缓存后,接口平均响应从 20ms 降到了个位数毫秒。对于爬到边缘节点上的流量,这个改善非常可观。
5. 进阶扩展能力与阶段性心得分享
5.1 给 cloudflare-os 挂上文件磁盘:R2 的接入
系统跑顺之后,自然会想要文件能力。R2 的接入比 S3 简单太多,它兼容 S3 API,但你不需要操心密钥轮换和桶权限。我用一个 Worker 把 R2 包成了简单的“图床接口”:
async handleUpload(request: Request, env: Env): Promise<Response> { const formData = await request.formData(); const file = formData.get("file") as File; const fileName = `${Date.now()}-${file.name}`; await env.MY_BUCKET.put(fileName, file.stream(), { httpMetadata: { contentType: file.type }, }); return Response.json({ path: `/files/${fileName}` }); }然后创建另一个 Worker 负责读取文件:
const object = await env.MY_BUCKET.get(name); return new Response(object.body, { headers: { "Content-Type": object.httpMetadata.contentType } });R2 最让人安心的一点是没有出口流量费。对图片、视频这类高频率的读请求,成本结构比传统对象存储清晰得多。把 R2 的域名也挂到files.example.com下,整套系统从“能跑 API”升级成了“能存文件”。
5.2 同步请求改异步:接入 Queues 与 Cron Triggers
如果你做过数据导入、导出、定时抓取这类任务,就会知道边缘函数不适合跑超过 30 秒的长耗时操作。Cloudflare Queues 可以把任务推给后台队列,由 Worker 按批消费。Cron Triggers 则能帮你做定时轮询。这两样的组合对 cloudflare-os 的扩展能力非常关键。
我引入 Cron 之后做了个“每天早上 8 点汇总前一天笔记并发送邮件”的小工具。只需要在wrangler.toml里配置:
[[triggers]] crons = ["0 8 * * *"]然后 Worker 里监听scheduled事件,在指定的时间执行逻辑。这不像传统 Cron 要保证服务器开机,边缘平台天然让定时任务分布在世界各地,按 UTC 时间触发。
5.3 关于这套方法的几条真实体会
把项目跑到现在,我最深的感受是:cloudflare-os 与其说是一种技术选型,不如说是一种思维模式。以前写代码的时候,潜意识里总有一台“中心机器”的概念,所有的状态都往这台机器上堆。后来我学会把系统拆成“请求处理、数据存储、外部服务”三个维度,每个维度都能独立部署和扩展,思路就清楚太多了。
另外一点是:不要一次性把所有 Cloudflare 产品全都用上。我很长一段时间只用 Pages + Workers + D1,直到需要文件上传了才加 R2,需要定时任务了才去开 Cron。每个组件都等到业务明确需要的时候再引入,系统一直保持足够简洁。如果一上来就把所有产品都堆上去,调试一个跨模块故障会非常痛苦。
至于这个项目未来还要怎么走,我目前正在尝试把 Workers AI 接进来,做一个小型的语义搜索,把笔记内容自动做向量化。Cloudflare 的 Workers AI 支持本地跑模型,不需要自己管理 GPU,对个人项目来说又是一个非常友好的入口。等我把这块跑通了,再写一篇专门的文章分享。现在这套 cloudflare-os 已经稳定运行了半个自然年,我没有半夜运维过,没有手动清过磁盘,甚至没有担心过证书过期,这种体验就是我会持续用它的理由。