我的博客原来有四组独立的 Next.js Route Handler:健康检查、搜索、内容附件和 MCP。代码并不多,但路由入口分散,缓存头和错误处理也各写各的。
我决定引入 Hono 时,最先遇到的不是路由怎么写,而是应该使用哪个适配器。项目由 Bun 运行,Hono 文档又给 Next.js 示例用了 hono/vercel,看起来似乎应该在 hono/bun 和 hono/vercel 之间选一个。
实际答案是:这里不需要二选一。Next.js 管服务器,Hono 只处理收到的标准 Request。
先确定谁拥有 HTTP Server
当前生产进程是:
tini -> bun server.jsserver.js 是 Next.js standalone 生成的入口。它负责页面渲染、静态资源、Route Handler、缓存和整个服务器生命周期。Bun 是执行这个入口的 JavaScript 运行时。
如果采用 Hono 的 Bun 启动方式,通常会导出:
export default {
port: 3000,
fetch: app.fetch,
};这代表 Hono 把 app.fetch 交给 Bun,让 Bun 创建 HTTP Server。它适合独立 Hono 服务,却不适合嵌在现有 Next.js 应用中。否则就要再启动一个端口,自己处理页面请求如何回到 Next.js,等于重写服务器边界。
用 catch-all Route Handler 接住 /api
Hono 的 Next.js 指南建议在 App Router 中创建:
app/api/[[...route]]/route.ts我也采用这个目录,但入口保持得很薄:
import { createApi } from '@/lib/api';
import { getBlogSearchPayload } from '@/lib/data';
import { mcpHandler } from '@/lib/mcp';
import { readPublicAttachment } from './read-public-attachment';
export const dynamic = 'force-dynamic';
export const runtime = 'nodejs';
const api = createApi({
getBlogSearchPayload,
handleMcp: mcpHandler,
readPublicAttachment,
});
const handler = (request: Request) => api.fetch(request);
export { handler as GET, handler as POST };Next.js 把请求交给 Route Handler,Route Handler 再交给 Hono。没有新端口,也没有第二个服务器。
为什么没有使用 hono/vercel
官方示例写的是:
import { handle } from 'hono/vercel';
export const GET = handle(app);
export const POST = handle(app);这个写法完全正确,也更接近 Hono 文档。不过我检查了项目安装的 Hono 4.13.5,当前 Vercel handler 对 App Router 的核心实现就是把 Request 传给 app.fetch()。
既然这个入口没有使用适配器的其他能力,我选择直接写:
const handler = (request: Request) => api.fetch(request);这不是说 hono/vercel 没用。部署环境需要它提供额外转换时应该继续用;这里只是没有必要给一层等价包装取一个容易让人误解为“必须部署到 Vercel”的名字。
Hono 核心只依赖 Web API
API 的依赖被定义为普通函数:
type ApiDependencies = {
getBlogSearchPayload: () => Promise<unknown>;
handleMcp: (request: Request) => Promise<Response>;
readPublicAttachment: (publicPath: string) => Promise<Blob | null>;
};路由本身只接触 Request、Response 和 Blob:
export function createApi({
getBlogSearchPayload,
handleMcp,
readPublicAttachment,
}: ApiDependencies) {
return new Hono()
.basePath('/api')
.get('/health', (context) => context.json({ ok: true }))
.get('/search', async (context) => {
context.header(
'Cache-Control',
'public, max-age=300, stale-while-revalidate=3600',
);
return context.json(await getBlogSearchPayload());
})
.on(['GET', 'POST'], '/mcp', (context) =>
handleMcp(context.req.raw),
);
}Hono 的 fetch() API面向标准 Fetch 接口。这个边界让测试很直接:创建 Hono app,传入假的搜索和 MCP 函数,然后用 api.request() 或 api.fetch() 发请求,不必启动 Next 开发服务器。
附件接口也适合交给 Hono
一开始我保留了原来的:
app/api/content-attachments/[access]/[...path]/route.ts后来发现这没有必要。附件本来就是一个 HTTP 边界,Hono 可以继续保持原 URL:
.get('/content-attachments/:access/:path{.+}', async (context) => {
if (context.req.param('access') !== 'public') {
return context.notFound();
}
const file = await readPublicAttachment(context.req.param('path'));
if (!file) {
return context.notFound();
}
return new Response(file, {
headers: {
'Cache-Control': 'public, max-age=31536000, immutable',
'Content-Type': file.type || 'application/octet-stream',
},
});
}):path{.+} 可以捕获包含斜杠的嵌套附件路径。access 仍先限制为 public,文件不存在则返回 404,缓存头和 MIME 与迁移前保持一致。
Bun.file() 留在存储适配层
本地读取函数只有一个职责:把公开路径解析成磁盘文件,再返回一个 Blob。
export async function readPublicAttachment(
publicPath: string,
): Promise<Blob | null> {
await contentMetadataReady;
const filePath = await resolvePublicContentAttachment(publicPath);
if (!filePath) return null;
return Bun.file(filePath);
}Bun.file() 出现在这里,不代表 Hono 核心绑定 Bun。它实现的是“当前容器怎样读取附件”。以后换成对象存储,只需要替换这个函数:
async function readPublicAttachment(path: string) {
return env.ATTACHMENTS.get(path);
}Hono 路由不需要知道文件来自本地磁盘还是对象存储。
这还不能叫边缘部署完成
Hono 使用 Web API,确实让核心路由更容易移植。但当前 Route Handler 明确使用 Next.js nodejs runtime,内容初始化会扫描本地 Markdown,MCP 依赖也没有在边缘平台验证。
所以我只把这次改动称为“留下了存储替换点”。要部署到边缘环境,还要处理:
- 内容在构建时生成还是放入对象存储。
- 附件使用 R2、S3 或其他 Blob 存储。
- MCP handler 是否使用目标平台不支持的 API。
- Next.js 部署适配器怎样生成和调用路由入口。
- 缓存是否能跨实例共享。
如果没有真实构建和部署结果,写“已经支持 Edge”只是在提前宣布成功。
验证时保留原来的 HTTP 契约
迁移后的测试没有检查文件名或 package script 字符串,而是实际请求:
GET /api/health返回{ "ok": true }。GET /api/search返回搜索数据和原缓存头。GET /api/content-attachments/public/...返回 JPEG 内容。- 非公开附件返回 404。
POST /api/mcp继续交给原 MCP handler。
生产构建的路由表只剩一个 /api/[[...route]] 动态入口,Docker 冒烟测试则验证了健康检查、搜索、附件和 MCP 初始化都返回 200。
这次引入 Hono 没有改变谁启动服务器,也没有为了未来的 Edge 想象重写内容系统。它只把分散的 API 收到一个可测试、可替换存储实现的 Web API 边界里。对这个博客来说,这已经够了。