GoLang webview czyli wieloplatformowa aplikacja
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 localhost
a 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