hono的学习笔记(一)

170 阅读4分钟

个人对hono和bun很感兴趣,近期在学习相关知识,水一贴。当然,bun的安装,配置,和基本知识在此不赘述。

Q:为什么选择hono?

A:在参考众多node端的框架后,选择hono的原因有两点:首先api逻辑简单,没什么学习的历史包袱。如果使用装饰器我为什么不去使用spring呢?其次,hono很明显符合现在时代的发展:无论是用在边缘计算也好,还是某些简单项目的快速云上部署,Cloudflare,Vercel,AWL Cloud之类的,都很方便。

新建一个项目

首先我们新建一个hono项目

bun create hono@latest hono-in-bun

cd my-app

bun install

hono是个很轻量,很简单的api服务,启动一个服务的简单代码如下:

const app = new Hono ();

// 声明根路由的api响应
app.get('/', (ctx) =>
  ctx.text('Hello Hono! ')
);

export default app;

我们只需要执行 bun run dev 即可,启动服务,可以通过导出port进行端口号指定:

import { Hono } from 'hono';

const app = new Hono ();
app.get('/', (ctx) =>
  ctx.text('Hello Hono! ')
);

export default {
  fetch: app.fetch,
  // 自定义你的端口,可以选择想办法从env中获取
  port: 3001,
};

路由声明

hono的路由声明十分简单,通配符部分,与tanstack-router,react-router等类似,就如同文章开头的那个路由。

由此我们可以拓展更多:

app.get('/item/:id', (ctx) => {
  const id = c.req.param('id');
  //查询后返回相关数据,逻辑代码
  const item = await db.select().from().where(eq('item.id',id))
  return ctx.json({ data:item });
});

app.delete('/:id', async (c) => {
  const id = c.req.param('id');
  // 删除相关记录
  await db.delete(items).where(eq(items.id, id));
  return c.json({ message: 'Item deleted', id });
})

中间件的使用

可以使用use语法去为服务载入中间件,让我们的服务更健壮,比如CORS,SecurityHeader,JWT等。

简单使用

我们可以在官网middleware一栏中找到很多hono官方做的中间件,我们可以先搞一个简单的Request Id中间件试试手:

import { Hono } from 'hono';
import { requestId } from 'hono/request-id';

const app = new Hono();

//通配所有路由
app.use('*', requestId());

app.get('/', (ctx) =>
  ctx.text(`Hello Hono! Your request id: ${ctx.get('requestId')}`)
);

export default {
  fetch: app.fetch,
  port: 3001,
};

如果我们想让代码健壮,提示信息全一点,可以导入requestId的type。

import { Hono } from 'hono';
import { requestId } from 'hono/request-id';
import type { RequestIdVariables } from 'hono/request-id';

//声明类型后,context.get就会有代码提示
const app = new Hono<{
  Variables: RequestIdVariables;
}>();

//通配所有路由
app.use('*', requestId());

app.get('/', (ctx) =>
  ctx.text(`Hello Hono! Your request id: ${ctx.get('requestId')}`)
);

export default {
  fetch: app.fetch,
  port: 3001,
};

进阶使用

我们现在可以自定义一个好玩的中间件了,我们新建一个middlewares文件夹,创建我们第一个中间件。来为请求增加很多很好玩的response header信息。

import type { Context, Next } from 'hono';

export const setHeaders = async (ctx: Context, next: Next) => {
  await next();
  ctx.header('X-Powered-By', 'hono');
  ctx.header('X-App-Id', 'hono-in-bun');
  ctx.header('X-Add-123123', 'hhh');
};

根据官网文档所知,我们的中间件只需要满足一个简单的条件:中间件不应返回任何内容,且使用 await next() 继续处理下一个中间件。

我们通过ctx获取context,获取header方法去设置header相关字段,next来表示去执行下一个middleware。

还有一个很好玩的用法的例子,是request-logger这种类型的,需要在request 请求后执行的方法,我们可以用这种方式巧妙地实现:

import type { Context, Next } from 'hono';
import { logger } from '../libs/logger';

export const requstLogger = async (ctx: Context, next: Next) => {
  const started = performance.now();
  try {
    await next();
  } finally {
    const duration = +(performance.now() - started).toFixed(2);
    logger.info({
      requestId: ctx.get('requestId'),
      method: ctx.req.method,
      path: ctx.req.path,
      status: ctx.res.status,
      durationMs: duration,
    });
  }
};

无需纠结logger是如何实现的,调用了什么工具类,重点是try...finally实现了这个功能,这个思路很值得学习。

除了手动声明,我们也可以使用hono提供的factory来生成中间件:

import { createMiddleware } from 'hono/factory'

const logger = createMiddleware(async (ctx, next) => {
  console.log(`[${ctx.req.method}] ${ctx.req.url}`)
  await next()
})

了解和明白这些以后,大概大家心里也有思路了,JWT,auth,或者什么其他基于完整/部分路由的特殊处理都可以交给中间件来处理了。

需要记住的地方

hono官方文档标注中间件按注册顺序依次进入;第一个中间件里 await next() 之前的逻辑最先执行;等内层全部跑完后,再按相反顺序执行 await next() 之后的逻辑(“回溯”阶段)。这是典型洋葱模型。

中间件报错后hono会主动捕获异常,要么传递给app.onError回调,要么在沿着中间件链向上返回之前自动将其转换为 500 响应。

小结

自己阅读文档,写代码时感觉理解的还挺到位,但是转化为文档的时候,就总会想着补全点,把之前“学来用”更深了一次,后续hono项目搭建和学习的相关文章会持续更新,直到项目搭建起来为止。会包含接入数据库,Redis等中间件,也有一些架构和openapi的相关搭建。