Привет, Хабр!
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:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>React with GopherJS</title> </head> <body> <div id="app"></div> <script src="your_compiled_gopherjs_script.js"></script> <script> ReactDOM.render( React.createElement(HelloMessage, {name: "World"}), document.getElementById('app') ); </script> </body> </html>
Далее компилируем код с помощью 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 мая: Изучаем методы трассировки программ: метрики. Записаться
