1. 一个文件写到底的阶段
前面几篇的示例代码,我们都是在一个 index.ts 里写完所有东西——路由、中间件、数据库操作、类型定义,全部塞在同一个文件。
项目小的时候这样做完全没问题。路由就那么几个,一眼就能看到全貌,拆文件反而多此一举。
但随着功能增加,你会遇到这些情况:
index.ts膨胀到三四百行,想找一个路由要上下滚很久- 用户路由的修改和认证中间件的修改混在同一个文件里,改一个担心影响另一个
- 两个人同时改
index.ts,提交代码时冲突不断
到了这个阶段,就该把代码拆分到不同文件里了。
2. 拆成什么样
一个中等规模的 Hono 项目,我们按职责来组织目录:
思路就一句话:相同职责的代码放在一起。路由放 routes/,中间件放 middleware/,数据库相关放 db/。入口文件 index.ts 只负责把它们串起来。
3. 入口文件:只负责组装
拆分之后,index.ts 变得非常简单——它不写任何业务逻辑,只做两件事:挂全局中间件、注册各个路由模块。
01import { Hono } from 'hono'02import { logger } from 'hono/logger'03import { cors } from 'hono/cors'04import { secureHeaders } from 'hono/secure-headers'05import type { AppEnv } from './types'06import { usersApp } from './routes/users'07import { postsApp } from './routes/posts'08import { authApp } from './routes/auth'0910const app = new Hono<AppEnv>()1112// 全局中间件:所有请求都要经过13app.use('*', logger())14app.use('*', cors())15app.use('*', secureHeaders())1617// 把路由模块挂载到对应的路径前缀下18app.route('/api/users', usersApp)19app.route('/api/posts', postsApp)20app.route('/api/auth', authApp)2122// 健康检查接口(运维用来确认服务是否正常)23app.get('/health', (c) => c.json({ status: 'ok' }))2425export default app
以后新增一个功能模块,只要在 routes/ 下新建一个文件,然后在这里加一行 app.route() 就行。
4. 路由模块:各管各的
每个路由文件创建一个独立的 Hono 实例,处理一组相关的接口:
01import { Hono } from 'hono'02import type { AppEnv } from '../types'03import { authMiddleware } from '../middleware/auth'0405export const usersApp = new Hono<AppEnv>()0607// 获取所有用户08usersApp.get('/', async (c) => {09const db = c.get('db')10const users = await db.select().from(usersTable)11return c.json(users)12})1314// 获取单个用户15usersApp.get('/:id', async (c) => {16const id = c.req.param('id')17// ...18return c.json(user)19})2021// 创建用户(需要先通过鉴权中间件)22usersApp.post('/', authMiddleware, async (c) => {23const body = await c.req.json()24// ...25return 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:
01export type AppEnv = {02Bindings: {03// 环境变量04JWT_SECRET: string05API_KEY: string0607// Cloudflare 服务绑定08DB: D1Database09KV: KVNamespace10BUCKET: R2Bucket11}12Variables: {13// 中间件通过 c.set() 设置的请求级变量14user: {15id: number16email: string17role: string18}19db: DrizzleD1Database20}21}
其他文件只需要 import type { AppEnv } from '../types',然后 new Hono<AppEnv>() 就能拿到完整的 c.env 和 c.get() 类型提示。加一个新的环境变量,改 types.ts 这一个地方就够了。
6. wrangler.jsonc 配置详解
代码结构理清了,接下来看配置。wrangler.jsonc 是 Cloudflare Workers 的配置文件,告诉 Cloudflare 怎么运行你的 Worker、绑定了哪些服务。
01{02"name": "my-api",03"main": "src/index.ts",04"compatibility_date": "2024-12-01",0506// 非敏感配置,直接写在这里07"vars": {08"APP_NAME": "My API",09"MAX_PAGE_SIZE": "50"10},1112// D1 数据库绑定13"d1_databases": [14{15"binding": "DB",16"database_name": "my-db",17"database_id": "xxxx-xxxx-xxxx"18}19],2021// KV 绑定22"kv_namespaces": [23{24"binding": "KV",25"id": "xxxx-xxxx-xxxx"26}27],2829// 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_databases、kv_namespaces、r2_buckets):把 Cloudflare 的服务挂到 Worker 上
7. 环境变量:哪些能提交,哪些不能
环境变量按敏感程度分两种处理方式。
不怕被看到的配置 — 直接写在 wrangler.jsonc 的 vars 里,跟代码一起提交到 Git:
1{2"vars": {3"APP_NAME": "My API",4"ALLOWED_ORIGINS": "https://example.com"5}6}
不能泄露的密钥 — 用 wrangler secret 命令单独设置,不会出现在任何文件里:
1wrangler secret put JWT_SECRET2wrangler secret put API_KEY
执行后会提示你输入值。这些密钥加密存储在 Cloudflare 的服务器上,你在控制台也只能看到"已设置",看不到具体内容。
不管是哪种方式设置的环境变量,代码里的用法完全一样,都是 c.env.变量名:
1app.get('/api/config', (c) => {2const appName = c.env.APP_NAME // 来自 vars3const secret = c.env.JWT_SECRET // 来自 wrangler secret4// 用法完全一样5})
8. 本地开发的密钥怎么办
wrangler secret 设置的密钥存在 Cloudflare 的云端。但本地 wrangler dev 的时候,请求不走云端,拿不到这些密钥。
解决办法是在项目根目录创建一个 .dev.vars 文件,把本地开发需要的密钥写在里面:
1JWT_SECRET=local-dev-secret-key2API_KEY=local-dev-api-key3DATABASE_URL=http://localhost:8787
wrangler dev 启动时会自动读取这个文件。
这个文件里有密钥,一定不能提交到 Git,把它加到 .gitignore:
1.dev.vars2.wrangler/3node_modules/
9. 多环境部署:不要在线上直接试
真实项目通常至少需要两套环境:一套用来测试验证(staging),一套对外提供服务(production)。两套环境跑同样的代码,但连接不同的数据库,互不干扰。这样你可以在 staging 上放心折腾,确认没问题了再部署到 production。
在 wrangler.jsonc 里用 env 字段来定义不同环境的配置:
01{02"name": "my-api",03"main": "src/index.ts",04"compatibility_date": "2024-12-01",0506// 默认配置(开发环境)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],1718"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},3334// ---- 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}
部署时指定环境:
1# 部署到 staging2wrangler deploy --env staging34# 部署到 production5wrangler deploy --env production
每个环境绑定独立的数据库和存储,数据完全隔离。staging 的数据库里插了一万条垃圾测试数据,也不会影响 production。
密钥也是按环境独立设置的:
1wrangler secret put JWT_SECRET --env staging2wrangler secret put JWT_SECRET --env production
10. 把常用命令整理到 scripts 里
wrangler d1 migrations apply my-db --env production --remote 这种命令又长又容易打错。把它们放到 package.json 的 scripts 里,起一个短名字:
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}
这样日常开发只需要记这几个短命令:
yarn dev— 启动本地开发服务器yarn db:migrate:local— 本地数据库跑迁移yarn deploy:staging— 部署到测试环境验证yarn db:migrate:prod— 线上数据库跑迁移yarn deploy— 确认没问题后部署到正式环境
11. 总结
回顾一下这篇的要点:
- 代码按职责拆分:路由放
routes/,中间件放middleware/,入口文件只负责组装 - 类型定义集中到
types.ts,改一处全局生效 - 不怕泄露的配置写
wrangler.jsonc的vars,密钥用wrangler secret单独设置 - 本地开发用
.dev.vars提供密钥,记得加到.gitignore - 多环境用
env字段配置,数据完全隔离
下一篇我们进入实战,用这套结构搭一个完整的 REST API。