这次我们来看一个完全免费、永久可用的公开图床方案,核心是利用 Cloudflare 的 Workers 和 R2 存储服务。对于需要稳定外链、不想为图床付费、又担心国内服务不稳定的开发者或博主来说,这个方案几乎是目前最理想的“自建”选择之一。
它的核心逻辑很简单:你写一个运行在 Cloudflare Workers 上的短小精悍的脚本,这个脚本负责接收用户上传的图片,然后将其安全地存储到 Cloudflare R2 这个免费的存储桶里,最后返回一个可以直接访问的图片链接。整个过程不经过你自己的服务器,完全依托于 Cloudflare 的全球网络,因此速度、稳定性和免费额度都很有保障。
本文将带你从零开始,一步步完成这个图床的部署和配置。你会了解到需要准备什么、每一步具体怎么做、如何测试功能,以及遇到常见问题如何排查。无论你是前端开发者、博客作者,还是仅仅想找一个可靠的图片托管方案,跟着做下来,半小时内就能拥有一个独属于自己的永久免费图床。
1. 核心能力速览
在开始动手之前,我们先快速了解这个方案的核心特性和能力边界,让你判断它是否适合你。
| 能力项 | 说明 |
|---|---|
| 项目类型 | 基于 Cloudflare Workers + R2 的 Serverless 图床 |
| 核心功能 | 通过 Web 页面上传图片,生成可公开访问的 CDN 链接 |
| 存储服务 | Cloudflare R2 (兼容 S3 API 的对象存储) |
| 计算服务 | Cloudflare Workers (无服务器函数) |
| 费用成本 | 完全免费(在 Cloudflare 免费额度内) |
| 存储额度 | 每月 10 GB 存储空间,100 万次 A 类操作 |
| 流量费用 | R2 流出流量免费, Workers 有免费请求次数 |
| 访问速度 | 依托 Cloudflare 全球 CDN,访问速度较快 |
| 数据持久性 | 数据存储在 Cloudflare R2,理论上永久保存 |
| 部署难度 | 中等,需要注册账号并完成多项配置 |
| 是否需要域名 | 必须,需要一个已添加到 Cloudflare 的域名 |
| 是否需要服务器 | 否,完全 Serverless |
| 是否支持 API | 是,Worker 本身提供上传 API |
| 是否支持批量 | 可通过脚本扩展,前端页面通常为单张上传 |
| 适合场景 | 个人博客图床、项目文档图片托管、小型应用静态资源托管 |
关键前提:你必须拥有一个自己的域名,并且能够将其 DNS 解析托管到 Cloudflare。这是整个方案能跑通的基石。
2. 适用场景与使用边界
2.1 谁适合使用这个方案?
- 个人技术博主/写作者:为 Markdown 博客、技术文档寻找稳定、高速的图片外链。
- 开源项目维护者:托管项目 README、Wiki 中的图片,避免使用可能失效的第三方图床。
- 前端/全栈开发者:学习 Serverless 架构和 Cloudflare 生态的实践案例。
- 小型项目或产品:需要托管用户头像、文章封面等轻量级图片资源,且预算有限。
2.2 它能解决什么问题?
- 外链稳定性:自建服务,链接控制在自己手中,避免因第三方图床关闭或规则变动导致图片失效(“图床挂了”)。
- 访问速度:Cloudflare 全球 CDN 加速,图片加载速度快。
- 零成本运营:在免费额度内,无需支付任何存储、流量或服务器费用。
- 管理自主:可以自行管理上传的图片,设置清理策略(需自行实现)。
2.3 不适合什么场景?
- 超大流量或存储需求:如果每月图片请求量巨大或存储超过 10GB,可能会触及免费额度上限,产生费用。
- 敏感或私密图片:这是一个公开图床,上传的图片链接是公开可访问的。切勿上传个人隐私、身份证件、商业机密等敏感图片。
- 需要复杂图片处理:如图片压缩、裁剪、水印、格式转换等。本方案核心是存储和分发,图片处理功能需要额外开发。
- 无法使用 Cloudflare 的地区或网络:由于服务完全构建在 Cloudflare 上,其可用性受本地网络环境影响。
2.4 合规与安全边界
- 版权与内容:你应对上传的内容负全部责任。确保你拥有图片的版权或上传权,禁止上传侵权、色情、暴力等违法内容。Cloudflare 有权根据其服务条款处理违规内容。
- 公开性:所有上传的图片默认是公开的。任何获得链接的人都可以访问。没有内置的权限验证机制。
- 防滥用:基础版本没有上传鉴权。这意味着如果你的上传接口地址泄露,他人也可能上传内容。生产环境使用建议增加简单的 Token 验证。
3. 环境准备与前置条件
开始部署前,请确保满足以下所有条件。
3.1 账号与域名准备
- Cloudflare 账号:访问 Cloudflare 官网 注册一个免费账号。
- 自有域名:你需要拥有一个域名(例如
yourdomain.com)。可以在任何域名注册商购买。 - 将域名接入 Cloudflare:这是最关键的一步。在你的 Cloudflare 控制台,点击“添加站点”,输入你的域名,按照指引更改域名的 DNS 服务器为 Cloudflare 提供的地址。此过程通常需要几分钟到几小时生效。务必等待域名状态在 Cloudflare 中显示为“有效”。
3.2 本地开发环境(可选但推荐)
虽然大部分操作在网页完成,但本地环境有助于测试和代码管理。
- Node.js 环境:用于使用 Wrangler CLI 工具。建议安装 LTS 版本(如 v18+)。
- 代码编辑器:如 VS Code。
- Git:用于版本管理(可选)。
3.3 知识准备
- 对 HTTP 请求、JSON 数据格式有基本了解。
- 能简单阅读和修改 JavaScript 代码。
- 会使用浏览器开发者工具查看网络请求。
4. 安装部署与启动方式
整个部署流程分为几个清晰的步骤:创建 R2 存储桶、创建 Worker、编写并部署代码、绑定自定义域名。
4.1 第一步:创建 Cloudflare R2 存储桶
R2 是我们存放图片的地方。
- 登录 Cloudflare 仪表板,在左侧菜单找到“R2”。
- 点击“创建存储桶”。
- 输入一个唯一的存储桶名称,例如
my-image-bed。名称全局唯一,请自定义。 - 其他设置保持默认,点击创建。
- 创建成功后,进入该存储桶,我们需要获取一个关键信息:存储桶的公开访问域名。在设置中,找到“公开访问”或“自定义域名”相关选项。Cloudflare 会为每个桶分配一个形如
https://pub-<bucket-id>.r2.dev的临时域名。记下这个域名,后续 Worker 代码中会用到。
4.2 第二步:创建 Cloudflare Worker
Worker 是我们的处理逻辑。
- 在 Cloudflare 仪表板,左侧菜单找到“Workers 和 Pages”。
- 点击“创建应用程序”->“创建 Worker”。
- 给 Worker 起个名字,例如
image-upload-worker。这决定了 Worker 的默认访问子域(如image-upload-worker.<你的子域>.workers.dev)。 - 点击“部署”后,会进入 Worker 的快速编辑界面。我们先不在这里写代码,直接点击右上角“编辑代码”旁边的“配置”按钮(或从 Workers 列表进入该 Worker 的“设置”)。
4.3 第三步:绑定 R2 存储桶到 Worker
为了让 Worker 能读写我们刚创建的 R2 桶,需要建立绑定。
- 在 Worker 的“设置”页面,找到“绑定”选项卡。
- 点击“添加绑定”,选择“R2 存储桶绑定”。
- 变量名称:填写一个在代码中使用的变量名,例如
MY_BUCKET。注意大小写。 - R2 存储桶:选择你刚才创建的存储桶(如
my-image-bed)。 - 点击“保存”。
4.4 第四步:编写并部署 Worker 代码
现在我们来编写核心的 Worker 脚本。它需要处理两件事:1. 提供一个上传图片的 API;2. 提供一个简单的前端上传页面。
返回到 Worker 的“编辑代码”界面。将默认代码全部替换为以下内容:
// 定义允许上传的 MIME 类型 const ALLOWED_MIME_TYPES = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml' ]; // 定义最大文件大小(例如 5MB) const MAX_FILE_SIZE = 5 * 1024 * 1024; // 简单的 HTML 上传页面 const uploadPage = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>免费图床上传</title> <style> body { font-family: sans-serif; max-width: 600px; margin: 2em auto; padding: 20px; } .container { border: 1px solid #ddd; padding: 20px; border-radius: 8px; } input, button { margin: 10px 0; padding: 10px; width: 100%; box-sizing: border-box; } #result { margin-top: 20px; padding: 15px; background: #f0f0f0; border-radius: 5px; word-break: break-all; } #preview { max-width: 100%; margin-top: 10px; } </style> </head> <body> <div class="container"> <h2>上传图片</h2> <input type="file" id="fileInput" accept="image/*"> <button onclick="uploadImage()">上传</button> <div id="result"></div> <img id="preview" style="display:none;"> </div> <script> async function uploadImage() { const fileInput = document.getElementById('fileInput'); const resultDiv = document.getElementById('result'); const previewImg = document.getElementById('preview'); if (!fileInput.files || fileInput.files.length === 0) { resultDiv.innerHTML = '<span style="color:red;">请选择文件</span>'; return; } const file = fileInput.files[0]; // 预览 previewImg.style.display = 'block'; previewImg.src = URL.createObjectURL(file); const formData = new FormData(); formData.append('file', file); resultDiv.innerHTML = '上传中...'; try { // 请求当前 Worker 自身的 /upload 路径 const response = await fetch('/upload', { method: 'POST', body: formData }); const data = await response.json(); if (response.ok) { const url = data.url; resultDiv.innerHTML = \` <strong>上传成功!</strong><br> <strong>图片链接:</strong><br> <input type="text" value="\${url}" readonly style="width:100%; padding:5px; margin:5px 0;"><br> <strong>Markdown:</strong><br> <input type="text" value="" readonly style="width:100%; padding:5px; margin:5px 0;"><br> <strong>HTML:</strong><br> <input type="text" value='<img src="\${url}" alt="image">' readonly style="width:100%; padding:5px; margin:5px 0;"> \`; } else { resultDiv.innerHTML = \`<span style="color:red;">上传失败:\${data.error || '未知错误'}</span>\`; } } catch (error) { resultDiv.innerHTML = \`<span style="color:red;">请求失败:\${error.message}</span>\`; } } </script> </body> </html> `; export default { async fetch(request, env) { const url = new URL(request.url); const path = url.pathname; const method = request.method; // 1. 处理根路径,返回上传页面 if (path === '/' && method === 'GET') { return new Response(uploadPage, { headers: { 'Content-Type': 'text/html;charset=UTF-8' }, }); } // 2. 处理上传请求 if (path === '/upload' && method === 'POST') { try { const contentType = request.headers.get('content-type') || ''; if (!contentType.includes('multipart/form-data')) { return new Response(JSON.stringify({ error: 'Content-Type must be multipart/form-data' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } const formData = await request.formData(); const file = formData.get('file'); if (!file || typeof file === 'string') { return new Response(JSON.stringify({ error: 'No file uploaded' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // 检查文件类型和大小 if (!ALLOWED_MIME_TYPES.includes(file.type)) { return new Response(JSON.stringify({ error: `File type not allowed. Allowed: ${ALLOWED_MIME_TYPES.join(', ')}` }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } if (file.size > MAX_FILE_SIZE) { return new Response(JSON.stringify({ error: `File too large. Max size: ${MAX_FILE_SIZE / 1024 / 1024}MB` }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // 生成唯一文件名(时间戳+随机数+原后缀) const fileExt = file.name.split('.').pop() || 'bin'; const timestamp = Date.now(); const randomStr = Math.random().toString(36).substring(2, 8); const fileName = \`\${timestamp}_\${randomStr}.\${fileExt}\`; // 将文件存入 R2 // 注意:这里的 `env.MY_BUCKET` 必须与你在“绑定”中设置的变量名完全一致 await env.MY_BUCKET.put(fileName, file.stream(), { httpMetadata: { contentType: file.type, }, }); // 构建图片访问 URL // 注意:这里使用你的 R2 存储桶的公开访问域名,或者你绑定的自定义域名 // 假设你的 R2 公开域名是 https://pub-xxxxxx.r2.dev const imageUrl = \`https://pub-xxxxxx.r2.dev/\${fileName}\`; // !!!需要修改!!! return new Response(JSON.stringify({ success: true, fileName: fileName, url: imageUrl }), { headers: { 'Content-Type': 'application/json' }, }); } catch (error) { console.error('Upload error:', error); return new Response(JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 3. 其他路径返回 404 return new Response('Not Found', { status: 404 }); }, };关键修改点: 找到代码中const imageUrl = \https://pub-xxxxxx.r2.dev/${fileName}`;这一行。将https://pub-xxxxxx.r2.dev` 替换为你之前在4.1 步骤中记下的 R2 存储桶的公开访问域名。
4.5 第五步:部署与测试 Worker
- 点击编辑器上方的“保存并部署”按钮。
- 部署成功后,你会看到一个 Worker 的默认访问地址,例如
https://image-upload-worker.<你的子域>.workers.dev。 - 在浏览器中访问这个地址。你应该能看到一个简单的图片上传界面。
- 尝试选择一张图片并点击上传。如果一切正常,页面会返回成功信息,并显示图片链接、Markdown 和 HTML 代码。
注意:第一次上传可能会失败,提示403或NoSuchKey错误。这是因为 R2 存储桶的公开访问策略可能未生效,或者 Worker 与 R2 的绑定权限需要一点时间同步。等待一两分钟再试,或者去 R2 存储桶的设置里确认公开访问已开启。
4.6 第六步:(高级)绑定自定义域名
使用workers.dev域名虽然可以,但不够“专属于自己”。我们可以绑定自己的域名。
- 在 Worker 的“设置”页面,找到“触发器”选项卡。
- 在“自定义域”部分,点击“添加自定义域”。
- 输入你想要用于图床的子域名,例如
img.yourdomain.com。请确保这个域名的 DNS 记录已经在 Cloudflare 管理,并且已开启代理(橙色云朵)。 - Cloudflare 会自动为你创建一条
CNAME记录指向你的 Worker。等待几分钟 DNS 生效。 - 生效后,你就可以通过
https://img.yourdomain.com来访问你的图床了。 - 重要:绑定自定义域名后,需要修改 Worker 代码中的
imageUrl。不再使用 R2 的pub-xxxxxx.r2.dev域名,而是使用你自己的域名,并让 Worker 或 R2 通过这个域名提供服务。更常见的做法是:为 R2 存储桶绑定自定义域名,然后在 Worker 代码中使用这个自定义域名。这需要在 R2 存储桶的设置中配置“自定义域”。
5. 功能测试与效果验证
部署完成后,需要进行全面的测试以确保图床工作正常。
5.1 基础功能测试:上传与访问
- 访问上传页面:在浏览器中打开你的 Worker 地址(或自定义域名)。
- 选择图片:点击选择文件,挑一张小于 5MB 的 JPG/PNG 图片。
- 点击上传:观察页面状态。成功应显示“上传成功”及各类链接。
- 验证链接:
- 直接访问:在新标签页中打开返回的图片链接,应能正常显示图片。
- Markdown 测试:将返回的 Markdown 代码
粘贴到支持 Markdown 的编辑器(如 Typora、Obsidian)或在线平台预览,查看图片是否正常渲染。 - HTML 测试:将返回的 HTML 代码
<img src="图片链接" alt="image">保存为一个.html文件并用浏览器打开,查看图片是否加载。
5.2 异常情况测试
- 上传非图片文件:尝试上传一个
.txt或.pdf文件。预期结果:页面应提示“File type not allowed”。 - 上传超大图片:尝试上传一个超过 5MB 的图片。预期结果:页面应提示“File too large”。
- 空文件上传:不选择文件直接点击上传按钮。预期结果:页面应提示“请选择文件”。
- 重复上传:多次上传同一张或不同的图片。预期结果:每次都应成功,且生成的文件名不同(时间戳和随机数保证唯一性)。
5.3 API 接口测试
我们的 Worker 也提供了纯 API 接口,方便其他程序调用。 使用curl命令或 Postman 进行测试:
# 使用 curl 测试上传,假设你的 Worker 地址是 https://img.yourdomain.com curl -X POST https://img.yourdomain.com/upload \ -F "file=@/path/to/your/image.jpg" \ -H "Content-Type: multipart/form-data"预期返回一个 JSON 响应:
{ "success": true, "fileName": "1712345678901_abc123.jpg", "url": "https://pub-xxxxxx.r2.dev/1712345678901_abc123.jpg" }5.4 存储验证
登录 Cloudflare 控制台,进入你的 R2 存储桶。你应该能看到刚刚上传的图片文件,文件名符合时间戳_随机数.后缀的格式。可以在这里进行文件管理,如下载或删除。
6. 接口 API 与批量任务
6.1 API 接口规范
我们的 Worker 提供了一个简单的 RESTful 接口:
- 端点:
POST /upload - Content-Type:
multipart/form-data - 参数:一个名为
file的表单字段,值为图片文件。 - 响应:成功返回 JSON 包含
url;失败返回 JSON 包含error。
6.2 使用 Python 脚本进行批量上传
虽然前端页面只支持单张上传,但我们可以很容易地编写脚本实现批量上传。
import requests import os from pathlib import Path def batch_upload_images(worker_url, image_folder): """ 批量上传文件夹内的图片到图床 :param worker_url: 你的 Worker 上传地址,如 https://img.yourdomain.com/upload :param image_folder: 本地图片文件夹路径 """ allowed_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'} image_folder = Path(image_folder) for image_file in image_folder.iterdir(): if image_file.is_file() and image_file.suffix.lower() in allowed_extensions: try: with open(image_file, 'rb') as f: files = {'file': (image_file.name, f, 'image/jpeg' if image_file.suffix in ['.jpg', '.jpeg'] else 'image/png')} response = requests.post(worker_url, files=files) data = response.json() if response.status_code == 200 and data.get('success'): print(f"[成功] {image_file.name} -> {data['url']}") # 可以将映射关系保存到文件,方便管理 with open('upload_log.txt', 'a') as log: log.write(f"{image_file.name},{data['url']}\n") else: print(f"[失败] {image_file.name}: {data.get('error', 'Unknown error')}") except Exception as e: print(f"[异常] {image_file.name}: {e}") if __name__ == '__main__': # 使用示例 YOUR_WORKER_UPLOAD_URL = "https://img.yourdomain.com/upload" # 请替换为你的地址 LOCAL_IMAGE_FOLDER = "./images_to_upload" # 请替换为你的本地文件夹路径 batch_upload_images(YOUR_WORKER_UPLOAD_URL, LOCAL_IMAGE_FOLDER)使用前注意:
- 安装
requests库:pip install requests。 - 将
YOUR_WORKER_UPLOAD_URL和LOCAL_IMAGE_FOLDER替换为你自己的值。 - 脚本会遍历文件夹,上传所有支持的图片,并将结果记录到
upload_log.txt。
6.3 扩展思路:增加鉴权
目前 API 是公开的,存在被滥用的风险。一个简单的改进方法是增加一个上传密钥(Token)验证。
- 修改 Worker 代码:在
/upload接口处理开始时,检查请求头或 URL 参数中是否包含预设的 Token。const UPLOAD_TOKEN = 'YOUR_SECRET_TOKEN_HERE'; // 设置一个复杂的令牌 if (request.headers.get('X-Upload-Token') !== UPLOAD_TOKEN) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 }); } - 修改上传脚本:在请求时带上这个 Token。
headers = {'X-Upload-Token': 'YOUR_SECRET_TOKEN_HERE'} response = requests.post(worker_url, files=files, headers=headers)
7. 资源占用与性能观察
由于本方案完全基于 Serverless 服务(Cloudflare Workers + R2),因此不存在传统意义上的服务器资源占用(CPU、内存、带宽)问题。你的关注点应放在 Cloudflare 平台的免费额度使用情况和性能表现上。
7.1 免费额度与成本监控
- Workers 请求次数:免费计划每日 100,000 次请求。对于个人图床,通常足够。
- Workers CPU 时间:免费计划每日 10 毫秒/请求的 CPU 时间。简单的图片接收和存储操作消耗极低。
- R2 存储:每月 10 GB 存储空间。注意清理无用图片。
- R2 操作次数:每月 100 万次 A 类操作(写、列),1000 万次 B 类操作(读)。上传是 A 类,访问是 B 类。
- R2 流出流量:免费。这是最大的优势之一,图片被访问不产生流量费用。
监控方法:定期登录 Cloudflare 仪表板,在“Workers 和 Pages”及“R2”页面查看使用量统计,确保在免费额度内。
7.2 性能观察
- 上传速度:取决于你的本地网络到 Cloudflare 边缘节点的速度,通常很快。
- 访问速度:图片通过 Cloudflare 全球 CDN 分发,首次访问可能需回源 R2 获取,后续访问会被缓存,速度极快。你可以通过浏览器开发者工具的“网络”选项卡查看图片加载的
timing信息。 - 可用性:Cloudflare 的服务 SLA 很高,但免费服务不提供 SLA 保证。对于个人使用,其稳定性完全足够。
8. 常见问题与排查方法
部署和使用过程中可能会遇到一些问题,以下是常见问题的排查思路。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
上传失败,提示403或NoSuchKey | 1. R2 存储桶未开启公开访问。 2. Worker 绑定的 R2 变量名与代码中使用的 env.XXX不匹配。3. R2 公开域名拼写错误。 | 1. 检查 R2 存储桶设置中的“公开访问”。 2. 核对 Worker 设置中的“绑定”变量名和代码中的 env.MY_BUCKET。3. 检查代码中 imageUrl的域名是否正确。 | 1. 开启 R2 存储桶公开访问。 2. 修改变量名使其一致。 3. 修正代码中的域名。 |
访问 Worker 地址显示错误页面或1101 | Worker 代码部署失败或运行时错误。 | 1. 进入 Worker 编辑界面,查看代码是否有语法错误。 2. 点击“日志”选项卡,查看实时日志。 | 1. 修正 JavaScript 语法错误。 2. 根据日志信息排查逻辑错误。 |
| 自定义域名绑定失败 | 1. 域名未成功接入 Cloudflare(状态不是“有效”)。 2. DNS 记录未生效或未开启代理(云朵不是橙色)。 | 1. 在 Cloudflare 仪表板检查域名状态。 2. 检查该子域名(如 img)的 DNS 记录是否正确。 | 1. 完成域名接入流程。 2. 确保 DNS 记录已开启代理,等待生效。 |
| 上传时提示“File type not allowed” | 上传了不在ALLOWED_MIME_TYPES列表中的文件类型。 | 检查文件的实际 MIME 类型。 | 1. 确保上传的是允许的图片格式。 2. 如需支持更多格式,修改 Worker 代码中的 ALLOWED_MIME_TYPES数组。 |
| 上传时提示“File too large” | 文件大小超过MAX_FILE_SIZE限制(默认 5MB)。 | 确认图片文件大小。 | 1. 压缩图片后再上传。 2. 如需调整,修改 Worker 代码中的 MAX_FILE_SIZE值。 |
| 图片链接可以访问,但博客中不显示 | 1. 某些平台屏蔽了非 HTTPS 或非常见域名的外链。 2. R2 的公开域名可能被某些网络环境屏蔽。 | 1. 确认图片链接是https://开头。2. 尝试在不同网络环境下访问该链接。 | 1.最佳实践:为 R2 绑定自定义域名(如cdn.yourdomain.com),并使用该域名。这能极大提高链接接受度。 |
| 批量上传脚本部分失败 | 1. 网络波动。 2. 单个文件触发大小或类型限制。 3. Worker 请求频率限制。 | 查看脚本打印的错误信息。 | 1. 为脚本添加重试机制。 2. 预处理图片,确保符合要求。 3. 在批量任务中加入延时(如 time.sleep(0.5))。 |
9. 最佳实践与使用建议
为了让你的免费图床更稳定、安全、好用,遵循以下建议:
- 务必绑定自定义域名:不要长期使用
workers.dev或r2.dev的子域名。为自己的图床服务绑定一个专属子域名(如img.xxx.com或cdn.xxx.com),并将其用于最终图片链接。这更专业,且能避免某些平台对非常见域名的屏蔽。 - 开启 R2 存储桶的公开访问:这是图片能被直接访问的前提。在 R2 存储桶设置中确认已开启。
- 实现简单的清理机制:免费额度有 10GB 限制。可以定期登录 R2 控制台手动清理,或编写一个简单的 Worker 定时任务(通过 Cron Trigger)来删除过期文件。
- 为上传 API 添加鉴权:如6.3节所述,至少增加一个 Token 验证,防止接口被恶意滥用。
- 备份你的 Worker 代码:将最终的 Worker 代码保存到本地或 Git 仓库中,防止误操作丢失。
- 管理你的图片:建议建立一个简单的索引文件或数据库,记录上传的文件名、原图名、上传时间、URL 等信息,方便后续查找和管理。
- 注意文件命名:当前的代码使用时间戳和随机数生成文件名,避免了冲突。不要修改为使用原文件名,否则可能因重名导致文件覆盖。
- 合规使用:再次强调,只上传你拥有合法权利的图片。这是你个人的服务,你需对内容负责。
10. 总结与下一步
基于 Cloudflare Workers 和 R2 搭建图床,是一个将 Serverless 理念落地的绝佳实践。它真正实现了“零服务器运维、零基础费用、全球高速访问”的目标,特别适合个人开发者和小型项目。
通过本文,你应该已经完成了从环境准备、服务创建、代码部署到功能测试的全过程。你现在拥有的是一个功能完整、可用的图床。接下来,你可以根据自身需求进行深度定制:
- 美化前端界面:修改 Worker 代码中的
uploadPageHTML 和 CSS,打造更美观的上传页面。 - 增加图片管理功能:在 Worker 中新增
/list、/delete等端点,并配套制作一个管理页面,实现网页端查看和删除图片。 - 集成图片处理:利用 Cloudflare 的 Image Resizing 服务,在返回的 URL 中加入参数实现实时缩略图、格式转换、水印等功能。
- 接入博客系统:为你使用的静态博客生成器(如 Hexo、Hugo)或动态博客(如 WordPress)编写插件,实现一键上传并插入图片。
这个方案的扩展性很强,核心逻辑稳定后,外围功能可以随需添加。建议你先稳定使用基础版本,熟悉整个流程和数据流向,再逐步尝试高级功能。