Лучший опыт

WebAssembly с Go: вывод веб-приложений на новый уровень.

В сообществе разработчиков участились обсуждения WebAssembly, или WASM. Потенциал этой технологии огромен, для нашего Open Source проекта  —  инфраструктуры Permify, с помощью которой разработчики создают и управляют детализированными разрешениями приложений,  —  она просто бесценна. Расскажем, почему и как мы интегрировали WASM в нашу интерактивную среду и получили преимущества от использования Golang. Каков функционал этой среды? Если вкратце, ?
WebAssembly с Go: вывод веб-приложений на новый уровень...

В сообществе разработчиков участились обсуждения WebAssembly, или WASM. Потенциал этой технологии огромен, для нашего Open Source проекта  —  инфраструктуры Permify, с помощью которой разработчики создают и управляют детализированными разрешениями приложений,  —  она просто бесценна.

Расскажем, почему и как мы интегрировали WASM в нашу интерактивную среду и получили преимущества от использования Golang.

Каков функционал этой среды? Если вкратце, это интерактивный модуль Permify, которым создаются и тестируются модели авторизации.

Содержание статьи:

  1. Краткое описание WASM и преимуществ его применения с Go.
  2. Что способствовало нашему решению интегрировать WASM в Permify?
  3. Реализация WASM, в том числе:
  • быстрая подготовка, реализация WASM с Go;
  • подробная разбивка кода WASM в Permify;
  • фронтенд, этапы внедрения Go WASM в приложение на React.

К концу у вас должно сложиться четкое представление о том, почему и как мы использовали возможности WASM для проекта Permify.

Понятие о WebAssembly

WebAssembly, или Wasm,  —  это важнейшая технология быстрого и эффективного выполнения кода в веб-браузерах, надежный мост между веб-приложениями и высокой производительностью, характерной обычно для нативных приложений.

1. Возможности WebAssembly

Wasm  —  это низкоуровневая виртуальная машина, выполняющая компактный двоичный код, преобразованный с высокоуровневых языков.

Основные преимущества:

  • Благодаря поддержке основных браузеров, Wasm обеспечивается стабильная производительность на различных платформах.
  • Значительно повышается реактивность веб-приложений, так как в Wasm двоичный код выполняется со скоростью нативных.

Мы приняли стратегическое решение включить Golang в фундаментальную основу Open Source проекта Permify, выбор этот обусловлен широким признанием статической типизации, конкурентного выполнения и оптимизации производительности Golang. А когда мы создали интерактивную среду Permify, обратили внимание на WebAssembly как важнейший элемент.

2. Сочетание Go и WebAssembly

  • Характеристики Go: благодаря оптимальной производительности и возможностям конкурентного выполнения Go обзавелся солидной репутацией в сообществе разработчиков.
  • Синергия с WebAssembly: код Go преобразуется в WebAssembly, и разработчики эффективно используют надежную производительность и управление параллелизмом Go прямо в браузере, создавая мощные, эффективные, масштабируемые веб-приложения.

Но объединением Go и WebAssembly мы не ограничимся: определим причины выбора Wasm технологией разработки интерактивной среды Permify и выгоды этого решения.

Почему WebAssembly?

Создав интерактивную среду Permify, мы задались вопросом: «Как продемонстрировать наши возможности, не опутываясь трудностями и проблемами сопровождения традиционных серверных архитектур?» Блестящим ответом стал WebAssembly.

Перейдя на этот двоичный формат инструкций, мы:

  • Работаем с интерактивной средой Permify прямо в браузере, избегая накладных расходов на сопровождение сервера и повторных API-вызовов, упрощая при этом текущее сопровождение по сравнению со старыми, серверными подходами.
  • Достигаем максимальной производительности, по которой приложения Go благодаря WebAssembly сопоставимы с нативными, совершенствуя пользовательское взаимодействие время отклика.

Технические преимущества и обратная связь

