どうやって動いているの?
Wailsは、webkitフロントエンドを備えた、何の変哲もないGoアプリです。 アプリ全体のうちGoの部分は、アプリのコードと、ウィンドウ制御などの便利な機能を提供するランタイムライブラリで構成されています。 フロントエンドはwebkitウィンドウであり、フロンドエンドアセットをウィンドウ上に表示します。 フロントエンドからも、JavaScriptでランタイムライブラリを呼び出すことができます。 そして最終的に、Goのメソッドはフロントエンドにバインドされ、ローカルのJavaScriptメソッドであるかのように、フロントエンドから呼び出すことができます。
アプリのメインコード
概要
アプリは、wails.Run()
メソッドを1回呼び出すことで、構成することができます。 このメソッドで、アプリのウィンドウサイズやウィンドウタイトル、使用アセットなどを指定することができます。 基本的なアプリを作るコードは次のとおりです:
package main
import (
"embed"
"log"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
app := &App{}
err := wails.Run(&options.App{
Title: "Basic Demo",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
},
OnStartup: app.startup,
OnShutdown: app.shutdown,
Bind: []interface{}{
app,
},
})
if err != nil {
log.Fatal(err)
}
}
type App struct {
ctx context.Context
}
func (b *App) startup(ctx context.Context) {
b.ctx = ctx
}
func (b *App) shutdown(ctx context.Context) {}
func (b *App) Greet(name string) string {
return fmt.Sprintf("Hello %s!", name)
}
オプション
上記のコードでは、次のオプションが指定されています:
Title
- ウィンドウのタイトルバーに表示されるテキストWidth
&Height
- ウィンドウの大きさAssets
- アプリのフロントエンドアセットOnStartup
- ウィンドウが作成され、フロントエンドの読み込みを開始しようとする時のコールバックOnShutdown
- アプリを終了しようとするときのコールバックBind
- フロントエンドにバインドさせたい構造体インスタンスのスライス
設定可能なすべてのオプションについては、オプションのリファレンスをご覧ください。
アセット
Wailsでフロントエンドアセット無しのアプリを作成することはできないため、Assets
オプションは必須オプションです。 アセットには、一般的なWebアプリケーションでよく見かけるような、html、js、css、svg、pngなどのファイルを含めることができます。アセットバンドルを生成する必要は一切なく、そのままのファイルを使用できます。 アプリが起動すると、アセット内のindex.html
が読み込まれます。この時点で、フロントエンドはブラウザとして動作するようになります。 embed.FS
を使ってアセットファイルが格納されたディレクトリを指定しますが、ディレクトリの場所はどこでも構いません。 embedで指定するパスは、frontend/dist
のように、アプリのメインコードから相対的に見たディレクトリパスになります:
//go:embed all:frontend/dist
var assets embed.FS
起動時に、Wailsはindex.html
が含まれるディレクトリを再帰的に探します。 他のすべてのアセットは、当該ディレクトリから相対的に読み込まれます。
本番用のバイナリファイルには、embed.FS
で指定されたアセットファイルが含まれるため、アプリ配布時に、バイナリファイルとは別にアセットファイルを付加させる必要はありません。
wails dev
コマンドを使って開発モードでアプリを起動した場合、アセットはディスクから読み込まれ、アセットファイルが更新されると、自動的にアプリがライブリロードされます。 アセットの場所は、embed.FS
での指定値から推定されます。
詳細は、アプリ開発ガイドをご覧ください。
アプリのライフサイクル
フロントエンドがindex.html
を読み込もうとする直前に、OnStartupで指定されたメソッドがコールバックされます。 Goの標準的なcontextがこのメソッドに渡されます。 このメソッドに引数で渡されるContextは、今後、Wailsのラインタイムを呼び出すときに必要になるため、通常は、このContextへの参照を保持しておいてください。 同様に、アプリがシャットダウンされる直前には、OnShutdownで指定されたコールバックが呼び出され、Contextも渡されます。 index.html
に含まれるすべてのアセットが読み込み終わったときに呼び出されるOnDomReadyコールバックもあります。これは、JavaScriptのbody onload
イベントと同等のものです。 また、OnBeforeCloseを指定すると、ウィンドウを閉じる(またはアプリを終了する)イベントにフックさせることもできます。
メソッドのバインド
Bind
オプションは、Wailsアプリで最も重要なオプションの1つです。 このオプションでは、フロントエンドに公開する、構造体のメソッドを指定することができます。 構造体は、従来のWebアプリにおける"コントローラ"の立ち位置であるとお考えください。 アプリが起動すると、Bind
オプションで指定されている構造体を対象に、その中にあるパブリックメソッド(大文字で始まるメソッド名)を探します。そして、フロントエンドのコードからそれらのメソッドを呼び出せるJavaScriptが生成されます。
Wailsで構造体を正しくバインドするためには、構造体のインスタンスをオプションで指定してください。
下記のコードでは、新しくApp
インスタンスを作成し、wails.Run
関数のBind
オプションの中で、そのインスタンスを追加しています:
package main
import (
"embed"
"log"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
app := &App{}
err := wails.Run(&options.App{
Title: "Basic Demo",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
},
Bind: []interface{}{
app,
},
})
if err != nil {
log.Fatal(err)
}
}
type App struct {
ctx context.Context
}
func (a *App) Greet(name string) string {
return fmt.Sprintf("Hello %s!", name)
}
構造体は、好きな数だけバインドできます。 Bind
には、構造体のインスタンスを渡すようにしてください:
//...
err := wails.Run(&options.App{
Title: "Basic Demo",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
},
Bind: []interface{}{
app,
&mystruct1{},
&mystruct2{},
},
})
wails dev
コマンド(または、wails generate module
コマンド)を実行すると、以下のものを含むフロントエンドモジュールが生成されます:
- バインドされたすべてのメソッドのJavaScript
- バインドされたすべてのメソッドのTypeScript宣言
- バインドされたメソッドの引数または返り値で使用されているGoの構造体のTypeScript宣言
これにより、強力な型付けがなされたデータ構造を使用して、フロントエンドから簡単にGoのコードを呼び出すことができます。
フロントエンド
概要
フロントエンドは、webkitによってレンダリングされるファイル群です。 ブラウザとWebサーバが一体となったような動きをします。 使用できるフレームワークやライブラリの制限はほぼありません1。 フロントエンドとGoコードとの相互のやり取りについて、主なポイントは次のとおりです:
- バインドされたGoメソッドの呼び出し
- ランタイムメソッドの呼び出し
バインドされたGoメソッドの呼び出し
wails dev
コマンドでアプリを起動すると、Go構造体のJavaScriptバインディングがwailsjs/go
ディレクトリに自動的に生成されます(wails generate module
コマンドでも生成可能)。 生成されたファイルには、アプリ内のGoパッケージ名が反映されています。 先の例では、Greet
というパブリックメソッドを持つapp
をバインドしていました。 この場合、次のようなファイルが生成されることになります:
wailsjs
└─go
└─main
├─App.d.ts
└─App.js
ご覧のとおり、main
パッケージ内のApp
構造体JavaScriptバインディングが、それらのメソッドのTypeScript型定義と並んで生成されていることが分かります。 フロントエンドからGreet
メソッドを呼び出すには、単純にメソッドをインポートし、通常のJavaScript関数と同じように呼び出してください:
// ...
import {Greet} from '../wailsjs/go/main/App'
function doGreeting(name) {
Greet(name).then((result) => {
// resultを使って何かする
})
}
TypeScript型定義ファイルは、バインドされたメソッドの正しい型を提供します:
export function Greet(arg1: string): Promise<string>;
生成されたメソッドはPromiseを返すようになっています。 呼び出しが成功すると、Goからの1番目の返り値がresolve
ハンドラに渡されます。 呼び出しに失敗したとみなされるのは、Goメソッドの2番目の返り値がerror型で、それがerrorインスタンスを返したときです。 これは、reject
ハンドラを介して返されます。 先の例では、Greet
メソッドはstring
型の返り値のみであるため、無効なデータが渡されない限り、JavaScript側でrejectハンドラが呼ばれることはありません。
すべてのデータ型は、GoとJavaScriptの間で正しく解釈されます。 もちろん構造体も正しく解釈されます。 Goから構造体が返された場合、フロントエンドにはJavaScriptのクラスとして返されます。
TypeScript型定義を正しく自動生成するために、構造体のフィールドには、有効なjson
タグを必ず付与するようにしてください。
ネストされた匿名構造体(無名構造体) は、現時点ではサポートされていません。
Goに対して引数として構造体を渡すこともできます。 構造体として取り扱ってほしいJavaScriptのマップやクラスを渡すと、構造体に変換されます。 あなたが簡単にこれらのことを把握できるように、dev
モードでは、バウンドされたGoメソッドで使用されている全構造体の型が定義された、Typescriptモジュールが生成されます。 このモジュールを使用すると、JavaScriptネイティブなオブジェクトを構築し、Goコードへ送信することができます。
また、シグネチャに構造体を使用するGoメソッドもサポートされています。 バインドされたメソッドで、引数または返り値として指定されているすべてのGo構造体は、Goのコードラッパーモジュールの一部として生成されたTypeScript定義を持っています。 れらを使用することで、GoとJavaScriptの間で、同じデータモデルを共有できます。
例: Greet
メソッドを更新して、文字列型の代わりにPerson
型を引数で受け付けてみる:
type Person struct {
Name string `json:"name"`
Age uint8 `json:"age"`
Address *Address `json:"address"`
}
type Address struct {
Street string `json:"street"`
Postcode string `json:"postcode"`
}
func (a *App) Greet(p Person) string {
return fmt.Sprintf("Hello %s (Age: %d)!", p.Name, p.Age)
}
wailsjs/go/main/App.js
ファイルには、次のコードが出力されます:
export function Greet(arg1) {
return window["go"]["main"]["App"]["Greet"](arg1);
}
しかし、wailsjs/go/main/App.d.ts
ファイルは次のコードが出力されます:
import { main } from "../models";
export function Greet(arg1: main.Person): Promise<string>;
見ると分かるように、"main"名前空間は、新しく生成された"models.ts"ファイルからインポートされています。 このファイルには、バインドされたメソッドで使用されるすべての構造体の型定義が含まれています。 この例では、Person
構造体の型定義が含まれています。 models.ts
を確認すれば、モデルがどのように定義されているかが分かります。
export namespace main {
export class Address {
street: string;
postcode: string;
static createFrom(source: any = {}) {
return new Address(source);
}
constructor(source: any = {}) {
if ("string" === typeof source) source = JSON.parse(source);
this.street = source["street"];
this.postcode = source["postcode"];
}
}
export class Person {
name: string;
age: number;
address?: Address;
static createFrom(source: any = {}) {
return new Person(source);
}
constructor(source: any = {}) {
if ("string" === typeof source) source = JSON.parse(source);
this.name = source["name"];
this.age = source["age"];
this.address = this.convertValues(source["address"], Address);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice) {
return (a as any[]).map((elem) => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}
フロントエンドのビルド構成にTypescriptを使用している限り、これらのモデルを次のように使用できます:
import { Greet } from "../wailsjs/go/main/App";
import { main } from "../wailsjs/go/models";
function generate() {
let person = new main.Person();
person.name = "Peter";
person.age = 27;
Greet(person).then((result) => {
console.log(result);
});
}
生成されたバインディングとTypescriptモデルの組み合わせによって、強力な開発環境を実現させています。
バインディングの詳細については、アプリ開発ガイドのバインディングメソッドをご覧ください。
ランタイムメソッドの呼び出し
Javascriptランタイムはwindow.runtime
に存在し、イベント発行やロギングなど、さまざまなタスクを実行するためのメソッドが含まれています:
window.runtime.EventsEmit("my-event", 1);
Javascriptランタイムの詳細については、ランタイムリファレンスをご覧ください。
- まれに、WebViewでサポートされていない機能を使用するライブラリがあります。 ほとんどの場合、それらは代替手段や回避方法がありますので、それらを検討してください。↩