为什么学习Hono
市面上已经有很多node.js的后端库和框架了,之前用过Nest.js做过一些小项目,功能很全,实现起来项目很快,但是很多功能对于我的小项目来说,实在浪费,而且封装太高,写起来自由度很低。
偶尔网上看到了Hono,看了一下文档:
- 小
- 支持多种运行时(Bun、Deno、Node...)
- 还支持JSX
- typescript
- 自带一些常用的功能:RPC、Validation
- 常用中间件都有:Auth、JWT、加上三方估计有几十个
很好,学起来。
Hello Word
可以通过设置port来修改端口
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Word!')
})
const port = 8998; // 可以通过设置port来修改端口
console.log(`Server is running on port ${port}`);
serve({ fetch: app.fetch, port,});
访问:[http://localhost:3000](http://localhost:3000)
路由
// HTTP Methods
app.get('/', (c) => c.text('GET /'))
app.post('/', (c) => c.text('POST /'))
app.put('/', (c) => c.text('PUT /'))
app.delete('/', (c) => c.text('DELETE /'))
层级路由
const apiRoutes = app
.basePath("/api")
.route("/expenses", route1)
.route("/", route2);
前面的basePath("/api")可以让所有路由前缀都加上/api
export const route1 = new Hono()
.post("/", async (c) => {
return c.json({ });
});
通过http://localhost:3000/api/expenses就可以访问到上面的路由了
请求
获取请求的param和query
app.get('/posts/:id', (c) => {
const page = c.req.query('page')
const id = c.req.param('id')
return c.text(`You want see ${page} of ${id}`)
})
查看结果:http://localhost:3000/posts/1?page=12
获取请求中body的内容
app.put("/posts/:id{[0-9]+}", async (c) => {
const data = await c.req.json();
return c.json(data);
})
响应
除了text()还有很多json()、html()、notFound()、redirect()等等,来让请求返回不同类型的数据;
html()可以直接返回JSX;
JSX
import type { FC } from 'hono/jsx'
const app = new Hono()
const Layout: FC = (props) => {
return (
<html>
<body>{props.children}</body>
</html>
)
}
const Top: FC<{ messages: string[] }> = (props: {
messages: string[]
}) => {
return (
<Layout>
<h1>Hello Hono!</h1>
<ul>
{props.messages.map((message) => {
return <li>{message}!!</li>
})}
</ul>
</Layout>
)
}
app.get('/', (c) => {
const messages = ['Good Morning', 'Good Evening', 'Good Night']
return c.html(<Top messages={messages} />)
})
export default app
只需要把文件后缀改为tsx,直接写JSX,很React;
验证器
通过 zod 和 @hono/zod-validator 来实现验证器,检查客户端发过来的请求是否符合指定的数据格式;
安装:yarn add zod @hono/zod-validator
比如我们需要验证一个请求中,客户端发过来的数据格式必须是:
account: string;password: string
import { z } from "zod";import { zValidator } from "@hono/zod-validator";
const loginSchema = z.object({
account: z.string(),
password: z.string(),
})
app.post("/login", zValidator("json", loginSchema), async (c) => {
// do something
const user = c.req.valid("json");
return c.json({ });
});
通过zod文档了解更多
而且ide是直接可以查看到 user 的类型提示;
RPC
还记得上面的路由吗?
const apiRoutes = app.basePath("/api")
.route("/expenses", route1)
.route("/", route2);
RPC包含服务端和客户端;
在项目下:我的frontend目录放着一个vite的vue项目;
server是我的后端的src目录
传统的方式,我们需要在前端项目中使用ajax请求后端接口获取到数据,然后渲染到页面上。
RPC可以直接在前端通过.route1.$get()的方式来请求到数据,不需要关心请求地址、而且ide会自动补全返回的数据格式;
假设
后端提供了一个接口可以返回所有的文章:
const fakeData = [
{ id: 1, title: "Rent" },
{ id: 2, title: "Groceries" },
{ id: 3, title: "Utilities" },
];
export const postsRoute = new Hono()
.get("/posts", async (c) => {
return c.json({ expenses: fakeData });
})
前端中传统方法通过ajax请求:http://localhost:3000/posts
// 这里我用fetch演示,实际上可能是axios之类的;
async function getAllPosts() {
const res = await fetch("http://localhost:3000/posts");
if (!res.ok) {
throw new Error("server error");
}
const data = await res.json();
return data;
}
但是这样有几个问题:这个data的数据类型是怎么样的,很可能我们还要看接口文档,写type;
但是RPC可以通过以下方式来获取数据
async function getAllExpenses() {
const res = await api.posts.$get();
if (!res.ok) {
throw new Error("server error");
}
const data = await res.json();
return data;
}
ide直接提供了类型提醒;
上面代码的api是怎么来的?
// @server是对应的server项目的目录地址,这里我用了alias,实际上可能是../../../server之类的
import { type ApiRoutes } from "@server/index";
import { hc } from "hono/client";
const client = hc<ApiRoutes>('http://localhost:3000/');
export const api = client.api;
这个ApiRoutes是什么,看服务端代码:
实现zod的一个验证器,
const route = app.post(
'/posts',
zValidator(
'form',
z.object({
title: z.string(),
body: z.string(),
})
),
(c) => {
// ...
return c.json(
{
ok: true,
message: 'Created!',
},
201
)
}
)
然后导出
export type ApiRoutes = typeof route
这样就搞定了;
先写到这里,后面写写RPC做请求拦截、JWT