Библиотека GopherJS в Golang
Привет, Хабр!
GopherJS позволяет переводить Go-код в JavaScript — он предоставляет полноценную совместимость с большинством пакетов стандартной библиотеки Go. Также Gopher поддерживает горутины и каналы!
В статье в общих деталях рассмотрим эту замечательную библиотеку.
Установим:
go get -u github.com/gopherjs/gopherjs
Основы работы
gopherjs build
— с этой командой можно компилировать Go-код в JavaScript. Она аналогична команде go build
, но вместо создания исполняемого файла Go она генерирует файл .js
. Например:
package main
import "github.com/gopherjs/gopherjs/js"
func main() {
js.Global.Get("document").Call("write", "Привет, Хабр!")
}
Для компиляции этого файла в JavaScript:
gopherjs build main.go
Это создаст файл main.js
, который можно подключить к HTML-странице.
gopherjs install
аналогична gopherjs build
, но вместо сохранения файла JavaScript в текущем каталоге, она устанавливает его в $GOPATH/bin
или $GOBIN
.
syscall/js
позволяет Go-коду взаимодействовать с JavaScript объектами и функциями в браузере.
Допустим, хочется изменить содержимое элемента на веб-странице. Сначала нужно получить доступ к этому элементу:
package main
import (
"syscall/js"
)
func main() {
// получение доступа к элементу DOM
document := js.Global().Get("document")
header := document.Call("getElementById", "header")
// изменение текста элемента
header.Set("innerHTML", "Новый заголовок")
}
Код на Go скомпилируется с GopherJS и изменит содержимое элемента с идентификатором header
на «Новый заголовок»:
Можно обрабатывать события DOM, используя функции обратного вызова:
package main
import (
"syscall/js"
)
func main() {
document := js.Global().Get("document")
button := document.Call("getElementById", "myButton")
clickFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
js.Global().Get("console").Call("log", "Кнопка нажата!")
return nil
})
button.Call("addEventListener", "click", clickFunc)
defer clickFunc.Release() // очистка памяти
}
В примере обработчик события добавляется к кнопке, и при каждом нажатии кнопки в консоль будет выводиться сообщение «Кнопка нажата!».
Можно создать простую анимацию с помощью GopherJS, где к примеру, элемент будет двигаться по горизонтали вправо и влево на страничке:
package main
import (
"syscall/js"
"math"
"time"
)
func main() {
window := js.Global()
document := window.Get("document")
body := document.Get("body")
// создаем элемент div и добавляем его в тело документа
div := document.Call("createElement", "div")
div.Set("innerHTML", "Анимированный блок")
div.Get("style").Set("position", "absolute")
div.Get("style").Set("top", "40px")
div.Get("style").Set("width", "100px")
div.Get("style").Set("height", "100px")
div.Get("style").Set("backgroundColor", "red")
body.Call("appendChild", div)
startTime := time.Now()
// функция для обновления позиции элемента
var updatePosition js.Func
updatePosition = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// вычисляем прошедшее время
elapsed := time.Since(startTime).Seconds()
// вычисляем новую позицию, используя функцию синуса для создания движения влево и вправо
left := 150 + 100*math.Sin(elapsed)
// обновляем позицию элемента
div.Get("style").Set("left", js.ValueOf(left).String()+"px")
// запрашиваем следующий кадр анимации
window.Call("requestAnimationFrame", updatePosition)
return nil
})
// запускаем анимацию
window.Call("requestAnimationFrame", updatePosition)
defer updatePosition.Release() // Освобождаем ресурсы
}
Интеграция с React или Angular
Основная идея в такой интеграции будет состоять в том, чтобы написать код на Go, который GopherJS скомпилирует в JS, способный взаимодействовать с React или Angular. Рассмотрим, как это можно сделать на примере React.
Для этого будем юзать пакетmyitcv.io/react
:
package main
import (
"github.com/gopherjs/gopherjs/js"
"myitcv.io/react"
)
type HelloMessage struct {
react.ComponentDef
}
func (h *HelloMessage) Render() *js.Object {
return react.JSX("div", nil, "Hello ", h.Props().Get("name"))
}
func main() {
js.Global.Set("HelloMessage", react.CreateFactory(new(HelloMessage)))
}
В HTML файле можно использовать этот компонент, как обычный React-компонент после того, как код скомпилирован GopherJS:
React with GopherJS
Далее компилируем код с помощью GopherJS видим результат.
Насчет горутин и каналов
Горутины в GopherJS работают аналогично их работе в Go, с ними можно выполнять функции асинхронно:
package main
import (
"github.com/gopherjs/gopherjs/js"
"time"
)
func printNumbers() {
for i := 1; i <= 5; i++ {
js.Global.Get("console").Call("log", i)
time.Sleep(time.Millisecond * 300)
}
}
func main() {
go printNumbers()
for i := 6; i <= 10; i++ {
js.Global.Get("console").Call("log", i)
time.Sleep(time.Millisecond * 300)
}
}
Функция printNumbers
запустится как горутина.
Каналы в GopherJS используются для синхронизации и обмена данными между горутинами:
package main
import (
"github.com/gopherjs/gopherjs/js"
)
func sendToChannel(ch chan int) {
for i := 1; i <= 5; i++ {
ch <- i
js.Global.Call("setTimeout", js.MakeFunc(func(this js.Value, args []js.Value) interface{} {
return nil
}), 300)
}
close(ch)
}
func main() {
ch := make(chan int)
go sendToChannel(ch)
for value := range ch {
js.Global.Get("console").Call("log", value)
}
}
Функция sendToChannel
отправляет числа от 1 до 5 в канал, а основная функция main
читает эти значения. Закрытие канала после отправки всех значений гарантирует, что цикл в main
завершится после получения всех данных.
Синхронизация горутин может быть также выполнена с использованием sync.WaitGroup
, который позволяет дождаться завершения работы всех горутин:
package main
import (
"github.com/gopherjs/gopherjs/js"
"sync"
)
func performTask(id int, wg *sync.WaitGroup) {
defer wg.Done()
js.Global.Get("console").Call("log", "Task", id, "started")
js.Global.Call("setTimeout", js.MakeFunc(func(this js.Value, args []js.Value) interface{} {
js.Global.Get("console").Call("log", "Task", id, "completed")
return nil
}), 1000*id)
}
func main() {
var wg sync.WaitGroup
wg.Add(3)
for i := 1; i <= 3; i++ {
go performTask(i, &wg)
}
wg.Wait()
js.Global.Get("console").Call("log", "All tasks completed")
}
Тесты
Можно использовать QUnit вместе с GopherJS QUnit bindings. После написания тестов можно автоматизировать их выполнение с помощью Agouti, она позволяет автоматом открывать браузер, переходить на нужную страницу с тестами и анализировать результаты.
Пример:
package main
import (
"github.com/sclevine/agouti"
"log"
)
func main() {
driver := agouti.ChromeDriver()
if err := driver.Start(); err != nil {
log.Fatalf("Failed to start driver: %v", err)
}
page, err := driver.NewPage()
if err != nil {
log.Fatalf("Failed to open page: %v", err)
}
if err := page.Navigate("http://localhost:10000"); err != nil {
log.Fatalf("Failed to navigate: %v", err)
}
// проверяем элементы на странице, используя Agouti методы
passed, err := page.Find("#test-result").Text()
if err != nil {
log.Fatalf("Failed to find test results: %v", err)
}
log.Println("Test passed:", passed)
}
Для модульного тестирования GopherJS кода, можно юзать дефолтный пакет testing
в Go. Пишем тесты так же, как и для обычных Go приложений, но запускаем их через GopherJS:
package main
import "testing"
func TestExample(t *testing.T) {
expected := "Hello, GopherJS"
result := MyFunction()
if result != expected {
t.Errorf("Test failed, expected '%s', got '%s'", expected, result)
}
}
Затем этот тест можно запустить с помощью команды gopherjs test
.
Подробней с библиотекой можно ознакомиться здесь.
В рамках курса OTUS «Golang Developer. Professional» пройдут открытые уроки, присоединяйтесь:
16 мая: Классические ошибки при собеседовании на позицию middle+ Go-разработчика. Записаться
23 мая: Изучаем методы трассировки программ: метрики. Записаться