Как стать автором
Обновить
934.55
Рейтинг
OTUS
Цифровые навыки от ведущих экспертов

Go в браузер. Создание веб-приложений с использованием Web Assembly на Go

Блог компании OTUS Разработка веб-сайтов *Go *WebAssembly *

Функции и методы в языке golang

Наиболее известным набором инструментов для компиляции в wasm32 является emscripten, с его помощью можно скомпилировать приложение, написанное на C/C++ или на любом языке, имеющим frontend-компилятор для LLVM. При этом компилятор подменяет вызовы OpenGL и POSIX на соответствующие аналоги в браузере, что например используется при компиляции библиотеки skia для браузера (canvaskit) из исходного кода на C++, а также портирование существующих библиотек (например, ffmpeg или opencv). Но некоторые языки программирования поддерживают wasm32 как одну из целевых платформ, среди которых можно выделить Kotlin (Native) и Go. В этой статье мы обсудим общие вопросы о запуске приложений Go в среде браузера и использование библиотеки Vecty для создания веб-приложений на основе переиспользуемых компонентов.

Компиляция в целевую платформу wasm32 поддерживается в Go, для этого нужно указать значения переменных окружения GOOS=js и GOARCH=wasm. Начнем с самой простой программы и постепенно будет усложнять возможности нашего веб-приложения.

package main
 
import "fmt"
 
func main() {
    fmt.Println("Hello, WebAssembly World!")
}

Выполним компиляцию исходного кода в wasm-байткод:

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

И далее создадим сценарий для загрузки и запуска wasm-файла в HTML:

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script src="wasm_exec.js"></script>
        <script>
            if (WebAssembly) {
                 const go = new Go();
                 WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
                    go.run(result.instance);
                 });
            } else {
               console.log("WebAssembly is not supported in your browser")
            }
        </script>
    </head>
    <body></body>
</html>

