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 开发,一起进步!

相关推荐
幸运小圣1 小时前
动态组件【vue3实战详解】
前端·javascript·vue.js·typescript
用户413079810611 小时前
终于不漏了-Android开发内存泄漏详解
前端
努力glow .1 小时前
彻底解决VMware下ROS2中gazebo启动失败的问题
前端·chrome
阿笑带你学前端1 小时前
开源记账 App 一个月迭代:从 v1.11 到 v2.2,暗黑模式、标签系统、预算管理全面升级
前端
AAA阿giao1 小时前
浏览器底层探秘:Chrome的奇妙世界
前端·chrome·gpu·多进程·单进程·v8引擎·浏览器底层
星空椰1 小时前
Windows 使用nvm多版本管理node.js
windows·node.js
王兆龙1681 小时前
Vue3组件传值
前端·javascript·vue.js
随风一样自由2 小时前
React中实现iframe嵌套登录页面:跨域与状态同步解决方案详解
前端·react.js·前端框架·跨域
测试人社区—52722 小时前
破茧成蝶:DevOps流水线测试环节的效能跃迁之路
运维·前端·人工智能·git·测试工具·自动化·devops