Open10

Wails Go+webアセットによるデスクトップアプリ開発メモ(bun+vue3利用)

yunayuna

Goで、Webアセット(html/css/js)を使ってデスクトップアプリ開発ができるWailsを触ってみたメモです。
現在私はマルチプラットフォームのデスクトップアプリを、RustによるTauriメインで開発を行っていますが、Rustのビルド時間が長く、アプリケーション開発効率を下げているため、
Wailsも技術候補として検証したいと思います。

https://wails.io/

ここ最近の利用数の伸びや、今後のRoadmap https://github.com/wailsapp/wails/discussions/1484 などから、
それなりに今後に期待できるプロジェクトと思います(企業スポンサーが付けばより安心ですが)

ちなみにGoでGUI開発はこれまでもwalkというモジュールを使って行ってきましたが、
すでに数年間、commitが止まっていること、
またUIの実装をGoで行うと、リッチな表現力のある言語と比べて厳しいこと、
Web frontに慣れ親しんだ身としては、webviewベースのツールに利点があることから、
別の選択肢を探していました。
https://github.com/lxn/walk

参考にさせていただいた記事

https://zenn.dev/nobonobo/articles/c775251fe739bc

yunayuna

windows開発環境メモ

Goが入っている事は前提ですが、省略します。

インストール

 go install github.com/wailsapp/wails/v2/cmd/wails@latest

プロジェクトの作成
project_nameディレクトリが作成され、その下にファイル群が自動で生成されます。
※私はvueが最も使い慣れているので、vueを選択します。react,svelte・・その他いくつか選択可能です

 wails init -n <project_name> -t vue
project_name:.
|   .gitignore
|   app.go
|   go copy.mod
|   go.mod
|   go.sum
|   main.go
|   README.md
|   tree.txt
|   wails.json
|   
+---build
|   |   appicon.png
|   |   README.md
|   |   
|   +---darwin
|   |       Info.dev.plist
|   |       Info.plist
|   |       
|   \---windows
|       |   icon.ico
|       |   info.json
|       |   wails.exe.manifest
|       |   
|       \---installer
|               project.nsi
|               wails_tools.nsh
|               
\---frontend
    |   index.html
    |   package.json
    |   README.md
    |   vite.config.js
    |   
    +---dist
    |       gitkeep
    |       
    +---src
    |   |   App.vue
    |   |   main.js
    |   |   style.css
    |   |   
    |   +---assets
    |   |   +---fonts
    |   |   |       nunito-v16-latin-regular.woff2
    |   |   |       OFL.txt
    |   |   |       
    |   |   \---images
    |   |           logo-universal.png
    |   |           
    |   \---components
    |           HelloWorld.vue
    |           
    \---wailsjs
        +---go
        |   \---main
        |           App.d.ts
        |           App.js
        |           
        \---runtime
                package.json
                runtime.d.ts
                runtime.js
yunayuna

開発環境で実行

wails dev

これだけで、稼働します。
さらに快適に開発を行うため、初期の変更を行いました。

viteのバージョンを上げる

frontendのpackage.jsonを見てみると、使われているviteのバージョンが低いので、
変更してみる

package.json
  "devDependencies": {
    "@vitejs/plugin-vue": "^3.0.3",
    "vite": "^3.0.7"
  }"devDependencies": {
    "@vitejs/plugin-vue": "^5.0.5",
    "vite": "^5.3.1"
  }

frontのビルドは、npmでなく、bunを使う

javascript runtimeは、高速なbunが安定してきたので、
こちらに変更。

wails.json
  "frontend:install": "bun install",
  "frontend:build": "bun run build",
  "frontend:dev:watcher": "bun run dev",

↓この部分を変更

  "frontend:install": "npm install",
  "frontend:build": "npm run build",
  "frontend:dev:watcher": "npm run dev",

これで、wails dev を実行しても、きちんと動きました。

yunayuna

ビルド

npm→bunへの変更、viteのバージョンを上げた状態で、
ビルドしてexeの生成をしてみます。

コマンドはこちら。

wails build

warning: GOPATH set to GOROOT (C:\Program Files\Go) has no effect
Done.
 INFO  Wails is now using the new Go WebView2Loader. If you encounter any issues with it, please report them to https://github.com/wailsapp/wails/issues/2004. You could also use the old legacy loader with `-tags native_webview2loader`, but keep in mind this will be deprecated in the near future.
Built 'C:\Project\project_name\build\bin\project_name.exe' in 12.231s.

 ♥   If Wails is useful to you or your company, please consider sponsoring the project:
https://github.com/sponsors/leaanthony

こちらも問題なくexeが生成され、実行確認できました。

yunayuna

感想