Применяя WebAssembly в интерактивной среде Permify, мы получили ощутимые технические преимущества и поддержку сообщества.

  • Быстрое выполнение: избегая взаимодействий с сервером и развертывая WebAssembly в браузере, мы обеспечили ультрамалое время отклика.
  • Простой пользовательский интерфейс: сосредоточив интерактивную среду в браузере, мы устранили сложности, связанные с многоинструментальными рабочими процессами, обеспечили четкое и понятное пользовательское взаимодействие.
  • Подтверждение сообщества: позитивная обратная связь и принятие сообществом разработчиков  —  это признание наших технологических решений и реализаций.

Рассмотрим подробнее технические нюансы, обратную связь и выводы, сделанные из тщательного анализа нашей работы с WebAssembly.

Реализация WASM с Go

Прежде чем изучать применение WebAssembly и Go в Permify, рассмотрим их сочетание в примере приложения. Поэтапно объединим их, предваряя глубокое погружение в реализацию Permify.

1. Преобразование Go в WebAssembly

  • Этапы:
  1. Сначала задаем целевую платформу компиляции WebAssembly в Go:
GOOS=js GOARCH=wasm go build -o main.wasm main.go

2. Затем применяем оптимизации, уменьшая размер файла и повышая производительность:

wasm-opt main.wasm --enable-bulk-memory -Oz -o play.wasm
  • Обработка событий:

Реакция функции Go на нажатие кнопки на веб-странице:

package main  import "syscall/js"  func registerCallbacks() { js.Global().Set("handleClick", js.FuncOf(handleClick)) }  func handleClick(this js.Value, inputs []js.Value) interface{} { println("Button clicked!") return nil }

И в HTML после загрузки модуля WebAssembly:

<button onclick="window.handleClick()">Click me</button>

2. Интеграция с веб-страницами

  • Инициализация Wasm:

Привязываем скрипт wasm_exec.js, затем инстанцируем модуль Wasm:

<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("play.wasm"), go.importObject).then((result) => {
go.run(result.instance);
});
</script>
  • Взаимодействие с DOM:

Доступ к веб-элементам и их изменение принципиально важны. Вот, например, изменение содержимого элемента абзаца из Go:

func updateDOMContent() {
document := js.Global().Get("document")
element := document.Call("getElementById", "myParagraph")
element.Set("innerText", "Updated content from Go!")
}

3. Увеличение эффективности и скорости

  • Горутины Go в браузере:

Вот пример операций выборки данных, выполняемых одновременно без блокировки основного потока:

