
最近我用bun+hono搭建了一个web服务,并尝试用docker打包部署。
在没有缓存的情况下,docker build
打包仅用了30s,如果是项目修改后再重新打包,更是连5s都用不了。
而这个小服务虽然只是刚起步,但已经具备了日志、响应和错误标准化返回、数据库连接(sqlite)、路由分组、环境变量配置、jwt鉴权这几项,可以说当成个人的小玩具已经够格了。
Bun is an all-in-one JavaScript runtime & toolkit designed for speed, complete with a bundler, test runner, and Node.js-compatible package manager.
Bun 是一款专为提高速度而设计的一体化 JavaScript 运行时和工具包,配有捆绑器、测试运行器和与 Node.js 兼容的包管理器。
Node.js 是一个免费、开源、跨平台的 JavaScript 运行时环境。
通过他们官方的一句话介绍,可以清晰的看到,他们都是为了让JavaScript脱离浏览器环境而创造的一个运行时环境。只不过Node使用C艹
编写,而Bun使用Zig
。Node基于V8引擎
(Goggle Chrome),而Bun使用JavaScriptCore
(Apple Safari)
以前我们用JavaScript都是和html配合写前端代码,写完后需要打开浏览器才能看到效果,而有了其他的运行时环境后,就可以像其他Python/Go语言一样直接在命令行执行JavaScript脚本,所以其功能也从操作Dom变为了操作系统级Api,如:文件IO、数据库等等。
在只有Node一家独大的时候,我们甚至可以在(某些)面试官问:「你会什么后端语言」的时候,说:「我会NodeJS」,而现在有了Deno
和Bun
,我岂不是会了三门后端语言?(🤫bushi
截止到当前,Node已经发布到了22.8.0
,庞大的开源module支撑起了整个社群的,而他的官方包管理器npm
有点让人一言难尽

于是又出现了yarn
、pnpm
,老外写这些东西可能真的是在解决需求,到了咱这边真的也就是给面试官多提供了一些出题思路。 而Bun本身就自带包管理器。
另外,现在要想写一个“时髦的”前端项目,还必须有一个打包器,因为在不同的运行环境中,不同的浏览器中,对JavaScript的支持标准大不相同,所以需要把新版本的JavaScript降级,或者把TypeScript转换为JavaScript。或者是把JavaScript文件大小进行压缩。
而Node生态下光打包器就有:Webpack、Rollup、Vite等等,更不要说Rust开始被大厂卷起来之后,又用Rust对以前打包速度、运行速度有上限的打包器进行重构。面试官的出题角度还在增加。 而Bun本身也是一个打包器。
另外还有测试运行器.. Vitest/Jest
所以现在可以明白,在2022年才发布的Bun究竟是想要做什么了
Hono🔥是一个基于 Web 标准构建的小型、简单且超快的 Web 框架。可以运行在所有JavaScript运行时,当然也包括了Bun
,所以作为尝鲜,就图个鲜上加鲜。
在使用任何框架前,我都习惯先通读一遍官方文档,这大概会花费我2-4小时的时候。
在读完一遍官方文档后,如果使用过其他框架完成过类似的项目,就会大概知道这个框架哪些是自带的,哪些需要借助第三方。
如果框架本身过于精简(比如Koa),你就不得不去研究一些官方插件或是第三方插件,或者说去拜读一些开源项目,或者去找一些快速启动模板,以便自己快速上手。
Hono则是在文档里提供了很多官方的插件(Helper),无需翻看其他文档就能实现功能
按照官方文档开始搭建,因为我这里使用的是Bun,所以需要先下载好Bun
Macos/Linux
curl -fsSL https://bun.sh/install | bash
然后创建项目
bun create hono@latest my-app
创建项目后会有一个入口index.ts
,在Bun中TS是一等公民,无需进行编译就能直接运行,所以速度非常快。
然后我们需要添加一些常用的中间件,如cors
、csrf
,然后给自己配置一下喜欢的端口号
import { Hono } from 'hono'
import { showRoutes } from 'hono/dev'
import { cors } from 'hono/cors'
import { csrf } from 'hono/csrf'
const app = new Hono()
// 统一的前缀
const api = app.basePath('/api')
// 预防csrf攻击
api.use(csrf())
// 所有接口设置cors, 也可以分别设置cors, 如user相关接口只允许指定ip访问
api.use('*', cors())
app.get('/', (c) => { return c.text('Hello Hono!') })
app.post('/', (c) => c.text('POST /'))
app.put('/', (c) => c.text('PUT /'))
app.delete('/', (c) => c.text('DELETE /'))
// 每个实例的err要自己监听
// api.onError(errorHandler)
// verbose 会显示详情信息, 如: 是否使用了中间件
// showRoutes(api, { verbose: false })
export default {
port: Bun.env.PORT,
fetch: app.fetch,
}
这样就完成了一个简单的服务,可以打开localhost:port
,看一下是否返回了Hello Hono
!
设置统一的前缀可以用basePath
,设置环境变量可以在.env
、.env.development
、.env.production
中配置(在bun的官方文档中),并使用Bun.env.XXXX
读取。
只有一个接口,我们可以写在index.ts
里,那如果有一堆接口呢,肯定要进行分组的
Hono中路由分组也比较简单,只要再用一次new Hono()
// user模块
const user = new Hono()
user.get('/list', (c) => c.text('List users')) // GET /user
user.get('/:id', (c) => {
// GET /user/:id
const id = c.req.param('id')
return c.text('Get user: ' + id)
})
user.post('/', (c) => c.text('Create user')) // POST /user
// indext.ts
const app = new Hono()
// 使用user路由组
app.route('/user', user)
所以,要想给路由分组,只需要在src下再新建一个user文件夹,里面实现user的路由,在从index.ts
里使用app.route('/user', user)
就可以了。
由于我们使用了basePath
,所以此时的user接口为/api/user/list
、/api/user/:id
我们在使用公司后端接口或者其他网站的开放接口接口时,经常会看到这样的结构/api/v1/user/a/b/c
在Hono中可以这样实现
user.get('/list', (c) => c.text('我是 user/list'))
v1.route('/user', user)
app.route('/v1', v1)
export default app
它会这样响应
GET /api/v1/user/list ---> `我是 user/list`
注意,如果上述代码中,route注册的顺序出错,则不会正常响应
当发生一些致命错误时,为了不让服务挂掉,我们需要catch住,并且返回给前端一些友好的提示。不然我们那些年骂过的xx后端就变成了自己。
在Hono中使用也很简单,不需要自己单独写个中间件
import { HTTPException } from 'hono/http-exception'
// ...
app.onError((err, c) => {
// 任何请求, http status 返回200, 错误码在返回体自定义
const status = 200;
// 记录原始的错误, 返回给前端的是友好的信息
// TODO Logger
const errorCode = 40001
const errorMsg = '不是我的错,想想前端的问题!'
if (err instanceof HTTPException) {
errorCode = ErrorCode.UNAUTHORIZED
}
const response = {
code: errorCode,
data: null,
message: errorMsg,
};
return c.json(response, status)
})
这样,在后端有任何错误的时候,我们都会以http status 200的状态码返回,并且可以在返回体中定义好固定的结果,并且返回出一个自定义的errorCode
,外加一个友好的前端能看得懂的errorMsg
随着项目的复杂度增加,可以把这个handler单独拆分出去,已达到精简入口文件的目的。
比如新建一个common
文件夹,里面写一个errorHandler.ts
,在index.ts
中或者在user模块src/user/index.ts
中,都可以分别使用app.onError()
和 user.onError()
具体处理通用的或者是自定义的错误处理逻辑!
这篇文章是一个入门篇,主要目的是讲述一下Node和Bun的区别,以及使用Bun+Hono的一个入门项目。
路由分组、错误捕捉这些功能很简单的就可以实现了,因为篇幅原因,我就把其他功能拆分成多篇教程了。后续教程会涉及:数据库、响应标准化、日志、jwt鉴权、docker/docker-compose打包部署等等,是一个完整闭环的小项目,代码也会开源分享出来,感兴趣的可以关注起来~
欢迎点赞催更👍