Wailsでデスクトップアプリを作る #3 Goとフロントエンドの連携 — メソッドバインディングとイベント
前回はapp.goのメソッドがwailsjsのJavaScript関数として公開される流れを確認しました。今回はその連携を本格的に使います。バインディングの規則を正確に整理し、TODOリストのバックエンドを作ってReactフロントエンドとつないだうえで、逆方向の通信であるイベントまで扱います。例はreact-tsテンプレート基準です。
全 8 編(本編 6 編 + 応用 2 編)です。
- #1 Wailsとは — Goで作る軽量デスクトップアプリ
- #2 プロジェクト構造と開発ループ — wails devとバインディング
- #3 Goとフロントエンドの連携 — メソッドバインディングとイベント ← この記事
- #4 システム統合 — ダイアログ・メニュー・ウィンドウ制御
- #5 実践機能 — 設定の保存とエラー処理
- #6 ビルドと配布 — プラットフォーム別パッケージング
バインディングの規則 — 何がどう公開されるのか #
Bindに登録した構造体のうち、フロントエンドに公開されるのは公開メソッド(大文字で始まる)だけです。Go側のシグネチャがJavaScript側でどう見えるのか、規則を表で整理します。
| Goメソッド | JavaScript側では |
|---|---|
func (a *App) Do() | Do() — Promise<void> |
func (a *App) Do() string | Do() — Promise<string> としてresolve |
func (a *App) Do() (string, error) | errorがnilならresolve、そうでなければreject |
func (a *App) Do(n int, s string) | Do(n, s) — 引数の順序そのまま |
| 小文字で始まるメソッド | 公開されない |
核心はerrorのマッピングです。Goの慣例どおり最後の戻り値をerrorにしておけば、フロントエンドではPromiseのrejectとして届き、try/catchで処理できます。両言語のエラー処理の慣例が自然につながる設計です。
ハンズオン — TODOリストのバックエンド #
メモリにTODOを保持するバックエンドを作ります。app.goに型とメソッドを追加します。
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
type App struct {
ctx context.Context
todos []Todo
nextID int
mu sync.Mutex
}
func (a *App) AddTodo(title string) (Todo, error) {
if strings.TrimSpace(title) == "" {
return Todo{}, errors.New("タイトルが空です")
}
a.mu.Lock()
defer a.mu.Unlock()
a.nextID++
todo := Todo{ID: a.nextID, Title: title}
a.todos = append(a.todos, todo)
return todo, nil
}
func (a *App) ListTodos() []Todo {
a.mu.Lock()
defer a.mu.Unlock()
return a.todos
}
func (a *App) DeleteTodo(id int) {
a.mu.Lock()
defer a.mu.Unlock()
a.todos = slices.DeleteFunc(a.todos, func(t Todo) bool {
return t.ID == id
})
}ごく普通のGoコードです。デスクトップアプリだからといって特別なことはなく、ゴルーチンからアクセスされる可能性があるためミューテックスで保護した程度がすべてです。保存先がメモリなのでアプリを閉じると消えますが、ファイルに残す方法は#5で解決します。
フロントエンドから呼び出す #
wails devを実行するとwailsjsに3つの関数が生成されます。Reactコンポーネントからそのまま使います。
import { useEffect, useState } from 'react';
import { AddTodo, ListTodos, DeleteTodo } from '../wailsjs/go/main/App';
import { main } from '../wailsjs/go/models';
function App() {
const [todos, setTodos] = useState<main.Todo[]>([]);
const [title, setTitle] = useState('');
useEffect(() => {
ListTodos().then(setTodos);
}, []);
async function handleAdd() {
try {
await AddTodo(title);
setTitle('');
setTodos(await ListTodos());
} catch (err) {
alert(err); // Goが返したerrorメッセージ
}
}
// ...入力欄とリストのレンダリング
}AddTodoが空のタイトルで呼ばれるとGo側がerrorを返し、フロントエンドではcatchで捕まります。サーバーなしでバックエンドとフロントエンドが1つのプロセスの中で関数呼び出しとしてつながるのが、Wails開発の感覚です。
構造体とTypeScriptモデル #
上のコードでmain.Todo型をimportしている部分が目を引きます。Wailsはバインディングされたメソッドがやり取りする構造体を解析し、TypeScriptモデルを自動生成します(wailsjs/go/models.ts)。Go構造体のjsonタグがフィールド名を決めるため、タグを付けておけばフロントエンドでは小文字のフィールドとして自然に扱えます。Goの型を変えればモデルも次のビルドで更新され、両側の型がずれる事故をコンパイル段階で捕まえてくれます。
イベント — Goからフロントエンドへプッシュする #
バインディングの呼び出しは常にフロントエンドが起点です。逆にGo側から先に知らせる必要がある場面があります。時間のかかる処理の進捗が代表例です。そこで使うのがイベントシステムです。
import "github.com/wailsapp/wails/v2/pkg/runtime"
func (a *App) ProcessFiles(paths []string) {
go func() {
for i, path := range paths {
process(path)
runtime.EventsEmit(a.ctx, "progress", map[string]any{
"done": i + 1,
"total": len(paths),
})
}
}()
}import { EventsOn } from '../wailsjs/runtime/runtime';
useEffect(() => {
const off = EventsOn('progress', (data) => {
setProgress(data);
});
return off; // コンポーネントのクリーンアップ時に購読解除
}, []);EventsEmitの最初の引数に使われているa.ctxが、#2で見たstartupのcontextです。Wailsのランタイム関数はこのcontextを通じてアプリとつながるため、startupでcontextを保管しておく慣例はこの先も繰り返し登場します。バインディングの呼び出しがゴルーチンの中でEventsEmitを呼ぶ上の構造は、重い処理でUIが止まらないようにする基本パターンです。
まとめ #
この記事の要点は3つです。
- バインディングは公開メソッドだけを公開し、戻り値はPromiseのresolveに、errorはrejectにマッピングされます。
- 構造体はjsonタグを基準にTypeScriptモデルが自動生成され、両側の型が同期されます。
- Goから先に知らせる情報はEventsEmitとEventsOnのイベントで伝え、重い処理はゴルーチンで回しながら進捗をイベントで報告します。
次回の「Wailsでデスクトップアプリを作る #4 システム統合 — ダイアログ・メニュー・ウィンドウ制御」では、デスクトップアプリらしい機能を付けていきます。ファイルを開くダイアログ、ネイティブメニュー、ウィンドウ制御のように、ブラウザではできなかったことをWailsランタイムで処理します。