nest.js / hono.js 一起学!hono的设计思想!

前言

欢迎到我们的交流群一起交流各种前端技术,同时欢迎访问我的 headless 组件库,同时感谢你的 star:


nest.js / hono.js 一起学系列,最终会封装一个通用的架子,例如有鉴权,日志收集,多环境配置等等功能,用两种框架去实现。 之前写了一篇

其实讲的是学习 nest.js 之前,你只有弄懂了这些基础概念,你才不迷糊,因为大多数前端对于 nest.js 这种类似 angular 的编程范式接触不多,在不理解其思想的前提下使用,会非常别扭。

这里我们第一小节,就补充一下, hono.js 的核心设计思想。

函数式和面向对象

在之前的文章:

我们提到了函数式编程的思想和面向对象的编程思想,nest.js 是典型的面向对象的编程思想,各种组件先写对象,然后调用,内部模块化做的很好,使用下来好处很明显,就是

  • 模块清晰,规范清晰,适合大型项目的组织结构

缺点也极为明显:

  • 不如 java spring 框架的依赖注入好用
  • 用起来很繁琐(虽然也有 cli 快捷键生成模板),但总体来说配置一个接口,涉及到的文件比较多,代码写起来比较麻烦
  • 还有本身 nest.js 尤其接入 express 框架后,性能是算是 node.js 框架中垫底的存在

反观 hono.js ,属于传统的类似 express,koa 这类函数式 node.js 框架,很轻巧,简单。我们用代码对比一下在不同编程范式下书写体验上的不同。

举例,新建一个路由:

nest.js 的方式

Nest.js 建路由要至少 3 层结构:

  1. controller.ts
kotlin 复制代码
import { Controller, Get } from '@nestjs/common'

@Controller('hello')
export class HelloController {
  @Get()
  getHello() {
    return 'Hello Nest!'
  }
}
  1. module.ts(必须注册)
python 复制代码
import { Module } from '@nestjs/common'
import { HelloController } from './hello.controller'

@Module({
  controllers: [HelloController],
})
export class HelloModule {}
  1. app.module.ts(还必须再注册)

也就是所有别的 module 都要注册到根 module - AppModule

kotlin 复制代码
@Module({
  imports: [HelloModule],
})
export class AppModule {}
  1. main.ts 启动 IoC 容器
csharp 复制代码
async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  await app.listen(3000)
}

bootstrap()

hono.js 的方式

javascript 复制代码
import { Hono } from 'hono'

const app = new Hono()

app.get('/hello', (c) => c.text('Hello Hono!'))

export default app

大家可以体会不同的编程范式,带来的书写体验的不同。接下来我们看下 hono.js 的主要的两个设计思想:

  • 洋葱模型(责任链模式)
  • 适配器模型(适配器模式)

洋葱模型

先看看 hono.js 基本的用法, 本文是适配的 node.js ,hono 还可以适配不同的平台,例如 deno, bun, next.js 等等:

javascript 复制代码
import { serve } from '@hono/node-server'
import { Hono } from 'hono'

const app = new Hono()

// 中间件 A
app.use('*', async (c, next) => {
  console.log('Before A')
  await next()
  console.log('After A')
})