Tauriに比べて、backend側のコード変更時に反映される時間が、圧倒的に速い。
気軽なツール系のアプリから試してみようと思います。

yunayuna

実装を進めて分かったこと

frontからbackendの関数call

  1. Appという、コアで動いているstructに、関数を追加します
func (a *App) Greet(name string) string {
	return fmt.Sprintf("Hello %s, It's show time2", name)
}
  1. front側で、関数をimportして、そのまま使えます。
import { Greet } from '../../wailsjs/go/main/App';

function greet() {
  Greet("なまえ").then(result => {
   alert(result);
  })
}

frontからbackendに渡すデータは、stringに限らず、
int, structなどもきちんと処理してくれます。
(javascriptのObjectが、structに対応しています)

逆に、backendからfrontへ返す値も同様に、stringに限らず、structを渡すとObjectに変換して渡してくれます。

例:

func (a *App) Greet(data struct {
	StartDate string
	EndDate   string
	Syubetsu  int
}) ProdData {
    //StartDate: "2021-02-01", EndDate: "2021-03-31", Syubetsu:3 を受け取ります
	fmt.Printf("%+v", data);

	return ProdData{
		StartDate: "2024-01-01",
		EndDate:   "2024-01-31",
		Syubetsu:  1,
	}
}
import { Greet } from '../../wailsjs/go/main/App';

Greet({
	 	StartDate: "2021-02-01",
	 	EndDate:   "2021-03-31",
	 	Syubetsu:  3,
	 }).then(result => {
    //Objectとして、こちらが返ってきます。
    //{
	//	StartDate: "2024-01-01",
	//	EndDate:   "2024-01-31",
	//	Syubetsu:  1,
	//}
     console.log('result:', result);
})

ただし、返り値を2つ以上にした場合、最初の返り値しか受け取れませんでした。
例:

func (a *App) Greet() (string, string) {
    return "返り値1", "返り値2"
}
import { Greet } from '../../wailsjs/go/main/App';

Greet().then(result => {
     console.log('result:', result);
    //返り値2の方は取得できません。
    //log: result: 返り値1
})

このような場合は、少し強引ですが、以下のようにすることで複数の値を返すことはできました。

func (a *App) Greet() []interface{} {
    return []interface{"返り値1", "返り値2"}
}
import { Greet } from '../../wailsjs/go/main/App';

Greet().then(([result1, result2]) => {
     console.log('result1:', result1, 'result2:', result2);    
    //log: result1: 返り値1 result2: 返り値2
})
yunayuna

wails + bun の生産性ヤバい

以前作ったwalkのアプリをwailsで再構築し、機能改善を行っていますが、
wails + bun + vue の生産性、やばいです。
普段frontの開発をされている方なら、基本的に普段使用のフレームワーク(reactでもsvelteでも)はそのまま使えると思いますので、
webアプリ開発と、ほぼ同じ感覚でデスクトップアプリが作れます。

yunayuna

小ネタ

起動時のオプションで少しだけ起動時間を短縮

wails dev時のgo mod tidyをスキップするオプション -m

開発環境での動作はwails devで行いますが、
この際、毎回go mod tidyが自動的に実行されます。

モジュールの変更を行っていないのであれば、余計なビルド時間が発生するので、
この処理を普段はスキップすることで効率的に開発がすすめられます。

(default)

PS C:\Project\sample> wails dev
Wails CLI v2.9.2

Executing: go mod tidy (初回は、しばらく固まる)

Generating bindings:
...


(skip go mod tidy)

PS C:\Project\sample>wails dev -m

Wails CLI v2.9.2

 Generating bindings:
...

devオプション一覧:
https://wails.io/docs/reference/cli/#dev

起動時のwindowの各種オプション

