Goで軽量なデスクトップアプリ作成
Lorca+SvelteKitでやってみる!
あらかじめ必要なもの
- go(version 1.17.2以降)
- nodejs(16.9.0以降),npm(7.21.1以降)
- Chrome/Chromium/Edgeのいずれか
プロジェクトの開始
mkdir sample-gui
cd sample-gui
go mod init sample-gui
npm init svelte@next frontend
// Choice "Svelte app template" is "Skelton Project".
// Choice "Use TypeScript" is No.
// Choice "ESLint" is No.
// Choice "Prettier" is No.
cd frontend
npm install
npm i -D @sveltejs/adapter-static@next
cd ..
go get github.com/zserge/lorca@master # masterしかEdgeをみてくれない
svelte.config.jsにスタティックアダプターを追加します。
frontend/svelte.config.js
/** @type {import('@sveltejs/kit').Config} */
import adapter from "@sveltejs/adapter-static";
const config = {
kit: {
// hydrate the <div id="svelte"> element in src/app.html
target: "#svelte",
adapter: adapter({
// default options are shown
pages: "build",
assets: "build",
fallback: null,
}),
},
};
export default config;
release.go
//go:build release
// +build release
package main
import (
"embed"
"io/fs"
"log"
"net/http"
)
//go:generate sh -c "cd frontend; npm run build"
//go:embed frontend/build/*
//go:embed frontend/build/_app/assets/pages/__layout.svelte-*.css
//go:embed frontend/build/_app/pages/__layout.svelte-*.js
var content embed.FS
func init() {
pub, err := fs.Sub(content, "frontend/build")
if err != nil {
log.Fatal(err)
}
http.Handle("/", http.FileServer(http.FS(pub)))
}
go:embed
は、アンダースコアで始まるファイルを埋め込み対象からスキップする挙動がありますので、別途アンダースコアで始まるファイルを追加で名指ししておきます。この問題の別解はfrontend/buildに出力されたファイル群をzipアーカイブにしてからgo:embed
で取り込むというものです。
development.go
//go:build !release
// +build !release
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func init() {
u, err := url.Parse("http://localhost:3000/")
if err != nil {
log.Fatal(err)
}
http.Handle("/", httputil.NewSingleHostReverseProxy(u))
}
開発時はfrontendの開発サーバーを起動しておき、そこへリバースプロキシすることで、開発サーバーのホットリロードによる即時反映機能を利用しつつ開発を進めることができます。
node開発サーバーの起動
事前に別のターミナルから開発用サーバーを起動しておきます。
cd frontend; npm run dev
main実装
main.go
package main
import (
"embed"
"fmt"
"log"
"net"
"net/http"
"sync"
"github.com/zserge/lorca"
)
type counter struct {
sync.Mutex
count int
}
func (c *counter) Add(n int) {
c.Lock()
defer c.Unlock()
c.count = c.count + n
}
func (c *counter) Value() int {
c.Lock()
defer c.Unlock()
return c.count
}
func main() {
log.SetFlags(log.Llongfile)
ui, err := lorca.New("", "", 480, 320)
if err != nil {
log.Fatal(err)
}
defer ui.Close()
ui.Bind("start", func() {
log.Println("UI is ready")
})
c := &counter{}
ui.Bind("counterAdd", c.Add)
ui.Bind("counterValue", c.Value)
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatal(err)
}
defer ln.Close()
go http.Serve(ln, nil)
ui.Load(fmt.Sprintf("http://%s/", ln.Addr()))
ui.Eval(`console.log("Hello, world!");`)
<-ui.Done()
}
起動
go run .
翻訳オファーダイアログ対策
SvelteKitテンプレートのままだと翻訳オファーのダイアログが表示される場合があります。
その場合、app.htmlに以下の三行を追加しておくことで抑制できます。
<html lang="ja">
<head>
<meta http-equiv="Content-Language" content="ja" />
<meta name="google" content="notranslate" />
...
</head>
...
</html>
ヒストリ機能が邪魔な場合
app.htmlに以下のスクリプトを追記しよう!
これで戻る・進むで何も変化しなくなります。
<html lang="ja">
<head>
...
</head>
<body>
...
</body>
<script>
history.pushState(null, null, null);
window.addEventListener("popstate", (e) => {
history.pushState(null, null, null);
e.preventDefault();
});
</script>
</html>
コンテキストメニューが邪魔な場合
app.htmlに以下のスクリプトを追記しよう!
右クリックメニューが表示できなくなります。
(上記もやっておくとキーショートカットで戻ったり進んだりもできない)
<html lang="ja">
<head>
...
</head>
<body>
...
</body>
<script>
window.addEventListener("contextmenu", (e) => {
e.preventDefault();
});
</script>
</html>
2本指またはタッチパネルのスワイプを無効化
app.htmlのbodyタグに以下のスタイルを追記しよう!
<body style="overscroll-behavior-x: none">
ファイル一覧
サンプルソース(時計): https://github.com/nobonobo/clock
├── frontend/
│ ├── README.md
│ ├── build/
│ ├── jsconfig.json
│ ├── node_modules/
│ ├── package-lock.json
│ ├── package.json
│ ├── src/
│ ├── static/
│ └── svelte.config.js
├── go.mod
├── go.sum
├── development.go
├── release.go
└── main.go
ビルド
go generate -tags release
go build -tags release
ls -lh sample-gui
-rwxr-xr-x@ 1 nobo staff 7.3M 10 17 22:48 sample-gui
まとめ
- コンテンツ込みで約7Mバイト前後のサイズ
- Electronと違いビルド速度、実行速度ともに早い
- macOS/Linux/WindowsとPC-OS環境を選ばず動く
- JSコンテキストにGoの処理関数を埋め込むことができる。
- Chrome/Chromium/Edgeのいずれもインストールされていない環境はレアケース
- ただのWebページなので任意のJSフロントエンド技術が使える
- 多言語圏で鍛えられたブラウザエンジンを利用するので日本語表示や日本語入力で困ることもない
- LorcaとSvelteKit双方シンプルなアプローチによるコンセプトの相性は良さそう
追記
その後、Wailsに出会う。
Discussion
多大な労力で作られるクロスプラットフォームGUIフレームワークの多くは日本語圏の課題修正は後回しになりがちで、どんなに良さそうに見えても日本語入力に問題があることで採用できないということが起こりうるし、レイアウトエンジンの挙動やWidgetsの組み方、制約などを学び直しになることが多いのでこの手法ならつまづきなく作り込みができるかなぁと思ったのですー。
あとは以下のことができるといいのかな?