Gracias a WebAssembly, puede escribir Frontend en Go

Artículo original

En febrero de 2017, un miembro del equipo de Brad Fitzpatrick propuso hacer que WebAssembly sea compatible con el idioma. Cuatro meses después, en noviembre de 2017, el autor de GopherJS Richard Muziol comenzó a implementar la idea. Y finalmente, la implementación completa se encontró en master. Los desarrolladores recibirán wasm alrededor de agosto de 2018, con la versión 1.11 de go . Como resultado, la biblioteca estándar asume casi todas las dificultades técnicas para importar y exportar funciones que le son familiares si ya ha intentado compilar C en wasm. Eso suena prometedor. Veamos qué se puede hacer con la primera versión.



Todos los ejemplos en este artículo se pueden iniciar desde los contenedores acoplables que se encuentran en el repositorio del autor :

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

Luego vaya a localhost : 32XXX /, y vaya de un enlace a otro.

Hola wasm


La creación del "mundo hola" básico y el concepto ya están bastante bien documentados (incluso en ruso ), así que pasemos a las cosas más sutiles.

Lo más esencial es una versión recién compilada de Go que admita wasm. No describiré paso a paso la instalación , solo sé que lo que se necesita ya está en el maestro.

Si no quiere preocuparse por esto, Dockerfile c go está disponible en el repositorio golub-wasm en github , o puede tomar una imagen de nlepage / golang_wasm aún más rápido.

Ahora puede escribir el helloworld.go tradicional y compilarlo con el siguiente comando:

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

Las variables de entorno GOOS y GOARCH ya están configuradas en la imagen nlepage / golang_wasm , por lo que puede usar un archivo Dockerfile como este para compilar:

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

El último paso es usar los archivos wasm_exec.html y wasm_exec.js disponibles en el repositorio go en el misc/wasm o en la imagen del acoplador nlepage / golang_wasm en el directorio /usr/local/go/misc/wasm/ para ejecutar test.wasm en navegador (wasm_exec.js espera el archivo binario test.wasm , así que usamos este nombre).
Solo necesita dar 3 archivos estáticos usando nginx, por ejemplo, luego wasm_exec.html mostrará el botón "ejecutar" (se test.wasm solo si test.wasm carga correctamente).

Es de destacar que test.wasm debe servirse con la application/wasm tipo MIME, de lo contrario el navegador se negará a ejecutarlo. (por ejemplo, nginx necesita un archivo mime.types actualizado ).

Puede usar la imagen nginx de nlepage / golang_wasm , que ya incluye el tipo MIME fijo, wasm_exec.html y wasm_exec.js en el wasm_exec.js de código> / usr / share / nginx / html /.

Ahora haga clic en el botón "Ejecutar", luego abra la consola de su navegador y verá el saludo console.log ("¡Hola Wasm!").


Un ejemplo completo está disponible aquí .

Llama a JS desde Go


Ahora que hemos lanzado con éxito el primer binario WebAssembly compilado desde Go, echemos un vistazo más de cerca a las características proporcionadas.

El nuevo paquete syscall / js se ha agregado a la biblioteca estándar. Considere el archivo principal, js.go
Está js.Value un nuevo tipo js.Value que representa un valor de JavaScript.

Ofrece una API simple para administrar variables de JavaScript:

  • js.Value.Get() y js.Value.Set() devuelven y establecen los valores de campo del objeto.
  • js.Value.Index() y js.Value.SetIndex() acceden al objeto mediante el índice de lectura y escritura.
  • js.Value.Call() llama al método objeto como una función.
  • js.Value.Invoke() llama al objeto en sí mismo como una función.
  • js.Value.New() llama al nuevo operador y utiliza su propio conocimiento como constructor.
  • Algunos métodos más para obtener el valor de JavaScript en el tipo Go correspondiente, por ejemplo js.Value.Int() o js.Value.Bool() .

Y métodos interesantes adicionales:

  • js.Undefined() le dará a js.Value el correspondiente undefined .
  • js.Null() le dará a js.Value null correspondiente.
  • js.Global() devolverá js.Value dando acceso al alcance global.
  • js.ValueOf() acepta tipos Go primitivos y devuelve el js.Value correcto

