Hono - 适用于任何 JavaScript 运行时的 Web 框架!

科技   2024-10-28 09:35   福建  

今天跟大家来介绍一款 JavaScript 运行时框架 - Hono,在 Github 上目前已经收获了 19.9K Star!

Hono,寓意为 “火焰🔥”,是一个小巧、简单且超快速的 Web 框架,构建在 Web 标准之上。

Hono 号称可以在任何 JavaScript 运行时环境中运行,包括 Cloudflare Workers、Fastly Compute、Deno、Bun、Vercel、Netlify、AWS Lambda、Lambda@EdgeNode.js

Hono 框架的诞生可以追溯到三年前,即 2021 年 12 月。当时,Hono 的作者(一位 Cloudflare 员工)希望能够为 Cloudflare Workers 创建一个应用程序,但不使用框架的代码会显得非常冗长,而且找不到适合我需求的框架。Itty-router 虽然很好,但太简单了;WorktopSunder 做的事情正是作者想做的,但它们的 API 不太符合作者的口味。另外作者还对创建一个基于 Trie 树结构的路由器感兴趣,因为这种结构非常快。于是,作者开始构建一个带有 Trie 树路由的 Web 框架。

不到 2 年拿下 73K Star 的前端开源项目!

一次编写,到处运行

Hono 的一个显著特性就是它可以真正地“一次编写,到处运行”,不仅限于 Cloudflare Workers,还可以在 Deno、BunNode.js 上运行。这主要归功于 Hono 不依赖于外部库,只使用了 Web 标准 API,而每个运行环境都支持这些 Web 标准。

让我们来看一个简单的例子:以下 src/index.ts 代码可以在 Cloudflare Workers、Deno 和 Bun 上运行:

import { Hono } from 'hono'

const app = new Hono()
app.get('/hello'(c) => c.text('code秘密花园:Hello Hono!'))

export default app

在 Cloudflare Workers 上运行这段代码,你需要执行以下命令:

wrangler dev src/index.ts

在 Deno 上运行:

deno serve src/index.ts

在 Bun 上运行:

bun run src/index.ts

这是一个简单的 “Hello World” 示例,但更复杂的应用程序也可以在 Cloudflare Workers 或其他运行时环境中运行。作为证明,几乎 Hono 所有的测试代码都可以在这些运行时中以同样的方式运行。这是真正的“一次编写,到处运行”体验。

或许你会疑惑,为什么 Cloudflare 的一名员工要创建一个可以在任何地方运行的框架?最初,Hono 只是为了与 Cloudflare Workers 配合使用而设计的。然而,从版本 2 开始,作者增加了对 Deno 和 Bun 的支持。这是一个非常明智的决定。如果 Hono 只针对 Cloudflare Workers,可能不会吸引那么多用户。通过在更多的运行时上运行,Hono 获得了更多的用户,从而发现更多的 bug 并获得更多的反馈,从而最终提高了软件质量。

速度极快

Hono 的路由器 RegExpRouter 是当前最快的路由器之一,避免了线性循环的使用,性能极其强悍。下表展示了 Hono 在 Cloudflare Workers 中的表现:

Hono x 402,820 ops/sec ±4.78% (80 runs sampled)
itty-router x 212,598 ops/sec ±3.11% (87 runs sampled)
sunder x 297,036 ops/sec ±4.76% (77 runs sampled)
worktop x 197,345 ops/sec ±2.40% (88 runs sampled)
Fastest is Hono
✨  Done in 28.06s.

谁在使用 Hono?

Hono 是一个类似于 Express 的简单 Web 应用框架,可以构建更加丰富的应用。通过结合中间件,Hono 可以实现以下几种使用场景:

  • 构建 Web API
  • 后端服务器的代理
  • CDN 前端
  • 边缘应用
  • 库的基础服务器
  • 全栈应用程序

目前,Hono 已被许多开发者和公司使用。例如,Unkey 使用 Hono 的 OpenAPI 功能将他们的应用程序部署到 Cloudflare Workers。以下是部分使用 Hono 的公司:

  • Cloudflare
  • Nodecraft
  • OpenStatus
  • Unkey
  • Goens
  • NOT A HOTEL
  • CyberAgent
  • AI shift
  • Hanabi.rest
  • BaseAI

