こんにちは、ふくちです。前回のブログを読んだうえで見ていただくと良いと思います。
Honoってあるじゃないですか。TypeScriptのWebフレームワークでめーちゃ有名なやつです。私はこの辺ガチエアプなので、具体的に何が良いのかわかってませんでした。ざっくり軽くて速くて書きやすいから良い、みたいな。
ただ前回の調査を経て、ちょっと解像度が上がった気がするのでそのアウトプットです。
Hono(日本語で「炎🔥」を意味する)は、Web標準に基づいて構築された、小型でシンプルかつ超高速なWebフレームワークです。Cloudflare Workers、Fastly Compute、Deno、Bun、Vercel、Netlify、AWS Lambda、Lambda@Edge、Node.jsなど、あらゆるJavaScriptランタイムで動作します。
Web標準のAPIってなんやねん
まずこれが何なのかわかってないので何が良いのかわかってなかったんですよね。なのでここを整理します。
曰くWeb標準のAPIとは、Webブラウザが標準で提供している機能をプログラムから使うためのものだそうです。DOM(ページの操作)、Fetch API(サーバーとの通信)、WebSocket APIなどが該当します。
そして今はDeno・Bun・Cloudflare Workers・Node.jsといったサーバー側の実行環境でも、これらブラウザの機能をAPIで実装しているんですね。DOMのようにブラウザでしか意味がないものは除いて、Fetch APIのRequest/ResponseやURL、Streamsなどはサーバー側でも使うことができるようです。
ちなみにサーバー側で共通して使えるAPIのセットは、WinterTCというEcmaの技術委員会がMinimum Common APIとして取りまとめているそうです。
例:Fetch Standard
Fetch APIの仕様は、HTMLやDOMなどの仕様を策定しているWHATWGという団体が、Fetch Standardとしてまとめています。
ざっくりHTTP通信をJavaScriptからどう扱うかという内容で、Request/Response/Headersなどの仕様が書かれています。
サンプルとして、次のコードで返したいHTTPレスポンスを作れます。
const response = new Response('Hello!', {
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
})
このResponseはFetch Standardで仕様が決まっていて、各実行環境がその仕様どおりに実装しているので、ブラウザでもDenoでもNode.jsでも同じように動きます。Requestも同様です。
Honoの実行環境
Honoのコアは、サーバー側でも使えるWeb標準APIだけで作られていて、Node.js独自のAPIは使っていません。なので色んな実行環境で動かせるということみたいです。
逆に言うと、従来のフレームワークはそうでないものが多かったということですね。
例えば下記のようなRequestを受け取ってResponseを返すHonoのアプリがあったとしましょう。これはWeb標準APIのRequestとResponseを使っています。
const app = new Hono()
app.get('/', (c) => c.text('hello'))
// app.fetch は Request を受け取って Response を返す関数
const res = await app.fetch(new Request('http://localhost/'))
console.log(await res.text()) // hello
Cloudflare Workers・Deno・Bunは、この形の関数を渡すだけでHTTPサーバーとして動きます。
Deno.serve(app.fetch) // Deno
export default app // Cloudflare Workers / Bun
一方、Node.jsの老舗フレームワークExpressの(req, res)はNode.jsのhttpモジュールのオブジェクトを拡張したものなので、Node.jsの外には持ち出せません。つまりNode.js以外の実行環境に移す場合はコード修正が必要になるということですね。
import http from 'node:http' // Node.js依存
import express from 'express'
const app = express()
app.get('/', (req, res) => {
console.log(req instanceof http.IncomingMessage) // true
console.log(res instanceof http.ServerResponse) // true
res.send('hello')
})
http.createServer(app).listen(3000)
両者を比較するとこうです。
// Hono:Requestを渡すとResponseが返ってくる関数
const res = await app.fetch(new Request('http://localhost/'))
// Express:Node.jsのhttpサーバーに渡して、req/resを作ってもらう関数
http.createServer(app).listen(3000)
HonoのappはRequest→Responseの関数なので、Web標準APIがある環境ならどこにでも移せます。
しかしExpressのappはNode.jsのhttpサーバーが作るreq/resを前提にしているので、Node.jsから移し替える際にはコード修正が必要になります。
ということで、Web標準APIにのみ依存しているHonoはいろんな環境で動かせて良いね!とされているようです。
補足:アダプターについて
それでもどうしても避けられない環境ごとの差分はありますよね。Lambdaだとhandler関数がエントリーポイントになっていたりして。
そういったところはコア部分とは別で提供されているアダプターというやつが吸収してくれます。hono/aws-lambdaみたいなやつです。
ただしこれらのアダプターは、実行環境固有の機能へ依存しています。
アダプターは特定の実行環境専用なので、その環境固有の機能を使っています。LambdaならLambda、Cloudflare WorkerならWorkers、みたいな感じで。
すなわちコアはWeb標準だけで作り、環境ごとの差分はアダプターに寄せる役割分担となっているようです。
例えばLambdaはHTTPリクエストではなくJSONのイベントを渡してくるので、アダプターがそれをRequestに変換し、返ってきたResponseをLambdaの戻り値の形に変換するような役割を担っています。
ここはコア部分と違うところなので少し注意が必要です。
おまけ:HonoとESM
前回の記事でESMとCommonJSによるビルド時の違い・注意点みたいなのに触れたので、ここでも同じ用に試してみました。
先程のようにHonoとExpressの両方でhelloを返すだけのアプリを書いて、esbuildでESM使ってビルドしてみるとこんな感じでした。
| 観点 | Hono(4.13.9) | Express(5.2.1) |
|---|---|---|
| コア部分の依存数 | 0 | 28 |
| バンドル後のサイズ | 2,396行・71KB | 24,355行・1.16MB |
| 圧縮後(minify) | 23KB | 815KB |
| CommonJSのコード | なし | あり |
| bannerなしで動くか | 動く | 動かず、Dynamic require of ... is not supportedが出る |
Expressは昔からあるフレームワークかつNode.js上で動くように作られているため、CommonJSがかなり多く使われているようです。つまりrequireとかがコード内にあるということですね。
それゆえ、bannerなど無しのままESMでビルドすると、前記事でも出てきたDynamic require...のエラーが起こります。
その一方でHonoはソースがESMで書かれており、ESMでビルドすればCommonJSのコードは混ざりません(一応CommonJSでも使えるようにはなっているらしいですが)。
なのでESMでビルドする際にめちゃ楽ちんということですね。
まとめ
Node.jsのことがちょっとわかると、Honoのこともさらにちょっとわかった気がします。
間違ってたらご指摘ください🙏
参考: