AI 电子伴侣
创建时间: 2026-04-18最后更新: 2026-08-04

1. 一个文件写到底的阶段

前面几篇的示例代码,我们都是在一个 index.ts 里写完所有东西——路由、中间件、数据库操作、类型定义,全部塞在同一个文件。

项目小的时候这样做完全没问题。路由就那么几个,一眼就能看到全貌,拆文件反而多此一举。

但随着功能增加,你会遇到这些情况:

  • index.ts 膨胀到三四百行,想找一个路由要上下滚很久
  • 用户路由的修改和认证中间件的修改混在同一个文件里,改一个担心影响另一个
  • 两个人同时改 index.ts,提交代码时冲突不断

到了这个阶段,就该把代码拆分到不同文件里了。

2. 拆成什么样

一个中等规模的 Hono 项目,我们按职责来组织目录:

index.ts入口文件,挂载路由和全局中间件
types.tsBindings、Variables 等类型定义
users.ts用户相关路由
posts.ts文章相关路由
auth.ts认证相关路由(登录、注册)
auth.ts鉴权中间件
logger.ts日志中间件
schema.tsDrizzle 表结构定义
index.tsDrizzle 实例工厂
errors.ts自定义错误类
response.ts统一响应格式
wrangler.jsoncCloudflare Workers 配置
drizzle.config.tsDrizzle ORM 配置
package.json依赖和脚本

思路就一句话:相同职责的代码放在一起。路由放 routes/,中间件放 middleware/,数据库相关放 db/。入口文件 index.ts 只负责把它们串起来。

3. 入口文件:只负责组装

拆分之后,index.ts 变得非常简单——它不写任何业务逻辑,只做两件事:挂全局中间件、注册各个路由模块。

src/index.ts
01
import { Hono } from 'hono'
02
import { logger } from 'hono/logger'
03
import { cors } from 'hono/cors'
04
import { secureHeaders } from 'hono/secure-headers'
05
import type { AppEnv } from './types'
06
import { usersApp } from './routes/users'
07
import { postsApp } from './routes/posts'
08
import { authApp } from './routes/auth'
09
10
const app = new Hono<AppEnv>()
11
12
// 全局中间件:所有请求都要经过
13
app.use('*', logger())
14
app.use('*', cors())
15
app.use('*', secureHeaders())
16
17
// 把路由模块挂载到对应的路径前缀下
18
app.route('/api/users', usersApp)
19
app.route('/api/posts', postsApp)
20
app.route('/api/auth', authApp)
21
22
// 健康检查接口(运维用来确认服务是否正常)
23
app.get('/health', (c) => c.json({ status: 'ok' }))
24
25
export default app

以后新增一个功能模块,只要在 routes/ 下新建一个文件,然后在这里加一行 app.route() 就行。

4. 路由模块:各管各的

每个路由文件创建一个独立的 Hono 实例,处理一组相关的接口:

src/routes/users.ts
01
import { Hono } from 'hono'
02
import type { AppEnv } from '../types'
03
import { authMiddleware } from '../middleware/auth'
04
05
export const usersApp = new Hono<AppEnv>()
06
07
// 获取所有用户
08
usersApp.get('/', async (c) => {
09
const db = c.get('db')
10
const users = await db.select().from(usersTable)
11
return c.json(users)
12
})
13
14
// 获取单个用户
15
usersApp.get('/:id', async (c) => {
16
const id = c.req.param('id')
17
// ...
18
return c.json(user)
19
})
20
21
// 创建用户(需要先通过鉴权中间件)
22
usersApp.post('/', authMiddleware, async (c) => {
23
const body = await c.req.json()
24
// ...
25
return c.json(newUser, 201)
26
})

注意这里的路径都是 //:id,没有写 /api/users。因为入口文件里写了 app.route('/api/users', usersApp),所以这个模块里的 / 实际上对应的就是 /api/users/:id 对应 /api/users/:id

这样做的好处是模块不跟路径绑死。哪天你想把用户接口从 /api/users 改到 /v2/users,只需要改入口文件那一行,路由模块的代码完全不用动。

