Go Wasm на прикладі створення інструменту для розробників
Привіт, у 2020 році я активно використовував онлайн-інструмент JSON-to-Go, а також на його основі зробив JSON-to-Proto.
Згідно зі статистикою від similarweb.com, JSON-to-Proto відвідують 5000+ разів на місяць, найактивніше з США, Індії та Китаю.
Заради цікавості вирішив глянути, чи потрібен комусь XML-to-Proto та створив відповідну сторінку, а також глянув існуючі інструменти XML-to-Go:
Обидва інструменти роблять перетворення на стороні сервера через API. onlinetool.io/xmltogo — це обгортка над бібліотекою github.com/miku/zek, а тому б було цікаво підключити бібліотеку через Wasm та прибрати виклик до сервера.
Підключити вдалось, про це й буде ця маленька стаття.
Каркас XML-to-Go
Структура інструменту складається з трьох елементів, поля вводу input, поля виводу output та функції, яка перетворює XML в Go xmlDataToGoTypeCode:
<div id="input" contenteditable></div> <div id="output"></div>
const $input = document.getElementById("input");
const $output = document.getElementById("output");
// буде замінена на Wasm
globalThis.xmlDataToGoTypeCode = function (input: string): string {
return "";
};
$input.addEventListener("keyup", function () {
$output.innerHTML = globalThis.xmlDataToGoTypeCode($input.innerText.trim());
});Далі нам потрібно реалізувати функцію xmlDataToGoTypeCode на Go та скомпілювати у Wasm.
Функція, яка перетворює XML в Go
Як писав раніше, інструмент onlinetool.io/xmltogo під капотом використовує бібліотеку github.com/miku/zek, яка написана на Go та має потрібний нам функціонал для перетворення XML в типи на Go.
Щоб зрозуміти, як використовувати бібліотеку github.com/miku/zek, треба розібрати CLI команду cmd/zek/main.go (permalink), я це вже зробив, тому продовжуйте читати статтю.
Функція xmlDataToGoTypeCode:
cat ./go/wasm/xml2go.go
package main
import (
"bytes"
"fmt"
"go/format"
"strings"
"time"
"github.com/miku/zek"
)
func xmlDataToGoTypeCode(content string, inline, compact, withJSON bool) string {
var rootNode = new(zek.Node)
_, err := rootNode.ReadFrom(strings.NewReader(content))
if err != nil {
// fmt print error
return ""
}
var (
buffer = new(bytes.Buffer)
sw = zek.NewStructWriter(buffer)
)
_ = inline // @TODO sw.Inline = inline after https://github.com/miku/zek/issues/14
sw.Compact = compact
sw.WithJSONTags = withJSON
err = sw.WriteNode(rootNode)
if err != nil {
// fmt print error
return ""
}
source, err := format.Source(buffer.Bytes())
if err != nil {
// fmt print error
return ""
}
return string(source)
}Компіляція Go Wasm
Функцію xmlDataToGoTypeCode потрібно зробити доступною для використання в JavaScript:
tree -h ./go/wasm/
./go/wasm/ ├── [ 551] main.go └── [1.1K] xml2go.go
cat ./go/wasm/main.go
package main
import (
"fmt"
"syscall/js"
)
func main() {
fmt.Println("Golang WebAssembly main")
// globalThis.xmlDataToGoTypeCode = function () {}
js.Global().Set("xmlDataToGoTypeCode", js.FuncOf(xmlDataToGoTypeCodeWasmWrapper))
done := make(chan struct{})
<-done
}
func xmlDataToGoTypeCodeWasmWrapper(this js.Value, args []js.Value) interface{} {
var (
content = args[0].String()
inline = args[1].Bool()
compact = args[2].Bool()
withJSON = args[3].Bool()
)
return xmlDataToGoTypeCode(content, inline, compact, withJSON)
}Компіляція:
GOOS=js GOARCH=wasm go build -o ./static/js/wasm/xml-to-go.wasm ./go/wasm/*.go
tree -h ./static/js/wasm
./static/js/wasm └── [4.5M] xml-to-go.wasm
Отже, маємо скомпільований файл xml-to-go.wasm, який важить 4.5 мегабайти.
Підключення Go Wasm в JavaScript
Для виконання Wasm потрібно підключити файл wasm_exec.js. В офіційній документації це виглядає так:
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
<html>
<head>
<meta charset="utf-8"/>
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
go.run(result.instance);
});
</script>
</head>
<body></body>
</html>Клас Go, go.importObject та go.run прописані у файлі wasm_exec.js:
cat wasm_exec.js
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
"use strict";
(() => {
// ...
globalThis.Go = class {
constructor() {
// ...
this.importObject = {
go: {
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
// ...
},
// ...
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
// ...
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
// ...
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
// ...
}
}
})();Зробимо так само:
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./src/wasm/
import "./wasm/wasm_exec"
const $input = document.getElementById("input");
const $output = document.getElementById("output");
const $inline = document.getElementById("inline") as HTMLInputElement;
const $withJSON = document.getElementById("with-json-tags") as HTMLInputElement;
const $compact = document.getElementById("compact") as HTMLInputElement;
$input.addEventListener("keyup", function () {
$output.innerHTML = globalThis.xmlDataToGoTypeCode(
$input.innerText.trim(),
$inline.checked,
$withJSON.checked,
$compact.checked,
);
});
// Go from ./wasm/wasm_exec
const go = new globalThis.Go();
WebAssembly
.instantiateStreaming(fetch("/static/js/wasm/xml-to-go.wasm"), go.importObject)
.then(function (result) {
go.run(result.instance);
});Весь код доступний в цьому репозиторії.
Епілог
WebAssembly — потужна технологія, є навіть приклади WebAssembly Go Playground:
- The WebAssembly Go Playground (47.18 MB / 11.95 MB transferred);
- go-playground-wasm.vercel.app (4.37 MB / 1.29 MB transferred).
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.
4 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів