В сообществе разработчиков участились обсуждения 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 — не только пример, но и маяк, высвечивающий конкретные, мощные возможности, достигаемые сочетанием этих технологических областей.
Читайте также:
- WebAssembly: секретное оружие в разработке высокооптимизированных и безопасных веб-приложений
- Как создавать легкие платформонезависимые приложения на Go — без JS и BS
- Введение в WebAssembly (WASM)
Читайте нас в Telegram, VK и Дзен
Перевод статьи Ege Aytin: WebAssembly with Go: Taking Web Apps to the Next Level