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



Книга 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  —  не только пример, но и маяк, высвечивающий конкретные, мощные возможности, достигаемые сочетанием этих технологических областей.



256   0  

Comments

    Ничего не найдено.