// 中间件 B
app.use('*', async (c, next) => {
  console.log('Before B')
  await next()
  console.log('After B')
})

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}`)
})

这种洋葱模型,跟 koa 框架的设计很相似,我们可以简单实现一下,如何让 app.use 可以串联起来这些函数,最终到达 '/hello' 路由。

我们实现一个简易版本

javascript 复制代码
class Hono {
  private middleware: Middleware[] = []
  private routes: Record<string, Middleware> = {}

  use(fn: Middleware) {
    this.middleware.push(fn)
    return this
  }

  get(path: string, handler: Middleware) {
    // 一次性 compose:全局中间件 + 路由 handler
    const chain = compose([...this.middleware, handler])
    this.routes[path] = chain
    return this
  }
  
  async fetch(req: Request) {
    const path = new URL(req.url).pathname
    const context: Context = { req, path }

    const fn = this.routes[path]
    if (!fn) return new Response('Not Found', { status: 404 })

    await fn(context)
    return context.res ?? new Response('OK')
  }
}

上面实现了很简单的,Hono 简易实现代码,我们来看看简单使用

javascript 复制代码
const app = new Hono()


// 中间件 A
app.use(async (c, next) => {
  console.log('Before A')
  await next()
  console.log('After A')
})

// 中间件 B
app.use(async (c, next) => {
  console.log('Before B')
  await next()
  console.log('After B')
})

这里相当于在内部 this.middleware 数组中添加了两个中间件函数。

接着我们调用

javascript 复制代码
// 路由处理
app.get('/hello', (c) => c.text('Hello'))

其中代码最主要的是,middleware 和 get 方法对应处理的 url 中的回调函数 handler 合并了

kotlin 复制代码
  get(path: string, handler: Middleware) {
    // 一次性 compose:全局中间件 + 路由 handler
    const chain = compose([...this.middleware, handler])
    this.routes[path] = chain
    return this
  }

主要通过的是 compose 函数合并,我们简单看下 compose 函数的实现:

javascript 复制代码
// compose.ts
import { Middleware, Context, Next } from './types'

export function compose(middleware: Middleware[]) {
  return function (context: Context, next?: Next): Promise<void> {
    let index = -1

    function dispatch(i: number): Promise<void> {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn: Middleware | undefined = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, () => dispatch(i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }

    return dispatch(0)
  }
}

说白了,compose 函数思路很简单,就是调用 dispatch(0) -> 然后内部调用 Promise.resolve(fn(context, () => dispatch(i + 1))),也就是将 dispatch 函数传递给中间函数,这个对应的是, 以下中间件的 next 函数,也就是 next 等于 dispatch(i + 1)

javascript 复制代码
// 中间件 A
app.use(async (c, next) => {
  console.log('Before A')
  await next()
  console.log('After A')
})

所以只有 next 函数自己调用的时候,中间件才会继续执行到下个中间件。

为什么叫洋葱模型

以下代码的打印顺序是这样的:

javascript 复制代码
const app = new Hono()

// 中间件 A
app.use('*', async (c, next) => {
  console.log('Before A')
  await next()
  console.log('After A')
})

// 中间件 B
app.use('*', async (c, next) => {
  console.log('Before B')
  await next()
  console.log('After B')
})

// 路由处理
app.get('/hello', (c) => c.text('Hello'))

打印

arduino 复制代码
 console.log('Before A')
 ↓
 console.log('Before B')
 ↓
 执行c.text('Hello')
 ↓
 console.log('After B')
 ↓
 console.log('After A')

就像包裹一层洋葱一样的感觉。

说完第一个 hono 核心设计思想洋葱模型,我们接下来看第二个!

适配器模式

Hono.js 需要在 不同运行时(Node.js、Deno、Bun、Cloudflare Workers 等)上运行,但用户希望用统一接口 app.get('/', handler):

不同 runtime 的 HTTP 请求接口不同:

  • Node.js:req: http.IncomingMessage / res: ServerResponse

  • Cloudflare Workers:req: Request

  • Deno:req: RequestEvent

Hono 不修改用户代码,而是通过 Adapter 层把不同 runtime 的请求和 Response 转换成统一的 Context

这个 Adapter 就是适配器。

适配器层是如何工作的?

当你使用 node 运行 Hono 时,你实际上不是直接用 Node 的 req/res,而是:

javascript 复制代码
import { serve } from '@hono/node-server'

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

Node 的 serve 会:

  • 接收 Node 原生的 req/res

  • 转换为 Fetch Request

  • 调用 app.fetch(request)

  • 将 Hono 返回的 Response 再转换回 Node 的 res

反正无论是什么平台,最终给 hono 的 app.fetch(request) 中的 request 满足 hono 定义的接口就行(一般都是 web 标准定义的 request)。

例如 deno 中,这样启动 deno 服务器:

javascript 复制代码
Deno.serve({ port: 8787 }, app.fetch)

这里为什么 Deno 没有引入 hono 的包去做适配呢,因为 Deno的去request 本身就是符合 web 标准的,只有 Node.js 不是完全符合的。

到现在大家明白了吧,也就是最终传给 app.fetch 的参数,一定要是 web 标准即可。

欢迎一起交流

最后欢迎大家一起交流前端全栈相关的各种知识,当然也包括 ai agent 开发,一起进步!

相关推荐
低保和光头哪个先来4 分钟前
解决 ios 使用 video 全屏未铺满页面问题
前端·javascript·vue.js·ios·前端框架
MacroZheng7 分钟前
全面升级!看看人家的后台管理系统,确实清新优雅!
前端·vue.js·typescript
Mintopia10 分钟前
一套简单但有效的"代码可读性"提升法:不用重构也能清爽
前端
禅思院20 分钟前
一个轻量级 Vue3 轮播组件:支持多视图、滑动距离决定切换数量,核心原理与 Swiper 对比
前端·vue.js·typescript
牛马11123 分钟前
Flutter BoxDecoration border 完整用法
开发语言·前端·javascript
CodeSheep24 分钟前
宇树科技的最新工资和招人标准
前端·后端·程序员
奔跑的卡卡29 分钟前
Web开发与AI融合-第二篇:TensorFlow.js实战:在浏览器中运行AI模型
前端·人工智能·tensorflow
IT_陈寒31 分钟前
Vue的响应式居然在这里埋坑,差点加班到天亮
前端·人工智能·后端
We་ct33 分钟前
LeetCode 149. 直线上最多的点数:题解深度剖析
前端·javascript·算法·leetcode·typescript
jarvisuni43 分钟前
JCode添加批量测试,一键同步运行6个Claude Code!
java·服务器·前端