Wailsでデスクトップアプリを作る #3 Goとフロントエンドの連携 — メソッドバインディングとイベント

読了 5分

前回は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() stringDo() — 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に型とメソッドを追加します。

app.go — TODOリストのバックエンド
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コンポーネントからそのまま使います。

frontend/src/App.tsx — 核心部分
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側から先に知らせる必要がある場面があります。時間のかかる処理の進捗が代表例です。そこで使うのがイベントシステムです。

app.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),
			})
		}
	}()
}
React — イベントの受信
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が止まらないようにする基本パターンです。

注記
イベントはフロントエンドからGoへも(EventsEmit)、フロントエンド同士でも使える双方向の経路です。ただしリクエストとレスポンスが対になる通信はバインディング呼び出しのほうが明確なので、イベントは「リクエストなしでプッシュする必要がある通知」に限定して使うと構造をシンプルに保てます。

まとめ #

この記事の要点は3つです。

  • バインディングは公開メソッドだけを公開し、戻り値はPromiseのresolveに、errorはrejectにマッピングされます。
  • 構造体はjsonタグを基準にTypeScriptモデルが自動生成され、両側の型が同期されます。
  • Goから先に知らせる情報はEventsEmitとEventsOnのイベントで伝え、重い処理はゴルーチンで回しながら進捗をイベントで報告します。

次回の「Wailsでデスクトップアプリを作る #4 システム統合 — ダイアログ・メニュー・ウィンドウ制御」では、デスクトップアプリらしい機能を付けていきます。ファイルを開くダイアログ、ネイティブメニュー、ウィンドウ制御のように、ブラウザではできなかったことをWailsランタイムで処理します。

X