HonoJS 是一个轻量的NodeJS服务端框架,作用跟我们常见的ExpressKoa等类似。Hono的特点是(官网自述):

  • 快速、轻量级、基于Web标准构建
  • 支持任何JavaScript运行时。

image.png

初始化

Hono 提供了初始化脚手架来简化项目创建,只需要通过一条命令即可完成创建。

$ pnpm create hono@latest hono-study

根据脚手架的指引选择实际的场景即可
image.png
创建完成之后即可启动项目体验

$ pnpm dev

# 浏览器访问localhost:3000 可以看到 【Hello Hono!】内容

Hono 项目默认使用了TypeScript,打开项目可以看到有一个index.ts文件,这个文件提供了我们刚才访问的内容

import { serve } from '@hono/node-server'
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.text('Hello Hono!')
})

serve({
  fetch: app.fetch,
  port: 3000
}, (info) => {
  console.log(`Server is running on http://localhost:${info.port}`)
})

可以看到还是经典的三段式结构。

API

Hono 的API 基本遵循了前辈们的使用习惯,并且加入了自己的特色用法。

App

每个Hono实例(const app = new Hono()产生的对象)都称之为一个App,实例上拥有的方法主要适用于绑定中间件、定义路由、启动服务等。

app.fetch(request, env, event) 用于获取应用入口,将这个入口传递给不同平台的启动器可以实现跨平台的兼容。上面的代码段中使用的就是将入口放到node-server中在NodeJS环境进行启动。
如果是在 Cloudflare Workers 中可以这样写:

export default {
  fetch(request: Request, env: Env, ctx: ExecutionContext) {
    return app.fetch(request, env, ctx)
  },
}

这里通过不同的适配器将不同平台的请求转为统一的标准的Web 请求。

app.use([path,]middleware) 用于绑定中间件,第一个参数是可选的请求路径,不指定路径时对所有请求生效。
例如我们添加一个logger中间件用于在控制台输出请求的信息

import { Hono } from 'hono'
import { logger } from 'hono/logger'

const app = new Hono()

app.use(logger())

此时触发请求后可以在控制台看到请求耗时等信息
image.png

中间件本质上就是一个函数,会对请求进行前置或者后置处理,理论上只要符合(c: Context, next: Next) => Promise<void>签名的函数都可以作为中间件使用。
例如logger中间件我们可以自己实现一个

app.use(async (c, next) => {
    console.log(`[${c.req.method}] ${c.req.url}`)
    await next()
})

app.HTTP_METHOD([path,]handler|middleware...) 用于绑定请求对应的处理函数,包括getpostputdeleteall

app.get('/', (c) => c.text('GET /'))
app.post('/', (c) => c.text('POST /'))
app.put('/', (c) => c.text('PUT /'))
app.delete('/', (c) => c.text('DELETE /'))
app.all('/all', (c) => c.text('Any Method /all'))

还有app.on(method|method[], path|path[], handler|middleware...)可以用于绑定请求处理,比起http方法的绑定,使用on绑定需要额外传一个请求方法的参数,使用on的优势是,可以批量绑定请求处理,因为方法参数和路径参数都支持数组类型。

app.on(['GET', 'POST'], ['/path1', 'paht2'], (c) => {
    return c.text('multi bind.')
})

app.basePath(path) 可以设置路由前缀,应用下所有的请求路径都会基于basPath来处理

const api = new Hono().basePath('/api')
api.get('/users', (c) => c.text('users')) // GET /api/users

basePath 必须在创建Hono实例时设置才会生效。

const api = new Hono()
api.basePath('/api')  // 这样设置不生效

app.notFound(handler) 可以用于绑定匹配不到路由的处理,notFound 只有在顶层应用可以设置

app.notFound((c) => {
  return c.text('Custom 404 Message', 404)
})

app.onError(handler) 可以绑定错误处理,当程序抛出异常是可以通过 onError 钩子来捕获处理,如果存在嵌套绑定 onError 会就近处理。

app.onError((err, c) => {
  console.error(`${err}`)
  return c.text('Custom Error Message', 500)
})

路由

Hono 的路由非常灵活,支持多种方式的路径匹配。

  • 基础路径:最普通最常见的额一种路由定义形式,指定固定的路径,将按照这个路径匹配请求。例如/normal/request
  • 动态路径:路径中存在动态部分,动态部分可以作为参数使用。例如/api/user/:userId
  • 正则路径:在动态路由的基础上添加匹配限制,只有符合正则表达式才会被匹配到。例如/regexp/:name{[a-z]+}可以匹配到'/regexp/feng',但是无法匹配到'/regexp/feng1'
  • 通配路由:所有的路由都会匹配到。使用*作为path即可
    前面有说过app.basePath,除了这种方式还可以通过以下方式对路径进行编组,也可以实现basePath的能力(可以和basePath叠加使用)。