Файл wasm_exec.js может быть получен, например, отсюда или из установки Go (по расположению misc/wasm). Он создает необходимый контекст для выполнения приложения на Go (например, создает реализации для syscall/js, который мы будем использовать для взаимодействия с браузером, а также регистрирует прототип Go, который будет использоваться для начальной загрузки байткода и запуска функции main.

Для обхода ограничений безопасности браузера (по умолчанию загрузка сетевых ресурсов запрещена, если страница открыта через локальную ссылку в схеме file://) запустим docker-контейнер с nginx.

docker run --name gotest -d -p 80:80 -v `pwd`:/usr/share/nginx/html nginx

После обращения к localhost можно увидеть, что сообщение выводится в консоль браузера. Теперь давайте подключимся к DOM и попробуем вызвать Javascript-функцию из нашего wasm-приложения. Для доступа к JS необходимо подключить модуль syscall/js и использовать глобальный контекст для доступа к зарегистрированным символам в js через вызов Global() для получения доступа к объекту window. Например, для получения текущего адреса можно использовать следующее выражение.

js.Global().Get("location").Get("href")

Для вызова функций из Javascript или встроенных в браузер можно использовать метод Call от объекта (или от глобального контекста). Например, для вывода диалога уведомления можно использовать вызов:

js.Global().Call("alert", "Hello, WASM")

Для добавления элемента в DOM можно комбинировать вызовы, через использование объекта document:

document := js.Global().Get("document")
body := document.Call("querySelector", "body")
div := document.Call("createElement", "div")
div.Set("innerHTML", "Hello, WASM")
body.Call("appendChild", div)

Аналогично можно вызвать JS-функции из кода на Go. В html-файле в <script> добавим новую функцию log с одним параметром.

function log(s) {
  console.log('Log from JS: ['+s+']');
}

И выполним обращение к ней из кода на Go:

js.Global().Call("log", "Message from Go")

Также можно экспортировать функцию из Go и сделать ее доступной в Javascript, для этого через функцию Set регистрируется функция через js.FuncOf. Сигнатура функции в общем виде должна возвращать значение произвольного типа и принимать список аргументов и ссылку на себя:

func function_name(this js.Value, inputs []js.Value) interface{}

Например, таким образом можно обернуть функции работы с изображениями в Go и сделать примитивный генератор капчи на стороне клиента.

package main

import (
	"bytes"
	"encoding/base64"
	captcha "github.com/dchest/captcha"
	"syscall/js"
)

func generateCaptcha(this js.Value, inputs []js.Value) interface{} {
	id := captcha.New()
	img_buf := bytes.NewBufferString("")
	captcha.WriteImage(img_buf, id, 128, 64)
	return "data:image/png;base64,"+base64.StdEncoding.EncodeToString(img_buf.Bytes())
}

func main() {
	js.Global().Set("captcha", js.FuncOf(generateCaptcha))
	<-make(chan bool)
}

Чтение из пустого канала в последней строке main необходимо, чтобы избежать завершения кода на wasm с удалением всех зарегистрированных символов. Теперь в любом месте после вызова функции main (запуска приложения через go.run) мы можем обратиться к зарегистрированной функции и получить изображение капчи:

img = document.createElement("img");
img.src = captcha();
document.body.appendChild(img);

Если нам нужно передать сложную структуру (например, отправить одновременно изображение и голосовую капчу), то здесь мы встретимся с проблемой, что структуры напрямую в JS не отправляются (возникает ошибка panic: ValueOf: invalid value). Одним из путей решения проблемы может быть сериализация в JSON или иные формы упаковки структур в единый объект, например Protobuf).

Таким образом, мы можем использовать готовые библиотеки, разработанные для Go внутри нашего браузера. Но создавать сайты таким способом крайне сложно и неэффективно и равноценно использованию. Обратим свое внимание теперь на библиотеки для создания динамических веб-приложений на Go.

Библиотека Vecty

Vecty во многом схож с подходами реактивных пользовательских интерфейсов на React и предлагает похожую модель компонентов, зависящих от состояния. Каждый компонент использует структуру vecty.Core и реализует метод Render, формирующий дерево элементов с использованием оберток вокруг HTML-тэгов (в hexops/vecty/elem), а также любых других компонентов, созданных в приложении. При определении компонента могут быть заданы дополнительные параметры, определяющие его состояние. Важным отличием от React является необходимость уведомления об изменении состояния, через вызов метода Rerender (при этом может быть перестроено не все приложение, а только часть дерева).

Например, компонент может быть определен следующим образом:

type ScreenView struct {
	vecty.Core
	id int
}

func (p *ScreenView) Render() vecty.ComponentOrHTML {
	return elem.Div(
		vecty.Markup(
			vecty.Class("screen"+strconv.Itoa(p.id)),
			vecty.MarkupIf(powerOn && active == p.id, vecty.Class("selected"))),
		elem.Canvas(
			vecty.Markup(
				prop.ID("canvas"+strconv.Itoa(p.id)),
				vecty.Style("background", "black"),
				vecty.Property("width", strconv.Itoa(screenWidth)),
				vecty.Property("height", strconv.Itoa(screenHeight)),
			),
		),
	)
}

Здесь состояние определяется глобальной переменной powerOn и внутренним идентификатором id. В vecty.Markup могут использоваться следующие структуры:

  • prop.ID - изменить значение свойства id для тэга;

  • prop.Name - изменить значение свойства name для тэга;

  • vecty.Class - значение атрибута class для тэга;

  • vecty.MarkupIf - условная вставка значений (или тэга), будет выполнена только при истинности первого аргумента;

  • vecty.Style - для переопределения атрибута style тэга;

  • vecty.Property - изменение произвольного поля объекта, связанного с HTML-тэгом;

  • vecty.Attribute - изменение произвольного атрибута тэга;

  • vecty.Data - изменение data-атрибутов тэга;

  • vecty.EventListener - для регистрации обработчика событий;

  • vecty.UnsafeHTML - вставка произвольного HTML-фрагмента.

В дальнейшем созданные компоненты могут быть включены в другие компоненты.

type Screens struct {
	vecty.Core
}

func (p *Screens) Render() vecty.ComponentOrHTML {
	return elem.Div(vecty.Markup(vecty.Class("centered")),
		elem.Div(
			vecty.Markup(vecty.Class("screens")),
			&LeftButton{},
			&ScreenView{id: 0},
			&ScreenView{id: 1},
			&ScreenView{id: 2},
			&ScreenView{id: 3},
			&RightButton{},
		),
	)
}

При необходимости регистрации обработчика событий можно интегрировать в vecty.Markup структуру EventListener:

return elem.Data(vecty.Markup(
		vecty.Class("fa-button"),
		vecty.Class("right"),
		&vecty.EventListener{Name: "click", Listener: func(event *vecty.Event) {
      powerOn = !powerOn
      vecty.Rerender(emulator)
		}}), vecty.Text("\uF054"))

Важно, что обновление состояние может выполняться асинхронно внутри goroutine, например после выполнения длительного сетевого запроса или выполнения сложных и длительных алгоритмических задач в Go.

Для создания приложений доступна готовая обертка для использования Material Design Components https://github.com/vecty-components/material.

Среди альтернативных решений можно назвать Go-App (реализует очень похожую на Vecty модель переиспользуемых компонентов), Tango (использует подход, сходный с AngularJS), Vugu (близка по используемой модели Vue.js).

Пример кода приложения с использованием Vecty можно посмотреть в Github: https://github.com/AirCube-Project/emulator/.

Также хочу пригласить всех желающих на бесплатный урок по теме: "Функции и методы в языке golang". Регистрация доступна по этой ссылке.

А тут можно посмотреть запись урока "Структуры языка golang", который проходит прямо сейчас во время публикации данного материала.

Теги:
Хабы:
Всего голосов 15: ↑12 и ↓3 +9
Просмотры 9.3K
Комментарии 10
Комментарии Комментарии 10

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
51–100 человек
Местоположение
Россия
Представитель
OTUS