news 2026/9/5 10:31:22

基于Cloudflare Workers与R2构建免费图床:Serverless实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Cloudflare Workers与R2构建免费图床:Serverless实践指南

这次我们来看一个完全免费、永久可用的公开图床方案,核心是利用 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 它能解决什么问题?

  1. 外链稳定性:自建服务,链接控制在自己手中,避免因第三方图床关闭或规则变动导致图片失效(“图床挂了”)。
  2. 访问速度:Cloudflare 全球 CDN 加速,图片加载速度快。
  3. 零成本运营:在免费额度内,无需支付任何存储、流量或服务器费用。
  4. 管理自主:可以自行管理上传的图片,设置清理策略(需自行实现)。

2.3 不适合什么场景?

  1. 超大流量或存储需求:如果每月图片请求量巨大或存储超过 10GB,可能会触及免费额度上限,产生费用。
  2. 敏感或私密图片:这是一个公开图床,上传的图片链接是公开可访问的。切勿上传个人隐私、身份证件、商业机密等敏感图片
  3. 需要复杂图片处理:如图片压缩、裁剪、水印、格式转换等。本方案核心是存储和分发,图片处理功能需要额外开发。
  4. 无法使用 Cloudflare 的地区或网络:由于服务完全构建在 Cloudflare 上,其可用性受本地网络环境影响。

2.4 合规与安全边界

  • 版权与内容:你应对上传的内容负全部责任。确保你拥有图片的版权或上传权,禁止上传侵权、色情、暴力等违法内容。Cloudflare 有权根据其服务条款处理违规内容。
  • 公开性:所有上传的图片默认是公开的。任何获得链接的人都可以访问。没有内置的权限验证机制。
  • 防滥用:基础版本没有上传鉴权。这意味着如果你的上传接口地址泄露,他人也可能上传内容。生产环境使用建议增加简单的 Token 验证。

3. 环境准备与前置条件

开始部署前,请确保满足以下所有条件。

3.1 账号与域名准备

  1. Cloudflare 账号:访问 Cloudflare 官网 注册一个免费账号。
  2. 自有域名:你需要拥有一个域名(例如yourdomain.com)。可以在任何域名注册商购买。
  3. 将域名接入 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 是我们存放图片的地方。

  1. 登录 Cloudflare 仪表板,在左侧菜单找到“R2”
  2. 点击“创建存储桶”
  3. 输入一个唯一的存储桶名称,例如my-image-bed。名称全局唯一,请自定义。
  4. 其他设置保持默认,点击创建。
  5. 创建成功后,进入该存储桶,我们需要获取一个关键信息:存储桶的公开访问域名。在设置中,找到“公开访问”或“自定义域名”相关选项。Cloudflare 会为每个桶分配一个形如https://pub-<bucket-id>.r2.dev的临时域名。记下这个域名,后续 Worker 代码中会用到。

4.2 第二步:创建 Cloudflare Worker

Worker 是我们的处理逻辑。

  1. 在 Cloudflare 仪表板,左侧菜单找到“Workers 和 Pages”
  2. 点击“创建应用程序”->“创建 Worker”
  3. 给 Worker 起个名字,例如image-upload-worker。这决定了 Worker 的默认访问子域(如image-upload-worker.<你的子域>.workers.dev)。
  4. 点击“部署”后,会进入 Worker 的快速编辑界面。我们先不在这里写代码,直接点击右上角“编辑代码”旁边的“配置”按钮(或从 Workers 列表进入该 Worker 的“设置”)。

4.3 第三步:绑定 R2 存储桶到 Worker

为了让 Worker 能读写我们刚创建的 R2 桶,需要建立绑定。

  1. 在 Worker 的“设置”页面,找到“绑定”选项卡。
  2. 点击“添加绑定”,选择“R2 存储桶绑定”
  3. 变量名称:填写一个在代码中使用的变量名,例如MY_BUCKET注意大小写
  4. R2 存储桶:选择你刚才创建的存储桶(如my-image-bed)。
  5. 点击“保存”。

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="![](\${url})" 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

  1. 点击编辑器上方的“保存并部署”按钮。
  2. 部署成功后,你会看到一个 Worker 的默认访问地址,例如https://image-upload-worker.<你的子域>.workers.dev
  3. 在浏览器中访问这个地址。你应该能看到一个简单的图片上传界面。
  4. 尝试选择一张图片并点击上传。如果一切正常,页面会返回成功信息,并显示图片链接、Markdown 和 HTML 代码。