func fetchData(url string, ch chan string) { // Имитируем выборку данных. ch <- "Data from " + url }  func main() { ch := make(chan string) go fetchData("<https://api.example1.com>", ch) go fetchData("<https://api.example2.com>", ch)  data1 := <-ch data2 := <-ch println(data1, data2) }

Перемещением по Go и WebAssembly демонстрируется мощное объединение параллельной обработки Go с быстрым выполнением WASM на стороне клиента.

Теперь рассмотрим применение этих технологических преимуществ в реальной масштабируемой системе авторизации Permify.

Подробный разбор кода WASM в Permify

Переходим к самой интеграции WebAssembly, изучим ключевые сегменты WASM-кода на Go.

1. Настройка среды Go-WASM

Готовим и указываем код на Go, компилируемый для среды выполнения WebAssembly:

// go:build wasm
// +build wasm

Эти строки  —  указания компилятору Go на то, что следующий код предназначен для среды выполнения WebAssembly, а именно:

  • //go:build wasm  —  ограничение сборки, которым обеспечивается компиляция кода только в цели WASM и согласно современному синтаксису.
  • // +build wasm  —  аналогичное ограничение со старым синтаксисом для совместимости с прошлыми версиями Go.

Так компилятору фактически указывается на то, что этот сегмент кода включается только при компиляции для архитектуры WebAssembly, обеспечивая соответствующую настройку и функционирование в этой конкретной среде выполнения.

2. Объединение JavaScript и Go с помощью функции «run»

package main  import ( "context" "encoding/json" "syscall/js"  "google.golang.org/protobuf/encoding/protojson"  "github.com/Permify/permify/pkg/development" )  var dev *development.Development  func run() js.Func { // Функцией «run» возвращается новая функция JavaScript, // в которую оборачивается функция Go. return js.FuncOf(func(this js.Value, args []js.Value) interface{} {  // «t» понадобится для хранения демаршалированных данных JSON. // Тип «interface{}» пустой, значит, может содержать значение любого типа. var t interface{}  // Демаршалируем JSON из аргумента функции JavaScript «args[0]» в структуру данных Go «map». // В «args[0].String()» от аргумента JavaScript получается строка JSON, // преобразуемая затем в байты и демаршалируемая — с выполнением парсинга — в карту «t». err := json.Unmarshal([]byte(args[0].String()), &t)  // Если при демаршалировании — парсинге — JSON возникает ошибка, // возвращается массив с сообщением об ошибке «invalid JSON» в JavaScript. if err != nil { return js.ValueOf([]interface{}{"invalid JSON"}) }  // Попытка подтвердить, что JSON с выполненным парсингом, то есть «t», — это карта со строковыми ключами. // На этом этапе демаршалированному JSON обеспечивается ожидаемый тип «map», то есть карта. input, ok := t.(map[string]interface{})  // Если утверждение ложно — это и означает «ok», — // возвращается массив с сообщением об ошибке «invalid JSON» в JavaScript. if !ok { return js.ValueOf([]interface{}{"invalid JSON"}) }  // Запускаем основную логику приложения с входными данными, над которыми выполнен парсинг. // Предполагается, что «input» каким-то образом обрабатывается этим «dev.Run» и возвращаются любые ошибки, обнаруженные во время этого процесса. errors := dev.Run(context.Background(), input)  // Если ошибок нет и длина среза «errors» равна 0, // в JavaScript возвращается пустой массив. Это означает, что запуск прошел успешно, без ошибок. if len(errors) == 0 { return js.ValueOf([]interface{}{}) }  // Если имеются ошибки, каждая ошибка в срезе «errors» маршалируется, преобразуется в строку JSON. // «vs» — это срез, в котором сохранится каждая из этих ошибок — строк JSON. vs := make([]interface{}, 0, len(errors))  // Перебираем каждую ошибку в срезе «errors». for _, r := range errors { // Преобразуем ошибку «r» в строку JSON и сохраняем ее в «result». // Если во время этого маршалирования возникает ошибка, в JavaScript возвращается массив с тем сообщением об ошибке. result, err := json.Marshal(r) if err != nil { return js.ValueOf([]interface{}{err.Error()}) } // Добавляем ошибку — строку JSON в срез «vs». vs = append(vs, string(result)) }  // Возвращаем в JavaScript срез «vs» со всеми ошибками — строками JSON. return js.ValueOf(vs) }) }

В Permify функция run  —  краеугольный камень, ею выполняется важнейшая операция объединения входных данных JavaScript и возможностей обработки на Go, организуется обмен данными в реальном времени в формате JSON, чем обеспечивается плавный и мгновенный доступ к основному функционалу Permify через интерфейс браузера.

Подробнее о run:

  • Обмен данными JSON: преобразованием входных данных JavaScript в используемый на Go формат этой функцией демаршалируется JSON  —  с передачей данных между JS и Go  —  и благодаря надежным возможностям обработки Go обеспечивается беспроблемное манипулирование входными данными браузера.
  • Обработка ошибок: обеспечивая ясность для пользователя, его осведомленность и удобство взаимодействия, во время парсинга и обработки данных проводится тщательная проверка ошибок с возвращением соответствующих сообщений об ошибках обратно в среду JavaScript.
  • Контекстная обработка: с помощью dev.Run функцией в определенном контексте обрабатываются входные данные, над которыми выполнен парсинг. Чтобы обеспечить стабильный контроль данных и обратную связь с пользователем, при обработке потенциальных ошибок осуществляется управление логикой приложения.
  • Двунаправленный обмен данными: поскольку ошибки маршалируются обратно в формат JSON и возвращаются в JavaScript, функцией обеспечивается двунаправленный поток данных с сохранением обеих сред в синхронизированной гармонии.

Таким образом, благодаря четкому управлению данными, обработке ошибок и гибкому двунаправленному каналу обмена данных, run является важным мостом, связывающим JavaScript и Go для бесперебойного функционирования Permify в реальном времени через интерфейс браузера.

Такое упрощение взаимодействия позволяет не только повысить удовлетворенность пользователей, но и применить в среде Permify соответствующие сильные стороны JavaScript и Go.

3. Выполнение и инициализация «main»

// Продолжаем рассмотренный выше код...  func main() { // Инстанцируем канал «ch» без буфера, это точка синхронизации для горутины. ch := make(chan struct{}, 0)  // Создаем новый экземпляр «Container» из пакета «development» и присваиваем его глобальной переменной «dev». dev = development.NewContainer()  // Присоединяем определенную ранее функцию «run» к глобальному объекту JavaScript, // делая ее вызываемой из среды JavaScript. js.Global().Set("run", run())  // Чтобы остановить горутину «main» и предотвратить завершение программы, используем выражение приема канала. <-ch }
  1. ch := make(chan struct{}, 0): для координации активности горутин  —  параллельных потоков на Go  —  создается канал синхронизации.
  2. dev = development.NewContainer(): из пакета разработки инициализируется новый экземпляр контейнера и присваивается переменной dev.
  3. js.Global().Set("run", run()): функция run предоставляется глобальному контексту JavaScript для вызова функций Go.
  4. <-ch: горутина main на неопределенное время останавливается, обеспечивая, что модуль Go WebAssembly остается активным в среде JavaScript.

В итоге кодом устанавливается среда Go, запускаемая в WebAssembly с конкретной функциональностью, то есть функцией run, на стороне JavaScript, и сохраняется активной и доступной для вызовов функций из JavaScript.

Встраивание кода Go в модуль WASM

Прежде чем переходить к функционалу Permify, важно разобрать этапы преобразования кода Go в модуль WASM, подготовив его для выполнения в браузере.

Полная кодовая база Go доступна в репозитории GitHub.

1. Компиляция в WASM

Запускаем преобразование приложения Go в бинарный код WASM:

GOOS=js GOARCH=wasm go build -o permify.wasm main.go

Этой командой компилятором Go создается двоичный файл .wasm для сред JavaScript, с источником main.go.

permify.wasm  —  это результат, краткое описание возможностей Go, подготовленное для веб-развертывания.

2. WASM Exec JS

Наряду с бинарным кодом WASM в экосистеме Go имеется незаменимая часть wasm_exec.js. Она важна для инициализации и упрощения модуля WASM в настройке браузера. Этот необходимый скрипт обычно находится внутри установки Go в misc/wasm.

Чтобы долго не искать, мы разместили wasm_exec.js прямо здесь:cp «$(go env GOROOT)/misc/wasm/wasm_exec.js» .

С этими файлами  —  двоичным WASM и JavaScript  —  мы готовы к объединению с фронтендом.

Этапы внедрения Go WASM в приложение React

1. Настройка структуры приложения React

Сначала разберемся со структурой каталогов. В ней код, связанный с WebAssembly, четко отделен от остального приложения, а самое главное происходит в папке loadWasm:

loadWasm/

├── index.tsx // Основной компонент React для интегрирования WASM.
├── wasm_exec.js // Этим скриптом Go объединяются WASM и JS.
└── wasmTypes.d.ts // Объявления типов TypeScript для WebAssembly.

Полная структура и специфика каждого файла доступны здесь.

2. Установка объявлений типов

Внутри wasmTypes.d.ts создаются глобальные объявления типов, которые распространяются на интерфейс Window для признания новых методов из WebAssembly:

declare global {
export interface Window {
Go: any;
run: (shape: string) => any[];
}
}
export {};

Так обеспечивается распознавание в TypeScript конструктора Go и метода run при вызове в глобальном объекте window.

3. Подготовка загрузчика WebAssembly

В index.tsx выполняются важные задачи:

  • Импорт зависимостей: сначала импортируются необходимые объявления JS и TypeScript:
import "./wasm_exec.js";
import "./wasmTypes.d.ts";
  • Инициализация WebAssembly: весь процесс осуществляется асинхронной функцией loadWasm:
async function loadWasm(): Promise<void> {
const goWasm = new window.Go();
const result = await WebAssembly.instantiateStreaming(
fetch("play.wasm"),
goWasm.importObject
);
goWasm.run(result.instance);
}

Здесь среда Go WASM инициализируется с помощью new window.Go(). В WebAssembly.instantiateStreaming модуль WASM извлекается, компилируется, и создается экземпляр. А активируется модуль WASM в goWasm.run.

  • Компонент React с пользовательским интерфейсом загрузчика: компонентом LoadWasm применяется хук useEffect для асинхронной загрузки WebAssembly при монтировании компонента:
export const LoadWasm: React.FC<React.PropsWithChildren<{}>> = (props) => { const [isLoading, setIsLoading] = React.useState(true);  useEffect(() => { loadWasm().then(() => { setIsLoading(false); }); }, []);  if (isLoading) { return ( <div className="wasm-loader-background h-screen"> <div className="center-of-screen"> <SVG src={toAbsoluteUrl("/media/svg/rocket.svg")} /> </div> </div> ); } else { return <React.Fragment>{props.children}</React.Fragment>; } };

Во время загрузки отображается ракета в SVG-формате. Это важная обратная связь: пользователи понимают, что инициализация продолжается. Как только загрузка завершится, отобразятся дочерние компоненты или содержимое.

4. Вызов функций WebAssembly

Метод Go WASM run вызывается так:

function Run(shape) {
return new Promise((resolve) => {
let res = window.run(shape);
resolve(res);
});
}

По сути, эта функция  —  мост, по которому фронтенд React взаимодействует с логикой бэкенда Go, инкапсулированной в WASM.

5. Реализация кнопки запуска в React

Кнопка, при нажатии которой запускается функция WebAssembly, интегрируется так:

1. Создание компонента кнопки

Сначала создаем простой компонент React с кнопкой:

import React from "react";  type RunButtonProps = { shape: string; onResult: (result: any[]) => void; };  function RunButton({ shape, onResult }: RunButtonProps) { const handleClick = async () => { let result = await Run(shape); onResult(result); };  return <button onClick={handleClick}>Run WebAssembly</button>; }

В этом коде компонентом RunButton принимается два свойства:

  • shape: аргумент формы для передачи в функцию WebAssembly run.
  • onResult: функция обратного вызова, получающая результат функции WebAssembly и обновляющая состояние или отображающая результат в пользовательском интерфейсе.

2. Интеграция кнопки в основной компонент

Теперь интегрируем RunButton:

import React, { useState } from "react"; import RunButton from "./path_to_RunButton_component"; // Заменяем на фактический путь  function App() { const [result, setResult] = useState<any[]>([]);  // Определяем содержимое «shape» const shapeContent = { schema: `|- entity user {}  entity account { relation owner @user relation following @user relation follower @user  attribute public boolean action view = (owner or follower) or public }  entity post { relation account @account  attribute restricted boolean  action view = account.view  action comment = account.following not restricted action like = account.following not restricted }`, relationships: [ "account:1#owner@user:kevin", "account:2#owner@user:george", "account:1#following@user:george", "account:2#follower@user:kevin", "post:1#account@account:1", "post:2#account@account:2", ], attributes: [ "account:1$public|boolean:true", "account:2$public|boolean:false", "post:1$restricted|boolean:false", "post:2$restricted|boolean:true", ], scenarios: [ { name: "Account Viewing Permissions", description: "Evaluate account viewing permissions for 'kevin' and 'george'.", checks: [ { entity: "account:1", subject: "user:kevin", assertions: { view: true, }, }, ], }, ], };  return ( <div> <RunButton shape={JSON.stringify(shapeContent)} onResult={setResult} /> <div> Results: <ul> {result.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> </div> </div> ); }

В этом примере App  —  это компонент с кнопкой RunButton. Когда кнопка нажимается, результат функции WebAssembly отображается в списке под кнопкой.

Заключение

Мы развернули интеграцию WebAssembly с Go для усовершенствованной веб-разработки и оптимального пользовательского взаимодействия в браузерах, настроили среду Go, преобразовали код Go в WebAssembly и выполнили его в веб-контексте.

В итоге получили интерактивную платформу play.permify.co  —  не только пример, но и маяк, высвечивающий конкретные, мощные возможности, достигаемые сочетанием этих технологических областей.