GoLang webview czyli wieloplatformowa aplikacja

GoLang webview czyli wieloplatformowa aplikacja
Spis treści

Raz na czas, potrzebuję napisać coś na tzw. “desktopa”. Czyli na ogół aplikację, która nie uruchamia się w przeglądarce i działa “lokalnie” na komputerze. Za starych dobrych czasów pisałem takie wynalazki na każdą platformę osobno - Windows -> Delphi, Mac -> Swift, Linux -> … Poszukiwanie wieloplatformowego rozwiązania stało się proste. Jest Flutter i Dart i wiele innych języków (Java ?), w których można pisać raz i kompilować na wiele platform.

Można też skorzystać np. z Electrona ale rozmiar binariów (>100mb) mnie przerasta. Jest też projekt Fyne ale jest brzydki, a nie chciałem spędzać miesięcy na jego poprawianiu.

Niestety presja czasu nie pozwala mi na eksperymenty z nowymi technologiami, najlepiej czuje się z poczciwym HTML. Jak się zabrać za temat, żeby było szybko, łatwo i przyjemnie.

Poniżej zrobię poprawny brain dump tego czego nauczyłem się w ciągu kilku dni pisania kodu. Więc nie będzie to typowy tutorial, tylko sprawy, na które należy zwrócić uwagę.

Problemy, które należy rozwiązać

Teoria jest prosta - serwujemy lokalnie stronę, którą potem wyświetlamy w jakimś komponencie (przeglądarce), która będzie udawała naszą aplikacje.

Można oczywiście uruchomić przeglądarkę jako samodzielną usługę - wtedy odpada nam jeden problem, pozostaje jedynie wrażenie dalszego korzystania ze strony internetowej.

Strona, jak wiemy, składa się z wielu elementów (html, js, css, …). Więc albo możemy to wszystko wpakować do folderu “assets” i kleić obok głównych plików wykonywalnych, albo wykombinować coś, co nam zapakuje wszystko do jednej binarki.

Nie wszystko da się ogarnąć “przeglądarką” więc będą nam też potrzebne natywne funkcje systemu. W moim przypadku dialog wyboru katalogu. Tego przeglądarka nie ma. Można skorzystać tylko input type=file i kombinować, ale ten dialog jest do wybierania plików, a nie wskazywania katalogu.

Uruchomienie przeglądarki

Oczywiście bierzemy na tapetę to, co znamy i kochamy, Go. Krótki research przedstawia nam właściwie dwie opcje: github.com/webview/webview oraz github.com/zserge/lorca .

Webview

WebView był moim pierwszym wyborem: uruchamia się pięknie na Macu, na Windowsie niestety nie. Problemów jest sporo, zaczynając od kompilacji:

GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ go build -ldflags="-H windowsgui" 

a kończąc na wyświetlaniu samych danych.

Problem pierwszy. Umknęło mi, że obok binarki głównej należy trzymać dwa magiczne pliki webview.dll i WebView2Loader.dll a następnie zęby rozbiłem o fakt, że musimy wiedzieć ile bitów ma nasz system i serwować odpowiednią wersję.

To nie koniec naszych problemów. Okazuje się, że Edge ma jakiś problem z ładowaniem zasobów z “samego siebie” - czyt. z localhosta w tym samym procesie. Znalazłem trochę opisów w sieci - problem nazywa się Loopback For Edge . Teoretycznie rozwiązanie polega na wpisaniu magicznej linijki z prawami administratora, ale to nie rozwiązało problemu w moim przypadki. Jednym słowem - nie działa.

Samo korzystanie z webview jest super proste:

    // if windows run browser
    if os.Getenv("OS") == "Windows_NT" {
        browser.OpenURL("http://127.0.0.1:8123/")
    }
	
    debug := true
    w := webview.New(debug)
    defer w.Destroy()
    
    w.SetTitle("Wesoly tytul")
    w.SetSize(800, 600, webview.HintNone)
    w.Navigate("http://127.0.0.1:8123")
    w.Run()

lorca

To rozwiązanie było dla mnie bardziej łaskawe, pomimo faktu, że jest “biedniejsze”. Nie można zamknąć ani ustawić rozmiarów okna, ale chodzi za to szybciej (działa na Chrome) i bindowanie zasobów jest bardziej intuicyjne.

	ui, err := lorca.New("http://127.0.0.1:8123/", "", 800, 600)
	if err != nil {
		log.Fatal(err)
	}
	defer ui.Close()
	// Wait until UI window is closed
	<-ui.Done()

Osadzanie zasobów

Tutaj na szczęście go radzi sobie doskonale. Pakiet embed radzi sobie doskonale. Można osadzać pojedyncze pliki jako []byte albo zapinać całe foldery jako wirtualny file system embed.FS

import "embed"

//go:embed content content/_head.html content/_foot.html
var content embed.FS

//go:embed static/favicon.ico
var staticFiles embed.FS

func main() {
	var staticFS = http.FS(staticFiles)
	fs := http.FileServer(staticFS)

	http.Handle("/static/", http.StripPrefix("/", fs))
}

I wszystko działa jak powinno bez zbędnego pocenia się. Mały bonus, dla tych co lubią porządek:

http.Handle("/favicon.ico", http.HandlerFunc(staticHandler))
func staticHandler(w http.ResponseWriter, r *http.Request) {
	filename := path.Base(r.URL.Path)
	http.ServeFile(w, r, "static/" + filename)
}

Natywne dialogi i alerty

Tutaj rozwiązanie było proste gen2brain/dlgs polecam też gen2brain/beeep