Первоначально опубликовано на www.krakend.io Даниэлем Лопесом.
KrakenD - это API-шлюз, написанный на Go, который использует один файл конфигурации для определения всего своего поведения. Поскольку файл конфигурации может быть сложным, KrakenDesigner представляет собой пользовательский интерфейс на основе javascript для редактирования этого файла, и нам не хватало возможности воспроизводить непосредственно на javascript существующие каналы шлюза, чтобы пользователи могли запускать ручные тесты над конфигурацией редактирования. .
В этом посте мы собираемся объяснить, как мы включили компоненты инфраструктуры KrakenD в файл .wasm
и как мы интегрировали его в наш существующий SPA. Это наш код, работающий на javascript.
Давайте начнем!
Golang 1.11 и WebAssembly
В течение последних 3 лет популярность WebAssembly (и его обещания заменить JS) росла головокружительной скоростью.
На домашней странице проекта:
WebAssembly (сокращенно Wasm) - это двоичный формат инструкций для виртуальной машины на основе стека. Wasm разработан как переносимая цель для компиляции языков высокого уровня, таких как C / C ++ / Rust, что позволяет развертывать клиентские и серверные приложения в Интернете.
Последний выпуск языка golang поставляется с экспериментальным портом на WebAssembly.
Быстрая настройка среды
В этом проекте есть две зависимости, убедитесь, что обе правильно установлены в вашей среде.
Чтобы воспроизвести проделанные нами шаги, давайте начнем с чистого проекта Go. Скопируйте JS и HTML, уже включенные в ваш дистрибутив golang, в свой новый проект.
$ cd myproject/
$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.html" index.html
Теперь добавьте очень простой Makefile
, который будет загружать все зависимости и генерировать .wasm
файл:
all: prepare build prepare: dep ensure -v build: GOARCH=wasm GOOS=js go build -o test.wasm main.go
Запуск make
- это все, что нужно для создания test.wasm
файла, но чтобы ускорить среду разработки, мы также использовали два удобных инструмента:
- Reflex отслеживает изменения в
main.go
файле и автоматически перекомпилирует. - Goexec помогает нам запускать однострочники Go, которые мы используем для запуска локального веб-сервера.
Откройте новый терминал, перейдите в свой проект и запустите задачу reflex
:
$ reflex -r '^main.go$' make build
Мы создаем main.go
контент за минуту. Теперь при любых изменениях, происходящих в файле main.go, каждый раз выполняется make build
.
Во втором терминале перейдите в свой проект и создайте базовый локальный HTTP-сервер для обслуживания файлов index.html
, wasm_exec.js
и test.wasm
$ goexec 'http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))'
Пришло время проверить, все ли работает правильно. Создайте файл с именем main.go
со следующим содержимым:
package main import "fmt" func main() { fmt.Println("Hello, WebAssembly!") }
Откройте браузер по адресу http: // localhost: 8080 и нажмите кнопку run
. Вы должны найти сообщение Hello, WebAssembly!
в консоли.
Использование go libs
Когда наша среда готова, мы можем начать проникать внутрь. Итак, давайте создадим минимально необходимый код, чтобы предоставить функцию golang в пространство JS под названием parse
.
package main import ( "fmt" "syscall/js" ) func parse(i []js.Value) { fmt.Println(i) } func main() { fmt.Println("WASM Go Initialized") js.Global().Set("parse", js.NewCallback(parse)) select {} }
Вы можете протестировать это в консоли (после нажатия на кнопку «запустить»):
WASM Go Initialized
> parse("aaaa", {a:42,b:true}, ["one","two"]); [aaaa [object Object] one,two]
Поскольку мы собираемся использовать внешние зависимости, мы будем использовать ту же систему, что и во фреймворке KrakenD: dep
. При использовании версий, заблокированных во фреймворке в нашем WASM, возникают некоторые проблемы, поэтому вы можете создать файл с именем Gopkg.toml
со следующим содержимым:
[[override]] name = "github.com/mattn/go-isatty" revision = "3fb116b820352b7f0c281308a4d6250c22d94e27" [[override]] name = "github.com/gin-gonic/gin" branch = "master" [[override]] version = "v2.2.1" source = "github.com/go-yaml/yaml" name = "gopkg.in/yaml.v2" [[constraint]] name = "github.com/devopsfaith/krakend" branch = "master" [prune] go-tests = true unused-packages = true
С Gopkg.toml
в корне вашего проекта подготовьте его, набрав
make prepare
Самый простой способ создать KrakenD - использовать только фреймворк. Давайте изменим функцию parse
так, чтобы она выводила результат фактического парсера конфигурации KrakenD:
func parse(i []js.Value) { cfg, err := config.NewParserWithFileReader(func(s string) ([]byte, error) { return []byte(s), nil }).Parse(i[0].String()) if err != nil { fmt.Println("error:", err.Error()) return } fmt.Printf("%d endpoints parsed:\n", len(cfg.Endpoints)) fmt.Printf("%+v\n", cfg) }
Все идет нормально. У нас уже есть работающая среда и четкий способ портировать существующий код golang в JS.
Создание эфемерного экземпляра KrakenD
Прежде чем углубляться в привязки и переводы функций, необходимые для использования библиотек golang из пространства JS, нам понадобится некоторая оболочка над эфемерными экземплярами KrakenD, чтобы мы могли использовать их без запуска HTTP-сервера внутри браузера .
Все router
реализации фреймворка принимают RunServerFunc
и делегируют ему настройку сервера для раскрытия http.Handler
, содержащего экземпляр KrakenD. Итак, если мы хотим избежать создания экземпляра сервера и захватить обработчик, мы можем создать собственную реализацию client.RunServer
. Как видите, мы также улавливаем cancel
функцию контекста экземпляра, поэтому оболочка может предложить способ закрыть встроенный сервис KrakenD.
type LocalServer struct { close func() handler func(rw http.ResponseWriter, req *http.Request) } func newServer(cfg string) (*LocalServer, error) { // parse the received config string serviceConfig, err := config.NewParserWithFileReader(func(s string) ([]byte, error) { return []byte(s), nil }).Parse(cfg) if err != nil { return nil, err } serviceConfig.Debug = true // instantiate the framework logger logger, err := logging.NewLogger("DEBUG", os.Stdout, "[KRAKEND]") if err != nil { return nil, err } // create a context for the ephimeral instance ctx, cancel := context.WithCancel(context.Background()) s := &LocalServer{ // capture the context cancel function close: cancel, // empty handler to override at the RunServer func handler: func(rw http.ResponseWriter, req *http.Request) {}, } routerFactory := krakendgin.NewFactory(krakendgin.Config{ Engine: gin.New(), ProxyFactory: proxy.DefaultFactory(logger), Logger: logger, HandlerFactory: krakendgin.EndpointHandler, // RunServer just captures the handler and waits for a context cancelation RunServer: func(ctx context.Context, _ config.ServiceConfig, handler http.Handler) error { s.handler = handler.ServeHTTP <-ctx.Done() return ctx.Err() }, }) // start the service in a goroutine go func(ctx context.Context, serviceConfig config.ServiceConfig) { routerFactory.NewWithContext(ctx).Run(serviceConfig) }(ctx, serviceConfig) return s, nil }
Когда LocalServer
готов, мы можем создать простейшего потребителя / клиента с некоторой помощью пакета httptest
:
type Client struct { Server *LocalServer } func (c Client) Do(req *http.Request) *http.Response { req.Header.Add("js.fetch:credentials", "omit") rw := httptest.NewRecorder() c.Server.handler(rw, req) return rw.Result() } func (c *Client) Close() { c.Server.close() }
Обратите внимание, что мы добавляем заголовок js.fetch:credentials
, поэтому файлы cookie не отправляются вместе с запросами. Это особенность адаптера golang функции JS fetch
. Комментарии к исходному коду stdlib golang показывают возможные значения.
// jsFetchMode is a Request.Header map key that, if present, // signals that the map entry is actually an option to the Fetch API mode setting. // Valid values are: "cors", "no-cors", "same-origin", "navigate" // The default is "same-origin". // // Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters const jsFetchMode = "js.fetch:mode" // jsFetchCreds is a Request.Header map key that, if present, // signals that the map entry is actually an option to the Fetch API credentials setting. // Valid values are: "omit", "same-origin", "include" // The default is "same-origin". // // Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters const jsFetchCreds = "js.fetch:credentials"
И это все, что нам нужно, чтобы встроить экземпляр KrakenD в простой клиент. В качестве сноски, попытка избежать проверки браузера CORS доставит вам удовольствие от встречи со слоем CORB.
Добавление обратных вызовов
Поскольку нам не разрешено возвращать какие-либо значения из переданных функций, мы должны принять какую-то функцию обратного вызова как для успеха, так и для случая ошибки ... вы можете думать об этом, как если бы это был какой-то особый вид конструктора обещаний.
Помня об этом важном моменте, мы можем определить единственную функцию, которую мы добавим в DOM: parse
.
func parse(i []js.Value) { if len(i) < 2 { println("not enough args") return } if i[1].Type() != js.TypeFunction { println("arg 1 should be a function") return } logger := func(msg string) { fmt.Println(msg) } if len(i) > 2 { if i[2].Type() == js.TypeFunction { logger = func(msg string) { i[2].Invoke(msg) } } } client, err := newJSClient(i[0].String(), logger) if err != nil { logger(err.Error()) return } i[1].Invoke(client.Value()) }
Как видите, эта функция определяет регистратор, создает JS-клиент шлюза KrakenD и вызывает обратный вызов. Регистратор по умолчанию основан на функции fmt.Println
, но parse
также принимает третий параметр в качестве регистратора ошибок.
Функция newJSClient
создает адаптер JS поверх уже определенного Client
.
func newJSClient(cfg string, logger func(string)) (*JSClient, error) { server, err := newServer(cfg) if err != nil { return nil, err } return &JSClient{ client: &Client{server}, logger: logger, }, nil } type JSClient struct { client *Client logger func(string) }
Структура JSClient
имеет единственный экспортированный метод Value
, поэтому она может генерировать объект JS с привязками к методам Close
и Do
, предоставляемым Client
.
func (j *JSClient) Value() js.Value { opt := js.Global().Get("Object").New() opt.Set("close", js.NewCallback(j.close)) opt.Set("test", js.NewCallback(j.test)) return opt } func (j *JSClient) close(_ []js.Value) { j.client.Close() }
Самая важная часть JSClient
- метод test
, в котором запрос создается и отправляется в независимой горутине. После синтаксического анализа ответа в JS-объект горутина выполняет внедренный обратный вызов и завершает работу.
func (j *JSClient) test(i []js.Value) { // check if there are enough arguments if len(i) < 5 { j.logger("the test function requires at least 5 arguments: method, path, body, headers and a callback") return } // inject the body if present var reqBody io.Reader if b := i[2].String(); b != "" { reqBody = bytes.NewBufferString(b) } // create the http request req, err := http.NewRequest(i[0].String(), i[1].String(), reqBody) if err != nil { j.logger(err.Error()) return } // parse and add the injected headers headers := map[string][]string{} if err := json.Unmarshal([]byte(i[3].String()), &headers); err != nil { j.logger(err.Error()) return } req.Header = headers go func() { // dispatch the request to the client resp := j.client.Do(req) body, err := ioutil.ReadAll(resp.Body) if err != nil { j.logger(err.Error()) return } resp.Body.Close() // parse the response headers := js.Global().Get("Object").New() for k, v := range resp.Header { headers.Set(k, v[0]) } jsResp := js.Global().Get("Object").New() jsResp.Set("statusCode", resp.StatusCode) jsResp.Set("header", headers) jsResp.Set("body", string(body)) // invoke the injected callback i[4].Invoke(jsResp) }() }
Тестирование KrakenD на JS
Мы не являемся фронтенд-разработчиками, и наши навыки и знания в этой области очень ограничены. Вот почему мы подумали, что это должна быть самая трудоемкая часть проекта. Однако этого не было!
Нам просто нужно было добавить этот фрагмент в наш шаблон, и мы были готовы вызвать функцию parse
, создать клиента и протестировать нашу конфигурацию KrakenD (обратите внимание, что wasm было переименовано, и оба файла были помещены в папку wasm
):
<script src='wasm/wasm_exec.js'></script> <script> if (!WebAssembly.instantiateStreaming) { // polyfill WebAssembly.instantiateStreaming = async (resp, importObject) => { const source = await (await resp).arrayBuffer(); return await WebAssembly.instantiate(source, importObject); }; } var krakendClient = {}; var onKrakendClientReady = function() {}; var krakendClientReady = new Promise(function(resolve, reject) { onKrakendClientReady = resolve; }); const go = new Go(); let mod, inst; WebAssembly.instantiateStreaming(fetch("wasm/main.wasm"), go.importObject).then(async (result) => { mod = result.module; inst = result.instance; await go.run(inst); }); </script>
Сценарий также определяет некоторые глобальные объекты, чтобы остальная часть кода JS знала, когда код WASM загружен и доступен. Поскольку обратный вызов уже существует, мы можем вызвать его из кода wasm / go:
func main() { fmt.Println("WASM Go Initialized") js.Global().Set("parse", js.NewCallback(parse)) js.Global().Get("onKrakendClientReady").Invoke() select {} }
Последний шаг - ввести в SPA следующие фрагменты кода:
$rootScope.krakendPrepare = function() { if ( 'undefined' !== typeof krakendClient.close ) { krakendClient.close(); console.log('Resetting KrakenD client'); } var cfg = JSON.stringify($rootScope.service); krakendClientReady.then(function(){ parse(cfg, function(c) { krakendClient = c; console.log("KrakenD Client is ready"); }) }) }; $rootScope.runEndpoint = function(requestMethod, requestURL, requestBody, requestHeaders) { if ( 'undefined' === typeof requestBody ) { // GET or HEAD methods requestBody = ""; } if ( 'undefined' === typeof requestHeaders || requestHeaders == "" ) { // GET or HEAD methods requestHeaders = "{}"; } krakendClient.test(requestMethod, requestURL, requestBody, requestHeaders, console.log); };
Отображение возвращаемых значений - это просто вопрос замены console.log
на имя функции, ответственной за привязку свойств ответа к полям представления.
Вывод
С очень небольшим дополнительным кодом мы встроили наш основной шлюз KrakenD в браузер! Мы можем сделать то же самое практически с любой существующей кодовой базой golang, если мы будем следовать урокам, полученным в этом эксперименте:
- поскольку код WASM загружается асинхронно, а основная функция блокируется пустым
select
, нам нужно уведомить код JS, когда WASM готов (перед тем, как войти в бесконечное ожидание). - Функции golang, перенесенные на WASM, ничего не возвращают. Они должны принимать обратные вызовы, если есть что передать / вернуть вызывающему.
- HTTP-запросы должны выполняться в специальной горутине. Выполнение той же горутины, что и вызывающая программа, приводит к возникновению тупиковых ситуаций.
- HTTP-запросы GET и HEAD должны иметь нулевое тело.
- преобразование типов между JS и Go сложно. Ручные преобразования очень подробны.
Вы можете увидеть это в действии на https://designer.krakend.io
Спасибо за чтение! Если вам понравился наш продукт, не забудьте поставить наш проект звездой!