实践: 使用 Hono 开发全栈应用

本文最后更新于:2025年8月7日 凌晨

场景

最近写了几个前后端都包含的应用,从最初的 Next.js 到后来的 SvelteKit,再到 Tanstack Router,终究不如熟悉的 Hono 框架那么好使。所有的 Web 元框架都在尝试将服务端加入到框架中,但没有一个做的足够好。例如 Cloudflare 上包含许多官方服务,作为一个服务端框架,Hono 的集成做的很棒,但 Web 元框架并非如此。

为什么使用 Hono

为什么 Web 元框架已经有服务端路由了,还要使用 Hono 呢?有几个原因

  • 抽象不一:每个元框架都有不同的语法和规则,例如 Next.js Server Components [1]、SvelteKit Server routing [2]、或者 TanStack Server Functions [3]
  • 功能残缺:处理简单的 JSON API?没问题。复杂的 API 结合 Cloudflare 多个服务?很困难。
  • 尺寸很大:元框架的 bundle size 非常庞大,即便以小巧著称的 SvelteKit 也有 132kb,而 Hono 构建后只有 18kb.

抽象不一

不管使用什么 Web 框架,Hono 的知识都是通用的。可以轻松的将 Hono 应用部署到任何地方,例如 Cloudflare、Vercel、Deno 等。而 Web 元框架。。。好吧,最好的说法是百花齐放。看几个例子

Next.js 声称在 React 组件中直接耦合数据库查询推荐的做法。

PHP:敢问今夕是何年?

1
2
3
4
5
6
7
8
9
10
11
12
import { db, posts } from '@/lib/db'

export default async function Page() {
const allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}

好吧,其实它也有 Route Handlers,像是下面这样。是的,需要 export 不同的函数来处理不同的请求,而路径则取决于文件相对路径。想要快速搜索特定路径的 API?抱歉,你需要在文件系统中找找看。

1
2
3
export async function GET() {
return Response.json({ message: 'Hello World' })
}

SvelteKit 也是类似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { error } from '@sveltejs/kit'
import type { RequestHandler } from './$types'

export const GET: RequestHandler = ({ url }) => {
const min = Number(url.searchParams.get('min') ?? '0')
const max = Number(url.searchParams.get('max') ?? '1')

const d = max - min

if (isNaN(d) || d < 0) {
error(400, 'min and max must be numbers, and min must be less than max')
}

const random = min + Math.random() * d

return new Response(String(random))
}

Tanstack 据称从 tRPC 得到了灵感,嗯。。。

1
2
3
4
5
6
// routes/hello.ts
export const ServerRoute = createServerFileRoute().methods({
GET: async ({ request }) => {
return new Response('Hello, World!')
},
})

好吧,它们有什么共通之处?嗯,显然基本概念是类似的,但除此之外?生态系统全部没有共享,想要连接 KV?数据库?OAuth2 登录?抱歉,你需要找到适合 Web 元框架的方法。

功能残缺

而且对于 Cloudflare 来说,Hono 的集成度相当高,包括 KV/D1、R2、Pages 等。而且对于其他服务端需要的功能,例如数据库、登录、OAuth2 以及测试集成都做的非常棒。

  • 数据库:对 D1/Postgresql 支持的都很好(不过推荐使用 Drizzle 而非 Prisma)[4]
  • 登录:支持 JWT 中间件,使用起来非常简单 [5]
  • OAuth2: 官方的 OAuth Providers [6] 比 Auth.js 和 Better Auth 更简单,也更容易理解和调试,它的黑盒部分较少,不关心数据如何存储
  • 测试:全面拥抱 vitest [7],某知名框架至今仍然优先支持 jest

尺寸很大

这是一个直观的对比,可以明显看到不管是构建时间还是最终 bundle 产物的大小差异都非常明显。

SvelteKit minimal
1753979669566.jpg

Hono starter
1753981327766.jpg

实现

谁在前面?

现在,同时使用 Hono 和 Web 元框架,例如 SvelteKit,来开发一个应用。问题来了,谁在前面?也就是说,Hono 在前面并转发路由,还是 SvelteKit 在前面并转发路由?由于下面几个特征,Hono 在前面会更好

  1. Hono 的代码更少,启动更快
  2. 元框架可能会有一些意外的行为,例如自动序列化所有 Response [8]
  3. 如果没有 SSR(例如 SPA/SSG),那么元框架根本不会有服务端代码

Hono 作为入口

现在,终于到了实现的部分,下面是 Hono 作为入口,静态资源转发到 SvelteKit 的静态产物。最终部署到 Cloudflare Workers 上。

首先确定静态资源在哪儿,例如在 SvelteKit 中,它是由 @sveltejs/adapter-cloudflare 插件配置的。例如下面配置的是 dist 目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// packages/client/svelte.config.js
const config = {
// other config...
kit: {
adapter: adapter({
pages: 'dist',
assets: 'dist',
fallback: undefined,
precompress: false,
strict: true,
}),
},
}

export default config

然后需要配置 wrangler.json 来将静态资源绑定到 ASSETS 上。例如下面配置的是 dist 目录。

1
2
3
4
5
6
7
8
9
10
// packages/server/wrangler.json
{
"name": "sveltekit-to-hono-demo",
"main": "src/index.ts",
"compatibility_date": "2025-01-24",
"assets": {
"directory": "../client/dist",
"binding": "ASSETS"
}
}

最后在 hono 的入口文件中将找不到的路由全部转发到 SvelteKit 的静态资源就好了。

1
2
3
4
5
6
7
8
9
// packages/server/src/index.ts
import { Hono } from 'hono'

const app = new Hono<{ Bindings: Env }>()

app.get('/api/ping', (c) => c.text('pong'))
app.all('*', (c) => c.env.ASSETS.fetch(c.req.raw))

export default app

现在,就可以在编码时服务端使用 Hono 而客户端使用喜欢的 Web 元框架了。

1
2
cd packages/client && pnpm build
cd ../server && pnpm wrangler dev --port 8787

缺点

说了这么多,这种模式的缺点是什么?

  • Hono 在前面时如果 SSR 需要调用服务端 API,不能在内部转换为函数调用,而是必须经过外部绕一圈请求回来。
  • 没有 Web 元框架提供的类型安全,当然这是一个可以解决的问题,例如 Trpc 或 OpenAPI 等。
  • 一般需要拆分为 monorepo 多模块,即 packages/server 和 packages/client,可能会增加一些复杂性
  • 如果仍然需要 SSR,那么还需要在 Hono 中拦截 404 请求并调用 Web 元框架构建出来的 server/index.js 动态执行

总结

Web 全栈开发是一个流行的趋势,将 Web 的前端/服务端放在一起写看起来很有吸引力,但最终可能在一如既往的绕远路,就像 Next.js 终究活成了一个怪物。另外对于不需要动态渲染 UGC [9] 的网站而言,SSR 通常增加的复杂性可能是没有必要的。


实践: 使用 Hono 开发全栈应用
https://blog.rxliuli.com/p/b39acb5cf6bd45e6b221b4175c8ad097/
作者
rxliuli
发布于
2025年6月7日
许可协议