ByteNoteByteNote
用 Hono、Workers、R2 搭边缘图片上传 API
字

字节笔记本

2026年10月6日 · 约 9 分钟读完

用 Hono、Workers、R2 搭边缘图片上传 API

API中转
¥120

现在越来越多的个人项目选择把后端整体搬进 Cloudflare 边缘网络:Workers 负责无服务器计算,R2 负责对象存储,D1 负责 SQL 数据库,Hono 则是这套体系里最流行的 Web 框架。它的思路很清晰:不租服务器,不维护容器,代码跑在离用户最近的边缘节点上,天然具备全球分发和自动扩缩容的能力。

这篇文章以一个最小可用的图片上传服务为例,把这条链路完整走一遍:从项目初始化、绑定配置,到接口实现、本地调试,最后部署上线。

技术选型:四件套各自负责什么

Hono 是为 Workers 设计的轻量 Web 框架,体积小、启动快,提供类似 Express 的中间件与路由写法,bearerAuth、logger 这类常用中间件开箱即用。

Cloudflare Workers 是跑在边缘的无服务器函数,运行时基于 V8 引擎,请求处理延迟极低,免费额度足够个人项目使用。

R2 是 S3 兼容的对象存储,与 Workers 同属边缘体系,通过绑定直接读写,也没有出口流量费。

D1 是基于 SQLite 的边缘 SQL 数据库,适合存结构化元数据。本例中它不是必需品,但配置先留好,为后续扩展做准备。

Hono 加 Cloudflare Workers 加 R2 加 D1 的边缘全栈架构图

初始化项目

先全局安装 Wrangler CLI,这是 Cloudflare 官方的开发和部署工具:

bash
npm install -g wrangler

用官方模板在当前目录创建一个 Hono 项目:

bash
npm init hono .

再补齐本次要用的依赖:

bash
npm install hono nanoid @cloudflare/workers-types

其中 nanoid 用来给上传文件生成唯一文件名,workers-types 提供 Workers 环境的 TypeScript 类型定义。

配置 wrangler.toml 与 Bindings

Bindings 是 Workers 连接外部资源的桥梁:在 wrangler.toml 里声明绑定,部署后代码里通过 c.env 就能直接访问对应的资源或变量,不需要自己管理连接串和密钥。

toml
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 如下:

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 才能通过。

图片上传接口从鉴权到写 R2 再返回 URL 的处理流程

上传接口的处理过程值得拆开看: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 时编辑器能直接补全和纠错。

本地调试与部署

代码写完先在本地验证:

bash
wrangler dev

它会在本地起一个模拟 Workers 运行时的服务,上传逻辑和绑定都能正常联调。测试通过后一条命令部署:

bash
wrangler publish

部署完成,这个服务就跑在了 Cloudflare 的边缘网络上:接口代码在离用户最近的节点执行,文件落在全球冗余存储的 R2 桶里。

可以继续做的扩展

这个例子搭好了骨架,往上叠功能都很自然:用 D1 存上传记录,文件名、大小、上传时间进 SQL 表,正好用上已声明的 DB 绑定;接口层补上列表和删除端点,把服务升级成完整的图床后台;上传时在 Worker 里实时生成缩略图或转换格式;再配一个前端页面,用 Cloudflare Pages 托管,从 Git 仓库自动构建部署,还能配合 WAF 和 DDoS 防护守住接口入口。

Hono、Workers、R2、D1 这套组合的价值在于:基础设施全部交给平台接管,开发者只需要关心路由和业务逻辑本身。对个人项目和中小团队来说,这是目前上手成本最低的边缘全栈方案之一。

相关文章

分享: