
字节笔记本
2026年10月6日 · 约 9 分钟读完
用 Hono、Workers、R2 搭边缘图片上传 API
现在越来越多的个人项目选择把后端整体搬进 Cloudflare 边缘网络:Workers 负责无服务器计算,R2 负责对象存储,D1 负责 SQL 数据库,Hono 则是这套体系里最流行的 Web 框架。它的思路很清晰:不租服务器,不维护容器,代码跑在离用户最近的边缘节点上,天然具备全球分发和自动扩缩容的能力。
这篇文章以一个最小可用的图片上传服务为例,把这条链路完整走一遍:从项目初始化、绑定配置,到接口实现、本地调试,最后部署上线。
技术选型:四件套各自负责什么
Hono 是为 Workers 设计的轻量 Web 框架,体积小、启动快,提供类似 Express 的中间件与路由写法,bearerAuth、logger 这类常用中间件开箱即用。
Cloudflare Workers 是跑在边缘的无服务器函数,运行时基于 V8 引擎,请求处理延迟极低,免费额度足够个人项目使用。
R2 是 S3 兼容的对象存储,与 Workers 同属边缘体系,通过绑定直接读写,也没有出口流量费。
D1 是基于 SQLite 的边缘 SQL 数据库,适合存结构化元数据。本例中它不是必需品,但配置先留好,为后续扩展做准备。

初始化项目
先全局安装 Wrangler CLI,这是 Cloudflare 官方的开发和部署工具:
npm install -g wrangler用官方模板在当前目录创建一个 Hono 项目:
npm init hono .再补齐本次要用的依赖:
npm install hono nanoid @cloudflare/workers-types其中 nanoid 用来给上传文件生成唯一文件名,workers-types 提供 Workers 环境的 TypeScript 类型定义。
配置 wrangler.toml 与 Bindings
Bindings 是 Workers 连接外部资源的桥梁:在 wrangler.toml 里声明绑定,部署后代码里通过 c.env 就能直接访问对应的资源或变量,不需要自己管理连接串和密钥。
name = "my-app"
type = "javascript"
account_id = "<YOUR_ACCOUNT_ID>"
workers_dev = true
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "<YOUR_BUCKET_NAME>"
[[d1_databases]]
binding = "DB"
database_name = "my_db"
database_id = "<YOUR_DB_ID>"
[vars]
USERNAME = "<YOUR_USERNAME>"
PASSWORD = "<YOUR_PASSWORD>"binding = "MY_BUCKET" 里的名字,就是之后代码里 c.env.MY_BUCKET 的来源。TOKEN 和 HOST 这类敏感或环境相关的值,放在 src/config.ts 里单独管理再注入,避免硬编码进仓库。
实现服务端代码
完整的 src/index.ts 如下:
import { Hono } from 'hono'
import { bearerAuth } from "hono/bearer-auth";
import { logger } from 'hono/logger'
import { nanoid } from "nanoid";
import { HOST, TOKEN } from './config'
type Bindings = {
MY_BUCKET: R2Bucket
USERNAME: string
PASSWORD: string
}
const app = new Hono<{ Bindings: Bindings }>()
export const customLogger = (message: string, ...rest: string[]) => {
console.log(message, ...rest)
}
app.use(logger(customLogger))
app.use('/api/*', bearerAuth({ token: TOKEN }))
app.notFound((c) => {
return c.text('Not Found', 404)
})
app.get('/', (c) => {
return c.json({ ok: true })
})
app.post('/api/v1/upload', async (c) => {
const key = nanoid(10)
const formData = await c.req.parseBody()
const file = formData['file']
if (file instanceof File) {
const fileBuffer = await file.arrayBuffer()
const ext = file.name.split('.').pop()
const path = `images/${key}.${ext}`
await c.env.MY_BUCKET.put(path, fileBuffer)
return c.json({ image: { url: `${HOST}${path}` } })
} else {
return c.text('Invalid file', 400)
}
})
export default app整个服务只有两个路由:GET / 是健康检查,返回 ok: true 用于探活;POST /api/v1/upload 是带鉴权的上传接口,挂在 /api/* 中间件之后,请求必须携带正确的 Bearer Token 才能通过。

上传接口的处理过程值得拆开看:parseBody 解析 multipart/form-data 表单并取出 file 字段;instanceof File 校验不通过就直接返回 400;通过后用 nanoid 生成 10 位随机 key,从原文件名里取出扩展名,拼成 images/<key>.<ext> 这样的存储路径;arrayBuffer 读出二进制内容,调用 c.env.MY_BUCKET.put 写进 R2;最后把 HOST 加上路径拼成公网地址,包在 JSON 里返回给调用方。
TypeScript 在这里的价值是类型安全:Bindings 类型把 Worker 环境里可用的资源声明清楚,访问 c.env.MY_BUCKET 时编辑器能直接补全和纠错。
本地调试与部署
代码写完先在本地验证:
wrangler dev它会在本地起一个模拟 Workers 运行时的服务,上传逻辑和绑定都能正常联调。测试通过后一条命令部署:
wrangler publish部署完成,这个服务就跑在了 Cloudflare 的边缘网络上:接口代码在离用户最近的节点执行,文件落在全球冗余存储的 R2 桶里。
可以继续做的扩展
这个例子搭好了骨架,往上叠功能都很自然:用 D1 存上传记录,文件名、大小、上传时间进 SQL 表,正好用上已声明的 DB 绑定;接口层补上列表和删除端点,把服务升级成完整的图床后台;上传时在 Worker 里实时生成缩略图或转换格式;再配一个前端页面,用 Cloudflare Pages 托管,从 Git 仓库自动构建部署,还能配合 WAF 和 DDoS 防护守住接口入口。
Hono、Workers、R2、D1 这套组合的价值在于:基础设施全部交给平台接管,开发者只需要关心路由和业务逻辑本身。对个人项目和中小团队来说,这是目前上手成本最低的边缘全栈方案之一。



