Dank WebAssembly können Sie Frontend on Go schreiben

Originalartikel .

Im Februar 2017 schlug ein Mitglied des go Brad Fitzpatrick-Teams vor , WebAssembly in der Sprache zu unterstützen. Vier Monate später, im November 2017, begann der GopherJS- Autor Richard Muziol, die Idee umzusetzen. Und schließlich wurde die vollständige Implementierung in master gefunden. Entwickler erhalten um August 2018 wasm mit go Version 1.11 . Infolgedessen übernimmt die Standardbibliothek fast alle technischen Schwierigkeiten beim Importieren und Exportieren von Funktionen, die Ihnen bekannt sind, wenn Sie bereits versucht haben, C in wasm zu kompilieren. Das klingt vielversprechend. Mal sehen, was mit der ersten Version gemacht werden kann.



Alle Beispiele in diesem Artikel können aus Docker-Containern gestartet werden, die sich im Repository des Autors befinden :

docker container run -dP nlepage/golang_wasm:examples # Find out which host port is used docker container ls 

Gehen Sie dann zu localhost : 32XXX / und wechseln Sie von einem Link zum anderen.

Hallo Wasm!


Die Entstehung der grundlegenden „Hallo Welt“ und das Konzept sind bereits recht gut dokumentiert (auch auf Russisch ). Kommen wir also zu den subtileren Dingen.

Am wichtigsten ist eine frisch kompilierte Version von Go, die wasm unterstützt. Ich werde die Installation nicht Schritt für Schritt beschreiben , sondern nur wissen, dass das, was benötigt wird, bereits im Master vorhanden ist.

Wenn Sie sich darüber keine Sorgen machen möchten, ist Dockerfile c go im golub-wasm-Repository auf github verfügbar , oder Sie können ein Bild von nlepage / golang_wasm noch schneller aufnehmen.

Jetzt können Sie die traditionelle helloworld.go schreiben und mit dem folgenden Befehl kompilieren:

 GOOS=js GOARCH=wasm go build -o test.wasm helioworld.go 

Die Umgebungsvariablen GOOS und GOARCH sind bereits im Bild nlepage / golang_wasm festgelegt , sodass Sie eine Dockerfile Datei wie diese zum Kompilieren verwenden können:

 FROM nlepage/golang_wasm COPY helloworld.go /go/src/hello/ RUN go build -o test.wasm hello 

Der letzte Schritt besteht darin, die Dateien wasm_exec.html und wasm_exec.js verwenden, die im go-Repository im misc/wasm misc / wasm oder im Docker-Image nlepage / golang_wasm im Verzeichnis /usr/local/go/misc/wasm/ misc / wasm / verfügbar sind , um test.wasm browser (wasm_exec.js erwartet die Binärdatei test.wasm , daher verwenden wir diesen Namen).
Sie müssen nur 3 statische Dateien mit nginx angeben, dann zeigt wasm_exec.html die Schaltfläche "Ausführen" an (sie wird nur test.wasm wenn test.wasm korrekt geladen ist).

Es ist bemerkenswert, dass test.wasm mit der application/wasm test.wasm vom Typ MIME test.wasm muss, da der Browser sonst die Ausführung verweigert. (z. B. benötigt nginx eine aktualisierte Datei mime.types ).

Sie können das nginx-Image aus nlepage / golang_wasm verwenden , das bereits den festen MIME-Typ wasm_exec.html und wasm_exec.js im Code> / usr / share / nginx / html / directory enthält.

Klicken Sie nun auf die Schaltfläche "Ausführen", öffnen Sie Ihre Browserkonsole und Sie sehen die Begrüßung console.log ("Hallo Wasm!").


Ein vollständiges Beispiel finden Sie hier .

Rufen Sie JS von Go an


Nachdem wir die erste aus Go kompilierte WebAssembly-Binärdatei erfolgreich gestartet haben, schauen wir uns die bereitgestellten Funktionen genauer an.

Das neue Paket syscall / js wurde der Standardbibliothek hinzugefügt. Betrachten Sie die Hauptdatei js.go
Es ist ein neuer js.Value Typ js.Value , der einen JavaScript-Wert darstellt.

Es bietet eine einfache API zum Verwalten von JavaScript-Variablen:

  • js.Value.Get() und js.Value.Set() geben die Feldwerte des Objekts zurück und legen sie fest.
  • js.Value.Index() und js.Value.SetIndex() greifen über den Lese- und js.Value.SetIndex() auf das Objekt zu.
  • js.Value.Call() ruft die Objektmethode als Funktion auf.
  • js.Value.Invoke() ruft das Objekt selbst als Funktion auf.
  • js.Value.New() ruft den neuen Operator auf und verwendet sein eigenes Wissen als Konstruktor.
  • Einige weitere Methoden, um den JavaScript-Wert im entsprechenden Go-Typ js.Value.Int() , z. B. js.Value.Int() oder js.Value.Bool() .

Und weitere interessante Methoden:

  • js.Undefined() gibt js.Value das entsprechende undefined .
  • js.Null() gibt js.Value entsprechende null .
  • js.Global() gibt js.Value und js.Value Zugriff auf den globalen Bereich.
  • js.ValueOf() akzeptiert primitive Go-Typen und gibt den korrekten js.Value

Anstatt die Nachricht in os.StdOut anzuzeigen, zeigen wir sie im Benachrichtigungsfenster mit window.alert() .

Da wir uns im Browser befinden, ist der globale Bereich ein Fenster. Sie müssen also zuerst alert () vom globalen Bereich abrufen:

 alert := js.Global().Get("alert") 

Jetzt haben wir eine js.Value in Form von js.Value , die auf window.alert JS window.alert , und Sie können die Funktion verwenden, um js.Value.Invoke() :

 alert.Invoke("Hello wasm!") 

Wie Sie sehen, muss js.ValueOf () nicht aufgerufen werden, bevor die Argumente an Invoke übergeben werden. Es wird eine beliebige Menge an interface{} und die Werte werden über ValueOf selbst übergeben.

Jetzt sollte unser neues Programm so aussehen:

 package main import ( "syscall/js" ) func main() { alert := js.Global().Get("alert") alert.Invoke("Hello Wasm!") } 

Wie im ersten Beispiel müssen Sie nur eine Datei mit dem Namen test.wasm erstellen und wasm_exec.html und wasm_exec.js lassen.
Wenn wir nun auf die Schaltfläche "Ausführen" klicken, wird ein Warnfenster mit unserer Nachricht angezeigt.

Ein funktionierendes Beispiel befindet sich im Ordner examples/js-call .

Rufen Sie Go von JS an.


Das Aufrufen von JS von Go aus ist ziemlich einfach. Schauen wir uns das Paket syscall/js an. Die zweite Datei, die syscall/js , ist callback.go .

  • js.Callback Wrapper-Typ für die Go-Funktion zur Verwendung in JS.
  • js.NewCallback() Funktion, die eine Funktion übernimmt (ein Stück js.Value akzeptiert und nichts zurückgibt) und js.Callback .
  • Einige Mechanismen zum Verwalten aktiver Rückrufe und js.Callback.Release() , die aufgerufen werden müssen, um den Rückruf zu zerstören.
  • js.NewEventCallback() ähnelt js.NewCallback() , aber die js.NewCallback() Funktion akzeptiert nur 1 Argument - ein Ereignis.

Versuchen wir etwas Einfaches: Führen Sie Go fmt.Println() von der JS-Seite aus.

Wir werden einige Änderungen an wasm_exec.html , um einen Rückruf von Go zu erhalten, um ihn aufzurufen.

 async function run() { console.clear(); await go.run(inst); inst = await WebAssembly.instantiate(mod, go.ImportObject); //   } 

Dadurch wird die wasm-Binärdatei gestartet und auf ihren Abschluss gewartet. Anschließend wird sie für den nächsten Lauf neu initialisiert.

Fügen wir eine neue Funktion hinzu, die den Go-Rückruf empfängt und speichert und den Status des Promise nach Abschluss ändert:

 let printMessage // Our reference to the Go callback let printMessageReceived // Our promise let resolvePrintMessageReceived // Our promise resolver function setPrintMessage(callback) { printMessage = callback resolvePrintMessageReceived() } 

Passen wir nun die Funktion run() an, um den Rückruf zu verwenden:

 async function run() { console.clear() // Create the Promise and store its resolve function printMessageReceived = new Promise(resolve => { resolvePrintMessageReceived = resolve }) const run = go.run(inst) // Start the wasm binary await printMessageReceived // Wait for the callback reception printMessage('Hello Wasm!') // Invoke the callback await run // Wait for the binary to terminate inst = await WebAssembly.instantiate(mod, go.importObject) // reset instance } 

Und das ist auf der Seite von JS!

Jetzt müssen Sie im Go-Teil einen Rückruf erstellen, ihn an die JS-Seite senden und warten, bis die Funktion benötigt wird.

  var done = make(chan struct{}) 

Dann sollten sie die eigentliche Funktion printMessage() schreiben:

 func printMessage(args []js.Value) { message := args[0].Strlng() fmt.Println(message) done <- struct{}{} // Notify printMessage has been called } 

Die Argumente werden durch das Slice []js.Value , daher müssen Sie js.Value.String() für das erste Slice-Element aufrufen, um die Nachricht in der Go-Zeile js.Value.String() .
Jetzt können wir diese Funktion in einen Rückruf einschließen:

 callback := js.NewCallback(printMessage) defer callback.Release() // to defer the callback releasing is a good practice 

Rufen Sie dann die JS-Funktion setPrintMessage() , genau wie beim Aufrufen von window.alert() :

 setPrintMessage := js.Global.Get("setPrintMessage") setPrintMessage.Invoke(callback) 

Als letztes müssen Sie warten, bis der Rückruf in main aufgerufen wird:

 <-done 

Dieser letzte Teil ist wichtig, da die Rückrufe in einer dedizierten Goroutine ausgeführt werden und die Hauptgoroutine auf den Aufruf des Rückrufs warten muss, da sonst die wasm-Binärdatei vorzeitig gestoppt wird.

Das resultierende Go-Programm sollte folgendermaßen aussehen:

 package main import ( "fmt" "syscall/js" ) var done = make(chan struct{}) func main() { callback := js.NewCallback(prtntMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback) <-done } func printMessage(args []js.Value) { message := args[0].Strlng() fmt.PrintIn(message) done <- struct{}{} } 

Erstellen Sie wie in den vorherigen Beispielen eine Datei mit dem Namen test.wasm . Wir müssen auch wasm_exec.html durch unsere Version ersetzen und können wasm_exec.js wiederverwenden.

Wenn Sie nun wie in unserem ersten Beispiel auf die Schaltfläche "Ausführen" klicken, wird die Nachricht in der Browserkonsole gedruckt, diesmal jedoch viel besser! (Und schwerer.)

Ein funktionierendes Beispiel in einem Docker-Datei-Gebot finden Sie im Ordner examples/go-call .

Lange Arbeit


Das Aufrufen von Go von JS aus ist etwas umständlicher als das Aufrufen von JS von Go aus, insbesondere auf der JS-Seite.

Dies liegt hauptsächlich an der Tatsache, dass Sie warten müssen, bis das Ergebnis des Go-Rückrufs an die JS-Seite übergeben wird.

Versuchen wir etwas anderes: Warum nicht die Wasm-Binärdatei organisieren, die nicht direkt nach dem Rückruf endet, sondern weiterhin funktioniert und andere Aufrufe annimmt?
Beginnen wir dieses Mal von der Go-Seite, und wie in unserem vorherigen Beispiel müssen wir einen Rückruf erstellen und an die JS-Seite senden.

Fügen Sie einen Anrufzähler hinzu, um zu verfolgen, wie oft die Funktion aufgerufen wurde.

Unsere neue Funktion printMessage() druckt die empfangene Nachricht und den printMessage() :

 var no int func printMessage(args []js.Value) { message := args[0].String() no++ fmt.Printf("Message no %d: %s\n", no, message) } 

Das Erstellen eines Rückrufs und das Senden an die JS-Seite erfolgt wie im vorherigen Beispiel:

 callback := js.NewCallback(printMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback) 

Diesmal haben wir jedoch keinen Kanal eingerichtet, um uns über die Beendigung der Hauptgoroutine zu informieren. Eine Möglichkeit könnte darin bestehen, das Haupt-Goroutin dauerhaft mit der leeren select{} zu sperren:

 select{} 

Dies ist nicht zufriedenstellend. Unser Binär-Wasm bleibt nur im Speicher hängen, bis die Browser-Registerkarte geschlossen wird.

Sie können das Ereignis vor dem beforeunload auf der Seite anhören. Sie benötigen einen zweiten Rückruf, um das Ereignis zu empfangen und die Haupt-Goroutine über den Kanal zu benachrichtigen:

 var beforeUnloadCh = make(chan struct{}) 

Dieses Mal akzeptiert die neue Funktion beforeUnload() das Ereignis nur als einzelnes js.Value Argument:

 func beforeUnload(event js.Value) { beforeUnloadCh <- struct{}{} } 

Wickeln Sie es dann mit js.NewEventCallback() in einen Rückruf ein und registrieren Sie es auf der JS-Seite:

 beforeUnloadCb := js.NewEventCallback(0, beforeUnload) defer beforeUnloadCb.Release() addEventLtstener := js.Global().Get("addEventListener") addEventListener.Invoke("beforeunload", beforeUnloadCb) 

Ersetzen Sie beforeUnloadCh die leere Blockierungsauswahl durch Lesen aus dem beforeUnloadCh Kanal:

 <-beforeUnloadCh fmt.Prtntln("Bye Wasm!") 

Das endgültige Programm sieht folgendermaßen aus:

 package main import ( "fmt" "syscall/js" ) var ( no int beforeUnloadCh = make(chan struct{}) ) func main() { callback := js.NewCallback(printMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback) beforeUnloadCb := js.NewEventCallback(0, beforeUnload) defer beforeUnloadCb.Release() addEventLtstener := js.Global().Get("addEventListener") addEventListener.Invoke("beforeunload", beforeUnloadCb) <-beforeUnloadCh fmt.Prtntln("Bye Wasm!") } func printMessage(args []js.Value) { message := args[0].String() no++ fmt.Prtntf("Message no %d: %s\n", no, message) } func beforeUnload(event js.Value) { beforeUnloadCh <- struct{}{} } 

Bisher sah der Download der wasm-Binärdatei auf der JS-Seite folgendermaßen aus:

 const go = new Go() let mod, inst WebAssembly .instantiateStreaming(fetch("test.wasm"), go.importObject) .then((result) => { mod = result.module inst = result.Instance document.getElementById("runButton").disabled = false }) 

Passen wir es an, um die Binärdatei unmittelbar nach dem Laden auszuführen:

 (async function() { const go = new Go() const { instance } = await WebAssembly.instantiateStreaming( fetch("test.wasm"), go.importObject ) go.run(instance) })() 

Ersetzen Sie die Schaltfläche "Ausführen" durch ein Nachrichtenfeld und eine Schaltfläche zum Aufrufen von printMessage() :

 <input id="messageInput" type="text" value="Hello Wasm!"> <button onClick="printMessage(document.querySelector('#messagelnput').value);" id="prtntMessageButton" disabled> Print message </button> 

Schließlich sollte die Funktion setPrintMessage() , die den Rückruf akzeptiert und speichert, einfacher sein:

 let printMessage; function setPrintMessage(callback) { printMessage = callback; document.querySelector('#printMessageButton').disabled = false; } 

Wenn wir jetzt auf die Schaltfläche "Nachricht drucken" klicken, sollten Sie eine Nachricht unserer Wahl und einen Anrufzähler in der Browserkonsole sehen.
Wenn wir das Kontrollkästchen "Beibehalten" der Browserkonsole aktivieren und die Seite aktualisieren, wird die Meldung "Bye Wasm!" Angezeigt.



Quellen finden Sie im Ordner examples/long-running auf github.

Und weiter?


Wie Sie sehen können, syscall/js die erlernte syscall/js API ihre Aufgabe und ermöglicht es Ihnen, komplexe Dinge mit ein bisschen Code zu schreiben. Sie können an den Autor schreiben, wenn Sie eine einfachere Methode kennen.
Es ist derzeit nicht möglich, einen Wert direkt aus dem Go-Rückruf an JS zurückzugeben.
Beachten Sie, dass alle Rückrufe im selben Goroutin ausgeführt werden. Wenn Sie also einige Blockierungsvorgänge im Rückruf ausführen, vergessen Sie nicht, ein neues Goroutin zu erstellen. Andernfalls blockieren Sie die Ausführung aller anderen Rückrufe.
Alle grundlegenden Sprachfunktionen sind bereits verfügbar, einschließlich der Parallelität. Im Moment werden alle Goroutins in einem Thread funktionieren, aber dies wird sich in Zukunft ändern .
In unseren Beispielen haben wir nur das fmt-Paket aus der Standardbibliothek verwendet, aber es ist alles verfügbar, was nicht versucht, aus der Sandbox zu entkommen.

Das Dateisystem scheint über Node.js unterstützt zu werden.

Was ist schließlich mit der Leistung? Es wäre interessant, einige Tests durchzuführen, um zu sehen, wie Go wasm mit äquivalentem reinem JS-Code verglichen wird. Jemand Hajimehoshi hat gemessen , wie verschiedene Umgebungen mit ganzen Zahlen funktionieren, aber die Technik ist nicht sehr klar.



Vergessen Sie nicht, dass Go 1.11 noch nicht offiziell veröffentlicht wurde. Meiner Meinung nach ist es sehr gut für experimentelle Technologie. Wer an Leistungstests interessiert ist, kann seinen Browser quälen .
Die Hauptnische ist, wie der Autor feststellt, die Übertragung des vorhandenen Go-Codes vom Server zum Client. Mit neuen Standards können Sie jedoch vollständig Offline-Anwendungen erstellen , und der Wasm-Code wird in kompilierter Form gespeichert. Sie können viele Dienstprogramme bequem ins Internet übertragen, stimmen Sie zu?

Source: https://habr.com/ru/post/de417563/


All Articles