メインコンテンツにスキップ
バージョン: 次期バージョン 🚧

どうやって動いているの?

Wailsは、webkitフロントエンドを備えた、何の変哲もないGoアプリです。 アプリ全体のうちGoの部分は、アプリのコードと、ウィンドウ制御などの便利な機能を提供するランタイムライブラリで構成されています。 フロントエンドはwebkitウィンドウであり、フロンドエンドアセットをウィンドウ上に表示します。 フロントエンドからも、JavaScriptでランタイムライブラリを呼び出すことができます。 そして最終的に、Goのメソッドはフロントエンドにバインドされ、ローカルのJavaScriptメソッドであるかのように、フロントエンドから呼び出すことができます。

アプリのメインコード

概要

アプリは、wails.Run()メソッドを1回呼び出すことで、構成することができます。 このメソッドで、アプリのウィンドウサイズやウィンドウタイトル、使用アセットなどを指定することができます。 基本的なアプリを作るコードは次のとおりです:

main.go
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 - フロントエンドにバインドさせたい構造体インスタンスのスライス

A full list of application options can be found in the Options Reference.

アセット

Wailsでフロントエンドアセット無しのアプリを作成することはできないため、Assetsオプションは必須オプションです。 アセットには、一般的なWebアプリケーションでよく見かけるような、html、js、css、svg、pngなどのファイルを含めることができます。アセットバンドルを生成する必要は一切なく、そのままのファイルを使用できます。 アプリが起動すると、アセット内のindex.htmlが読み込まれます。この時点で、フロントエンドはブラウザとして動作するようになります。 embed.FSを使ってアセットファイルが格納されたディレクトリを指定しますが、ディレクトリの場所はどこでも構いません。 embedで指定するパスは、frontend/distのように、アプリのメインコードから相対的に見たディレクトリパスになります:

main.go
//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オプションの中で、そのインスタンスを追加しています:

main.go
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{},
},
})

You may bind enums types as well. In that case you should create array that will contain all possible enum values, instrument enum type and bind it to the app via EnumBind:

app.go
type Weekday string

const (
Sunday Weekday = "Sunday"
Monday Weekday = "Monday"
Tuesday Weekday = "Tuesday"
Wednesday Weekday = "Wednesday"
Thursday Weekday = "Thursday"
Friday Weekday = "Friday"
Saturday Weekday = "Saturday"
)

var AllWeekdays = []struct {
Value Weekday
TSName string
}{
{Sunday, "SUNDAY"},
{Monday, "MONDAY"},
{Tuesday, "TUESDAY"},
{Wednesday, "WEDNESDAY"},
{Thursday, "THURSDAY"},
{Friday, "FRIDAY"},
{Saturday, "SATURDAY"},
}
    //...
err := wails.Run(&options.App{
Title: "Basic Demo",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
},
Bind: []interface{}{
app,
&mystruct1{},
&mystruct2{},
},
EnumBind: []interface{}{
AllWeekdays,
},
})

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型を引数で受け付けてみる:

main.go
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ファイルには、次のコードが出力されます:

App.js
export function Greet(arg1) {
return window["go"]["main"]["App"]["Greet"](arg1);
}

しかし、wailsjs/go/main/App.d.tsファイルは次のコードが出力されます:

App.d.ts
import { main } from "../models";

export function Greet(arg1: main.Person): Promise<string>;

見ると分かるように、"main"名前空間は、新しく生成された"models.ts"ファイルからインポートされています。 このファイルには、バインドされたメソッドで使用されるすべての構造体の型定義が含まれています。 この例では、Person構造体の型定義が含まれています。 models.tsを確認すれば、モデルがどのように定義されているかが分かります。

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 && a.map) {
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を使用している限り、これらのモデルを次のように使用できます:

mycode.js
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に存在し、イベント発行やロギングなど、さまざまなタスクを実行するためのメソッドが含まれています:

mycode.js
window.runtime.EventsEmit("my-event", 1);

More details about the JS runtime can be found in the Runtime Reference.


  1. まれに、WebViewでサポートされていない機能を使用するライブラリがあります。 ほとんどの場合、それらは代替手段や回避方法がありますので、それらを検討してください。