En lugar de mostrar el mensaje en os.StdOut, vamos a mostrarlo en la ventana de notificación usando window.alert() .

Dado que estamos en el navegador, el alcance global es una ventana, por lo que primero debe obtener una alerta () del alcance global:

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

Ahora tenemos una variable de alert , en forma de js.Value , que es una referencia a window.alert JS, y puede usar la función para llamar a través de js.Value.Invoke() :

 alert.Invoke("Hello wasm!") 

Como puede ver, no es necesario llamar a js.ValueOf () antes de pasar los argumentos a Invoke, toma una cantidad arbitraria de interface{} y pasa los valores a través de ValueOf.

Ahora nuestro nuevo programa debería verse así:

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

Como en el primer ejemplo, solo necesita crear un archivo llamado test.wasm , y dejar wasm_exec.html y wasm_exec.js como estaban.
Ahora, cuando hacemos clic en el botón "Ejecutar", aparece una ventana de alerta con nuestro mensaje.

Un ejemplo de trabajo está en la carpeta examples/js-call .

Llame Go desde JS.


Llamar a JS desde Go es bastante simple, echemos un vistazo más de cerca al paquete syscall/js , el segundo archivo para ver es callback.go .

  • js.Callback contenedor js.Callback para la función Go, para usar en JS.
  • js.NewCallback() función que toma una función (acepta una porción de js.Value y no devuelve nada), y devuelve js.Callback .
  • Algunas mecánicas para administrar devoluciones de llamada activas y js.Callback.Release() , que deben js.Callback.Release() para destruir la devolución de llamada.
  • js.NewEventCallback() similar a js.NewCallback() , pero la función envuelta acepta solo 1 argumento: un evento.

Intentemos hacer algo simple: ejecutar Go fmt.Println() desde el lado JS.

Haremos algunos cambios en wasm_exec.html para poder recibir una devolución de llamada de Ir para llamarlo.

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

Esto inicia el wasm binario y espera a que se complete, luego lo reinicializa para la próxima ejecución.

Agreguemos una nueva función que recibirá y guardará la devolución de llamada Go y cambiará el estado de Promise al finalizar:

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

Ahora adaptemos la función run() para usar la devolución de llamada:

 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 } 

¡Y esto está del lado de JS!

Ahora, en la parte Ir, debe crear una devolución de llamada, enviarla al lado JS y esperar a que se necesite la función.

  var done = make(chan struct{}) 

Luego deberían escribir la función real printMessage() :

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

Los argumentos se pasan a través del segmento []js.Value , por lo que debe llamar a js.Value.String() en el primer elemento de segmento para obtener el mensaje en la línea Go.
Ahora podemos ajustar esta función en una devolución de llamada:

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

Luego llame a la función JS setPrintMessage() , al igual que llamando a window.alert() :

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

Lo último que debe hacer es esperar a que se llame a la devolución de llamada en main:

 <-done 

Esta última parte es importante porque las devoluciones de llamada se ejecutan en una rutina rutinaria dedicada, y la rutina principal debe esperar a que se llame la devolución de llamada, de lo contrario, el binario wasm se detendrá prematuramente.

El programa Go resultante debería tener este aspecto:

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

Como en los ejemplos anteriores, cree un archivo llamado test.wasm . También necesitamos reemplazar wasm_exec.html con nuestra versión, y wasm_exec.js reutilizar wasm_exec.js .

Ahora, cuando presiona el botón "Ejecutar", como en nuestro primer ejemplo, el mensaje se imprime en la consola del navegador, ¡pero esta vez es mucho mejor! (Y más duro)

Un ejemplo de trabajo en una oferta de archivo acoplable está disponible en la carpeta examples/go-call .

Largo trabajo


Llamar a Go desde JS es un poco más engorroso que llamar a JS desde Go, especialmente en el lado de JS.

Esto se debe principalmente al hecho de que debe esperar hasta que el resultado de la devolución de llamada Go se pase al lado JS.

