tug v0.45.0

Go + Inertia.js v3

API を挟まず、Go のハンドラーから直接 React のページを。

tug は、フロントエンドに Inertia.js を使う Go アプリのための Web フレームワークです。ハンドラーは型付きの props で React、Vue、Svelte のページをレンダリングし、アプリ全体が単一のバイナリになります。

ゼロからバイナリ 1 つまで
$ go install github.com/cuonggt/tug/cmd/tug@latest
$ tug new blog          # -auth for accounts
$ cd blog
$ tug dev
tug  │ serving http://localhost:8080, built in 268ms
^C
$ tug build
tug build: blog, 9.6 MB, frontend included

仕組み

Go の構造体が、ページの props にも、その型にもなる。

ハンドラーは構造体を渡してページをレンダリングし、ページのコンポーネントはその構造体を props として受け取ります。tug gen が構造体の TypeScript を書き出すので、コンポーネントは Go のコードに照らしてチェックされます。Go のコードが変わると、tug dev が書き出し直します。

main.go
type PostsIndexProps struct {
	Posts []Post                   `json:"posts"`
	Stats inertia.DeferProp[Stats] `json:"stats"`
}

var PostsIndex = tug.Page[PostsIndexProps]("Posts/Index")

func index(c *tug.Ctx) error {
	return PostsIndex.Render(c, PostsIndexProps{
		Posts: store.List(),
		Stats: inertia.Defer(store.Count), // after the page shows
	})
}
resources/js/tug/pages.ts
// Code generated by tug gen from the Go types. DO NOT EDIT.

export interface PostsIndexProps {
  posts: Post[]
  stats?: Stats
}
resources/js/pages/Posts/Index.tsx
import { Deferred, Link } from '@inertiajs/react'
import type { PageProps } from '../../tug/pages'
import { route } from '../../tug/routes'

type Props = PageProps<'Posts/Index'>

export default function Index({ posts, stats }: Props) {
  return (
    <>
      <Deferred data="stats" fallback={<p>Counting…</p>}>
        {stats && <p>{stats.posts} posts</p>}
      </Deferred>
      {posts.map((post) => (
        <Link
          key={post.id}
          href={route('posts.show', { id: post.id })}
        >
          {post.title}
        </Link>
      ))}
    </>
  )
}

機能

アプリに必要なものが、はじめから Inertia と連携済み。

セッション、フラッシュメッセージ、バリデーションエラー、CSRF は、設定なしで Inertia と連携して動きます。土台は Go の標準ライブラリです。

  • Inertia v3 をまるごと

    遅延・マージ・once・オプショナルの各 props、パスを指定した部分リロード、無限スクロール、Go から設定するページのタイトルと説明。

  • Go から生成される型

    tug gen は、各ページの props、名前付きルート、各フォームが送信する内容、フラッシュデータのキーを TypeScript で書き出します。

  • 検証が組み込まれたフォーム

    c.BindValid はリクエストをバインドし、構造体タグで検証します。検証に通らないフォームはエラーとともに戻され、各フィールドはフォーカスが外れた時点で検証されます。

  • フラグ 1 つでアカウント機能

    tug new -auth で、ユーザー登録、メールアドレスの確認、二要素認証のコード、パスキー、API トークン、通知が揃います。データベースは SQLite、Postgres、MySQL から選べます。

  • バックグラウンドジョブ

    失敗すると再実行され、どのタイムゾーンでもスケジュールでき、何度投入されても 1 回だけ実行でき、アプリ自身のトランザクションの中で投入できるジョブ。

  • バイナリ 1 つ

    tug build は、フロントエンドを内蔵した静的バイナリを 1 つ作ります。フロントエンドはアプリ自身が gzip で圧縮して配信します。アプリには Dockerfile も付いています。

  • 必要ならサーバーでレンダリング

    tug new -ssr なら、初回訪問のページを Inertia 自身の SSR でレンダリングします。レンダリングはアプリと並んで動く Node が行い、Node がなければページはブラウザーでレンダリングされます。

  • ブラウザーなしでテスト

    tugtest パッケージは、Go のテストで使う Inertia のクライアントです。訪問、フォーム、アップロード、ログインを扱い、ページの props はその Go の構造体に読み込みます。

  • すべてのインスタンスにイベントを

    broadcast パッケージは、チャンネル上のイベントをアプリのデータベース経由で、すべてのインスタンスで開かれているページに届けます。ページは変わったものを再読み込みします。

なぜ Go か

すべてのコアで動き、ミリ秒で起動する小さなバイナリ 1 つ。

Go なら、Web アプリは静的バイナリ 1 つになり、1 つのプロセスですべてのコアを使い、ミリ秒単位で起動し、数十 MB のメモリで動きます。tug は Inertia のアプリでも、そのすべてを保ちます。フロントエンドのビルドを内蔵した静的バイナリ 1 つを、Go 標準の HTTP サーバーが配信します。

  • パフォーマンス

    ノートパソコンでも毎秒数万リクエストを処理します。すべてのコアを使う 1 つのプロセスで、RAM は数十 MB です。

  • 起動

    ミリ秒単位で起動するので、ゼロまでスケールするプラットフォームでもコストを抑えられます。

  • デプロイ

    コピーするのはファイル 1 つです。インストールするランタイムも依存関係もなく、Mac から Linux や ARM 向けにビルドできます。

  • 開発

    小さく、コンパイルの速い言語です。標準ライブラリの HTTP サーバーは、本番環境でそのまま使えます。

