最近のHonoX
HonoXを公開して1年が立ちました。以下はその当時の記事です。
今回はこの1年間を振り返り、最近のHonoXについて知ってもらいます。
そんなに変わってない
実は1年前の公開以降、HonoXはそんなに変わってないです。リリースノートを遡るとわかりますが、feat
と書かれた新機能が少ないです。つまり当初のコンセプトはズレてないです。また、HonoXはHonoとViteのメタフレームワークなので、HonoX自体は機能を提供していません。ですので、新機能の導入はすごく少ないです。
今の課題は「やれるはずなのにできないこと」をなくすことです。これもだいぶ潰れてきました。
次はハマりどころをなくして、ドキュメントを整備するところだと思っています。ドキュメントはこれまで、GitHubのhonojs/honox
のREADMEに書いていました。最近は別にドキュメント用のWebサイトを作るプロジェクトを始めました。
「そんなに変わってない」というのは今後も続きそうで、使い勝手が変わらなくていいかと思います。
アルファステージ
現在の最新バージョンは「v0.1.38」です。未だに「alpha stage」を謳っており、セマンティックバージョニングのルールに関わらず、破壊的変更が入る可能性があります。
以降はベータ、「1.0」と進んでいきます。使っている事例が増えて、ステーブルになったと判断したら先に進めようと思います。
得意・不得意
HonoXの得意なこと、逆に不得意なところが分かってきました。
- 得意 - インタラクションの少ないWebサイト
- 不得意 - インタラクションの多いWebアプリ
そもそも、ページごとにフルHTMLをサーバサイドでレンダリングするMPA = Multi Page Applicationを作るためのフレームワークです。ですので、SPA = Single Page Applicationを作ることはできない、もしくは超不得意です。
HonoXが得意なMPA
これと同じ理由、つまりクライアントサイドでインタラクションが実行されるのは不得意なのでぐいぐい動く「Webアプリ」を作るのには向いていません。Islands Architectureを採用しているので、インタラクションを足すことはできます。しかしWebアプリを作ろうとすると、大きなIslandをボンと置くようになってしまいます。これだとIslands Architectureの意味がなくなってしまうし、だったら他のSPAやクライアントサイドが得意なフレームワークを使った方がいいです。
逆に「Webサイト」を作るのにHonoXは向いています。インタラクションがあっても一部分だけIslandにすればいい。もしくは遅延表示だけしたいから、Suspense
さえ使えればいい。
以下はSupense
を使ったコード例です。2秒間経ったあとにDone!
と表示される非同期コンポーネントをSuspense
で囲むとfallback
で指定したLoading...
が初回のHTMLで表示されます。
import { createRoute } from 'honox/factory'
import { Suspense } from 'hono/jsx'
const Component = async () => {
await new Promise((resolve) => setTimeout(resolve, 2000))
return <div>Done!</div>
}
export default createRoute(async (c) => {
return c.render(
<div>
<Suspense fallback={<div>Loading...</div>}>
<Component />
</Suspense>
</div>
)
})
これの面白いところはサーバーサイドの実装だけで、クライントに動きを追加できることです。fallback
に適切なコンポーネントを渡すことで、Web APIやDBなど取得に時間のかかる外部のリソースをストレス少なく表示することができます。
最初にLoading...が表示され、2秒経ったらDoneが表示される
MPAのアプリをJSXでサクサク作れる、というのは優れたDXだと思います。他のMPAを得意とするAstroやGatsbyともうまいこと差別化できる可能性はあります。
使用例
使用例も少ないながら出てきました。
はてラボの「はてなアイコン」というサービスで使ってもらっています。
また、「2ちゃんねる風のスレッドフロート型BBS」というVakKarmaではソースコードを読むことができます。HonoXの実装例として優れているものです。
また、他にも仕事でHonoXを使っているという話も聞きます。
アップデート
では、機能のアップデートを紹介します。
$component
でIslandに
これまではIslandコンポーネントは/app/islands
以下に置く必要がありましたが、先頭に$
をつけたファイルがIslandコンポーネントとして認識されるようになりました。関心事で、ルートとコンポーネントをまとめることが容易です。
app
├── client.ts
├── global.d.ts
├── routes
│ ├── $counter.tsx // ここに置けるようになった
│ ├── _404.tsx
│ ├── _error.tsx
│ ├── _renderer.tsx
│ └── index.tsx
├── server.ts
├── style.css
└── types.ts
以下はそれを使うコード。
import { createRoute } from 'honox/factory'
import Counter from './$counter' // app/routes/$counter.tsx
export default createRoute(async (c) => {
return c.render(
<div>
<Counter />
</div>
)
})
Script
とLink
コンポーネント
Script
とLink
コンポーネントが導入されました。これらはJavaScriptやCSSファイルを読み込み、ビルドする際にいい感じにパスを展開してくれます。開発時とプロダクションでアセットのパスが変わるのを明示的に書いて対応しなくてはいけなかったのを、これらを使うとManifestを参照して、勝手にやってくれます。Viteの設定も少なくなるケースがあり、よりシンプルになりました。
以下がこれまでのコード例です。import.meta.env.PROD
というフラグで本番環境かどうかを判断し、パスを変えています。
import { jsxRenderer } from 'hono/jsx-renderer'
export default jsxRenderer(({ children }) => {
return (
<html lang='en'>
<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
{import.meta.env.PROD ? (
<>
<script type='module' src='/static/client.js'></script>
<link href='/static/style.css' rel='stylesheet' />
</>
) : (
<>
<script type='module' src='/app/client.ts'></script>
<link href='/app/style.css' rel='stylesheet' />
</>
)}
</head>
<body>{children}</body>
</html>
)
})
以下が導入されたScript
、Link
コンポーネントを使った場合です。開発時のパス/app/style.css
と/app/client.ts
がビルド時に適切に変換されます。
import { jsxRenderer } from 'hono/jsx-renderer'
import { Link, Script } from 'honox/server'
export default jsxRenderer(({ children }) => {
return (
<html lang='en'>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<Link href='/app/style.css' rel='stylesheet' />
<Script src='/app/client.ts' async />
</head>
<body>{children}</body>
</html>
)
})
title
等タグの巻き上げ
HonoXの機能というか、hono/jsx
の新機能です。React 19で導入されたtitle
やmeta
やlink
タグが巻き上げられる機能がhono/jsx
でも利用することができるようになりました。
これは、HonoXにとっては熱くて、いちいちレンダラーに値を渡すことなく、ルートの中でtitle
などを書くことができます。レンダラーの実装や型定義が少なくなります。
これまではまず、以下のようにレンダラーの型定義をしていました。
import type {} from 'hono'
type Head = {
title?: string
}
declare module 'hono' {
interface ContextRenderer {
(content: string | Promise<string>, head?: Head):
| Response
| Promise<Response>
}
}
レンダラーではtitle
を引数で受け取ってました。
import { jsxRenderer } from 'hono/jsx-renderer'
export default jsxRenderer(({ children, title }) => {
return (
<html lang='en'>
<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
{title ? <title>{title}</title> : <></>}
</head>
<body>{children}</body>
</html>
)
})
ルートでは、c.render
の第2引数にtitle
を渡しています。
import { createRoute } from 'honox/factory'
export default createRoute(async (c) => {
return c.render(
<div>
<h1>Hello!</h1>
</div>,
{
title: 'Top page!',
}
)
})
新しいtitle
巻き上げの機能を使うと、JSXのタグの中にtitle
と書けば巻き上げてくれてhead
タグに入れてくれます。c.render
の引き数にする必要はありません。さらに、上記のレンダラーの型定義が必要なくなり、レンダラー内でtitle
タグを書かなくてすみます。
import { createRoute } from 'honox/factory'
export default createRoute(async (c) => {
return c.render(
<div>
<title>Top page!</title>
<h1>Hello!</h1>
</div>
)
})
Islandsがない時はJavaScriptオフ
上記で紹介したScript
コンポーネントを使うと、ページ内にIslandsがない時はJavaScriptが配信されません。無駄なリクエストが減ります。サイトのパフォーマンスも上がります。
アプリをビルドしてプレビューした様子。Islandを外すと*.js
ファイルが配信されない
Tailwind CSS
Tailwind CSSにも対応しています。現在、create-hono
コマンドで作成できるHonoXのスターターテンプレートではTailwind CSSを使っています。Tailwind CSSを使うための設定は簡単です。嫌だったら剥がすのは簡単です。
以下はTailwind CSSを使う場合のvite.config.ts
の例です。簡単です。
import tailwindcss from '@tailwindcss/vite'
import honox from 'honox/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
honox({
client: { input: ['./app/style.css'] }
}),
tailwindcss()
]
})
Cloudflare Workers
デプロイ先の推奨がCloudflare PagesだったのをClooudflare Workersにしていきます。これはCloudflareの意向で、PagesとWorkersを統合する計画があり、Workersに寄せていくからです。スターターテンプレートではnpm run deploy
を実行すれば、スイスイとWorkersにデプロイすることができます。
MPAのシンプルなアプリケーションをデプロイが早いCloudflare Workersに置くことは非常に開発者体験がよいです。
ビルド、デプロイ、動作確認まで20秒もかからない
もちろん、HonoXの実態はHonoなので、Cloudflareの他にも、DenoやBun、Node.jsの環境へデプロイできます。
まとめ
以上、最近のHonoXについて紹介してきました。
冒頭で書いた通り、大きな変更はなく、当初のコンセプトがそのまま残っています。今後もこれはブレないので、使いだしたら同じフィーリングで使い続けることができるフレームワークになるのではないかと思っています。
使用例が増えて、フィードバックができったらベータ、そして「1.0」へと進んでいきたいと思います。
Discussion