此外,诸如 Prisma、Resend、Vercel AI SDK、SupabaseUpstash 等大型 Web 服务或库,也在其示例中使用了 Hono。

甚至有一些开发者将 Hono 作为 Express 的替代品。

Hono 和 Cloudflare 是完美的搭配

Hono 和 Cloudflare 的结合可以为开发者带来最好的体验。许多网站,包括 Cloudflare 的文档,都介绍了如下所示的 “原生”JavaScript 作为 Cloudflare Workers 的 “Hello World”:

export default {
  fetch() => {
    return new Response('code秘密花园:Hello World!')
  }
}

虽然这有助于理解 Workers 的原理,但如果你想创建一个返回 JSON 响应的接口,比如对请求 /books 的 GET 请求,你需要写类似这样的代码:

export default {
  fetch(req) => {
    const url = new URL(req.url)
    if (req.method === 'GET' && url.pathname === '/books') {
      return Response.json({
        oktrue
      })
    }
    return Response.json(
      {
        okfalse
      },
      {
        status404
      }
    )
  }
}

使用 Hono,你可以这样编写:

import { Hono } from 'hono'

const app = new Hono()

app.get('/books'(c) => {
  return c.json({
    ok: true
  })
})

export default app

代码不仅简洁,还能直观地理解它处理对 /books 的 GET 请求。如果需要处理 GET 请求到 /authors/yusuke 并从路径中提取变量 yusuke (即 yusuke 是可变的),原生 JavaScript 代码如下:

if (req.method === 'GET') {
  const match = url.pathname.match(/^\/authors\/([^\/]+)/)
  if (match) {
    const author = match[1]
    return Response.json({
      Author: author
    })
  }
}

使用 Hono,你不需要编写 if 语句,只需添加接口定义,更不需要编写正则表达式来获取变量 yusuke,而是可以使用 c.req.param() 函数:

app.get('/authors/:name'(c) => {
  const author = c.req.param('name')
  return c.json({
    Author: author
  })
})

当路由越来越多时,代码维护将变得复杂。使用 Hono,代码会非常简洁易于维护。此外,Hono 使用“上下文模型”轻松处理绑定到 Cloudflare 的产品,如 KV、R2 和 D1。上下文是一个容器,持有应用程序的状态,直到接收到请求并返回响应。你可以使用上下文来检索请求对象、设置响应头以及创建自定义变量。它还包含 Cloudflare 的绑定。例如,如果你设置了名为 MY_KV 的 Cloudflare KV 命名空间,你可以通过 TypeScript 类型补全来访问它:

import { Hono } from 'hono'

type Env = {
  Bindings: {
    MY_KV: KVNamespace
  }
}

const app = new Hono<Env>()

app.post('/message'async (c) => {
  const message = c.req.query('message') ?? 'Hi'
  await c.env.MY_KV.put('message', message)
  return c.text('message is set'201)
})

使用 Hono 代码书写简单直观,但没有任何限制。你可以使用 Hono 实现 Cloudflare Workers 所有可能的功能。

按需引用功能

Hono 非常小巧,使用最小的预设 hono/tiny,你可以在 12 KB 内写一个 "Hello World" 程序。这是因为它仅使用运行时内置的 Web 标准 API,且功能最小化。相较之下,Express 的打包大小为 579 KB。

然而,你仍然可以实现许多功能。例如,实现基本身份验证略显麻烦,但 Hono 内置了基本身份验证中间件,你可以这样简单地把基本身份验证应用到 /auth/page 路径:

import { Hono } from 'hono'
import { basicAuth } from 'hono/basic-auth'

const app = new Hono()

app.use(
  '/auth/*',
  basicAuth({
    username: 'hono',
    password: 'acoolproject',
  })
)

app.get('/auth/page'(c) => {
  return c.text('You are authorized')
})

Hono 包包含的内置中间件还允许 Bearer 和 JWT 认证,以及 CORS 的简单配置。这些内置中间件不依赖外部库,但亦可使用许多第三方中间件,这些中间件允许使用外部库,例如使用 Clerk 和 Auth.js 进行身份验证的中间件,以及使用 Zod 和 Valibot 进行验证的中间件。

