Пишем frontend на golang
Ой, все чудесится и чудесится!
— Л. Кэррол, Алиса в Стране Чудес
Вас задрало, что node_modules на простом сайте соревнуются по количеству используемого места с вашей коллекцией музыки?
Вы перечитали инструкцию к Redux в шестидесятый раз и поняли две вещи: "До меня кажется доходит..." и "Думаю, мне стоит перечитать это ещё раз!"
Вы в очередной раз узнали, что 1 + "1" == "11", а [] - {} == NaN?
Билд скрипт в webpack занимает больше места чем ваша библиотека на javascript?
Тогда заходите под кат, я покажу вам, как можно перевести ваш фронтэнд на го.
Встречаем, vugu. Молодая (сразу предупреждаю, не релизнутая) и очень интересная библиотека, которая позволяет вам использовать golang напрямую в html. Естественно, так как пока не существует браузеров со встроенной поддержкой golang, то реализовывать всё пришлось через WASM.
Vugu это очень молодая библиотека и упоминаний о ней на хабре я не нашёл, за исключением пары дайджестов.
Давайте посмотрим и потрогаем эту библиотеку изнутри. И так, что же такое vugu?
Для начала, давайте представим, что вы пишите html компонент, только скрипты имеют тип application/x-go вместо javascript:
<div>
<p vg-if='c.ShowText'>
Conditional text here.
</p>
</div>
<script type="application/x-go">
type Root struct { // component for "root"
ShowText bool `vugu:"data"`
}
</script>
Вы сохраняете вышеописанное безобразие в файл с расширением *.vugu и запускаете стороннюю библиотеку, которая жрёт 5 гигов памяти. Ладно, шучу, на самом деле vugu был создан с целью упростить процесс разработки, вместо того, чтобы его усложнить. Всё что вам необходимо делать в vugu можно сделать средствами самого go. Для компиляции приложения можно воспользоваться простыми:
go generate
go build
./file-name
В комплекте идёт утилита, для облегчения разработки, под названием vgrun, но это просто обёртка над стандартными командами. Для простоты иллюстраций я буду использовать именно эту библиотеку.
vgrun devserver.go
запустит простой сервер, и начнёт следить за файлами на предмет изменений. Если оные находятся, то программа автоматически перезапускает сервер и обновляет приложение. Просто и без наворотов.
Что же, пришло время проследить путь vugu файла до конечного пользователя.
Файл парсится html парсером. Да, файл должен содержать полностью рабочий HTML.
После этого файл ещё раз парсится vugu парсером. Тут файл разбирается на части и пересобирается в go. Сгенерированный код это полностью рабочий код на golang.
Полученный файл компилируется в WASM и упаковывается в web assembly для запуска на клиенте.
PROFIT!
Ну вот и всё. Можете расходиться. Тут всё понятно.
Что? Надо больше? Ладно, так уж и быть. Давайте закапываться глубже и делать больше. Давайте для начала посмотрим на сгенерированный golang файл. Файл называется 0_components_vgen.go. В него и пойдём.
Код из <script type="application/x-go">
из vugu переносится в golang без каких либо вопросов и изменений. Приятно. В дополнение к этому, в файле создаётся функция Build, которая генерирует HTML интерфейс.
func (c *Root) Build(vgin *vugu.BuildIn) (vgout *vugu.BuildOut) {
vgout = &vugu.BuildOut{}
var vgiterkey interface{}
_ = vgiterkey
var vgn *vugu.VGNode
vgn = &vugu.VGNode{Type: vugu.VGNodeType(3), Namespace: "", Data: "div", Attr: []vugu.VGAttribute{vugu.VGAttribute{Namespace: "", Key: "class", Val: "demo"}}}
vgout.Out = append(vgout.Out, vgn) // root for output
{
vgparent := vgn
_ = vgparent
vgn = &vugu.VGNode{Type: vugu.VGNodeType(1), Data: "\n "}
vgparent.AppendChild(vgn)
vgn = &vugu.VGNode{Type: vugu.VGNodeType(3), Namespace: "", Data: "button", Attr: []vugu.VGAttribute(nil)}
vgparent.AppendChild(vgn)
vgn.DOMEventHandlerSpecList = append(vgn.DOMEventHandlerSpecList, vugu.DOMEventHandlerSpec{
EventType: "click",
Func: func(event vugu.DOMEvent) { c.HandleCat(event) },
// TODO: implement capture, etc. mostly need to decide syntax
})
Выглядит запутанно, как и любой сгенерированный код, но если присмотреться, то можно запросто увидеть что это просто наш HTML, созданный программно.
После всего, vugu собирает все компоненты вашего сайта в единый программный блок (а можно и не в единый) и отправляет всё это на клиент. Библиотека в состоянии отслеживать DOM и DOMEvents для того, чтобы передавать события от HTML компонентов в ваш код на golang.
Ну вот. Всё достаточно просто. На самом деле, всё очень просто. vugu построена на этом подходе. Vugu - это не фреймворк. Это библиотека, которую вы можете использовать в некоторых частях вашего проекта. Вы можете программно вызывать рендер определённых компонентов там, где вам это нужно. Вам не придётся зависеть от create-react-app или чего-то ещё. Всё очень легковесно.
Ну что же, хватит болтовни, давайте напишем простенькое приложение, чтобы показать, что можно и чего нельзя делать с помощью vugu.
Для начала сделаем:
go get -u github.com/vugu/vgrun
vgrun -install-tools
После этого можно создать проект из темплейта:
vgrun -new-from-example=simple .
vgrun devserver.go
Если в этот момент у вас вылетит пара ошибок о том, что у вас не достаёт каких-либо модулей, следуйте инструкциям и запустите go get. Последняя версия golang не признаёт зависимостей в go.mod и вам придётся загрузить их руками.
vscode имеет функцию подсветки синтаксиса для vugu. Приятный плюс. Устанавливаем эту подсветку и начинаем писать наш root.vugu.
Для простоты душевной напишем программу, которая будет показывать фотографию котиков. Интернет ведь создавался для котиков, так ведь?
В начале каждого vugu файла находится HTML разметка компонента.
<div class="demo">
<button @click="c.HandleCat(event)">Get a cat!</button>
<div vg-if='c.IsLoading'>Loading...</div>
<div vg-if='len(c.Cats) > 0'>
<div vg-for='c.Cats'>
<img :src='value.URL' alt="cat"></img>
</div>
</div>
</div>
Тут всё достаточно просто. И в принципе, понятно для любого человека, который работал с vue.js. Для тех, кто не работал с vue, разобраться не составит большого труда.
События определяются с помощью @
. @click
, например, это ваш обработчик события, который запустится по нажатию на кнопку. Самое приятное, сюда можно запихнуть функцию или напрямую писать golang код.
Вы можете показывать определённый контент используя аттрибут vg-if
.
<div vg-if='c.IsLoading'>Loading...</div>
Соответственно Loading... будет показан только когда переменная IsLoading равняется true (Откуда взялся этот "с" я объясню попозжее). Сюда тоже можно запихивать любой golang код, как видно на следующей строке.
После этого мы будем использовать vg-for
для того, чтобы сгенерировать вывод для каждого элемента в коллекции.
Ну и на закуску, если вы добавляете двоеточие в начале HTML атрибута, то значение этого атрибута будет взято из golang кода.
Дальше у нас начинаются чудеса и самая интересная часть программы.
<script type="application/x-go">
import (
"encoding/json"
"net/http"
"log"
)
type Root struct {
IsLoading bool `vugu:"data"`
Cats []Cat `vugu:"data"`
}
type Cat struct {
ID string `json:"id"`
URL string `json:"url"`
Width int `json:"width"`
Height int `json:"height"`
}
Здесь я определяю две структуры, Cat, это для поддержки котиков в API и Root, основной структуры в программе. Название этой структуры должно совпадать с названием файла, и она должна быть экспортирована (начинаться с заглавной буквы). Поля этой структуры должны быть отмечены тэгом `vugu:"data"`
. Всё отмеченное этим тегом будет доступно в нашем HTML коде через переменную с.
Root это название нашего компонента. Root это специальный компонент в vugu. Маунт-поинт вашего приложения начинается с Root.
Количество vugu файлов не ограничено. Создавайте столько компонентов, сколько душе угодно. Если вам приспичило назвать что-то двумя словами, то файл должен называться: koshki-sobachki.vugu а основной тип в этом файле KoshkiSobachki.
Раутинг будет создан автоматически, основываясь на называниях компонентов. Соответственно вышеописанный компонент будет создан и смонтирован по адресу /KoshkiSobachki. Хотя, опять же, vugu не очень любит всю эту магию и синтаксический сахар. Весь раутинг можно переопределить вручную.
Ладно, давайте закончим писать наш простой сайт.
func (c *Root) HandleCat(event vugu.DOMEvent) {
ee := event.EventEnv()
go func() {
ee.Lock()
c.IsLoading = true
ee.UnlockRender()
client := &http.Client{}
req, _ := http.NewRequest("GET", "https://api.thecatapi.com/v1/images/search?limit=3&size=full", nil)
req.Header.Set("x-api-key", "710c211b")
res, err := client.Do(req)
if err != nil {
log.Printf("Error fetching: %v", err)
return
}
defer res.Body.Close()
var newcat []Cat
err = json.NewDecoder(res.Body).Decode(&newcat)
if err != nil {
log.Printf("Can't unmarshal the json: %v", err)
return
}
ee.Lock()
defer ee.UnlockRender()
c.Cats = newcat
c.IsLoading = false
}()
}
Код достаточно прост. Идём на https://thecatapi.com, регистрируемся, получаем бесплатный API-Key и грузим его в код. (Не бойтесь, ключ в этом примере не валидный, так что вы не сможете угнать у меня мой любимый сервис генерации котиков). Две вещи, которые надо здесь упомянуть это:
Код обработчика события напрямую запускает goroutine и не блокирует сам обработчик событий.
В коде goroutine мы будем использовать EventEnvironment для того, чтобы синхронизировать доступ к данным. Перед тем, как вы обновляете поля структуры с вам необходимо вызвать Lock() а сразу после UnlockRender().
ddНу и запустить всё это дело на локальном девсервере:
По нажатию на кнопку, мы получаем json с тремя объектами, содержащими ссылки на котиков. Парсим этот json в массив типа Cat, который мы создали заранее. Сохраняем эти данные обратно в переменную Cats в структуре с. Попутно меняем значение переменной IsLoading для того, чтобы отобразить и скрыть div который оповещает о загрузке.
Проверяем:
Замечательно, всё работает.
Давайте погрузимся в некоторые детали vugu.
Главная деталь — ничего не происходит автоматически и без вашего участия. Такова позиция главного разработчика vugu. Всё что вы видите на экране происходит по вашему велению.
Посему, например, CSS, объявленный в vugu файлах будет просто вставлен в ваш HTML. Никаких примочек. Ничего не будет переименовано, и если вы напишите .header
в двух разных модулях, у вас произойдёт коллизия стилей. Так что аккуратно.
Компонент, написанный единожды можно использовать внутри других компонентов (так же как и в React и Blazor). Компоненты могут находиться в четырёх состояниях:
Init(ctx vugu.InitCtx) компонент создан, но ещё не успел повидать жизнь и выпить пивка.
Compute(ctx vugu.ComputeCtx) компонент скоро увидит свет. Пересчитываем переменные, обновляем значения.
Rendered(ctx vugu.RenderedCtx) по компоненту как следует прошлись и отрендерили по самые помидоры.
Destroy(ctx vugu.DestroyCtx) компонент никому не сдался, и ему пора на свалку. Выгорание, что ещё сказать.
Всё достаточно просто. Все компоненты это просто набор структур с тэгами `vugu:"data"`. Таких структур может быть много.
Приведу ещё примеров с сайта создателя vugu:
<!-- root.vugu -->
<div class="root">
<ul>
<main:MyLine FileName="example.txt" :LineNumber="rand.Int63n(100)" ></main:MyLine>
</ul>
</div>
<script type="application/x-go">
import "math/rand"
</script>
<!-- my-line.vugu -->
<li class="my-line">
<strong vg-content='c.FileName'></strong>:<span vg-content='c.LineNumber'></span>
</li>
<script type="application/x-go">
type MyLine struct {
FileName string vugu:"data"
LineNumber int vugu:"data"
}
</script>
В этом примере вы можете видеть, как просто создавать и использовать компоненты в vugu.
Ну и напоследок, wasm файл, который получается на выходе, весит 7 метров. Это на порядок лучше, чем то, что выдаёт Blazor, но мы можем пойти глубже.
Что если я скажу, что вы можете запустить ваш проект написанный на vugu в tinygo? Именно это я вам и говорю.
Идём на https://www.vugu.org/doc/tinygo и запускаем билд либо через докер, либо с помощью синей изоленты и кузькиной матери. На выходе мы получаем замечательный wasm файл, который весит 500килобайт. Ура! Всем по кошаку! Получайте, сколько хотите!
Ладно, хватит разглагольствовать, идём и читаем официальную документацию на https://www.vugu.org/doc.
После этого можно идти и читать намного более объёмную документацию на https://pkg.go.dev/github.com/vugu/vugu/.
Я связался с автором проекта и проверил, проект не запущен, хотя документация устарела. Я лично предложил свою помощь в обновлении документации и развитии проекта, так что vugu в массы. Но, несмотря на это, вот вам официальное заявление:
Проект, всё же, находится в режиме тестирования. Например, последняя версия tinigo сломала компиляцию wasm кода. Так что пользуйтесь на свой страх и риск. Но, пользуйтесь. Это весело.
Если у кого-то есть вопросы — создавайте issue, делайте PR или пишите мне в личку, я могу написать создателям в Slack.
Удачного всем погружения в vugu!