注意:第一次上传可能会失败,提示403NoSuchKey错误。这是因为 R2 存储桶的公开访问策略可能未生效,或者 Worker 与 R2 的绑定权限需要一点时间同步。等待一两分钟再试,或者去 R2 存储桶的设置里确认公开访问已开启。

4.6 第六步:(高级)绑定自定义域名

使用workers.dev域名虽然可以,但不够“专属于自己”。我们可以绑定自己的域名。

  1. 在 Worker 的“设置”页面,找到“触发器”选项卡。
  2. 在“自定义域”部分,点击“添加自定义域”
  3. 输入你想要用于图床的子域名,例如img.yourdomain.com。请确保这个域名的 DNS 记录已经在 Cloudflare 管理,并且已开启代理(橙色云朵)。
  4. Cloudflare 会自动为你创建一条CNAME记录指向你的 Worker。等待几分钟 DNS 生效。
  5. 生效后,你就可以通过https://img.yourdomain.com来访问你的图床了。
  6. 重要:绑定自定义域名后,需要修改 Worker 代码中的imageUrl。不再使用 R2 的pub-xxxxxx.r2.dev域名,而是使用你自己的域名,并让 Worker 或 R2 通过这个域名提供服务。更常见的做法是:为 R2 存储桶绑定自定义域名,然后在 Worker 代码中使用这个自定义域名。这需要在 R2 存储桶的设置中配置“自定义域”。

5. 功能测试与效果验证

部署完成后,需要进行全面的测试以确保图床工作正常。

5.1 基础功能测试:上传与访问

  1. 访问上传页面:在浏览器中打开你的 Worker 地址(或自定义域名)。
  2. 选择图片:点击选择文件,挑一张小于 5MB 的 JPG/PNG 图片。
  3. 点击上传:观察页面状态。成功应显示“上传成功”及各类链接。
  4. 验证链接
    • 直接访问:在新标签页中打开返回的图片链接,应能正常显示图片。
    • Markdown 测试:将返回的 Markdown 代码![图片](图片链接)粘贴到支持 Markdown 的编辑器(如 Typora、Obsidian)或在线平台预览,查看图片是否正常渲染。
    • HTML 测试:将返回的 HTML 代码<img src="图片链接" alt="image">保存为一个.html文件并用浏览器打开,查看图片是否加载。

5.2 异常情况测试

  1. 上传非图片文件:尝试上传一个.txt.pdf文件。预期结果:页面应提示“File type not allowed”。
  2. 上传超大图片:尝试上传一个超过 5MB 的图片。预期结果:页面应提示“File too large”。
  3. 空文件上传:不选择文件直接点击上传按钮。预期结果:页面应提示“请选择文件”。
  4. 重复上传:多次上传同一张或不同的图片。预期结果:每次都应成功,且生成的文件名不同(时间戳和随机数保证唯一性)。

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-Typemultipart/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)

使用前注意

  1. 安装requests库:pip install requests
  2. YOUR_WORKER_UPLOAD_URLLOCAL_IMAGE_FOLDER替换为你自己的值。
  3. 脚本会遍历文件夹,上传所有支持的图片,并将结果记录到upload_log.txt

6.3 扩展思路:增加鉴权

目前 API 是公开的,存在被滥用的风险。一个简单的改进方法是增加一个上传密钥(Token)验证。

  1. 修改 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 }); }
  2. 修改上传脚本:在请求时带上这个 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. 常见问题与排查方法

部署和使用过程中可能会遇到一些问题,以下是常见问题的排查思路。

问题现象可能原因排查方式解决方案
上传失败,提示403NoSuchKey1. R2 存储桶未开启公开访问。
2. Worker 绑定的 R2 变量名与代码中使用的env.XXX不匹配。
3. R2 公开域名拼写错误。
1. 检查 R2 存储桶设置中的“公开访问”。
2. 核对 Worker 设置中的“绑定”变量名和代码中的env.MY_BUCKET
3. 检查代码中imageUrl的域名是否正确。
1. 开启 R2 存储桶公开访问。
2. 修改变量名使其一致。
3. 修正代码中的域名。
访问 Worker 地址显示错误页面或1101Worker 代码部署失败或运行时错误。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. 最佳实践与使用建议