Hono 还提供了一些内置工具,如 Streaming 助手,对于实现 AI 功能非常有用。这些工具可以按需添加,并且只在添加时增加文件大小。在 Cloudflare Workers 中,Worker 的文件大小有一定限制。保持核心小巧,并通过中间件和助手扩展功能是非常合理的做法。

下面是一些 Hono 具备的中间件和辅助工具,能够大大提升开发效率:

  • Basic Authentication
  • Bearer Authentication
  • Body Limit
  • Cache
  • Compress
  • Context Storage
  • Cookie
  • CORS
  • ETag
  • html
  • JSX
  • JWT Authentication
  • Logger
  • Pretty JSON
  • Secure Headers
  • SSG
  • Streaming
  • GraphQL Server
  • Firebase Authentication
  • Sentry 等

通过引入中间件,Hono 可以实现更为复杂的功能。例如,添加基本身份验证中间件:

import { Hono } from 'hono'
import { basicAuth } from 'hono/basic-auth'

const app = new Hono()

app.use(
  '/auth/*',
  basicAuth({
    username: 'hono',
    password: 'acoolproject',
  })
)

app.get('/auth/page'(c) => {
  return c.text('You are authorized')})

洋葱模型

Hono 的重要概念是“处理器”和“中间件”。处理器是用户定义的用来接收请求并返回响应的函数。例如,你可以写一个处理器,获取查询参数的值,从数据库中检索数据,并以 JSON 格式返回结果。中间件可以处理来到处理器的请求和处理器返回的响应。你可以将中间件与其他中间件结合起来,构建更大、更复杂的应用程序,结构像一个洋葱。

你可以非常简洁地创建中间件。例如,记录请求日志的自定义日志记录器可以这样编写:

app.use(async (c, next) => {
  console.log(`[${c.req.method}${c.req.path}`)
  await next()
})

如果你想在响应中添加自定义头,可以这样写:

app.use(async (c, next) => {
  await next()
  c.header('X-Message''Hi, this is Hono!')
})

将其与 HTMLRewriter 结合起来会很有意思。如果一个接口返回 HTML,可以编写修改 HTML 标签的中间件,如下所示:

app.get('/pages/*'async (c, next) => {
  await next()

  class AttributeRewriter {
    constructor(attributeName) {
      this.attributeName = attributeName
    }
    element(element) {
      const attribute = element.getAttribute(this.attributeName)
      if (attribute) {
        element.setAttribute(this.attributeName, attribute.replace('oldhost''newhost'))
      }
    }
  }
  const rewriter = new HTMLRewriter().on('a'new AttributeRewriter('href'))

  const contentType = c.res.headers.get('Content-Type')

  if (contentType!.startsWith('text/html')) {
    c.res = rewriter.transform(c.res)
  }
})

要创建中间件,你需要记住的内容很少,只需与上下文工作即可。

RPC 调用

Hono 拥有强大的类型系统。其中一个功能是 RPC(远程过程调用)。通过 RPC,你可以用 TypeScript 类型表达服务器端 API 规范。当这些类型在客户端作为泛型加载时,每个 API 端点的路径、参数和返回类型都会被推断出来,就像魔法一样。

例如,假设有一个用于创建博客文章的端点。这个端点接受一个 number 类型的 id 和一个 string 类型的 title。使用 Zod(一个支持 TypeScript 推断的验证库),可以定义如下模式:

import { z } from 'zod'

const schema = z.object({
  id: z.number(),
  title: z.string()
})

创建一个处理程序,以 JSON 格式通过 POST 请求接收这个对象,并使用 Zod Validator 检查是否匹配模式。响应将具有一个名为 message 的字符串类型属性:

import { zValidator } from '@hono/zod-validator'

const app = new Hono().basePath('/v1')

// ...

const routes = app.post('/posts', zValidator('json', schema), (c) => {
  const data = c.req.valid('json')
  return c.json({
    message: `${data.id.toString()} is ${data.title}`
  })
})

