Graças ao WebAssembly, você pode escrever o Frontend on Go

Artigo original

Em fevereiro de 2017, um membro da equipe do go Brad Fitzpatrick propôs dar suporte ao WebAssembly no idioma. Quatro meses depois, em novembro de 2017, o autor do GopherJS , Richard Muziol, começou a implementar a ideia. E, finalmente, a implementação completa foi encontrada no master. Os desenvolvedores receberão o wasm por volta de agosto de 2018, com a versão 1.11 . Como resultado, a biblioteca padrão assume quase todas as dificuldades técnicas na importação e exportação de funções familiares para você, se você já tentou compilar C no wasm. Isso parece promissor. Vamos ver o que pode ser feito com a primeira versão.



Todos os exemplos deste artigo podem ser iniciados a partir de contêineres do docker que estão no repositório do autor :

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

Em seguida, vá para localhost : 32XXX / e vá de um link para outro.

Oi Wasm!


A criação do “olá mundo” básico e o conceito já estão bem documentados (mesmo em russo ), então vamos passar para as coisas mais sutis.

O mais essencial é uma versão recém-compilada do Go que suporta wasm. Não descreverei passo a passo a instalação , apenas sei que o que é necessário já está no mestre.

Se você não quer se preocupar com isso, o Dockerfile c go está disponível no repositório golub-wasm no github , ou você pode tirar uma imagem do nlepage / golang_wasm ainda mais rapidamente.

Agora você pode escrever o helloworld.go tradicional e compilá-lo com o seguinte comando:

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

As variáveis ​​de ambiente GOOS e GOARCH já estão definidas na imagem nlepage / golang_wasm , para que você possa usar um arquivo Dockerfile como este para compilar:

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

A última etapa é usar os arquivos wasm_exec.js e wasm_exec.js disponíveis no repositório go no misc/wasm ou na imagem do docker nlepage / golang_wasm no /usr/local/go/misc/wasm/ para executar test.wasm em browser (wasm_exec.js espera o arquivo binário test.wasm , portanto, usamos esse nome).
Você só precisa fornecer 3 arquivos estáticos usando o nginx, por exemplo, então wasm_exec.html exibirá o botão "executar" (ele será test.wasm apenas se o test.wasm carregado corretamente).

Vale ressaltar que test.wasm deve ser servido com o application/wasm tipo MIME, caso contrário, o navegador se recusará a executá-lo. (por exemplo, nginx precisa de um arquivo mime.types atualizado ).

Você pode usar a imagem nginx do nlepage / golang_wasm , que já inclui o tipo MIME fixo, wasm_exec.html e wasm_exec.js no diretório> / usr / share / nginx / html /.

Agora clique no botão “executar”, abra o console do navegador e você verá a saudação console.log (“Hello Wasm!”).


Um exemplo completo está disponível aqui .

Ligar para JS de Go


Agora que lançamos com sucesso o primeiro binário do WebAssembly compilado a partir do Go, vamos dar uma olhada nos recursos fornecidos.

O novo pacote syscall / js foi adicionado à biblioteca padrão. Considere o arquivo principal, js.go
Está js.Value um novo tipo js.Value que representa um valor JavaScript.

Ele oferece uma API simples para gerenciar variáveis ​​JavaScript:

  • js.Value.Get() e js.Value.Set() retornam e definem os valores do campo do objeto.
  • js.Value.Index() e js.Value.SetIndex() acessam o objeto pelo índice de leitura e gravação.
  • js.Value.Call() chama o método de objeto como uma função.
  • js.Value.Invoke() chama o próprio objeto como uma função.
  • js.Value.New() chama o novo operador e usa seu próprio conhecimento como construtor.
  • Mais alguns métodos para obter o valor JavaScript no tipo Go correspondente, por exemplo js.Value.Int() ou js.Value.Bool() .

