Zenn
🆕

最近のHonoX

2025/03/03に公開
20

HonoXを公開して1年が立ちました。以下はその当時の記事です。

https://zenn.dev/yusukebe/articles/724940fa3f2450

今回はこの1年間を振り返り、最近のHonoXについて知ってもらいます。

そんなに変わってない

実は1年前の公開以降、HonoXはそんなに変わってないです。リリースノートを遡るとわかりますが、featと書かれた新機能が少ないです。つまり当初のコンセプトはズレてないです。また、HonoXはHonoとViteのメタフレームワークなので、HonoX自体は機能を提供していません。ですので、新機能の導入はすごく少ないです。

今の課題は「やれるはずなのにできないこと」をなくすことです。これもだいぶ潰れてきました。

次はハマりどころをなくして、ドキュメントを整備するところだと思っています。ドキュメントはこれまで、GitHubのhonojs/honoxのREADMEに書いていました。最近は別にドキュメント用のWebサイトを作るプロジェクトを始めました。

https://github.com/honojs/honox-website/issues/1

「そんなに変わってない」というのは今後も続きそうで、使い勝手が変わらなくていいかと思います。

アルファステージ

現在の最新バージョンは「v0.1.38」です。未だに「alpha stage」を謳っており、セマンティックバージョニングのルールに関わらず、破壊的変更が入る可能性があります。

以降はベータ、「1.0」と進んでいきます。使っている事例が増えて、ステーブルになったと判断したら先に進めようと思います。

得意・不得意

HonoXの得意なこと、逆に不得意なところが分かってきました。

  • 得意 - インタラクションの少ないWebサイト
  • 不得意 - インタラクションの多いWebアプリ

そもそも、ページごとにフルHTMLをサーバサイドでレンダリングするMPA = Multi Page Applicationを作るためのフレームワークです。ですので、SPA = Single Page Applicationを作ることはできない、もしくは超不得意です。

MPA
HonoXが得意なMPA

これと同じ理由、つまりクライアントサイドでインタラクションが実行されるのは不得意なのでぐいぐい動く「Webアプリ」を作るのには向いていません。Islands Architectureを採用しているので、インタラクションを足すことはできます。しかしWebアプリを作ろうとすると、大きなIslandをボンと置くようになってしまいます。これだとIslands Architectureの意味がなくなってしまうし、だったら他のSPAやクライアントサイドが得意なフレームワークを使った方がいいです。

逆に「Webサイト」を作るのにHonoXは向いています。インタラクションがあっても一部分だけIslandにすればいい。もしくは遅延表示だけしたいから、Suspenseさえ使えればいい。

以下はSupenseを使ったコード例です。2秒間経ったあとにDone!と表示される非同期コンポーネントをSuspenseで囲むとfallbackで指定したLoading...が初回のHTMLで表示されます。

app/routes/index.tsx
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など取得に時間のかかる外部のリソースをストレス少なく表示することができます。

Suspense
最初にLoading...が表示され、2秒経ったらDoneが表示される

MPAのアプリをJSXでサクサク作れる、というのは優れたDXだと思います。他のMPAを得意とするAstroやGatsbyともうまいこと差別化できる可能性はあります。

使用例

使用例も少ないながら出てきました。

はてラボの「はてなアイコン」というサービスで使ってもらっています。

https://developer.hatenastaff.com/entry/2025/02/07/180711

また、「2ちゃんねる風のスレッドフロート型BBS」というVakKarmaではソースコードを読むことができます。HonoXの実装例として優れているものです。

https://github.com/calloc134/vakkarma-main

また、他にも仕事で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

以下はそれを使うコード。

app/routes/index.tsx
import { createRoute } from 'honox/factory'
import Counter from './$counter' // app/routes/$counter.tsx

export default createRoute(async (c) => {
  return c.render(
    <div>
      <Counter />
    </div>
  )
})

ScriptLinkコンポーネント

ScriptLinkコンポーネントが導入されました。これらはJavaScriptやCSSファイルを読み込み、ビルドする際にいい感じにパスを展開してくれます。開発時とプロダクションでアセットのパスが変わるのを明示的に書いて対応しなくてはいけなかったのを、これらを使うとManifestを参照して、勝手にやってくれます。Viteの設定も少なくなるケースがあり、よりシンプルになりました。

以下がこれまでのコード例です。import.meta.env.PRODというフラグで本番環境かどうかを判断し、パスを変えています。

app/routes_renderer.tsx
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>
  )
})

以下が導入されたScriptLinkコンポーネントを使った場合です。開発時のパス/app/style.css/app/client.tsがビルド時に適切に変換されます。

app/routes/_renderer.tsx
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で導入されたtitlemetalinkタグが巻き上げられる機能がhono/jsxでも利用することができるようになりました。

これは、HonoXにとっては熱くて、いちいちレンダラーに値を渡すことなく、ルートの中でtitleなどを書くことができます。レンダラーの実装や型定義が少なくなります。

これまではまず、以下のようにレンダラーの型定義をしていました。

app/global.d.ts
import type {} from 'hono'

type Head = {
  title?: string
}

declare module 'hono' {
  interface ContextRenderer {
    (content: string | Promise<string>, head?: Head):
      | Response
      | Promise<Response>
  }
}

レンダラーではtitleを引数で受け取ってました。

app/routes/_renderer.tsx
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を渡しています。

app/routes/index.tsx
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タグを書かなくてすみます。

app/routes/index.tsx
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が配信されません。無駄なリクエストが減ります。サイトのパフォーマンスも上がります。

Network
アプリをビルドしてプレビューした様子。Islandを外すと*.jsファイルが配信されない

Tailwind CSS

Tailwind CSSにも対応しています。現在、create-honoコマンドで作成できるHonoXのスターターテンプレートではTailwind CSSを使っています。Tailwind CSSを使うための設定は簡単です。嫌だったら剥がすのは簡単です。

以下はTailwind CSSを使う場合のvite.config.tsの例です。簡単です。

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に置くことは非常に開発者体験がよいです。

Deploy
ビルド、デプロイ、動作確認まで20秒もかからない

もちろん、HonoXの実態はHonoなので、Cloudflareの他にも、DenoやBun、Node.jsの環境へデプロイできます。

まとめ

以上、最近のHonoXについて紹介してきました。

冒頭で書いた通り、大きな変更はなく、当初のコンセプトがそのまま残っています。今後もこれはブレないので、使いだしたら同じフィーリングで使い続けることができるフレームワークになるのではないかと思っています。

使用例が増えて、フィードバックができったらベータ、そして「1.0」へと進んでいきたいと思います。

https://github.com/honojs/honox

20

Discussion

ログインするとコメントできます