🦜

Goで軽量なデスクトップアプリ作成

2021/10/21に公開
2

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に出会う。
https://zenn.dev/nobonobo/articles/6cc4c510988e82

Discussion

NoboNoboNoboNobo

多大な労力で作られるクロスプラットフォームGUIフレームワークの多くは日本語圏の課題修正は後回しになりがちで、どんなに良さそうに見えても日本語入力に問題があることで採用できないということが起こりうるし、レイアウトエンジンの挙動やWidgetsの組み方、制約などを学び直しになることが多いのでこの手法ならつまづきなく作り込みができるかなぁと思ったのですー。

NoboNoboNoboNobo

あとは以下のことができるといいのかな?

  • Windowsアイコンリソースの割り当て
  • macOSのアイコンとApp化
  • Ubuntuのアイコンと.desktopファイル作成