这是一个“典型”的 Hono 处理程序,但是你可以通过 typeof 获取的 routes 类型将包含其 Web API 规范的信息。在此例中,它包括创建博客文章的端点——向 /posts 发送 POST 请求会返回一个 JSON 对象。

export type AppType = typeof routes

现在,我们来创建一个客户端。你将先前的 AppType 作为泛型传递给 Hono 客户端对象。

import { hc } from 'hono/client'
import { AppType } from '.'

const client = hc<AppType>('http://localhost:8787')

设置完毕后,你就可以开始魔法操作了。代码补全工作完美无缺。当你写客户端代码时,不再需要完全了解 API 规范,这也有助于消除错误。

服务端 JSX

Hono 提供了内置的 JSX,这是一种允许你在 JavaScript 中编写类似 HTML 标签的代码的语法。提到 JSX,你可能首先想到 React,这是一个前端 UI 库。然而,Hono 的 JSX 最初是为了仅在服务器端运行而开发的。当首次开始开发 Hono 时,作者在寻找用于渲染 HTML 的模板引擎。大多数模板引擎,如 Handlebars 和 EJS,都在内部使用 eval,而 eval 在 Cloudflare Workers 上不被支持。然后作者想到了使用 JSX。

Hono 的 JSX 独特之处在于它将标签视为一个字符串。因此,以下代码实际上是可行的:

console.log((<h1>Hello!</h1>).toString())

不需要像在 React 中那样调用 renderToString()。如果你想渲染 HTML,只需返回这个字符串即可:

app.get('/'(c) => c.html(<h1>Hello</h1>))

非常有趣的是创建 Suspense —— React 中的一个特性,它允许你在等待异步组件加载时显示一个后备 UI —— 无需任何客户端实现。异步组件在仅服务器实现中运行。

服务器端 JSX 比你想象的要好玩。你可以用同样的方式为 Hono 的 JSX 复用 React 的 JSX 工具链,包括在编辑器中完成标签的功能,它们将成熟的前端技术带到了服务端。

编写测试

测试非常重要。幸运的是,使用 Hono 你可以轻松编写测试。

例如,让我们为一个接口编写测试。要测试对 / 的 GET 请求返回状态码 200,你可以这样编写:

it('should return 200 response'async () => {
  const res = await app.request('/')
  expect(res.status).toBe(200)
})

很简单,这种测试的美妙之处在于你不需要启动服务器。Web 标准 API 将服务器层黑箱化。Hono 的内部测试代码有 20000 行,但大多数都像上面那样写成,不需要启动服务器。

走向全栈

Hono 于2024年2月发布了新的主要版本 4。有三个突出的主要功能:

  • 静态站点生成
  • 客户端组件
  • 基于文件的路由

通过这些功能,我们可以在 Hono 中创建具有用户界面的全栈应用程序。客户端组件的引入支持 JSX 在客户端中工作,我们可以为页面添加交互,静态站点生成允许我们创建博客等,而不必将它们打包成一个 JavaScript 文件。

Hono 还启动了一个名为 HonoX 的实验项目。这是一个使用 Hono 和 Vite 的元框架,提供基于文件的路由和将客户端组件与服务端生成的 HTML 相结合的机制。更容易创建与 Cloudflare Pages 或 Workers 完美匹配的大型应用程序。

此外,Hono 还计划将其作为现有全栈框架(如 Remix 和 Qwik)的基础服务器运行。与起始于客户端的 React 项目 Next.js 相比,Hono 尝试从服务器端成为全栈框架。

开始使用 Hono

要开始使用 Hono,只需以下几步:

通过 npm:

npm create hono@latest

通过 yarn:

yarn create hono

通过 pnpm:

pnpm create hono@latest

通过 bun:

bun create hono@latest

通过 deno:

deno run -A npm:create-hono@latest

最后

参考:

  • https://hono.dev/docs/
  • https://github.com/honojs/honox
  • https://github.com/honojs/hono/releases/tag/v4.0.0
  • https://blog.cloudflare.com/the-story-of-web-framework-hono-from-the-creator-of-hono/

全栈修仙之路
专注分享 TS、Vue3、前端架构和源码解析等技术干货。
 最新文章