Привет, Хабр!

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 мая: Изучаем методы трассировки программ: метрики. Записаться

Комментарии (3)


  1. gudvinr
    04.05.2024 14:59
    +2

    А зачем это нужно, если официальный тулчейн может собрать .wasm?


  1. Octabun
    04.05.2024 14:59
    +6

    Горутины в GopherJS работают аналогично их работе в Go, с ними можно выполнять функции асинхронно

    "Аналогично" оставляет много простора для домыслов и интерпретаций. Что горутины работают иначе - выявлется первым же тестом. А как именно - я не разбирался. Отсюда вопрос

    В GopherJS и WASM горутины могут создавать потоки операционной системы?

    Это и важно и интересно. Потому что если могут, а это на первый взгляд значит запускать web worker и отображать на каналы, то круто весьма. А если не могут, то имя им - фейк.


  1. Turbine
    04.05.2024 14:59

    Это что-то для извращенцев? Иначе я не понимаю, зачем оно нужно?