Hono 前后端分离系列 02:项目结构和本地开发环境
wxk1991 Lv5

Hono 前后端分离系列 02:项目结构和本地开发环境

前后端分离项目一开始最容易被低估的事情,是目录结构。

很多人觉得目录只是文件怎么摆。

不是。

目录结构其实是在告诉团队:

1
2
3
4
什么代码属于前端
什么代码属于后端
什么东西可以共享
什么东西不能互相越界

Hono 很轻,所以它不会强迫你使用某一种工程结构。自由是好事,但自由也意味着你要提前做一些约定。

这篇文章讲一个适合中小型项目的 Hono 前后端分离结构,并把本地开发环境跑起来。


一、先选单仓库还是双仓库

前后端分离有两种常见组织方式。

第一种是双仓库:

1
2
my-web
my-api

优点是边界清楚,部署独立,权限也容易分开。

缺点是共享类型和联调会麻烦一些。比如后端改了接口返回结构,前端不一定第一时间感知。

第二种是 monorepo:

1
2
3
4
5
6
my-app/
apps/
web/
api/
packages/
shared/

优点是类型、常量、工具函数、接口客户端都可以共享。缺点是工程配置稍微复杂一点。

如果你是个人项目、小团队、前后端都用 TypeScript,我更推荐 monorepo。

如果前后端团队完全独立,部署和权限也分开,双仓库更自然。

本文用 monorepo 来讲。


二、推荐目录结构

一个比较干净的起点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
hono-separated-app/
├── apps/
│ ├── api/
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ ├── routes/
│ │ │ ├── middlewares/
│ │ │ ├── services/
│ │ │ └── schemas/
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── web/
│ ├── src/
│ │ ├── main.ts
│ │ ├── api/
│ │ ├── pages/
│ │ └── components/
│ ├── package.json
│ └── vite.config.ts
├── packages/
│ └── shared/
│ ├── src/
│ └── package.json
├── package.json
└── pnpm-workspace.yaml

这里最重要的是 apps/api

Hono 后端可以很小,但不要把所有代码都堆进 index.ts。一个文件写到几百行以后,后面一定会痛。

我通常这样拆:

1
2
3
4
routes       路由入口,只处理 HTTP 层
schemas 入参校验和响应结构
services 业务逻辑
middlewares 鉴权、日志、错误处理、上下文注入

这不是为了显得“架构很高级”。

这是为了让代码在第 20 个接口出现时,还能看懂。


三、创建 workspace

根目录初始化:

1
2
3
mkdir hono-separated-app
cd hono-separated-app
pnpm init

pnpm-workspace.yaml

1
2
3
packages:
- apps/*
- packages/*

根目录 package.json 可以这样写:

1
2
3
4
5
6
7
8
9
{
"name": "hono-separated-app",
"private": true,
"scripts": {
"dev": "pnpm -r --parallel dev",
"dev:api": "pnpm --filter api dev",
"dev:web": "pnpm --filter web dev"
}
}

创建目录:

1
mkdir -p apps/api/src apps/web/src packages/shared/src

四、创建 Hono API

进入 API 项目:

1
2
3
4
cd apps/api
pnpm init
pnpm add hono
pnpm add -D typescript tsx @types/node

apps/api/package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "api",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"hono": "^4.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}
}

如果你跑在 Node.js,需要用 Node 适配器:

1
pnpm add @hono/node-server

src/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { serve } from '@hono/node-server'
import { Hono } from 'hono'

const app = new Hono()

app.get('/api/health', (c) => {
return c.json({
ok: true,
runtime: 'node',
})
})

serve({
fetch: app.fetch,
port: 8787,
})

console.log('API server running at http://localhost:8787')

Hono 官方文档里也提到,Hono 最初不是为 Node.js 设计的,但通过 Node.js Adapter 可以运行在 Node.js 上。实际生产中,如果你部署到 Cloudflare Workers,则入口会不同。


五、创建 Vite 前端

回到根目录:

1
2
cd ../../
pnpm create vite apps/web --template vue-ts

前端开发时,把 API 地址放到环境变量里。

apps/web/.env.development

1
VITE_API_BASE_URL=http://localhost:8787

封装一个请求方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL

export async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE_URL}${path}`, {
headers: {
'Content-Type': 'application/json',
...init?.headers,
},
...init,
})

if (!res.ok) {
throw new Error(`Request failed: ${res.status}`)
}

return res.json() as Promise<T>
}

页面里调用:

1
const health = await request<{ ok: boolean; runtime: string }>('/api/health')

到这里,一个最基础的前后端分离本地开发环境就跑通了。


六、开发时到底要不要用代理

前端联调后端有两种方式。

第一种是直接请求后端完整地址:

1
http://localhost:8787/api/health

这样最接近真实跨域场景,可以尽早发现 CORS 问题。

第二种是 Vite 代理:

1
2
3
4
5
6
7
export default defineConfig({
server: {
proxy: {
'/api': 'http://localhost:8787',
},
},
})

前端只请求:

1
/api/health

这样开发体验更顺,但它会把跨域问题藏起来。到了部署环境,如果前端和后端不在同一个域名下,你仍然要正确配置 CORS。

我的建议是:

1
2
本地早期可以用 Vite proxy
进入联调阶段一定要测试真实跨域

七、先不要急着上复杂工具

很多项目刚开始就引入一堆东西:

1
2
3
4
5
6
7
ORM
OpenAPI
RPC
缓存
队列
权限系统
日志平台

这些都可能有用,但不是第一天都要上。

前后端分离的第一步应该是:

1
2
3
4
5
项目能启动
接口能访问
目录边界清楚
环境变量清楚
错误能看懂

Hono 很适合这种渐进式开发。

先让 API 层清楚,再逐步加校验、鉴权、数据库、类型安全客户端和部署。


参考资料