E métodos interessantes adicionais:

  • js.Undefined() fornecerá a js.Value o valor undefined correspondente.
  • js.Null() dará a js.Value null correspondente.
  • js.Global() retornará js.Value dando acesso ao escopo global.
  • js.ValueOf() aceita tipos Go primitivos e retorna o js.Value correto

Em vez de exibir a mensagem no os.StdOut, vamos exibi-la na janela de notificação usando window.alert() .

Como estamos no navegador, o escopo global é uma janela; primeiro, você precisa receber alert () do escopo global:

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

Agora, temos uma variável de alert , na forma de js.Value , que é uma referência ao window.alert JS, e você pode usar a função para chamar js.Value.Invoke() :

 alert.Invoke("Hello wasm!") 

Como você pode ver, não há necessidade de chamar js.ValueOf () antes de passar os argumentos para Invoke, é necessária uma quantidade arbitrária de interface{} e passa os valores pelo próprio ValueOf.

Agora, nosso novo programa deve ficar assim:

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

Como no primeiro exemplo, você só precisa criar um arquivo chamado test.wasm e deixar wasm_exec.html e wasm_exec.js como estavam.
Agora, quando clicamos no botão "Executar", uma janela de alerta aparece com a nossa mensagem.

Um exemplo de trabalho está na pasta examples/js-call .

Chame Go from JS.


Chamar JS a partir do Go é bem simples, vamos dar uma olhada no pacote syscall/js , o segundo arquivo a ser visualizado é callback.go .

  • js.Callback tipo de wrapper para a função Go, para uso em JS.
  • js.NewCallback() função que aceita uma função (aceita uma fatia de js.Value e não retorna nada) e retorna js.Callback .
  • Algumas mecânicas para gerenciar retornos de chamada ativos e js.Callback.Release() , que devem ser chamados para destruir o retorno de chamada.
  • js.NewEventCallback() semelhante a js.NewCallback() , mas a função js.NewCallback() aceita apenas 1 argumento - um evento.

Vamos tentar fazer algo simples: execute Go fmt.Println() no lado JS.

Faremos algumas alterações no wasm_exec.html para obter um retorno de chamada de Go para chamá-lo.

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

Isso inicia o wasm binário e aguarda a conclusão, e reinicializa-o para a próxima execução.

Vamos adicionar uma nova função que receba e salve o retorno de chamada Go e altere o estado da Promise após a conclusão:

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

Agora vamos adaptar a função run() para usar o retorno de chamada:

 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 } 

E isso está do lado do JS!

Agora, na parte Ir, você precisa criar um retorno de chamada, enviá-lo para o lado JS e aguardar a função ser necessária.

  var done = make(chan struct{}) 

Em seguida, eles devem escrever a função real printMessage() :

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

Os argumentos são passados ​​pela fatia []js.Value , portanto, é necessário chamar js.Value.String() no primeiro elemento da fatia para obter a mensagem na linha Ir.
Agora podemos agrupar essa função em um retorno de chamada:

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

Em seguida, chame a função JS setPrintMessage() , assim como chamar window.alert() :

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

A última coisa a fazer é esperar que o retorno de chamada seja chamado principal:

 <-done 

Esta última parte é importante porque os retornos de chamada são executados em uma goroutine dedicada, e a goroutine principal deve aguardar a chamada do retorno, caso contrário, o wasm binário será interrompido prematuramente.

O programa Go resultante deve ficar assim:

 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 nos exemplos anteriores, crie um arquivo chamado test.wasm . Também precisamos substituir wasm_exec.html por nossa versão e wasm_exec.js reutilizar wasm_exec.js .

Agora, quando você pressiona o botão "executar", como no nosso primeiro exemplo, a mensagem é impressa no console do navegador, mas desta vez é muito melhor! (E mais difícil.)

Um exemplo de trabalho em um lance de arquivo docker está disponível na pasta examples/go-call .

Trabalho longo


Chamar Go de JS é um pouco mais complicado do que chamar JS de Go, especialmente no lado JS.