5. 类型定义:集中放一个地方

前面的文章里,我们在每个文件里都写过 type Bindings = { DB: D1Database } 这样的类型声明。文件少的时候还行,文件一多就会出现同一个类型到处重复定义、改了一处忘了另一处的问题。

所以把所有共享的类型定义集中放到 types.ts

src/types.ts
01
export type AppEnv = {
02
Bindings: {
03
// 环境变量
04
JWT_SECRET: string
05
API_KEY: string
06
07
// Cloudflare 服务绑定
08
DB: D1Database
09
KV: KVNamespace
10
BUCKET: R2Bucket
11
}
12
Variables: {
13
// 中间件通过 c.set() 设置的请求级变量
14
user: {
15
id: number
16
email: string
17
role: string
18
}
19
db: DrizzleD1Database
20
}
21
}

其他文件只需要 import type { AppEnv } from '../types',然后 new Hono<AppEnv>() 就能拿到完整的 c.envc.get() 类型提示。加一个新的环境变量,改 types.ts 这一个地方就够了。

6. wrangler.jsonc 配置详解

代码结构理清了,接下来看配置。wrangler.jsonc 是 Cloudflare Workers 的配置文件,告诉 Cloudflare 怎么运行你的 Worker、绑定了哪些服务。

wrangler.jsonc
01
{
02
"name": "my-api",
03
"main": "src/index.ts",
04
"compatibility_date": "2024-12-01",
05
06
// 非敏感配置,直接写在这里
07
"vars": {
08
"APP_NAME": "My API",
09
"MAX_PAGE_SIZE": "50"
10
},
11
12
// D1 数据库绑定
13
"d1_databases": [
14
{
15
"binding": "DB",
16
"database_name": "my-db",
17
"database_id": "xxxx-xxxx-xxxx"
18
}
19
],
20
21
// KV 绑定
22
"kv_namespaces": [
23
{
24
"binding": "KV",
25
"id": "xxxx-xxxx-xxxx"
26
}
27
],
28
29
// R2 存储桶绑定
30
"r2_buckets": [
31
{
32
"binding": "BUCKET",
33
"bucket_name": "my-bucket"
34
}
35
]
36
}

几个要点:

  • name:部署后的 Worker 名称,也是默认的子域名前缀
  • main:入口文件路径
  • compatibility_date:运行时兼容日期,决定了可用的 API 版本
  • vars:非敏感的环境变量,会被提交到 Git
  • 绑定(d1_databaseskv_namespacesr2_buckets):把 Cloudflare 的服务挂到 Worker 上

7. 环境变量:哪些能提交,哪些不能

环境变量按敏感程度分两种处理方式。

不怕被看到的配置 — 直接写在 wrangler.jsoncvars 里,跟代码一起提交到 Git:

wrangler.jsonc
1
{
2
"vars": {
3
"APP_NAME": "My API",
4
"ALLOWED_ORIGINS": "https://example.com"
5
}
6
}

不能泄露的密钥 — 用 wrangler secret 命令单独设置,不会出现在任何文件里:

terminal
1
wrangler secret put JWT_SECRET
2
wrangler secret put API_KEY

执行后会提示你输入值。这些密钥加密存储在 Cloudflare 的服务器上,你在控制台也只能看到"已设置",看不到具体内容。

不管是哪种方式设置的环境变量,代码里的用法完全一样,都是 c.env.变量名

code.ts
1
app.get('/api/config', (c) => {
2
const appName = c.env.APP_NAME // 来自 vars
3
const secret = c.env.JWT_SECRET // 来自 wrangler secret
4
// 用法完全一样
5
})

8. 本地开发的密钥怎么办

wrangler secret 设置的密钥存在 Cloudflare 的云端。但本地 wrangler dev 的时候,请求不走云端,拿不到这些密钥。

解决办法是在项目根目录创建一个 .dev.vars 文件,把本地开发需要的密钥写在里面:

