场景
最近写了几个前后端都包含的应用,从最初的 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

Hono starter

实现
谁在前面?
现在,同时使用 Hono 和 Web 元框架,例如 SvelteKit,来开发一个应用。问题来了,谁在前面?也就是说,Hono 在前面并转发路由,还是 SvelteKit 在前面并转发路由?由于下面几个特征,Hono 在前面会更好
- Hono 的代码更少,启动更快
- 元框架可能会有一些意外的行为,例如自动序列化所有 Response [8]
- 如果没有 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 通常增加的复杂性可能是没有必要的。