windowのサイズや背景色など、アプリケーションの設定は
go内での起動時に設定可能。
詳しくはこちらで
https://wails.io/docs/reference/options/#webviewistransparent
例:

  err := wails.Run(&options.App{
        Title:              "Menus Demo",
        Width:              800,
        Height:             600,
        DisableResize:      false,
        Fullscreen:         false,
        WindowStartState:   options.Maximised,
        Frameless:          true,
        MinWidth:           400,
        MinHeight:          400,
        MaxWidth:           1280,
        MaxHeight:          1024,
        StartHidden:        false,
        HideWindowOnClose:  false,
        BackgroundColour:   &options.RGBA{R: 0, G: 0, B: 0, A: 255},
        AlwaysOnTop:        false,
        AssetServer: &assetserver.Options{
            Assets:     assets,
            Handler:    assetsHandler,
            Middleware: assetsMidldeware,
        },
        Menu:               app.applicationMenu(),
        Logger:             nil,
        LogLevel:           logger.DEBUG,
        LogLevelProduction: logger.ERROR,
        OnStartup:          app.startup,
        OnDomReady:         app.domready,
        OnShutdown:         app.shutdown,
        OnBeforeClose:      app.beforeClose,
        CSSDragProperty:   "--wails-draggable",
        CSSDragValue:      "drag",
        EnableDefaultContextMenu: false,
        EnableFraudulentWebsiteDetection: false,
        Bind: []interface{}{
            app,
        },
        EnumBind: []interface{}{
            AllWeekdays,
        },
        ErrorFormatter: func(err error) any { return err.Error() },
        SingleInstanceLock: &options.SingleInstanceLock{
          UniqueId:               "c9c8fd93-6758-4144-87d1-34bdb0a8bd60",
          OnSecondInstanceLaunch: app.onSecondInstanceLaunch,
        },
        DragAndDrop: &options.DragAndDrop{
          EnableFileDrop:       false,
          DisableWebViewDrop:   false,
          CSSDropProperty:      "--wails-drop-target",
          CSSDropValue:         "drop",
        },
        Windows: &windows.Options{
            WebviewIsTransparent:              false,
            WindowIsTranslucent:               false,
            BackdropType:                      windows.Mica,
            DisablePinchZoom:               false,
            DisableWindowIcon:                 false,
            DisableFramelessWindowDecorations: false,
            WebviewUserDataPath:               "",
            WebviewBrowserPath:                "",
            Theme:                             windows.SystemDefault,
            CustomTheme: &windows.ThemeSettings{
                DarkModeTitleBar:   windows.RGB(20, 20, 20),
                DarkModeTitleText:  windows.RGB(200, 200, 200),
                DarkModeBorder:     windows.RGB(20, 0, 20),
                LightModeTitleBar:  windows.RGB(200, 200, 200),
                LightModeTitleText: windows.RGB(20, 20, 20),
                LightModeBorder:    windows.RGB(200, 200, 200),
            },
            // ZoomFactor is the zoom factor for the WebView2. This is the option matching the Edge user activated zoom in or out.
            ZoomFactor:           float64,
            // IsZoomControlEnabled enables the zoom factor to be changed by the user.
            IsZoomControlEnabled: bool,
            // User messages that can be customised
            Messages: *windows.Messages
            // OnSuspend is called when Windows enters low power mode
            OnSuspend: func()
            // OnResume is called when Windows resumes from low power mode
            OnResume: func(),
            // Disable GPU hardware acceleration for the webview
            WebviewGpuDisabled: false,
        },
        Mac: &mac.Options{
            TitleBar: &mac.TitleBar{
                TitlebarAppearsTransparent: true,
                HideTitle:                  false,
                HideTitleBar:               false,
                FullSizeContent:            false,
                UseToolbar:                 false,
                HideToolbarSeparator:       true,
                OnFileOpen: app.onFileOpen,
                      OnUrlOpen:  app.onUrlOpen,
            },
            Appearance:           mac.NSAppearanceNameDarkAqua,
            WebviewIsTransparent: true,
            WindowIsTranslucent:  false,
            About: &mac.AboutInfo{
                Title:   "My Application",
                Message: "© 2021 Me",
                Icon:    icon,
            },
        },
        Linux: &linux.Options{
            Icon: icon,
            WindowIsTranslucent: false,
            WebviewGpuPolicy: linux.WebviewGpuPolicyAlways,
            ProgramName: "wails"
        },
        Debug: options.Debug{
            OpenInspectorOnStartup: false,
        },
    })
yunayuna

windowsでのビルド高速化

winで起動(wails dev)が遅いとの議論がされてます。
https://github.com/wailsapp/wails/issues/2545

私も実際windowsでは初回の起動に時間がかかっているのですが、
原因が分からずにいました。

上記のgo mod tidyをskipする -m 以外のところでいうと、
windows defenderのCPU利用率が毎回上がっていることが観測されていました。

同じことを指摘しているコメントを発見。
https://github.com/wailsapp/wails/issues/2545#issuecomment-1605252047

Windows Defender > Exclusions > Add an exclusion > Process > C:\Program Files\Go\bin\go.exe
And while I'm at it I added VSCode and Node.

Note : It's important to add the exclusion as a Process and not a File so it excludes any child process that Go spawns, adding it as a file/folder exclusion didn't give the same results.

解決

windows defenderの利用をやめ、ESET Internet Security(有料)に切り替えたところ、
goらしい超速ビルドになりました。

yunayuna

wails製アプリ本番リリースしました

経理業務のためのツールを開発し、
クライアントに、nsisによるビルドで、windowsインストーラとして展開していますが、
問題なく稼働してます。

リリースまでにハマるポイントは少なかったように思います。
参考までに、ビルドサイズはインストーラ8Mb, exe16Mbでした。