Grâce à WebAssembly, vous pouvez écrire Frontend on Go

Article d' origine .

En février 2017, un membre de l'équipe go Brad Fitzpatrick a proposé de prendre en charge WebAssembly dans la langue. Quatre mois plus tard, en novembre 2017, l'auteur de GopherJS , Richard Muziol, a commencé à mettre en œuvre l'idée. Et enfin, l'implémentation complète a été trouvée dans master. Les développeurs recevront du wasm vers août 2018, avec la version go 1.11 . En conséquence, la bibliothèque standard prend en charge presque toutes les difficultés techniques liées à l'importation et à l'exportation de fonctions qui vous sont familières si vous avez déjà essayé de compiler C dans wasm. Cela semble prometteur. Voyons ce qui peut être fait avec la première version.



Tous les exemples de cet article peuvent être lancés à partir de conteneurs Docker qui se trouvent dans le référentiel de l'auteur :

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

Ensuite, accédez à localhost : 32XXX / et passez d'un lien à un autre.

Salut Wasm!


La création du «bonjour monde» de base et le concept sont déjà assez bien documentés (même en russe ), alors passons aux choses plus subtiles.

Le plus essentiel est une version fraîchement compilée de Go qui prend en charge le wasm. Je ne décrirai pas étape par étape l'installation , sachez simplement que ce qui est nécessaire est déjà en master.

Si vous ne voulez pas vous en soucier, Dockerfile c go est disponible dans le référentiel golub-wasm sur github , ou encore plus rapidement vous pouvez prendre une image depuis nlepage / golang_wasm .

Vous pouvez maintenant écrire le helloworld.go traditionnel et le compiler avec la commande suivante:

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

Les variables d'environnement GOOS et GOARCH sont déjà définies dans l'image nlepage / golang_wasm , vous pouvez donc utiliser un fichier Dockerfile comme celui-ci pour compiler:

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

La dernière étape consiste à utiliser les fichiers wasm_exec.html et wasm_exec.js disponibles dans le référentiel go dans le répertoire misc/wasm ou dans l'image docker nlepage / golang_wasm dans le /usr/local/go/misc/wasm/ pour exécuter test.wasm dans navigateur (wasm_exec.js attend le fichier binaire test.wasm , nous utilisons donc ce nom).
Vous avez juste besoin de donner 3 fichiers statiques en utilisant nginx, par exemple, puis wasm_exec.html affichera le bouton «exécuter» (il ne test.wasm que si test.wasm chargé correctement).

