CloudflareでもFastlyでもVercelでもDenoでもBunでもService Workerでも動く
HonoというWebフレームワークを作っています。
当初はCloudflare Workers向けに作っていたのですが、同じCDNであるFastlyのエッジランタイム、Compute@Edgeでも動くことが分かりました。また、Next.jsのEdge MiddlewareもしくはEdge API RoutesとしてVercelの環境でも動きます。そして、少々手を加えるとDenoでも動きました。もちろんDeno Deployにもデプロイできます。さらに、先程レポジトリが一般公開されたYet AnotherなJavaScriptランタイムのBunでも手を加えず動きました。
この「CloudflareでもFastlyでもVercelでもDenoでもBunでも動いた件」が、なかなか面白かったのでそれについて書きます。
Web標準のAPI
これらの環境で同じように動くのは、JavaScriptでかつ全てがWeb標準のAPIを採用しているからです。よく使うのがRequest
、Response
とオブジェクトです。Request
オブジェクトからはリクエストのURLや、メソッド、ヘッダ、リクエストボディなどを取得することができます。これを元に適切なルーティングをしたり、クエリストリングからパラメータを取り出したりします。レスポンスを返す時にはResponse
オブジェクトを使います。第一引数がボディ、第二引数がヘッダを含むオプションとなっています。
return new Response('Hello World!', {
headers: {
'X-Custom-Message': 'foo!',
},
})
この2つさえあれば、「あるルート"/"に来たリクエストに対して、"Hello World"を返す」というアプリが作れちゃいます。これがWeb標準のアプリの基本です。実にシンプルです。
Honoは、このRequest
/Response
をベースとした非常に小さなフレームワークです。なので、エントリーポイントの書き方さえ違えど、Web標準を採用している環境ならどこでも動くのです。
Hello World
では、それぞれで「Hello World」してみましょう。といっても当然ながらほとんど同じです。なので、共通部分を紹介します。ルーティングはExpressライクにこう書きます。上記したRequest
オブジェクトの中身を見てパスやメソッドごとにハンドリングしてます。
const app = new Hono()
app.get('/', (c) => c.text('Hono!!'))
その他、機能諸々あるのですが、他の記事にたくさん書いたので、割愛します。
じゃー行ってみよう。
Cloudflare Workers
Cloudflare WorkersはWeb標準の中でも「Service Worker」のfetch
イベントを採用していて、それがエントリーポイントになっています。素で書くとこうです。
addEventListener('fetch', event => {
event.respondWith(new Response('Hello world'));
});
これは後述するFastly Compute@Edgeでも採用されています。Honoではこのfetch
イベントをapp.fire()
というメソッドでラップしています。なので、一気通貫だとこう書けます。
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hono!!'))
app.fire()
また、Service Workerに加え「Module Worker」のシンタックスもあります。app.fire()
の代わりにこうします。
export default app
Module Workerだと、環境変数などのバインディングがローカルに縛れたり、Durable Objectsが使えたりするので、最近推奨されるようになりました。
Fastly Compute@Edge
Fastly Compute@EdgeはCloudflareのService Workersと全く同じ文法で書けます。
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hono!!'))
app.fire()
Compute@EdgeはJavaScriptの他にRustとAssemblyScriptで書けて、最終的にSpiderMonkey ベースのWebAssemblyにコンパイルされてエッジで実行されます。CloudflareはV8なので全く違う環境なのですが、全く同じ文法で書けるのが面白いです。
開発はFastlyが提供しているCLIでできます。このCLIがWasmへのコンパイルやデプロイをしてくれるわけです。
以下はコマンド一覧です。コマンドはサブコマンドになっているものがあるので、まだまだあります。fastly log-tail
とやるとデプロイした本番環境のログがtail -f
みたいに流れるのが面白いです。
CloudflareのWranglerにせよ「開発サーバーを立ち上げる、デプロイする、本番のログを閲覧する…」といったことがCLIひとつでできちゃうのはDXいいですね。CLIさえ動けば環境差異がありません。なにせ、Web標準のAPIがエミュレートされています。Miniflareや下記のEdge RuntimeではJestの環境も用意されているのでE2Eっぽいテストもできます。なので、例えば「Dockerを用意して云云」という手間が必要ありません。
Vercel
VercelのEdge Functions、つまりNext.jsのAPI RoutesとMiddlewareのエッジ版もWeb標準が元になっています。configでexperimental-edge
モードにすればHonoがそのまま動きます。エントリーの書き方が違いますが、ルーティングやレスポンスの返し方なので肝の部分は同じです。
import { NextRequest } from 'next/server'
import { Hono } from 'hono'
const app = new Hono()
app.get('/api/hello', (c) => {
return c.json({ message: 'Hello from Hooo' })
})
export default (req) => app.request(req)
export const config = {
runtime: 'experimental-edge',
}
それもそのはず、VercelのEdge FunctionsはCloudflare Workersで動いているらしいです。 => 昔はCloudflare Workersで動いていたとのことですが、先日のCloudflareの障害でもVercelは稼働していたので、今は違うんじゃないか説があって、ちょっと謎です。
追記
やっぱり、Cloudflare Workersで動いてみたいです!
ちなみに、先日Vercelでもローカルで再現するための環境「Edge Runtime」を公開しました。それについては以下に書きました。
Deno
Denoに対応するのは少々面倒でした。ランタイムが違うのは当然ですが、import
周りの構文がESとは違うのが手こずります。つまり、拡張子ありでimportするし、だいたいのライブラリをHTTPのURLから参照することになります。また、TypeScriptのチェックが厳しいです。
そのままのコードでは動かなかったので、「denoify」というコマンドを使ってDeno用のコードを出力することにしました。Nano JSXやEtaで使われていて、それなりに信頼できます。
こいつがimport周りと、必要に応じてpolyfillをしてくれます。
具体的にはdenoify
コマンドを実行するとsrc
の中のファイルを読んでくれて、変換されたDeno用のTSをdeno_dist
に吐いてくれます。これをそのまま参照すれば動きます。
そしていよいよ公開します。GitHubリポジトリと連動させdeno_dist
ディレクトリを指定すれば、deno.land/xに上げることができました。めでたくHonoはDenoのライブラリになったのです。
import { serve } from 'https://deno.land/std@0.146.0/http/server.ts'
import { Hono } from 'https://deno.land/x/hono@v1.6.3/mod.ts'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Deno!')
})
serve(app.fetch)
ローカルではdeno
コマンドで立ち上げることができます。
deno run --allow-net server.ts
ビルトインミドルウェアもほぼ動きます。ファイルシステムの扱いがCloudflalreと違う、というかそもそもCloudflare等にはファイルシステムがなく、Denoにはあるのですが、「serve-static」というミドルウェアもDeno用に中身を変えて作りました。JSXミドルウェアも合わせると割と高度なことができます。
/** @jsx jsx */
import { serve } from 'https://deno.land/std@0.146.0/http/server.ts'
import {
Hono,
logger,
poweredBy,
serveStatic,
jsx,
html,
} from 'https://deno.land/x/hono@v1.6.3/mod.ts'
const app = new Hono()
app.use('*', logger(), poweredBy())
app.all('/favicon.ico', serveStatic({ path: './public/favicon.ico' }))
type Props = {
title: string
children?: any
}
const Layout = (props: Props) => html`<!DOCTYPE html>
<html>
<head>
<title>${props.title}</title>
</head>
<body>
${props.children}
</body>
</html>`
app.get('/', (c) => {
return c.html(
<Layout title='Hello Deno!'>
<h1>Hono JSX example</h1>
</Layout>
)
})
serve(app.fetch)
もちろん、Deno Deployでも動きます。
ちなみに、Honoは"Ultrafast"を謳っており、本当に速いです。いくつかあるDenoのフレームワークとNode.jsの一部のベンチマークを取っているリポジトリがあったので、腕試しをしたところ、フレームワークの中ではNo.1でした!
本家リポジトリにプルリク出してるところです。
ところが、このベンチマークはただ「Hello World」するだけのアプリケーションを比べているので、あまり参考にならないんじゃないか…とも思っています。とはいえ、複雑なルーティングをしたとしても、Honoのルーターは非常に高速なので上位にランクインするはずです。
Bun
さて、さきほど公開されたBunです。BunもWeb標準のAPIを備えていることが分かったので、早速試してみました。そしたら見事に動きました。
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.html('Hello Bun!')
})
export default {
port: 3000,
fetch: app.fetch,
}
bun run ./index.ts
ベンチマークも取ってみました。Deno版と比べると負けますがまずまずのスコアです。
とはいえ、完璧にはサポートしきれていません。例えば、JSXミドルウェア。BunではJSX Pragmaをまだサポートしていなかったりで、使えないのです。そのうち対応すると思うので、待ちたいと思います。
ちなみに、早速「Bun」をサポートする予定はありますか?とIssueが立ったので「もうほぼ対応してます!」と答えておきました。
まとめ
以上、エッジランタイムやDeno、Bun、どこでも動くアプリケーションが作れることが分かりました。これはHonoに限ったことではないと思います。このような環境では、fsに依存していたり、evalしているNode.jsのライブラリは使えなくなります。ところが、Request
/Response
をベースとしたシンプルなWebアプリの開発は上記の通りDXが非常によいです。
そういえば、以前、Wranglerのメイン開発者「@threepointone」がHonoのことをツイートしてくれのですが、その際にBunの作者のが「APIがほぼ同じだからBunでも動くじゃん!」ってレスしてたのが現実になりました。感慨深いです。
また、Web標準を使う者通し、Cloudflare、Vercel、Deno、その他で協力していこうぜ!というワーキンググループ「WinterCG」も立ち上がっているのでその動向も気になります。
「Cloudflare、Fastly、Vercel、Deno、Bun」を試していなければ、どの環境でもいいので、動かしてみるといいと思います。楽しいですよ!
おまけ「Service Worker」
Cloudflare WorkersがService Workerのシンタックスだということは「(Honoは)ブラウザでも動くんじゃないか?」と思ったら動きました。すると「サーバーが自分自身と同じプログラムを配信して、それをブラウザがロードして、どちらでも同じコードが実行され、サーバーだけではなくブラウザからもレスポンスを返す」ことができました。
これ、個人的に「Sercie Worker Magic」と呼んでいます。興味ある方は以下を見てください。
Discussion