En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly (con Golang) en Vue 3 con Vite y Pinia. Voy a explicar cómo comunicar las funciones de WASM con JavaScript y viceversa, dejando mucho código del lado de Golang con WASM.
Toma en cuenta que es una documentación muy específica a mi modo de trabajo. No esperes un tutorial paso a paso.
Comlink es una librería que facilita el trabajo de los workers para que parezcan funciones locales. Normalmente uno se comunica con postMessage y escucha la respuesta de vuelta en el onmessage, pero es muy complejo.
Primero decidí no usar Comlink pero conforme la app fue creciendo decidí usar Comlink. A Golang compilado como WASM no le importa si usas o no Comlink, pero a Vue 3 sí.
Si tú no quieres usar Comlink entonces veamos la función que convierte una función de Golang normal y la convierte en una función JavaScript. Aquí estoy armando un mensaje de respuesta con la acción y el valor para que el invocador (en onmessage
) sepa qué acción se ha realizado y los datos que lleva esa acción.
func factoryParaWorker(laFuncion func(js.Value, []js.Value) js.Value, accion string) js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) any {
valor := laFuncion(this, args)
return armarMensajeDeRespuesta(accion, []any{valor})
})
}
Y la de armar mensaje de respuesta queda así:
func armarMensajeDeRespuesta(accion string, argumentos []any) js.Value {
objeto := js.Global().Get("Object").New()
arreglo := js.Global().Get("Array").New(argumentos...)
objeto.Set("accion", accion)
objeto.Set("datos", arreglo)
return objeto
}
Necesitamos la acción porque en el onmessage necesitamos saber cuál acción acaba de ser ejecutada. Y necesitamos el factoryParaWorker
porque las funciones tienen la siguiente firma para que puedan ser invocadas entre ellas:
func miFuncion(this js.Value, parametros []js.Value) js.Value {
return js.ValueOf("Hola")
}
Anteriormente yo invocaba a postmessage
al final de cada función pero eso no servía para cuando quería usar las funciones entre ellas, así que las expongo con el factory para worker y ya luego en mi worker hago:
const resultados = await self["parzibyte"][accion](...datos);
self.postMessage(resultados);
Y desde cualquier lugar solo hay que escuchar el onmessage del worker.
Pero ahora con Comlink es distinto. Por alguna extraña razón no puedo exponer la función directamente porque el resultado es undefined
del lado del worker. Entonces lo siguiente es incorrecto:
objetoParzibyte.Set("iniciarBaseDeDatos", IniciarBaseDeDatosConPromesa)
Porque aunque IniciarBaseDeDatosConPromesa
devuelve un js.Value
, recibo undefined
al invocarlo. Tuve que crear otra función así:
func crearFuncionDeJSDevolviendoJsValue(laFuncion func(js.Value, []js.Value) js.Value) js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) any {
valor := laFuncion(this, args)
return armarOtroMensajeDeRespuesta(valor)
})
}
Y la función que arma el mensaje:
func armarOtroMensajeDeRespuesta(argumentos any) js.Value {
objeto := js.Global().Get("Object").New()
objeto.Set("resultado", argumentos)
return objeto
}
Así es, debo crear un objeto “dummy” que guarda los resultados en su propiedad “resultado” y devolverlo. Veamos cómo iniciar la base de datos, ya que la promesa estará en resultado.resultado
:
const respuesta = await self.parzibyte.iniciarBaseDeDatos();
await respuesta.resultado;
Para otros casos que devuelven un arreglo, simplemente modifiqué la store:
const resultado = await envolturaWorkerPuenteWasm.ejecutarAccionEnWasm(accion, datos);
return resultado.resultado;
Y listo.
Comlink.expose({
iniciar: async () => {
// Código aquí
},
ejecutarAccionEnWasm: async (accion, datos) => {
return await self["parzibyte"][accion](...datos);
}
});
En iniciar cargo el wasm, lo instancio y le digo a wasm que haga su propio init.
Luego expongo la función ejecutarAccionEnWasm
que simplemente invocará a la función del objeto parzibyte
. Lo he hecho en una store:
import { defineStore } from 'pinia'
import * as Comlink from "comlink"
import ArchivoWorkerPuenteWasm from "@/worker_puente_wasm?worker"
const workerPuenteWasm = new ArchivoWorkerPuenteWasm();
const envolturaWorkerPuenteWasm = Comlink.wrap(workerPuenteWasm);
await envolturaWorkerPuenteWasm.iniciar();
export const useWasmStore = defineStore('wasm', () => {
async function ejecutarAccionEnWasm(accion:string, datos: Array<any>){
return envolturaWorkerPuenteWasm.ejecutarAccionEnWasm(accion, datos);
}
return {ejecutarAccionEnWasm }
})
Me gusta que puedo hacer un await para el iniciar,
así me aseguro de que la BD está lista. Luego expongo ejecutarAccionEnWasm
que es solo una envoltura para la misma función del worker. Entonces la comunicación es:
Quien sea que invoque a la store -> worker -> objeto parzibyte de WASM
Y finalmente en Vue dentro de la app (que es la principal) hago el init
estando seguro de que nada se va a renderizar hasta que la store termine de esperar a las promesas:
import { useWasmStore } from './stores/wasmStore';
// La cargamos sin usar para que la
// store pueda iniciar la BD esperando
// que la promesa se resuelva
const wasmStore = useWasmStore();
Luego ya puedo usarla en cualquier lugar, por ejemplo en otro componente que no es la App:
const plantillaRecienInsertada = await wasmStore.ejecutarAccionEnWasm("insertarPlantilla", [imagenCodificada, plantilla.value.nombre])
Y en este caso se me va a devolver lo que Golang devuelva con WASM. El js.Value
que devuelve la función invocada es lo que yo tendré al resolverse la promesa, todo limpio sin onmessage
. Solo promesas.
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…
Hoy te voy a enseñar a agregar un salto de línea a un texto para…
Esta web usa cookies.