const api = new Hono()
api.get('/hello', (c) => c.text('Hello API.'))

const app = new Hono()
app.route('/api', api)

路由会按照注册顺序进行匹配,并且只会匹配一次

app.get('/api/:name', (c) => c.text('name'))
app.get('/api/a', (c) => c.text('a'))

上面这两个注册顺序下,访问/book/a时获取的结果是name,所有需要注意的是优先将固定路径的路由放在前面注册。

上下文

Context对象在每个请求中实例化,可用于存储值、设置HTTP状态码和头信息,以及访问HonoRequest和Response对象,Context对象在每个请求中实例化,直到返回响应才销毁。

  • req req是HonoRequest的实例,用于访问请求相关的信息,如请求头等。
  • status() 使用c.status()可以设置HTTP状态码,默认为200。如果状态码为200,可以不使用此方法。
  • header() 使用c.header()可以设置响应的HTTP头信息。
  • body() 使用c.body()可以返回HTTP响应体。当返回文本或HTML时,推荐使用c.text()或c.html()。
  • text() 使用c.text()可以将文本作为Content-Type:text/plain返回。
  • json() 使用c.json()可以将JSON作为Content-Type:application/json返回。
  • html() 使用c.html()可以将HTML作为Content-Type:text/html返回。
  • notFound() 使用c.notFound()可以返回一个404状态码的响应,也可以通过app.notFound()进行自定义。
  • redirect() 使用c.redirect()可以进行重定向,默认状态码为302。
  • res 可以通过c.res访问将要返回的Response对象。
  • set() / get() 使用c.set()和c.get()可以在当前请求的生命周期内存储和获取任意键值对,这允许在中间件或从中间件到路由处理器之间传递特定值。
  • var 可以通过c.var访问变量的值。如果需要创建提供自定义方法的中间件,可以像文中示例那样编写。
  • render() / setRenderer() 使用c.setRenderer()可以在自定义中间件中设置布局,然后使用c.render()在该布局内创建响应。
  • error 如果处理器抛出错误,错误对象将被放置在c.error中。可以在中间件中访问它。
    其他的都很好理解不做过多介绍,要着重提及的是c.html()支持jsx语法的渲染,这对于模板渲染来说非常方便。
// RenderComponent.tsx
export function RenderComponent(props: RenderComponentProps) {
    const {name} = props
    
    return <div>Your name is {name}</div>
}

// index.tsx
app.get('/react/:name', (c) => {
  const name = c.req.param('name') as string
  return c.html(<RenderComponent name={name} />)
})

image.png

req

事关请求参数的获取 HonoRequest 有必要单开一节,HonoRequest 是基于 Request的一个对象,包含了Hono对于请求体的一些封装。
通过req可以获取请求数据的方式有很多

  • query([key]):获取请求路径中的query参数,如?name=feng,可以使用const name = c.req.query('name')来获取,如果同名参数有多个也可以使用const names = c.req.queries('name')获取数组;
  • param([key]):获取请求路径中的param参数,如/user/1,在路径为/user/:id的请求处理中可以使用const id = c.req.param('id')来获取;
  • header([key]):从请求头中获取制定的字段,const UA = c.req.header('User-Agent'),⚠️注意当使用header()获取全部请求头时,所有的请求头Key都是小驼峰格式;
  • body:请求的 body可以有很多种类型,Hono支持直接转换的格式如下
    • parseBody():用于格式化 multipart/form-data 和 application/x-www-form-urlencoded类型的请求体;
    • json():用于解析application/json格式的请求体
    • text():用于解析text/plain格式的请求体
    • arrayBuffer():格式化请求体为ArrayBuffer类型
    • blob():格式化请求体为Blob类型
    • formData():格式化请求体为FormData类型
      Hono 对请求数据提供了校验的能力,通过validator中间件来定义校验的规则
import { validator } from 'hono/validator'

app.post(
  '/user',
  validator('form', (data, c) => {
    if (!data.name) {
      return c.json({ error: 'Name is required' }, 400)
    }

    return data
  }),
  (c) => {
    const user = c.req.valid('form')
    return c.json({ message: 'User created', user })
  }
)

validator中我们可以通过zod等校验工具来实现业务校验。

到这里我们已经掌握了Hono的基础用法,可以用Hono来实现一些简单的Demo了,当然Hono的能力并不只有这些,更多的使用姿势可以去官网发现


前端小白