Il est à noter que test.wasm doit être servi avec l' application/wasm type MIME, sinon le navigateur refusera de l'exécuter. (par exemple, nginx a besoin d'un fichier mime.types mis à jour ).

Vous pouvez utiliser l'image nginx de nlepage / golang_wasm , qui inclut déjà le type MIME fixe, wasm_exec.html et wasm_exec.js dans le code> / usr / share / nginx / html / directory.

Maintenant, cliquez sur le bouton "Exécuter", puis ouvrez la console de votre navigateur et vous verrez le message d'accueil console.log ("Bonjour Wasm!").


Un exemple complet est disponible ici .

Appelez JS depuis Go


Maintenant que nous avons lancé avec succès le premier binaire WebAssembly compilé à partir de Go, examinons de plus près les fonctionnalités fournies.

Le nouveau package syscall / js a été ajouté à la bibliothèque standard. Considérez le fichier principal, js.go
Un nouveau type js.Value est js.Value qui représente une valeur JavaScript.

Il propose une API simple pour gérer les variables JavaScript:

  • js.Value.Get() et js.Value.Set() renvoient et définissent les valeurs de champ de l'objet.
  • js.Value.Index() et js.Value.SetIndex() accèdent à l'objet par lecture et écriture d'index.
  • js.Value.Call() appelle la méthode objet en tant que fonction.
  • js.Value.Invoke() appelle l'objet lui-même en tant que fonction.
  • js.Value.New() appelle le nouvel opérateur et utilise ses propres connaissances en tant que constructeur.
  • Quelques méthodes supplémentaires pour obtenir la valeur JavaScript dans le type Go correspondant, par exemple js.Value.Int() ou js.Value.Bool() .

Et d'autres méthodes intéressantes:

  • js.Undefined() donnera à js.Value l' undefined correspondant.
  • js.Null() donnera à js.Value null correspondante.
  • js.Global() renverra js.Value donnant accès à la portée globale.
  • js.ValueOf() accepte les types Go primitifs et renvoie le bon js.Value

Au lieu d'afficher le message dans os.StdOut, affichons-le dans la fenêtre de notification à l'aide de window.alert() .

Puisque nous sommes dans le navigateur, la portée globale est une fenêtre, donc vous devez d'abord obtenir alert () de la portée globale:

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

Nous avons maintenant une variable d' alert , sous la forme de js.Value , qui est une référence à window.alert JS, et vous pouvez utiliser la fonction pour appeler via js.Value.Invoke() :

 alert.Invoke("Hello wasm!") 

Comme vous pouvez le voir, il n'est pas nécessaire d'appeler js.ValueOf () avant de passer les arguments à Invoke, cela prend une quantité arbitraire d' interface{} et passe les valeurs via ValueOf lui-même.

Maintenant, notre nouveau programme devrait ressembler à ceci:

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

Comme dans le premier exemple, il vous suffit de créer un fichier appelé test.wasm et de laisser wasm_exec.html et wasm_exec.js tels wasm_exec.js .
Maintenant, lorsque nous cliquons sur le bouton "Exécuter", une fenêtre d'alerte apparaît avec notre message.

Un exemple de travail se trouve dans le dossier examples/js-call .

Appelez Go de JS.


Appeler JS à partir de Go est assez simple, regardons de plus près le syscall/js , le deuxième fichier à visualiser est callback.go .

  • js.Callback encapsuleur js.Callback pour la fonction Go, à utiliser dans JS.
  • js.NewCallback() fonction qui prend une fonction (accepte une tranche de js.Value et ne renvoie rien), et retourne js.Callback .
  • Quelques mécanismes pour gérer les rappels actifs et js.Callback.Release() , qui doivent être appelés pour détruire le rappel.
  • js.NewEventCallback() similaire à js.NewCallback() , mais la fonction js.NewCallback() n'accepte qu'un seul argument - un événement.

Essayons de faire quelque chose de simple: exécutez Go fmt.Println() du côté JS.

Nous allons apporter quelques modifications à wasm_exec.html pour pouvoir obtenir un rappel de Go pour l'appeler.

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

Cela lance le binaire wasm et attend qu'il se termine, puis le réinitialise pour la prochaine exécution.

Ajoutons une nouvelle fonction qui recevra et enregistrera le rappel Go et changer l'état de Promise à la fin:

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

Maintenant adaptons la run() pour utiliser le rappel:

 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 } 

Et c'est du côté de JS!

Maintenant, dans la partie Go, vous devez créer un rappel, l'envoyer du côté JS et attendre que la fonction soit nécessaire.

  var done = make(chan struct{}) 

Ensuite, ils devraient écrire la vraie fonction printMessage() :

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

Les arguments sont transmis via la tranche []js.Value , vous devez donc appeler js.Value.String() sur le premier élément slice pour obtenir le message dans la ligne Go.
Maintenant, nous pouvons encapsuler cette fonction dans un rappel:

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

Appelez ensuite la fonction JS setPrintMessage() , tout comme l'appel de window.alert() :

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

La dernière chose à faire est d'attendre que le rappel soit appelé en principal:

 <-done 

Cette dernière partie est importante car les rappels sont exécutés dans un goroutine dédié, et le goroutine principal doit attendre que le rappel soit appelé, sinon le binaire wasm sera arrêté prématurément.

Le programme Go résultant devrait ressembler à ceci:

 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{}{} } 

Comme dans les exemples précédents, créez un fichier appelé test.wasm . Nous devons également remplacer wasm_exec.html par notre version, et wasm_exec.js pouvons réutiliser wasm_exec.js .

Maintenant, lorsque vous appuyez sur le bouton "Exécuter", comme dans notre premier exemple, le message est imprimé dans la console du navigateur, mais cette fois, c'est beaucoup mieux! (Et plus dur.)

Un exemple de travail dans une offre de fichier Docker est disponible dans le dossier examples/go-call .

Travail long


Appeler Go de JS est un peu plus lourd que d'appeler JS de Go, surtout du côté JS.

