大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
R2 对象存储
这一课你将能做到
- 创建 R2 Bucket 并在 Worker 中完成对象上传、下载、删除、列表操作
- 生成预签名 URL(Presigned URL)实现前端直传,绕过 Worker 流量
- 配置 R2 生命周期规则(自动删除过期文件)和自定义域名公开访问
为什么要学
用户要上传头像、视频、备份文件。如果全走你的服务器,流量费爆炸,存储费也爆炸。如果直接传 S3,跨区域延迟和出口流量费一样吓人。R2 是 Cloudflare 推出的对象存储,兼容 S3 API,但零出口流量费——无论你从哪里下载,都不收流量钱。
人话:R2 就是一个「不限量 CDN 加速的 S3」。你可以把它当成图片、视频、备份文件的廉价仓库,配合 Worker 做签名、权限控制、自动处理(如图片压缩)。
先建立图像
用户浏览器
│
├── 直接上传 ──> R2 Bucket (通过 Presigned URL)
│
└── 请求 Worker ──> 生成 Presigned URL ──> 返回给用户
│
└── 也可以直接读写 R2 (S3 API)
R2 Bucket
├── 自动同步到边缘节点 (读加速)
├── 生命周期规则 (30天后自动删除)
└── 自定义域名 (cdn.example.com)
跟着做
步骤 1:创建 Bucket
npx wrangler r2 bucket create my-uploads
绑定到 Worker(wrangler.toml):
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-uploads"
步骤 2:Worker 中上传/下载
export interface Env {
MY_BUCKET: R2Bucket;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const key = url.searchParams.get('key');
if (request.method === 'PUT' && key) {
// 上传
const body = await request.arrayBuffer();
await env.MY_BUCKET.put(key, body, {
httpMetadata: { contentType: request.headers.get('content-type') ?? 'application/octet-stream' },
customMetadata: { uploadedBy: 'user123' },
});
return new Response('Uploaded', { status: 201 });
}
if (request.method === 'GET' && key) {
// 下载
const object = await env.MY_BUCKET.get(key);
if (!object) return new Response('Not Found', { status: 404 });
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('etag', object.httpEtag);
headers.set('Cache-Control', 'public, max-age=31536000');
return new Response(object.body, { headers });
}
if (request.method === 'DELETE' && key) {
await env.MY_BUCKET.delete(key);
return new Response('Deleted');
}
return new Response('Bad Request', { status: 400 });
},
} satisfies ExportedHandler<Env>;
步骤 3:生成预签名 URL(前端直传)
前端直传可以避免 Worker 成为流量瓶颈。用 @aws-sdk/s3-request-presigner:
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
export interface Env {
R2_ACCOUNT_ID: string;
R2_ACCESS_KEY_ID: string;
R2_SECRET_ACCESS_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const key = url.searchParams.get('key');
if (!key) return new Response('Missing key', { status: 400 });
const s3 = new S3Client({
region: 'auto',
endpoint: `https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
},
});
const presignedUrl = await getSignedUrl(
s3,
new PutObjectCommand({ Bucket: 'my-uploads', Key: key, ContentType: 'image/jpeg' }),
{ expiresIn: 3600 }
);
return Response.json({ url: presignedUrl });
},
} satisfies ExportedHandler<Env>;
前端拿到 url 后直接 PUT 上传,不经过 Worker。
步骤 4:生命周期规则(自动清理)
npx wrangler r2 bucket lifecycle add my-uploads --expire-days 30 --prefix "tmp/"
这条命令给 tmp/ 前缀的对象设置 30 天自动过期。
步骤 5:自定义域名公开访问
在 Dashboard → R2 → 对应 Bucket → Settings → Public Access 中绑定自定义域名(如 cdn.example.com)。之后 https://cdn.example.com/image.jpg 就能直接访问,走 Cloudflare CDN 边缘缓存。
讲清楚要点
- R2 兼容 S3 API:你可以用 AWS SDK 操作 R2,只需把 endpoint 改成
https://<account_id>.r2.cloudflarestorage.com。现有 S3 迁移成本极低。 - 零出口流量费:这是 R2 最大的卖点。S3 下载 1TB 要收几十美元,R2 不收。但注意:R2 的存储费比 S3 略高,适合「读多写少、全球分发」的场景。
- Worker 直传 vs 预签名直传:小文件(< 5MB)可以走 Worker 中转,方便做鉴权和处理;大文件或高并发上传必须用预签名 URL,否则 Worker 的 CPU 时间和内存会成为瓶颈。
- R2 不是实时同步:写入后对象立即可读,但全球边缘缓存的更新有延迟(类似 CDN)。如果用户刚上传就要立刻下载,建议强制刷新 CDN 或直接用
r2.dev域名(不缓存)。 - 生命周期规则按前缀匹配:可以给不同前缀设置不同过期策略。
tmp/30 天,avatars/永不过期。
你可能会懵的地方
-
R2 和 KV 怎么选? 小数据(< 25MB)、高频读、需要 key-value 语义用 KV;大文件、图片、视频、备份用 R2。KV 的 value 有 25MB 上限,R2 单对象可达 5TB。
-
预签名 URL 安全吗? 安全,但 URL 本身包含签名和过期时间。不要泄露给未授权用户;敏感操作(如删除)不要生成预签名 URL,让 Worker 做鉴权后再操作。
-
R2 有文件夹概念吗? 没有。对象存储是扁平的,
avatars/user1.jpg中的avatars/只是 key 的一部分,不是真实目录。但你可以用/做前缀来模拟目录结构。 -
怎么限制用户只能上传到自己的目录? 生成预签名 URL 时,把
Key设为uploads/${userId}/${filename},用户只能操作这个前缀下的对象。 -
R2 支持图片处理吗? 本身不支持。但你可以用 Cloudflare Images(付费)或 Worker +
@cf-wasm/photon等 WASM 库在边缘做缩放、格式转换。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 前端直接把文件 POST 给 Worker 再转 R2 | 大文件会撑爆 Worker 内存和 CPU | 用预签名 URL 让前端直传 R2 |
| 把 R2 当数据库用,存大量小 JSON | 成本高、延迟高 | 小数据用 KV 或 D1 |
不设置 Cache-Control | 每次下载都回源 R2,浪费带宽 | 下载时设置 public, max-age=31536000 |
用 r2.dev 域名做生产 CDN | r2.dev 有速率限制,不适合生产 | 绑定自定义域名,走 Cloudflare CDN |
| 删除对象后立刻检查列表 | 列表可能有延迟 | 删除后直接返回成功,或加延迟重试 |
卡点急救
- 预签名 URL 上传 403:检查
ContentType是否和实际上传的一致;检查 URL 是否过期;检查 Access Key 是否有PutObject权限。 - Worker 里
env.MY_BUCKET is undefined:检查wrangler.toml的binding名;重启wrangler dev。 - 下载时中文文件名乱码:设置
content-disposition: attachment; filename*=UTF-8''${encodeURIComponent(filename)}。
面试常问
-
R2 和 S3 的核心区别是什么? R2 零出口流量费,S3 按下载流量收费;R2 深度集成 Cloudflare CDN,S3 需要额外配 CloudFront;R2 的 API 兼容 S3,但部分高级功能(如 S3 Object Lock)尚未支持。
-
预签名 URL 的原理? 用 Access Key 对请求参数(Bucket、Key、过期时间、Content-Type)做 HMAC 签名,生成一个临时 URL。持有 URL 的人可以在过期前执行指定操作,无需知道真实密钥。
-
R2 怎么实现图片 CDN? 绑定自定义域名到 R2 Bucket,上传图片后通过
https://cdn.example.com/image.jpg访问。Cloudflare 会自动做边缘缓存,配合 Worker 可以做动态缩放、格式转换(WebP/AVIF)。
自测三问
- 为什么大文件上传要用预签名 URL 而不是走 Worker 中转?
- R2 的「零出口流量费」是什么意思?什么场景下这个优势最明显?
- 如果用户上传了一个文件但立刻访问 404,可能是什么原因?
小结一句话
R2 是 S3 兼容的边缘对象存储,零出口流量费 + 全球 CDN 加速,配合预签名 URL 和 Worker 可以实现安全、低成本的大文件存储与分发。
延伸
- R2 官方文档
- R2 Pricing
- 下一课:D1 数据库,学习边缘 SQLite 的 schema 设计与迁移。