为了让你的免费图床更稳定、安全、好用,遵循以下建议:

  1. 务必绑定自定义域名:不要长期使用workers.devr2.dev的子域名。为自己的图床服务绑定一个专属子域名(如img.xxx.comcdn.xxx.com),并将其用于最终图片链接。这更专业,且能避免某些平台对非常见域名的屏蔽。
  2. 开启 R2 存储桶的公开访问:这是图片能被直接访问的前提。在 R2 存储桶设置中确认已开启。
  3. 实现简单的清理机制:免费额度有 10GB 限制。可以定期登录 R2 控制台手动清理,或编写一个简单的 Worker 定时任务(通过 Cron Trigger)来删除过期文件。
  4. 为上传 API 添加鉴权:如6.3节所述,至少增加一个 Token 验证,防止接口被恶意滥用。
  5. 备份你的 Worker 代码:将最终的 Worker 代码保存到本地或 Git 仓库中,防止误操作丢失。
  6. 管理你的图片:建议建立一个简单的索引文件或数据库,记录上传的文件名、原图名、上传时间、URL 等信息,方便后续查找和管理。
  7. 注意文件命名:当前的代码使用时间戳和随机数生成文件名,避免了冲突。不要修改为使用原文件名,否则可能因重名导致文件覆盖。
  8. 合规使用:再次强调,只上传你拥有合法权利的图片。这是你个人的服务,你需对内容负责。

10. 总结与下一步

基于 Cloudflare Workers 和 R2 搭建图床,是一个将 Serverless 理念落地的绝佳实践。它真正实现了“零服务器运维、零基础费用、全球高速访问”的目标,特别适合个人开发者和小型项目。

通过本文,你应该已经完成了从环境准备、服务创建、代码部署到功能测试的全过程。你现在拥有的是一个功能完整、可用的图床。接下来,你可以根据自身需求进行深度定制:

  • 美化前端界面:修改 Worker 代码中的uploadPageHTML 和 CSS,打造更美观的上传页面。
  • 增加图片管理功能:在 Worker 中新增/list/delete等端点,并配套制作一个管理页面,实现网页端查看和删除图片。
  • 集成图片处理:利用 Cloudflare 的 Image Resizing 服务,在返回的 URL 中加入参数实现实时缩略图、格式转换、水印等功能。
  • 接入博客系统:为你使用的静态博客生成器(如 Hexo、Hugo)或动态博客(如 WordPress)编写插件,实现一键上传并插入图片。

这个方案的扩展性很强,核心逻辑稳定后,外围功能可以随需添加。建议你先稳定使用基础版本,熟悉整个流程和数据流向,再逐步尝试高级功能。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 10:31:07

泪河高度自动测量:干眼筛查的新工具

系列三 临床应用泪河高度自动测量&#xff1a;干眼筛查的新工具干眼门诊是国内眼科增长最快的板块之一。泪河高度&#xff08;TMH&#xff09;是评估泪液储留的重要指标&#xff0c;传统测量依赖裂隙灯目测&#xff0c;主观性强。OPV-30 支持泪河高度非侵入式自动测量&#xf…

作者头像 李华
网站建设 2026/9/5 10:26:45

Three.js与WebGL实战:AI辅助开发3D网页游戏完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 10:26:42

AI Agent接管PR流程:从辅助写代码到自动合入的架构与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 10:23:03

汽车轮胎百年简史:从实心橡胶到充气胎的演化之路

今天你开车上路&#xff0c;轮胎碾过坑洼路面&#xff0c;车内只有轻微的震动。但如果你穿越回一百多年前&#xff0c;坐进一辆装有实心橡胶轮胎的汽车&#xff0c;感受会完全不同——颠簸、震动、噪音&#xff0c;像坐在一辆没有弹簧的板车上。从实心橡胶到充气轮胎&#xff0…

作者头像 李华
网站建设 2026/9/5 10:20:24

PlayStation肉鸽模式10月1日上线:可接悬赏任务、解锁新角色!

PlayStation肉鸽模式&#xff1a;玩法与上线信息PlayStation发布了即将推出的肉鸽模式&#xff08;roguelike mode&#xff09;的全新预告片。在这个模式里&#xff0c;玩家能够接受悬赏任务&#xff0c;追捕敌人并赚取奖励&#xff0c;还可以解锁包括境井仁&#xff08;Jin Sa…

作者头像 李华