Cela est principalement dû au fait que vous devez attendre que le résultat du rappel Go soit transmis au côté JS.

Essayons autre chose: pourquoi ne pas organiser le binaire wasm, qui ne se terminera pas juste après l'appel de rappel, mais continuera à fonctionner et à accepter d'autres appels.
Cette fois, commençons par le côté Go, et comme dans notre exemple précédent, nous devons créer un rappel et l'envoyer du côté JS.

Ajoutez un compteur d'appels pour suivre combien de fois la fonction a été appelée.

Notre nouvelle fonction printMessage() imprime le message reçu et la valeur du compteur:

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

La création d'un rappel et son envoi du côté JS est la même que dans l'exemple précédent:

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

Mais cette fois, nous n'avons pas done canal pour nous informer de la fin du goroutine principal. Une façon pourrait être de verrouiller en permanence le goroutin principal avec une select{} vide select{} :

 select{} 

Ce n'est pas satisfaisant, notre wasm binaire va juste se bloquer en mémoire jusqu'à ce que l'onglet du navigateur soit fermé.

Vous pouvez écouter l'événement beforeunload sur la page, vous aurez besoin d'un deuxième rappel pour recevoir l'événement et notifier le goroutine principal via le canal:

 var beforeUnloadCh = make(chan struct{}) 

Cette fois, la nouvelle fonction beforeUnload() n'acceptera l'événement que comme un seul argument js.Value :

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

Ensuite, enveloppez-le dans un rappel en utilisant js.NewEventCallback() et enregistrez-le du côté JS:

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

Enfin, remplacez la select blocage vide par la lecture du canal beforeUnloadCh :

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

Le programme final ressemble à ceci:

 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{}{} } 

Auparavant, du côté JS, le téléchargement du binaire wasm ressemblait à ceci:

 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 }) 

Adaptons-le pour exécuter le binaire immédiatement après le chargement:

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

Et remplacez le bouton «Exécuter» par un champ de message et un bouton pour appeler printMessage() :

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

Enfin, la fonction setPrintMessage() , qui accepte et stocke le rappel, devrait être plus simple:

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

Maintenant, lorsque nous cliquons sur le bouton "Imprimer le message", vous devriez voir un message de notre choix et un compteur d'appels imprimés dans la console du navigateur.
Si nous cochons la case Conserver le journal de la console du navigateur et actualisons la page, nous verrons le message «Bye Wasm!».



Les sources sont disponibles dans le dossier examples/long-running sur github.

Et alors?


Comme vous pouvez le voir, l'API syscall/js apprise fait son travail et vous permet d'écrire des choses complexes avec un peu de code. Vous pouvez écrire à l' auteur si vous connaissez une méthode plus simple.
Il n'est actuellement pas possible de renvoyer une valeur à JS directement à partir du rappel Go.
Gardez à l'esprit que tous les rappels sont effectués dans le même goroutin, donc si vous effectuez des opérations de blocage dans le rappel, n'oubliez pas de créer un nouveau goroutin, sinon vous bloquerez l'exécution de tous les autres rappels.
Toutes les fonctionnalités linguistiques de base sont déjà disponibles, y compris la concurrence. Pour l'instant, tous les goroutins fonctionneront dans un seul thread, mais cela changera à l'avenir .
Dans nos exemples, nous avons utilisé uniquement le package fmt de la bibliothèque standard, mais tout est disponible qui n'essaie pas de s'échapper du bac à sable.

Le système de fichiers semble être pris en charge via Node.js.

Enfin, qu'en est-il des performances? Il serait intéressant d'exécuter des tests pour voir comment Go Wasm se compare au code JS pur équivalent. Quelqu'un hajimehoshi a mesuré comment différents environnements fonctionnent avec des nombres entiers, mais la technique n'est pas très claire.



N'oubliez pas que Go 1.11 n'a pas encore été officiellement publié. À mon avis, c'est très bon pour la technologie expérimentale. Ceux qui sont intéressés par les tests de performances peuvent tourmenter leur navigateur .
La niche principale, comme le note l'auteur, est le transfert du serveur au client du code go existant. Mais avec de nouvelles normes, vous pouvez créer des applications complètement hors ligne et le code wasm est enregistré sous forme compilée. Vous pouvez transférer de nombreux utilitaires sur le Web, d'accord, facilement?

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


All Articles