Страница веб-приложения на Go: заменить Javascript на Go во фронтенде

Интересует один вопрос (возможно глупый).

Можно ли при создании фронта веб-приложения заменить JS в графических элементах(всплывающие окна, вложенные списки, и т.д.) на методы из Go?

P.S.: прошу палками не бить и помидорами не закидывать))


Ответы (1 шт):

Автор решения: Pak Uula

Короткий ответ

Да, можно. Но геморройно-о-о-о

Развёрнутый

Современные браузеры умеют загружать модули WebAssembly (WASM). Go умеет компилировать в WASM, и есть обвязка для корректной загрузки WASM модуля в браузер.

Вот пример. Этот код на Go

  • находит на странице элемент #_target и пишет в него приветствие
  • находит на странице кнопку #_goBtn и прикрепляет к ней обработчик кликов - Go-шную функцию onClick
  • функция onClick пишет в #_target сколько раз на неё нажали.
package main

import (
    "fmt"
    "log"
    "syscall/js"
)

func onClick(this js.Value, args []js.Value) any {
    count++
    target := js.Global().Get("document").Call("getElementById", "_target")
    if target.IsNull() {
        log.Panic("No element with id _target")
    }
    html := fmt.Sprintf("<p>Button clicked %d times</p>", count)
    target.Set("innerHTML", html)

    return js.ValueOf(true)
}

var done = make(chan struct{})
var count = 0

func main() {
    defer func() { log.Println("WASM execution finished") }()

    log.Println("WASM execution started")
    target := js.Global().Get("document").Call("getElementById", "_target")
    if target.IsNull() {
        log.Panic("No element with id _target")
    }
    html := "<p>Hello from Go!</p>"
    target.Set("innerHTML", html)

    btn := js.Global().Get("document").Call("getElementById", "_goBtn")
    if btn.IsNull() {
        log.Panic("No element with id _goBtn")
    }
    btn.Set("onclick", js.FuncOf(onClick))
    btn.Set("disabled", false)

    // Wait until `done` is closed
    <-done
}

На что нужно обратить внимание.

  1. если функция main завершается, onClick перестаёт работать. В консоль выводится сообщение об ошибке Go application finished. Поэтому функция main должна работать вечно. Этого можно достичь многими разными способами, в примере main ждёт сигнал из канала. Пока в канал done не будет что-то записано, или канал не будет закрыт, main будет висеть в ожидании.

  2. В HTML нельзя написать <button onclick=wasm.onClick>Click me!</button>. WASM модуль, собранный в Go, не экспортирует функции. Поэтому обработчики необходимо назначать в Go-шной функции main.

  3. Насколько я знаю, для Go пока нет аналога jQuery, поэтому все операции можно выполнять только через DOM API, во всеми этими getElementById, append и ручным перебором элементов в списках.

Как компилировать:

GOOS=js GOARCH=wasm go build -o example.wasm .

Пример HTML страницы, которая использует example.wasm:

<!doctype html>
<!--
Основано на wasm_exe.html, созданном The Go Authors.
Файл wasm_exec.html лежит у вас в <GOROOT>/misc/wasm/, 
где <GOROOT> - каталог, в который установлен дистрибутив Go,
    печатается командой `go env GOROOT`
-->
<html>

<head>
    <meta charset="utf-8">
    <title>Пример Go WASM</title>
</head>

<body>
    <!-- Скрипт wasm_exec.js лежит в <GOROOT>/misc/wasm/ -->
    <script src="wasm_exec.js"></script>
    <!-- поменяйте путь к загружаемому модулю wasm в вызове WebAssembly.instantiateStreaming -->
    <script>
        if (!WebAssembly.instantiateStreaming) { 
            WebAssembly.instantiateStreaming = async (resp, importObject) => {
                const source = await (await resp).arrayBuffer();
                return await WebAssembly.instantiate(source, importObject);
            };
        }

        const go = new Go();
        let mod, inst;
        WebAssembly.instantiateStreaming(fetch("example.wasm"), go.importObject).then((result) => {
            mod = result.module;
            inst = result.instance;
        }).catch((err) => {
            console.error(err);
        });

        async function run() {
            console.clear();
            await go.run(inst);
            inst = await WebAssembly.instantiate(mod, go.importObject); // reset instance
        }
        // автоматический запуск `func main()` из wasm
        window.onload = run
    </script>

    <div id="_target"></div>
    <button id="_goBtn" disabled>Click Me!</button>
</body>

</html>

Пример с Makefile для сборки и запуска: https://github.com/pakuula/StackOverflow/tree/main/go/1551838

Далее можете погуглить go wasm web

PS. Работало в go1.19 и go1.21, не работало в go1.20

→ Ссылка