F FisherHub Docs

04. 部署与优化

Hono 的一大优势是原生支持 Cloudflare Workers、Deno、Bun 等边缘运行时。本文以 Cloudflare Workers 为例,详细介绍部署流程和性能优化策略。

wrangler.toml 配置

Cloudflare Workers 使用 wrangler.toml 管理项目配置:

name = "my-hono-app"
main = "src/index.ts"
compatibility_date = "2025-01-01"

# 环境变量配置
[vars]
JWT_SECRET = "your-secret-key"
DATABASE_URL = "https://db.example.com"
APP_ENV = "production"

# R2 存储桶绑定
[[r2_buckets]]
binding = "ASSETS_BUCKET"
bucket_name = "my-app-assets"

# KV 命名空间绑定
[[kv_namespaces]]
binding = "CACHE_KV"
id = "your-kv-namespace-id"

# D1 数据库绑定
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "your-database-id"

环境变量管理

在代码中通过 c.env 访问环境变量:

import { Hono } from 'hono'

type Bindings = {
  JWT_SECRET: string
  APP_ENV: string
  ASSETS_BUCKET: R2Bucket
  CACHE_KV: KVNamespace
  DB: D1Database
}

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

app.get('/config', (c) => {
  return c.json({
    env: c.env.APP_ENV,
    // 注意:切忌将敏感信息返回给客户端
  })
})

本地开发时可以在 .dev.vars 文件中设置环境变量:

JWT_SECRET=local-dev-secret
DATABASE_URL=http://localhost:3000

部署到 Cloudflare Workers

登录 Cloudflare 账号并部署:

# 登录 Cloudflare
bun wrangler login

# 部署到生产环境
bun wrangler deploy

# 部署到预览环境
bun wrangler deploy --env preview

部署完成后,你会获得一个 *.workers.dev 域名。你也可以在 Cloudflare 控制台中绑定自定义域名。

R2 文件存储

R2 是 Cloudflare 的对象存储服务,兼容 S3 API,且没有出口流量费:

app.post('/upload', async (c) => {
  const bucket = c.env.ASSETS_BUCKET
  const formData = await c.req.formData()
  const file = formData.get('file') as File | null

  if (!file) {
    return c.json({ error: '请选择文件' }, 400)
  }

  // 生成唯一文件名
  const key = `${Date.now()}-${file.name}`
  const arrayBuffer = await file.arrayBuffer()

  await bucket.put(key, arrayBuffer, {
    httpMetadata: { contentType: file.type },
    customMetadata: {
      uploadedBy: 'hono-app',
    },
  })

  return c.json({
    url: `https://assets.example.com/${key}`,
    key,
    size: file.size,
  })
})

app.get('/files/:key', async (c) => {
  const bucket = c.env.ASSETS_BUCKET
  const key = c.req.param('key')

  const object = await bucket.get(key)
  if (!object) {
    return c.json({ error: '文件未找到' }, 404)
  }

  return new Response(object.body, {
    headers: {
      'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
      'Cache-Control': 'public, max-age=31536000',
    },
  })
})

缓存策略

使用 Cache API

Cloudflare Workers 提供了 Cache API,可以将响应缓存到边缘节点:

app.get('/api/posts', async (c) => {
  const cache = caches.default
  const cacheKey = new Request(c.req.url)
  const cachedResponse = await cache.match(cacheKey)

  if (cachedResponse) {
    return cachedResponse
  }

  // 模拟数据库查询
  const posts = [
    { id: 1, title: '文章一' },
    { id: 2, title: '文章二' },
  ]

  const response = c.json({ posts })
  // 设置缓存,有效期 5 分钟
  response.headers.set('Cache-Control', 'public, max-age=300')
  c.executionCtx.waitUntil(cache.put(cacheKey, response.clone()))

  return response
})

使用 KV 做缓存层

KV 适合存储频繁读取但不常修改的数据:

app.get('/api/popular', async (c) => {
  const cacheKey = 'popular_posts'
  const cached = await c.env.CACHE_KV.get(cacheKey)

  if (cached) {
    return c.json(JSON.parse(cached))
  }

  const data = { posts: ['热门文章1', '热门文章2'] }

  // 缓存 1 小时
  await c.env.CACHE_KV.put(cacheKey, JSON.stringify(data), {
    expirationTtl: 3600,
  })

  return c.json(data)
})

条件请求

对于频繁变动的资源,使用 ETag 或 Last-Modified 减少传输:

app.get('/api/data', async (c) => {
  const data = { version: 2, content: '最新数据' }
  const etag = `"${crypto.randomUUID()}"`

  // 检查客户端缓存
  const ifNoneMatch = c.req.header('If-None-Match')
  if (ifNoneMatch === etag) {
    return new Response(null, { status: 304 })
  }

  const response = c.json(data)
  response.headers.set('ETag', etag)
  return response
})

日志与监控

结构化日志

使用 console.log 配合 JSON 格式输出,方便日志系统收集:

app.use('*', async (c, next) => {
  const start = Date.now()
  await next()
  const duration = Date.now() - start

  console.log(JSON.stringify({
    level: 'info',
    method: c.req.method,
    path: c.req.path,
    status: c.res.status,
    duration,
    timestamp: new Date().toISOString(),
  }))
})

请求 ID 追踪

为每个请求分配唯一 ID,便于追踪问题:

import type { MiddlewareHandler } from 'hono'

const requestId: MiddlewareHandler = async (c, next) => {
  const id = crypto.randomUUID()
  c.set('requestId', id)
  c.res.headers.set('X-Request-Id', id)
  await next()
}

app.use('*', requestId)

错误告警

生产环境应当记录异常到外部监控服务:

app.onError(async (err, c) => {
  const requestId = c.get('requestId') || 'unknown'

  // 发送到日志服务(示例使用 fetch)
  await fetch('https://logs.example.com/error', {
    method: 'POST',
    body: JSON.stringify({
      requestId,
      error: err.message,
      stack: err.stack,
      path: c.req.path,
    }),
  })

  return c.json({
    error: '服务器内部错误',
    requestId,
  }, 500)
})

性能优化建议

减少包体积

Hono 本身就很小,但注意不要引入不必要的依赖。用 bun run build 检查构建产物大小。

使用响应式缓存

对 GET 请求优先使用 Cache API 或 KV 缓存,减少回源请求。

预热

对于已知的热门数据,在 Workers 启动时通过 scheduled 事件预填充缓存:

export default {
  fetch: app.fetch,
  async scheduled(event: ScheduledEvent, env: any, ctx: ExecutionContext) {
    // 每小时刷新缓存
    const data = await fetchPopularData()
    await env.CACHE_KV.put('popular', JSON.stringify(data))
  },
}

压缩响应

Cloudflare 会自动对响应进行 Brotli / Gzip 压缩,确保响应内容不要过于冗余。

小结

本文介绍了 Hono 应用在 Cloudflare Workers 上的完整部署流程,包括 wrangler 配置、环境变量管理、R2 文件存储、缓存策略和日志监控。借助 Cloudflare 的边缘网络,你的 Hono 应用可以获得全球范围内的低延迟访问。

至此,Hono 教程系列的四篇文章全部完成。你应该已经能够独立使用 Hono 构建、部署和优化一个生产级别的 Web 应用了。