🙆‍♀️

フルスタックアプリを1つの実行ファイルで!PocketBase に Vite を埋め込む方法

2025/02/03に公開

PocketBase は、実行ファイル1つで動作する BaaS です。PocketBase の実行ファイルをそのままバックエンドとして利用するだけでも十分便利ですが、Go で拡張することで、さらに多くの機能を追加できます。

今回はその例として、Vite で作成したフロントエンドを go:embed を用いて PocketBase の実行ファイルに埋め込み、フロントエンドとバックエンドを1つの実行ファイルに統合する方法を紹介します。

PocketBase 自体については、こちらの記事で説明しています。
https://zenn.dev/ikumasudo/articles/60791ecce05b16

サンプルコード

GitHub にコードを公開しています。
https://github.com/ikumasudo/pocketbase-embed

ディレクトリ構成

ディレクトリの構造は以下のようになっています。Go のプロジェクト内に /site ディレクトリがあり、その中に Vite(React)のプロジェクトが含まれています。

.
├── Makefile
├── go.mod
├── go.sum
├── main.go
├── pb_data
└── site
    ├── README.md
    ├── bun.lock
    ├── embed.go  <-- `go:embed`
    ├── eslint.config.js
    ├── index.html
    ├── package.json
    ├── public
    │   └── vite.svg
    ├── src
    │   ├── App.css
    │   ├── App.tsx
    │   ├── assets
    │   ├── index.css
    │   ├── main.tsx
    │   └── vite-env.d.ts
    ├── tsconfig.app.json
    ├── tsconfig.json
    ├── tsconfig.node.json
    └── vite.config.ts

embed.go の実装

embed.go には、フロントエンドを埋め込むための設定が記述されています。

package site

import (
	"embed"
	"io/fs"
)

//go:embed all:dist
var distDir embed.FS

// DistDirFS は、埋め込まれた `dist` ディレクトリのファイル群を指す
var DistDirFS, _ = fs.Sub(distDir, "dist")

ここで指定している dist ディレクトリは、フロントエンドのビルド後に生成される静的ファイルを格納する場所です。これにより、フロントエンドのファイルを実行ファイル内に埋め込むことができます。

main.go での活用

埋め込まれた静的ファイルは main.go で以下のように使用されます。

func main() {
    // 開発環境かどうかを判定
	isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())

	app := pocketbase.New()

	app.OnServe().BindFunc(func(se *core.ServeEvent) error {
		switch isGoRun {
		case true:
            // 開発環境では Vite の開発サーバーへ転送
			proxy := httputil.NewSingleHostReverseProxy(&url.URL{
				Scheme: "http",
				Host:   "localhost:5173",
			})
			se.Router.GET("/", func(e *core.RequestEvent) error {
				proxy.ServeHTTP(e.Response, e.Request)
				return nil
			})
		default:
            // 本番環境では埋め込まれた静的ファイルを提供
			se.Router.GET("/{path...}", apis.Static(site.DistDirFS, true))
		}
		return se.Next()
	})

	if err := app.Start(); err != nil {
		log.Fatal(err)
	}
}

PocketBase では、/api 以下のパスはバックエンド API 用、/_ 以下のパスは管理画面用に予約されています。上記のコードでは、それ以外のパスをすべて静的ファイルへルーティングするよう設定しています。

また、apis.Static(site.DistDirFS, true) の第2引数を true にすることで Index Fallback を有効化しています。これにより、対応する静的ファイルが存在しないパスへのリクエストが index.html にフォールバックされるため、SPA のルーティングが適切に機能します。

実行ファイルのビルド

以下のコマンドを実行すると、フロントエンドとバックエンドが統合された実行ファイルが作成されます。GitHub のコードでは Makefile に記載されています。

cd site && bun install && bun run build
cd .. && go build

開発環境でのフロントエンド更新

本番環境ではフロントエンドを実行ファイルに埋め込む形を取りますが、開発環境ではフロントエンドを変更するたびに再ビルドするのは手間がかかります。

そのため、開発環境では httputil.NewSingleHostReverseProxy を用いて、リクエストを Vite の開発サーバー (localhost:5173) へ転送する仕組みを導入しています。これにより、Vite のホットリロードを活用できます。

開発時の実行方法は以下のとおりです。

cd site && bun dev

別のターミナルで:

go run . serve

まとめ

この方法を使うことで、PocketBase のシンプルなバックエンド機能と、Vite を用いたモダンなフロントエンドを1つの実行ファイルに統合できます。開発環境では Vite のホットリロードを活用し、本番環境では静的ファイルを埋め込むことでシンプルなデプロイが可能になります。

Beszel

この記事は Beszel という PocketBase 製のサーバー監視ソフトウェアのリポジトリを参考にしました。シンプルで使いやすく,とても気に入っているソフトウェアのひとつです。

https://beszel.dev

https://github.com/henrygd/beszel/tree/main

Discussion