Isso ocorre principalmente pelo fato de você precisar aguardar até que o resultado do retorno de chamada Go seja passado para o lado JS.

Vamos tentar outra coisa: por que não organizar o binário wasm, que não termina logo após a chamada de retorno de chamada, mas continua a funcionar e aceita outras chamadas.
Agora, vamos começar pelo lado Ir e, como no exemplo anterior, precisamos criar um retorno de chamada e enviá-lo para o lado JS.

Adicione um contador de chamadas para rastrear quantas vezes a função foi chamada.

Nossa nova função printMessage() imprimirá a mensagem recebida e o valor do contador:

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

Criar um retorno de chamada e enviá-lo para o lado JS é o mesmo que no exemplo anterior:

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

Mas desta vez não temos canal para nos notificar sobre o término da goroutina principal. Uma maneira seria bloquear permanentemente a goroutin principal com a select{} vazia select{} :

 select{} 

Isso não é satisfatório, nosso wasm binário ficará travado na memória até que a guia do navegador seja fechada.

Você pode ouvir o evento beforeunload na página, precisará de um segundo retorno de chamada para receber o evento e notificar a goroutine principal por meio do canal:

 var beforeUnloadCh = make(chan struct{}) 

Dessa vez, a nova função beforeUnload() aceitará apenas o evento como um único argumento js.Value :

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

Em seguida, envolva-o em um retorno de chamada usando js.NewEventCallback() e registre-o no lado JS:

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

Por fim, substitua a select bloqueio vazia pela leitura do canal beforeUnloadCh :

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

O programa final é assim:

 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, no lado JS, o download do binário wasm era assim:

 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 adaptá-lo para executar o binário imediatamente após o carregamento:

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

E substitua o botão "Executar" por um campo de mensagem e um botão para chamar printMessage() :

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

Por fim, a função setPrintMessage() , que aceita e armazena o retorno de chamada, deve ser mais simples:

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

Agora, quando clicarmos no botão "Imprimir mensagem", você verá uma mensagem de nossa escolha e um contador de chamadas impresso no console do navegador.
Se marcarmos a caixa Preserve log do console do navegador e atualizar a página, veremos a mensagem “Bye Wasm!”.



As fontes estão disponíveis na pasta examples/long-running no github.

E então?


Como você pode ver, a API syscall/js aprendida faz seu trabalho e permite que você escreva coisas complexas com um pouco de código. Você pode escrever para o autor se souber um método mais simples.
Atualmente, não é possível retornar um valor para JS diretamente do retorno de chamada Go.
Lembre-se de que todos os retornos de chamada são executados na mesma goroutin; portanto, se você realizar algumas operações de bloqueio no retorno de chamada, não esqueça de criar uma nova goroutin; caso contrário, você bloqueará a execução de todos os outros retornos de chamada.
Todos os recursos básicos do idioma já estão disponíveis, incluindo simultaneidade. Por enquanto, todas as goroutins funcionarão em um segmento, mas isso mudará no futuro .
Em nossos exemplos, usamos apenas o pacote fmt da biblioteca padrão, mas tudo está disponível que não tenta escapar da sandbox.

O sistema de arquivos parece ser suportado pelo Node.js.

Finalmente, e o desempenho? Seria interessante executar alguns testes para ver como o Go wasm se compara ao código JS puro equivalente. Alguém hajimehoshi fez medições de como diferentes ambientes trabalham com números inteiros, mas a técnica não é muito clara.



Não esqueça que o Go 1.11 ainda não foi lançado oficialmente. Na minha opinião, é muito bom para a tecnologia experimental. Aqueles que estão interessados ​​em testes de desempenho podem atormentar o navegador .
O nicho principal, como observa o autor, é a transferência do servidor para o cliente do código go existente. Mas com novos padrões, você pode criar aplicativos completamente offline e o código wasm é salvo na forma compilada. Você pode transferir muitos utilitários para a web, concorda, convenientemente?

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


All Articles