.dev.vars
1
JWT_SECRET=local-dev-secret-key
2
API_KEY=local-dev-api-key
3
DATABASE_URL=http://localhost:8787

wrangler dev 启动时会自动读取这个文件。

这个文件里有密钥,一定不能提交到 Git,把它加到 .gitignore

.gitignore
1
.dev.vars
2
.wrangler/
3
node_modules/

9. 多环境部署:不要在线上直接试

真实项目通常至少需要两套环境:一套用来测试验证(staging),一套对外提供服务(production)。两套环境跑同样的代码,但连接不同的数据库,互不干扰。这样你可以在 staging 上放心折腾,确认没问题了再部署到 production。

wrangler.jsonc 里用 env 字段来定义不同环境的配置:

wrangler.jsonc
01
{
02
"name": "my-api",
03
"main": "src/index.ts",
04
"compatibility_date": "2024-12-01",
05
06
// 默认配置(开发环境)
07
"vars": {
08
"APP_NAME": "My API (dev)"
09
},
10
"d1_databases": [
11
{
12
"binding": "DB",
13
"database_name": "my-db-dev",
14
"database_id": "dev-xxxx-xxxx"
15
}
16
],
17
18
"env": {
19
// ---- Staging 环境 ----
20
"staging": {
21
"name": "my-api-staging",
22
"vars": {
23
"APP_NAME": "My API (staging)"
24
},
25
"d1_databases": [
26
{
27
"binding": "DB",
28
"database_name": "my-db-staging",
29
"database_id": "staging-xxxx-xxxx"
30
}
31
]
32
},
33
34
// ---- Production 环境 ----
35
"production": {
36
"name": "my-api-production",
37
"vars": {
38
"APP_NAME": "My API"
39
},
40
"d1_databases": [
41
{
42
"binding": "DB",
43
"database_name": "my-db-prod",
44
"database_id": "prod-xxxx-xxxx"
45
}
46
]
47
}
48
}
49
}

部署时指定环境:

terminal
1
# 部署到 staging
2
wrangler deploy --env staging
3
4
# 部署到 production
5
wrangler deploy --env production

每个环境绑定独立的数据库和存储,数据完全隔离。staging 的数据库里插了一万条垃圾测试数据,也不会影响 production。

密钥也是按环境独立设置的:

terminal
1
wrangler secret put JWT_SECRET --env staging
2
wrangler secret put JWT_SECRET --env production

10. 把常用命令整理到 scripts 里

wrangler d1 migrations apply my-db --env production --remote 这种命令又长又容易打错。把它们放到 package.jsonscripts 里,起一个短名字:

package.json
01
{
02
"scripts": {
03
"dev": "wrangler dev",
04
"deploy": "wrangler deploy --env production",
05
"deploy:staging": "wrangler deploy --env staging",
06
"db:migrate:local": "wrangler d1 migrations apply my-db --local",
07
"db:migrate:staging": "wrangler d1 migrations apply my-db --env staging --remote",
08
"db:migrate:prod": "wrangler d1 migrations apply my-db --env production --remote",
09
"db:studio": "drizzle-kit studio",
10
"generate": "drizzle-kit generate"
11
}
12
}

这样日常开发只需要记这几个短命令:

  1. yarn dev — 启动本地开发服务器
  2. yarn db:migrate:local — 本地数据库跑迁移
  3. yarn deploy:staging — 部署到测试环境验证
  4. yarn db:migrate:prod — 线上数据库跑迁移
  5. yarn deploy — 确认没问题后部署到正式环境

11. 总结

回顾一下这篇的要点:

  • 代码按职责拆分:路由放 routes/,中间件放 middleware/,入口文件只负责组装
  • 类型定义集中到 types.ts,改一处全局生效
  • 不怕泄露的配置写 wrangler.jsoncvars,密钥用 wrangler secret 单独设置
  • 本地开发用 .dev.vars 提供密钥,记得加到 .gitignore
  • 多环境用 env 字段配置,数据完全隔离

下一篇我们进入实战,用这套结构搭一个完整的 REST API。