速さ

毎秒 60,061 回の訪問を、19.9 MB のメモリで。

計測するのは、投稿とそのコメントを表示する Inertia のページ 1 つです。tug new が作るとおりのアプリが、セッション、CSRF、Content-Security-Policy を含めて、すべてのコアを使う 1 つのプロセスで配信します。

  • 60,061 1 秒あたりの訪問数

    Inertia のクライアントからのページの要求に JSON で応答します。100 回中 99 回は 4.38 ms 以内です。

  • 56,146 1 秒あたりの初回訪問数

    ブラウザーからのページの要求に HTML で応答します。100 回中 99 回は 5.09 ms 以内です。

  • 19.9 MB 負荷時のメモリ使用量

    起動後のアイドル時は 6.4 MB です。

  • 15 ms 起動時間

    バイナリを起動してから、ページに最初に応答するまで。

  • 9.6 MB デプロイ時のサイズ

    静的バイナリ 1 つで、ほかにインストールするものはありません。

  • 75 ns リクエストごとの追加時間

    Go 標準のルーターである ServeMux に、tug の App が加える時間です(1 つのプロセス内で計測)。

に Apple M1 Max、10 コア、macOS 27.0.1 で計測しました。 すべての数値と、その計測方法

ガイド

新しいアプリから、デプロイまで。

全 18 章を、アプリに必要になる順に並べています。さらに詳しいことは、pkg.go.dev にある各パッケージのドキュメントを参照してください。

  1. はじめに

    tug をインストールし、アプリを作って実行し、ページとフォームを追加します。

  2. ルーティングとハンドラー

    App、ルート、絶対 URL と署名付きリンク、グループ、ミドルウェア(セキュリティヘッダーもその 1 つ)、リクエストに応答したルート、プロキシの背後にいるクライアントのアドレス、Ctx、ファイル、ダウンロード、ストリームとイベント、リクエストのバインド、エラー。

  3. ページ

    Inertia のページとその props、ルートテンプレートとその nonce、後から計算される props、共有 props、リダイレクト、ページでのダウンロードとイベント、エラーページ、Vite、Inertia の DevTools。

  4. サーバーサイドレンダリング

    初回訪問のページを、アプリと並んで動く Node がサーバーでレンダリングする仕組みと、ssr パッケージ。

  5. フォームとセッション

    バリデーション、アプリ独自のルール、フォーカスが外れた時点で各フィールドを検証するフォーム、フラッシュメッセージ、セッション、CSRF。

  6. 多言語対応

    tug とアプリの文言を言語ごとのファイルから読み、リクエストの言語で表示する仕組み、lang パッケージ、tug lang。

  7. ファイル

    サイズと種類で検証されるアップロード、storage パッケージによるアプリのディスクや S3 への保存、公開または署名付きのファイルへのリンク。

  8. アカウント

    SQLite、Postgres、MySQL で動く認証スターターと、その API トークンと通知。auth パッケージと、CC や BCC、添付ファイル、ワンクリックの配信停止リンクに対応した mail パッケージ。

  9. マイグレーション

    SQL ファイルによるデータベースのテーブルの作成と変更。各ファイルは、アプリの起動時か migrate コマンドで、順番に 1 回ずつ実行されます。次のファイルを作る tug migrate new と、migrate パッケージ。

  10. 認可

    ユーザーがある対象に対して何をしてよいか、auth パッケージのアビリティ、理由を伝える 403 としての拒否、ページのユーザーにできることを props で渡す方法。

  11. 暗号化

    アプリのキー、tug がそのキーで暗号化・署名するもの、アプリ独自の値のための crypt パッケージ、キーのローテーション。

  12. バックグラウンドジョブ

    リクエストより長くかかる処理のための queue パッケージ。失敗したときの再実行、スケジュールによる実行(タイムゾーンの指定も可能)、何度投入されても 1 つしか待機しないジョブ、すべてのインスタンスを通じた同時実行数や 1 秒あたりの実行数の制限、最終的な失敗と各実行の結果の通知。

  13. キャッシュ

    計算に時間のかかるものを、アプリのどのインスタンスからも見つけられる場所に保存する cache パッケージと、同時に二重に実行してはならない処理のためのロック。

  14. ブロードキャスト

    チャンネル上のイベントを扱う broadcast パッケージ。イベントはアプリのデータベースを通じて、すべてのインスタンスで開かれているページに届き、ページは変わったものを再読み込みします。

  15. TypeScript

    tug gen が Go のコードから書き出す型。

  16. テスト

    アプリのページ、フォーム、アップロード、ログインを Go でテストするための Inertia のクライアントである tugtest パッケージ、メールのための mailtest、イベントのテスト。

  17. デプロイ

    バイナリ 1 つ、Dockerfile、環境変数、アプリ自身が提供する HTTPS と Let's Encrypt の証明書、ブラウザーがアプリのページで何をしてよいかを伝えるヘッダー(Content-Security-Policy もその 1 つ)、ログ、メトリクス。

  18. CLI

    tug new、tug dev、tug gen、tug routes、tug lang、tug migrate、tug build、tug key の詳細。

タグボートは小さくても、自分の何倍も大きな船を動かします。

最初のアプリを作る
tug