WebAssembly с Go: вывод веб-приложений на новый уровень.
В сообществе разработчиков участились обсуждения WebAssembly, или WASM. Потенциал этой технологии огромен, для нашего Open Source проекта — инфраструктуры Permify, с помощью которой разработчики создают и управляют детализированными разрешениями приложений, — она просто бесценна.
Расскажем, почему и как мы интегрировали WASM в нашу интерактивную среду и получили преимущества от использования Golang.
Каков функционал этой среды? Если вкратце, ?
WebAssembly с Go: вывод веб-приложений на новый уровень...
В сообществе разработчиков участились обсуждения WebAssembly, или WASM. Потенциал этой технологии огромен, для нашего Open Source проекта — инфраструктуры Permify, с помощью которой разработчики создают и управляют детализированными разрешениями приложений, — она просто бесценна.
Расскажем, почему и как мы интегрировали WASM в нашу интерактивную среду и получили преимущества от использования Golang.
Каков функционал этой среды? Если вкратце, это интерактивный модуль Permify, которым создаются и тестируются модели авторизации.
Содержание статьи:
- Краткое описание WASM и преимуществ его применения с Go.
- Что способствовало нашему решению интегрировать WASM в Permify?
- Реализация 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
- Этапы:
- Сначала задаем целевую платформу компиляции 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 }
ch := make(chan struct{}, 0)
: для координации активности горутин — параллельных потоков на Go — создается канал синхронизации.dev = development.NewContainer()
: из пакета разработки инициализируется новый экземпляр контейнера и присваивается переменнойdev
.js.Global().Set("run", run())
: функцияrun
предоставляется глобальному контексту JavaScript для вызова функций Go.<-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
: аргумент формы для передачи в функцию WebAssemblyrun
.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 — не только пример, но и маяк, высвечивающий конкретные, мощные возможности, достигаемые сочетанием этих технологических областей.