Cloudflare Workers 这个边缘计算平台,配合 Hono 这个轻量级 Web 框架,加上 R2 对象存储,可以非常优雅地解决“我要一个 API 接口 + 文件上传下载”的全套需求。我最近用这套组合做了一个文件服务,把 API 路由、文件流式上传、断点下载、权限校验全部跑通,整体体验比传统服务器方案舒服太多。这篇文章会把整个实现过程、关键代码、遇到的坑以及最终的优化方案完整分享出来,适合想在边缘节点上构建轻量 API 服务或文件存储服务的开发者参考。
1. 为什么把 API 和文件存储搬到边缘:这套组合解决了什么问题
先说结论:Cloudflare Workers + Hono + R2 这套技术栈,最适合的场景是“轻量 API 服务 + 文件存取”,尤其是个人项目、小型团队内部工具、前端项目的静态资源托管,以及那些不想维护服务器的场景。
1.1 Worker 到底是什么,和传统服务器有什么区别
Cloudflare Workers 是一个运行在 Cloudflare 全球边缘节点上的 Serverless 执行环境。你写的代码会被部署到全球 300 多个城市的节点上,用户请求到达最近的节点后直接由该节点响应,不需要回源到某个固定的服务器。
传统架构下,你的 API 部署在一台云服务器上,用户从全国各地(甚至全球)访问,请求要先经过公网路由到达服务器所在机房,再返回响应。这个过程中的网络延迟是不可控的,尤其是跨地区访问时明显。而 Worker 的模型是“代码跟着用户走”——用户在上海访问,代码就在上海最近的节点执行;用户在洛杉矶访问,代码就在洛杉矶的节点执行。
R2 则是 Cloudflare 的对象存储服务,对标 AWS S3,但最大的差异化优势是出口流量零费用。这一点对文件下载场景极其关键,S3 的流量费是成本大头,而 R2 直接免掉,对个人开发者和小团队来说是实打实的省钱。用我自己的项目举例,之前文件服务跑在传统 VPS 上,带宽费和存储空间是两笔固定开销,迁到 R2 之后存储费用极低,流量费归零。
1.2 Hono 在这个架构里的角色
Hono 是一个特别适合边缘环境的 Web 框架,名字在日语里是“火焰”的意思。它最大的特点是“小”和“快”——整个框架核心只有十几 KB,专门为 Cloudflare Workers、Deno、Bun 这类运行时设计,同时也兼容 Node.js。
选 Hono 而不是 Express 或 Koa,是因为 Express 这类框架依赖 Node.js 的原生 API,无法直接在 Worker 环境运行。Hono 的 API 设计风格类 Express(路由、中间件、上下文对象很相似,上手基本零成本),却在底层针对 Worker 的运行时做了优化,能直接使用 Worker 的 fetch 事件模型。
我选择 Hono 还有一个实际原因:TypeScript 支持极其友好。Hono 的源码本身就是 TypeScript 编写的,类型推断做得非常完善,路由参数、请求体、响应体的类型都能自动推导,写代码时的体验比普通 JavaScript 框架好很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 项目初始化和 R2 存储桶配置:从零搭起可运行的基础框架
开始写代码之前,需要把 Cloudflare 账号、Wrangler CLI、R2 存储桶这些基础设施准备好。这部分的坑不少,我按实际操作顺序整理。
2.1 Wrangler CLI 安装与登录
Wrangler 是 Cloudflare 官方的命令行工具,负责 Worker 的本地开发、调试和部署。安装方式很简单,用 npm 全局安装:
bash复制npm install -g wrangler
安装完成后需要登录 Cloudflare 账号:
bash复制wrangler login
这条命令会打开浏览器,跳转到 Cloudflare 的授权页面,确认后 Wrangler 会获取一个 API Token 存在本地。这里有个我踩过的坑:如果你在服务器(无浏览器环境)上执行 wrangler login,会失败,需要用 wrangler login --browser=false 这种模式手动复制授权链接到本地浏览器打开,或者直接用 CLOUDFLARE_API_TOKEN 环境变量方式认证。
2.2 创建 R2 存储桶
登录后,先创建 R2 存储桶。命令是:
bash复制wrangler r2 bucket create my-file-bucket
存储桶名称是全局唯一的?实际上 R2 的桶名在同一个账号下唯一,不同账号之间可以重复,所以命名不用太纠结。我习惯用项目名做前缀,比如 myapp-uploads、myapp-public,方便多个项目共用时区分。
R2 控制台里也可以直接创建,但我推荐用命令行,因为后续要在 wrangler.toml 里配置绑定关系,命令行创建后复制一下存储桶名称就行。
2.3 项目初始化与配置绑定
创建一个新项目:
bash复制npm create cloudflare@latest my-api
cd my-api
npm install hono
安装完成后,项目目录里会有 wrangler.toml 文件,这是 Worker 的配置文件。需要把 R2 存储桶绑定到 Worker 上,在配置文件里加上:
toml复制name = "my-api"
main = "src/index.ts"
compatibility_date = "2024-11-01"
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-file-bucket"
binding 字段是在 Worker 代码里访问存储桶时用的变量名,我习惯叫 BUCKET,全大写,与 env 中的其他环境变量区分。绑定关系建立后,部署 Worker 时 Cloudflare 会注入对应的 R2 客户端实例。
2.4 本地开发环境的完整配置
本地开发时用 wrangler dev 启动开发服务器。这里有个重要的细节:R2 的本地模拟需要额外处理。
新版本 Wrangler 支持 wrangler dev --remote 直接连远程 R2 调试,但这样每次读写都会消耗真实存储,调试频率高时不方便。Wrangler 也内置了本地模拟器,但需要提前创建本地存储目录,在 wrangler.toml 里加:
toml复制[dev]
ip = "127.0.0.1"
port = 8787
local_protocol = "http"
启动本地开发服务器:
bash复制wrangler dev
默认情况下,Wrangler 会先尝试连远程,然后回退到本地模拟。如果你只想跑本地模拟,加 --local 参数。我实测下来,--local 模式启动更快,且 R2 数据会存在 .wrangler/state/v3/r2 目录下,方便反复测试清理。
Wrangler 现在还推荐用 wrangler.jsonc 格式的配置文件,支持注释,比 TOML 更灵活,看个人习惯。
3. API 接口的具体实现:上传、下载、列表、删除的核心路由与关键代码
基础框架搭好后,进入核心部分——用 Hono 实现文件相关的 API。我把常用的五个接口都实现了一遍:上传、下载、删除、列表、获取元信息。每个接口的实现思路和注意事项逐一说明。
3.1 Hono 入口文件的初始化结构
src/index.ts 的入口结构如下:
typescript复制import { Hono } from 'hono';
type Bindings = {
BUCKET: R2Bucket;
};
const app = new Hono<{ Bindings: Bindings }>();
// 全局中间件:请求日志
app.use('*', async (c, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${c.req.method} ${c.req.path} - ${ms}ms`);
});
// 健康检查
app.get('/health', (c) => c.json({ status: 'ok' }));
// 业务路由...
export default app;
Hono<{ Bindings: Bindings }> 是 Hono 的类型参数,把 Worker 的环境变量类型传进去,之后在任意路由里通过 c.env.BUCKET 访问 R2 时,TypeScript 都能自动提示类型,不会出现 any 满天飞的情况。
3.2 文件上传接口:从请求体读取并存入 R2
文件上传是文件服务的核心接口。R2 支持通过 put 方法直接将二进制数据写入存储桶,Hono 则能从请求体里读取原始数据,组合起来非常直接:
typescript复制app.post('/upload', async (c) => {
const file = await c.req.parseBody();
// file 的类型可能是 File | string,需要判断
const uploadFile = file['file'];
if (!(uploadFile instanceof File)) {
return c.json({ error: '缺少文件字段' }, 400);
}
const key = crypto.randomUUID() + '-' + uploadFile.name;
const uploaded = await c.env.BUCKET.put(key, uploadFile.stream(), {
httpMetadata: {
contentType: uploadFile.type || 'application/octet-stream',
},
});
return c.json({
key: uploaded.key,
size: uploadFile.size,
etag: uploaded.httpEtag,
}, 201);
});
解析请求体时,Hono 的 parseBody() 如果表单里包含文件字段,会把字段值解析成 File 对象(自动使用 request.formData() 方法),如果只是普通文本字段则是字符串。判断 instanceof File 是个保险做法,防止客户端传错格式。
R2 支持三种数据源:ReadableStream(流式)、ArrayBuffer(二进制缓冲)、string(文本)。上传大文件时必须用 .stream(),先把数据转成流,避免一次性加载进内存造成 Worker 内存溢出。Worker 的内存限制是 128MB,如果直接传 ArrayBuffer,大文件很容易撑爆。
密钥生成我用 crypto.randomUUID() 加原始文件名,原因有二:一是 UUID 保证全局唯一,不会出现同一文件名覆盖的问题;二是如果直接用用户文件名做 key,容易被恶意传 ../../etc/passwd 之类的路径穿越字符串,虽然 R2 不会真发生路径穿越,但 key 里的特殊字符会污染 URL。
3.3 文件下载接口:流式返回与断点续传支持
下载接口需要处理的核心问题是如何高效地把 R2 上的对象返回给客户端。直接用 get() 拿到对象后,把它的 body 塞进 c.body() 返回是最基本的做法:
typescript复制app.get('/download/:key', async (c) => {
const key = c.req.param('key');
const object = await c.env.BUCKET.get(key);
if (!object) {
return c.json({ error: '文件不存在' }, 404);
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('etag', object.httpEtag);
// 支持断点续传
const range = c.req.header('range');
if (range) {
headers.set('content-range', `bytes ${range}`);
return new Response(object.body, {
status: 206,
headers,
});
}
return new Response(object.body, {
headers,
});
});
这里有个重要细节:R2 的 object.writeHttpMetadata(headers) 方法会把对象存储时设置的 Content-Type、Cache-Control、Content-Disposition 等 HTTP 元数据自动写入响应头,比自己手动设置省事很多。Etag 也建议显式设置,浏览器缓存和断点续传都会用到。
断点续传这块,我没有自定义实现 Range 请求的切分逻辑,因为 R2 的 get() 方法本身支持 range 参数,直接透传请求头即可让 R2 自动处理:
typescript复制if (range) {
const object = await c.env.BUCKET.get(key, { range });
// 返回 206 状态码
}
这样实现更优雅,但需要注意:R2 对 Range 请求的响应头 Content-Range 需要自己拼,否则部分下载工具无法正确解析。
3.4 文件列表、删除与元信息接口
列表接口使用 R2 的 list() 方法,支持分页和前缀过滤:
typescript复制app.get('/files', async (c) => {
const prefix = c.req.query('prefix') || '';
const cursor = c.req.query('cursor');
const limit = Math.min(Number(c.req.query('limit')) || 100, 1000);
const listed = await c.env.BUCKET.list({
prefix,
cursor,
limit,
});
return c.json({
files: listed.objects.map((obj) => ({
key: obj.key,
size: obj.size,
etag: obj.httpEtag,
uploaded: obj.uploaded,
})),
cursor: listed.truncated ? listed.cursor : null,
});
});
分页用 cursor 机制而不是 page/offset,这是对象存储的标准做法。cursor 是 R2 返回的不透明字符串,下次请求时原样传回即可,不需要自己维护偏移量。
删除接口就一行:
typescript复制app.delete('/delete/:key', async (c) => {
const key = c.req.param('key');
await c.env.BUCKET.delete(key);
return c.json({ success: true });
});
删除操作需要注意:R2 删除不存在的对象不会报错,返回成功。所以如果你的业务逻辑需要“删除成功”和“文件不存在”两种状态区分,需要先 get() 判断一次再删,且 get() 和 delete() 之间理论上存在竞态窗口(极少发生,但集群高并发下存在)。
3.5 绑定 Hono 路由到 Worker 入口
Hono 应用默认导出一个 fetch 处理器,正好对应 Worker 的入口。但官方的 Worker 脚手架默认生成的 src/index.ts 是 export default { fetch() {...} } 对象形式的,需要改成:
typescript复制export default app;
Hono 的 app 实现了 fetch 方法,可以直接作为 Worker 默认导出对象。如果项目里还有其他逻辑要混入,可以用 Hono 的 app.fetch 手动包装:
typescript复制export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
// 自定义逻辑...
return app.fetch(request, env, ctx);
},
};
这个写法更灵活,可以在请求进入 Hono 之前做自定义处理。
4. 大文件上传的优化思路:流式处理、分片上传与前端配合
小文件上传很简单,但实际生产环境中大文件(几百 MB 甚至几 GB)的上传是绕不开的难点。Worker 的请求体大小限制是 100MB(免费套餐),单次请求传大文件直接会被拒。需要配合前端做分片上传,同时利用 R2 的流式处理能力提升可靠性。
4.1 Worker 请求体大小限制分析
Cloudflare Workers 的免费套餐限制单个请求 body 最大 100MB;付费套餐(Workers Paid)最大 200MB。这个限制是平台硬性的,遇到大文件必须换思路,不能硬传。
另外还有个容易被忽略的限制:Worker 的执行时间限制。免费套餐是 10ms CPU 时间,付费套餐是 30s CPU 时间。虽然 await 等网络 I/O 不算 CPU 时间,但文件上传涉及数据流转,整体耗时比普通 API 长,计划内任务也需要考虑超时风险。
4.2 分片上传的完整实现方案
分片上传思路是:前端把大文件切成多个小分片,每个分片单独通过 API 上传,全部上传完成后,后端再通知 R2 合并。R2 目前没有原生 multipart upload API(就是 S3 的 CreateMultipartUpload / UploadPart / CompleteMultipartUpload),所以需要用一个规范来管理分片状态。简化做法是用 R2 的临时目录:
- 前端将文件切成固定大小分片(如 5MB/片)
- 每个分片通过 API 上传到 R2 的临时路径:
uploads/{fileId}/part-{index} - 所有分片传完后,调用一个合并接口,后端把临时路径下的分片逐个读出来,写入最终 key
- 清理临时分片
合并的代码:
typescript复制app.post('/merge', async (c) => {
const { fileId, fileName, totalParts, contentType } = await c.req.json();
const finalKey = crypto.randomUUID() + '-' + fileName;
// 逐片读取并写入最终对象
const chunks = [];
for (let i = 0; i < totalParts; i++) {
const partKey = `uploads/${fileId}/part-${i}`;
const partObj = await c.env.BUCKET.get(partKey);
if (!partObj) {
return c.json({ error: `缺失分片 ${i}` }, 400);
}
chunks.push(partObj.body);
// 这里要注意:R2 的 get 返回的 body 是 ReadableStream,直接 concat 会内存爆炸
}
// 需要把多个 ReadableStream 合并成一个
const merged = new ReadableStream({
async start(controller) {
for (const chunk of chunks) {
const reader = chunk.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
controller.enqueue(value);
}
}
controller.close();
},
});
await c.env.BUCKET.put(finalKey, merged, {
httpMetadata: { contentType },
});
// 清理临时分片
await Promise.all(
chunks.map((_, i) => c.env.BUCKET.delete(`uploads/${fileId}/part-${i}`))
);
return c.json({ key: finalKey });
});
上面 ReadableStream 合并多个分片的代码,有两个问题需要优化:一是内存,逐个分片读入并 enqueue 不会把全部数据加载到内存,但 controller 内部会缓冲;二是 chunks 数组里存的其实是 ReadableStream 对象,如果前面的分片还没读完就读取下一个,会导致多个流同时打开。
更稳妥的做法是串行读取:先读完第一个分片的流,再读第二个,然后把它们顺序拼成一个流。但要注意 ReadableStream 一旦开始读取就不能暂停丢弃,所以分片顺序必须严格按队列进行。实践中还有一个更简单的方案:需要所有分片先合并成 ArrayBuffer 再统一 put。如果分片总量不大(比如总大小小于 100MB),这种方案最简单可靠。
4.3 前端上传组件的配合实现
前端我用了一个简单但可靠的上传流程:
typescript复制const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB/片
async function uploadFile(file: File) {
const fileId = crypto.randomUUID();
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
// 逐片上传
for (let i = 0; i < totalParts; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('fileId', fileId);
formData.append('partIndex', String(i));
await fetch('/upload/part', {
method: 'POST',
body: formData,
});
}
// 合并
await fetch('/merge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileId,
fileName: file.name,
totalParts,
contentType: file.type,
}),
});
}
因为每个分片只有 5MB,即使差一点碰到 Worker 的限制也不会超限,同时上传和合并接口都由 Hono 处理。用户上传大文件时,进度条就是“已上传分片数 / 总分片数”,交互体验也比较自然。
4.4 大文件上传失败的容错与断点续传
分片上传天然就支持断点续传。前端记录已成功上传的分片索引,下次重试时跳过这些分片即可。上传接口返回分片索引时,客户端要保存这个进度:
typescript复制const uploadedParts = new Set<number>();
// 每次上传成功后
uploadedParts.add(partIndex);
// 重试时跳过
for (let i = 0; i < totalParts; i++) {
if (uploadedParts.has(i)) continue;
// 上传...
}
完整断点续传还可以增加一个“查询已上传分片”的接口,前端初始化时调用,把已传的分片打上标记。实际业务中如果临时分片被清理了,已上传标记可能失效,所以清理临时分片时建议加一个过期时间(临时目录定期清理)。
5. 鉴权、CORS 与公开访问策略:安全问题怎么收敛
接口写完了,不能裸奔。文件服务涉及数据读写,鉴权是必须的。这部分的策略设计直接决定了服务的可用性和安全性。
5.1 接口鉴权的几种方案对比
| 方案 | 复杂度 | 安全性 | 适用场景 |
|---|---|---|---|
| 无鉴权 | 低 | 极差 | 仅内部测试 |
| 静态 API Token | 低 | 一般 | 个人项目、内部工具 |
| JWT 鉴权 | 中 | 较好 | 多用户系统 |
| Cloudflare Access | 较高 | 很好 | 企业内部系统 |
个人项目的文件服务,我推荐静态 API Token 加上简单的前缀校验。实现方式简单,效果好,足够挡住大部分恶意请求:
typescript复制const API_TOKEN = c.env.API_TOKEN;
app.use('/api/*', async (c, next) => {
const auth = c.req.header('Authorization');
if (auth !== `Bearer ${API_TOKEN}`) {
return c.json({ error: '未授权' }, 401);
}
await next();
});
Token 通过环境变量注入,不要硬编码在代码里。Cloudflare 控制台 → Worker → Settings → Variables 里配置,或者用 wrangler secret put API_TOKEN 命令设置。用 secret 管理比环境变量更安全,秘密不会出现在部署文件中。
5.2 公开下载与私有下载的区分
R2 默认不公开,只有 Worker 绑定后才能访问。这意味着如果不做任何处理,所有下载都要走 Worker 接口。对公开文件(如博客图片、静态资源),可以通过自定义域名公开访问,设置存储桶公共访问。
但是对私有文件,必须走 Worker 做鉴权。公开下载和私有下载在架构设计上应该分开:
- 公开文件:直接通过自定义域名访问 R2,不走 Worker,零计算成本
- 私有文件:通过 Worker 鉴权后,重定向或转发到 R2
Worker 转发私有文件的代码,就是我前面写的下载接口实现。需要注意:如果文件是私有的,但通过公开 URL 猜测到了存储桶路径,仍然可能直接访问。所以私有文件建议单独用一个存储桶,与公开文件的存储桶物理隔离。这是我实际项目中的教训——一开始公开私有混用一个桶,后来发现一个桶设置公开后,所有文件都暴露了。
5.3 CORS 跨域配置的完整处理
前端调用 API 必然涉及跨域。Cloudflare Workers 本身没有 CORS 限制,但浏览器会拦截跨域请求。需要手动在 Worker 里配置 CORS 头:
typescript复制app.use('*', async (c, next) => {
await next();
c.header('Access-Control-Allow-Origin', '*');
c.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
c.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
});
// 处理预检请求
app.options('*', (c) => {
c.header('Access-Control-Allow-Origin', '*');
c.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
c.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
c.header('Access-Control-Max-Age', '86400');
return c.body(null, 204);
});
预检请求(OPTIONS)很关键。浏览器发送非简单请求(如带 Authorization 头、Content-Type: application/json)之前,会先发一个 OPTIONS 预检请求,如果 Worker 不处理,浏览器会直接拦截真正的请求。Cloudflare Workers 自带一个特性是可以通过路由配置处理 OPTIONS 请求,我习惯在代码里统一处理,这样逻辑内聚且容易测试。
注意:真实场景中 Access-Control-Allow-Origin 不建议用 *,因为 * 不能和 Authorization 头配合使用(浏览器规范)。要么用具体域名,要么用 c.req.header('Origin') 动态反射回显,配合白名单机制:
typescript复制const allowedOrigins = ['https://example.com', 'https://admin.example.com'];
const origin = c.req.header('Origin');
if (origin && allowedOrigins.includes(origin)) {
c.header('Access-Control-Allow-Origin', origin);
c.header('Vary', 'Origin');
}
5.4 限流与防滥用策略
Worker 免费套餐的请求次数限制是每天 10 万次,付费套餐支持更高的配额。虽然一般用不完,但如果接口可以被无限调用,恶意刷请求还是能造成成本问题。Cloudflare 提供了 Rate Limiting 功能,可以在控制台配置:
- 每 10 秒内最多 100 次请求
- 超出后返回 429 状态码
- 可以设置封禁 IP 时长
也可以自己实现简单的计数器限流(基于 Workers KV 的原子操作),但比较复杂。实践中我还是推荐优先用 Cloudflare 控制台自带的 Rate Limiting 规则,简单有效。
6. 我踩过的坑与调试技巧:Wrangler 本地调试和线上排错心得
这套技术栈整体很顺,但实际开发中还是踩了一些不太明显的坑。把这些记录下来,希望帮大家少走弯路。
6.1 类型定义引入失败导致编译报错
新建 Hono 项目时,如果 TS 配置没有正确引入 @cloudflare/workers-types,写 R2Bucket 等类型时会出现找不到类型的报错。
需要确认 tsconfig.json 里包含:
json复制{
"compilerOptions": {
"types": ["@cloudflare/workers-types"]
}
}
如果项目里同时用了 Node.js 相关的类型,可能需要处理 types 冲突,简单的做法是把 @cloudflare/workers-types 直接 import 到入口文件顶部:
typescript复制/// <reference types="@cloudflare/workers-types" />
6.2 R2 对象的流只能读取一次
R2 的 get() 返回的 object.body 是 ReadableStream,而且只能读取一次。如果你在同一个请求里既想读取内容做校验,又想把内容返回给客户端,就会遇到流已消耗(stream locked)的报错。
解决思路:
- 先把数据读成
ArrayBuffer,再自行new Response(arrayBuffer)返回 - 不要同时多次读取同一个流
如果文件很大,读了 ArrayBuffer 会占用 Worker 内存,但文件超过 100MB 的情况我基本都会走流式直接透传,避免二次读取造成的资源浪费。
6.3 本地调试时 R2 数据不在远程的问题
wrangler dev 的默认行为在不同版本之间变化很大。2024 年后的版本默认走本地模拟,数据存在本地 .wrangler/state 目录,部署时不会自动同步到远程 R2。
调试时如果需要验证线上 R2 数据,必须用 wrangler dev --remote。但 --remote 模式下,代码每次改动都需要走一次构建步骤,热更新不如本地模拟快。我的做法是:高频开发用本地模拟,涉及真实数据验证才用 --remote。
6.4 部署注意事项:wrangler deploy 构建流程与 Secrets
部署用 wrangler deploy,它会自动执行构建(如果配置了 build 命令),然后把产物上传到 Cloudflare。部署完成后会输出一个 *.workers.dev 域名,可以直接访问。
这里有几个容易忽略的点:
- 环境变量:
wrangler.toml里的[vars]是明文存在的,不适合放敏感信息。API Token 之类的一定要用wrangler secret put设置 - 回滚:Cloudflare 控制台 → Workers → Deployments 里可以查看历史版本并回滚,发布出问题不要慌,直接在控制台点回滚即可
- 日志:线上排错用
wrangler tail看实时日志,展示每次请求的 URL、状态码和 console.log 输出
6.5 实测性能表现
最后说一下我实际部署后的性能数据。Worker 在北京地区访问延迟在 50ms 左右(免费套餐可能略高),文件上传速度取决于客户端到最近边缘节点的带宽。R2 读文件的响应时间在 20-40ms,下载速度实测能跑到 500Mbps 级别,比传统 VPS 快很多。
对比之前跑在 VPS 上的文件服务(Nginx + 本地磁盘),这套架构的运维负担明显更低:不需要处理系统更新、不需要担心磁盘扩容、不需要配置进程守护。免费套餐每个月 10 万次请求、10GB 存储、100GB 流量(这里注意,R2 的免费额度是 10GB 存储、每天 100 万次读操作,和 Workers 免费额度独立),个人项目的量级几乎可以覆盖。
我在实际使用中最满意的一点是:整个服务拆开后,每个部分都是独立的,即使某个边缘节点的网络抖动,Cloudflare 会自动把请求路由到其他节点,可用性比我之前单机部署高一个数量级。如果你还在纠结文件服务怎么搭,这套组合值得试一下。