Intentemos algo más: por qué no organizar el wasm binario, que no terminará justo después de la llamada de devolución de llamada, sino que continuará funcionando y aceptará otras llamadas.
Esta vez, comencemos desde el lado Go, y como en nuestro ejemplo anterior, necesitamos crear una devolución de llamada y enviarla al lado JS.

Agregue un contador de llamadas para rastrear cuántas veces se ha llamado a la función.

Nuestra nueva función printMessage() imprimirá el mensaje recibido y el valor del contador:

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

Crear una devolución de llamada y enviarla al lado JS es lo mismo que en el ejemplo anterior:

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

Pero esta vez no hemos done ningún canal para notificarnos la finalización de la rutina principal. Una forma podría ser bloquear permanentemente el goroutin principal con la select{} vacía select{} :

 select{} 

Esto no es satisfactorio, nuestro wasm binario se quedará en la memoria hasta que se cierre la pestaña del navegador.

Puede escuchar el evento antes de beforeunload en la página, necesitará una segunda devolución de llamada para recibir el evento y notificar a la goroutina principal a través del canal:

 var beforeUnloadCh = make(chan struct{}) 

Esta vez, la nueva función beforeUnload() solo aceptará el evento, como un solo argumento js.Value :

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

Luego envuélvalo en una devolución de llamada usando js.NewEventCallback() y regístrelo en el lado JS:

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

Finalmente, reemplace la select bloqueo vacía con lectura del canal beforeUnloadCh :

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

El programa final se ve así:

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

Anteriormente, en el lado de JS, la descarga del binario wasm se veía así:

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

Vamos a adaptarlo para ejecutar el binario inmediatamente después de cargar:

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

Y reemplace el botón "Ejecutar" con un campo de mensaje y un botón para llamar a printMessage() :

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

Finalmente, la función setPrintMessage() , que acepta y almacena la devolución de llamada, debería ser más simple:

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

Ahora, cuando hacemos clic en el botón "Imprimir mensaje", debería ver un mensaje de nuestra elección y un contador de llamadas impreso en la consola del navegador.
Si marcamos la casilla Conservar registro de la consola del navegador y actualizamos la página, veremos el mensaje "¡Adiós Wasm!".



Las fuentes están disponibles en la carpeta examples/long-running en github.

Y entonces?


Como puede ver, la API syscall/js aprendida hace su trabajo y le permite escribir cosas complejas con un poco de código. Puede escribir al autor si conoce un método más simple.
Actualmente no es posible devolver un valor a JS directamente desde la devolución de llamada Go.
Tenga en cuenta que todas las devoluciones de llamada se realizan en el mismo goroutin, por lo que si realiza algunas operaciones de bloqueo en la devolución de llamada, no olvide crear un nuevo goroutin, de lo contrario, bloqueará la ejecución de todas las demás devoluciones de llamada.
Todas las funciones básicas del lenguaje ya están disponibles, incluida la concurrencia. Por ahora, todas las goroutinas funcionarán en un hilo, pero esto cambiará en el futuro .
En nuestros ejemplos, utilizamos solo el paquete fmt de la biblioteca estándar, pero todo está disponible que no intente escapar del entorno limitado.

El sistema de archivos parece ser compatible a través de Node.js.

Finalmente, ¿qué pasa con el rendimiento? Sería interesante ejecutar algunas pruebas para ver cómo Go wasm se compara con el código JS puro equivalente. Alguien hajimehoshi hizo mediciones de cómo funcionan los diferentes entornos con enteros, pero la técnica no es muy clara.



No olvide que Go 1.11 aún no se ha lanzado oficialmente. En mi opinión, es muy bueno para la tecnología experimental. Quienes estén interesados ​​en las pruebas de rendimiento pueden atormentar su navegador .
El nicho principal, como señala el autor, es la transferencia del servidor al cliente del código go existente. Pero con los nuevos estándares, puede crear aplicaciones completamente fuera de línea , y el código wasm se guarda en forma compilada. Puede transferir muchas utilidades a la web, ¿está de acuerdo, convenientemente?

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


All Articles