Wails 実践講座 #1 実践プロジェクトの設計 — 何を作り、どう分けるか
Wails でデスクトップアプリを作る 入門シリーズで、バインディング、ランタイム統合、ビルドまで概念を習得しました。ただしそのシリーズの ToDo アプリはデータがメモリだけにあり、アプリを閉じると消える学習用の骨組みでした。この実践シリーズはその骨組みに肉を付け、実際にインストールして使えるローカルノートアプリを 6 編にわたって完成させます。最初の記事は、コードを書く前に何を作るかを決め、構造の境界を引く設計です。
全 10 編、2 部構成です。第 1 部で配信可能なアプリを作り、第 2 部でリピーターが生まれる完成度を扱います。
第 1 部 — アプリを作って配信する
- #1 実践プロジェクトの設計 — 何を作り、どう分けるか ← この記事
- #2 SQLite ローカルデータベース — CGO なしの純粋な Go で
- #3 全文検索とデータフロー — FTS5 とイベント駆動の更新
- #4 トレイ常駐とグローバルショートカット — バックグラウンドから素早くキャプチャ
- #5 署名と公証 — デプロイしたアプリが信頼される仕組み
- #6 CI/CD 自動リリース — GitHub Actions で 3 プラットフォームへ配信
第 2 部 — 完成度とリピーター
- #7 ノートエディタの実践 — マークダウンプレビューと一覧の UX
- #8 テスト戦略 — サービスとリポジトリを検証する
- #9 自動更新の実装 — 新しいバージョンを安全に届ける
- #10 仕上げと完成度 — 設定・ダークモード・データバックアップ
何を作るか — ローカルノートアプリ #
作るのはマークダウンのノートをローカルに保存して素早く検索するデスクトップアプリです。クラウドなしで自分の端末の中で完結するオフラインファーストのツールで、次の機能を目標にします。
- ノートの作成・編集・削除、ローカルデータベースへの永続保存(#2)
- タイトル・本文の全文検索(#3)
- トレイに常駐し、グローバルショートカットでどこからでも新規ノートをキャプチャ(#4)
- 署名・公証されたインストーラーとして配布(#5)
- タグをプッシュすると 3 プラットフォームのリリースが自動生成(#6)
小さくても実際のアプリが備えるべき要素(永続性、検索、バックグラウンド常駐、配布の信頼、自動化)をすべて通過するように選んだ範囲です。画面は単純です。左にノート一覧と検索ボックス、右にエディタが一つあれば足ります。
境界を引く — 三つの層 #
実践アプリと学習用の骨組みの最初の違いは、すべてのロジックをアプリ構造体の一つに詰め込まないことです。入門シリーズでは App 構造体にメソッドを直接付けましたが、機能が増えるとその構造体がデータ保存、検索、ウィンドウ制御をすべて知る巨大な塊になります。最初から三つの層に分けます。
| 層 | 役割 | 本シリーズの例 |
|---|---|---|
| バインディング層(App) | フロントエンドに公開される薄い入口 | App.CreateNote, App.Search |
| サービス層 | 実際のドメインロジック | NoteService — 検証、検索、ルール |
| リポジトリ層 | データの永続化 | NoteRepository — SQLite アクセス |
バインディング層はフロントエンドのリクエストを受け取ってサービスに渡し、結果を返すだけです。ドメインロジック(タイトルが空なら拒否、検索語の正規化といったルール)はサービスに、DB を実際に読み書きするコードはリポジトリに置きます。こう分けると、リポジトリを SQLite から別のものに替えてもサービスはそのままで、サービスのロジックを Wails なしの純粋な Go テストで検証できます。
// App(バインディング) → NoteService(ドメイン) → NoteRepository(保存)
type App struct {
ctx context.Context
notes *NoteService
}
func (a *App) CreateNote(title, body string) (Note, error) {
// バインディング層はサービスに委譲するだけ
return a.notes.Create(title, body)
}依存は一方向です。バインディングがサービスを知り、サービスがリポジトリを知りますが、逆にリポジトリがバインディングを知ることはありません。このルール一つが、アプリが大きくなっても構造が崩れないように守ってくれます。
ドメインモデル — 最小限のノート #
ノート 1 件を表す型をまず決めます。実践では識別子と時刻の情報が必ず必要です。一覧のソート、編集の追跡、後の同期まで、これらのフィールドが基盤になります。
type Note struct {
ID int64 `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}構造体タグの json:"..." は 入門 #3 で扱ったとおり、フロントエンドに渡るときのフィールド名です。Go の CreatedAt が JavaScript で createdAt になるようにキャメルケースで合わせておきます。
フロントエンドの状態 — 何を選ぶか #
フロントエンドは入門シリーズと同じバニラの組み合わせでも動きますが、実践アプリは状態が増えます。一覧、選択中のノート、検索語、編集中の内容、保存の状態が同時に動きます。ここで選択肢が分かれます。
- バニラ JS: 依存がなくバンドルが最も小さいです。状態が単純なときに適しますが、上の状態が絡み合うと手動の DOM 更新がすぐに複雑になります。
- Svelte・Vue・React: 状態と画面を自動で結び付けます。Wails はフロントエンドを選ばないので何でも動き、このシリーズは状態管理の負担を減らすために軽量なコンポーネントフレームワークを前提に説明します(コードはフレームワークに大きく依存しないように保ちます)。
核心の原則は一つです。フロントエンドは画面と入力だけを担い、データの真実は常に Go 側に置きます。 検索結果もノート一覧もフロントエンドが自前で管理せず、Go に問い合わせます。こうすればデータの整合性を一箇所だけで守れば済みます。
まとめ #
- 実践シリーズはオフラインファーストのローカルノートアプリを 6 編にわたって、永続性・検索・トレイ・署名・CI まで完成させます。
- 学習用の骨組みとの最初の違いは層の分離です。バインディング(App)・サービス・リポジトリの三層に分け、依存は一方向にのみ流します。
- ドメインモデルには ID と作成・更新の時刻を必ず入れ、JSON タグでフロントエンドのフィールド名をキャメルケースに合わせます。
- フロントエンドは画面と入力だけを担い、データの真実は Go に置きます。状態が絡み合うなら、軽量なコンポーネントフレームワークが手動の DOM 更新より優れます。
- 次の記事ではリポジトリ層を SQLite で実装しますが、CGO なしでクロスコンパイルできる純粋な Go のドライバーを使います。