Поскольку несколько недель назад WebAssembly был повышен до официальной Рекомендации W3C, я подумал, что было бы интересно написать приложение для Mashroom Portal на базе WebAssembly. вместо JavaScript. Например написано Go.

Я решил использовать Go, потому что он изначально поддерживает WebAssembly в качестве цели сборки, начиная с версии 1.11. Но можно также написать его на C / C ++, C # или Rust или на любом другом языке, который компилируется в WebAssembly.

Написание веб-приложения на Go

Само приложение написать оказалось довольно просто. Я просто следил за существующими онлайн-ресурсами, такими как:

И я использовал библиотеку godom для взаимодействия с DOM.

HTML-шаблон

Шаблон HTML можно создать с помощью конкатенации строк или библиотеки text / template, а затем просто установить с помощью SetI nnerHtml ():

import (
   . "github.com/siongui/godom/wasm"
)
func main() {
hostElement := Document.GetElementById("app-host");
    hostElement.SetInnerHTML(`
        <div>
            <p>The app</p>
            <button id="my-button">Click me</button>
        </div>
    `);
}

Вызов JavaScript из Go

godom упрощает вызов функций JavaScript (или установку свойств JavaScript). После получения объекта вы можете использовать Call () для вызова произвольных функций (или Set / Get для свойств):

import (
   . "github.com/siongui/godom/wasm"
   "syscall/js"
)
func main() {
    buttonElement := Document.GetElementById('my-button');
    buttonElement.Call("addEventListener", "click", js.FuncOf(onClickHandler));
    buttonElement.Set("onclick", js.FuncOf(onClickHandler));
}

Если вы хотите передать значения или функции Go, вам необходимо преобразовать их с помощью js.ValueOf () или js.FuncOf ().

Вызов Go из JavaScript

Библиотека syscall / js ожидает определенной сигнатуры для функций, которые могут быть вызваны из JavaScript. После преобразования их с помощью js.FuncOf () вы можете передать их как обработчик событий или предоставить его глобально с помощью Window.Set ()

import (
   . "github.com/siongui/godom/wasm"
   "syscall/js"
)
func myGlobalFunc(this js.Value, inputs []js.Value) interface{} {
    // TODO
}
func main() {
    Window.Set("myGlobalFunc", js.FuncOf(myGlobalFunc);
}

И в коде JavaScript:

window.myGlobalFunc('what', 'ever');

Компиляция в WebAssembly

Чтобы скомпилировать вашу программу в WebAssembly, просто выполните:

GOOS=js GOARCH=wasm go build -o public/main.wasm ./src

… Когда src содержит ваш файл main.go.

Загрузка и запуск веб-приложения Go

Go поставляется с утилитой JavaScript в каталоге misc / wasm, который необходимо загрузить в первую очередь:

<script src="wasm_exec.js"></script>

Затем вы можете загрузить WebAssembly следующим образом:

const go = new Go();
			WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
    go.run(result.instance);
});

Передача параметров запуска

Микрофронтенд обычно должен быть параметризованным и иметь некоторую конфигурацию или элемент DOM, в который он должен быть встроен. Программа Go может принимать только массив строк в качестве списка параметров, объекты невозможны:

// In JavaScript
const go = new Go()
go.argv = ['param1', 'param2', 'param3']
// In Go
import (
    "os"
)
func main() {
    param1 := os.Args[0]
    param2 := os.Args[1]
    param3 := os.Args[2]
    // ...
}

Сохранение приложения Go

Чтобы предотвратить выход приложения Go после выхода из функции main (), просто создайте канал и дождитесь любого значения:

var c = make(chan bool)
func main() {
    //...
    <-c
    Println("Go app execution stopped")
}

Интеграция веб-приложения Go в портал Mashroom

Определение плагина

Определение плагина в package.json должно вызвать загрузку Mashroom Portal:

  • wasm_exec.js как общий ресурс (загружается первым и только один раз, даже если это требуется нескольким приложениям)
  • Код начальной загрузки (загрузчика), в моем случае index.js

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

Предположим, что файлы находятся в общедоступной папке, а веб-приложение принимает параметр конфигурации name, определение будет выглядеть следующим образом:

"mashroom": {
  "plugins": [
    {
      "name": "My Go WASM App",
      "type": "portal-app",
      "bootstrap": "startMyGoWasmApp",
      "sharedResources": {
        "js": [
          "wasm_exec.js"
        ]
      },
      "resources": {
        "js": [
          "index.js"
        ]
      },
      "defaultConfig": {
        "resourcesRoot": "./public",
        "appConfig": {
          "name": "Jürgen"
        }
      }
    }
  ]
}

Бутстрап

Загрузочная программа загружает WebAssembly из resourcesBasePath и затем запускает ее:

const bootstrap = (hostElement, appSetup, clientServices) => {
    const { resourcesBasePath, appConfig: { name }} = appSetup;
    const { messageBus } = clientServices;

    return new Promise((resolve, reject) => {
        if (WebAssembly && typeof WebAssembly.instantiate === 'function') {
            const go = new Go();
            // Start parameters
            go.argv = [name];
           
            WebAssembly.instantiateStreaming(fetch(resourcesBasePath + '/main.wasm'), go.importObject).then(
                (result) => {
                    go.run(result.instance);
                }, (error) => {
                    reject(error);
                });
        } else {
            reject(new Error('WebAssembly not supported!'));
        }
    });
};

global.startMyGoWasmApp = bootstrap;

Передача ведущего элемента

Конечно, в приведенном выше примере веб-приложение не может знать hostElement, в который оно должно быть встроено. Мы также должны передать его как параметр запуска:

// JavaScript
// Make sure the hostElement has an id
if (!hostElement.id) {
    hostElement.id = 'MyGoApp_' + Math.trunc(Math.random() * 1000000);
}
// Start parameters
go.argv = [hostElement.id, name];
// Go
func main() {
    hostElementId := os.Args[0] 
    hostElement = Document.GetElementById(hostElementId)
    // hostElement.SetInnerHTML(...)
    // ...
}

Интеграция с MessageBus

Поскольку мы хотим взаимодействовать с другими расширениями Microfrontend, объект MessageBus необходимо каким-то образом передать приложению Go. Лучший способ, который я нашел, - назначить его глобальному (оконному) свойству и передать имя свойства приложению:

// JavaScript
const messageBusObjName = `__${hostElement.id}`;
window[messageBusObjName] = messageBus;
go.argv = [hostElement.id, name, messageBusObjName];
// Go
func main() {
    messageBusObjName = os.Args[2] 
    messageBus := Window.Get(messageBusObjName) 
    messageBus.Call("subscribe", "ping", js.FuncOf(onPingReceived))
}

Обработка ошибок

Приведенная выше программа начальной загрузки уже обрабатывает некоторые ошибки, например, когда WebAssembly не может быть загружен или браузер не поддерживает WebAssembly. Но когда приложение неожиданно закрывается во время запуска, пользователю ничего не отображается. В этом случае немного сложно справиться. Лучший способ, который я нашел, - назначить собственный обработчик выхода и проверить, был ли он вызван после go.run ():

let exitCode = 0;
go.exit = (code) => {
    exitCode = code;
};
WebAssembly.instantiateStreaming(/* ... */), go.importObject).then(
    (result) => {
        go.run(result.instance); 
        if (exitCode) {
            reject(new Error('Go app exited with code: ' +  exitCode))
        }
    }
)

Разгрузка

Когда приложение удаляется со страницы, выполнение необходимо остановить. В нашем случае это просто означает, что мы должны отправить что-то на канал, который приложение прослушивает. И поскольку мы уже интегрировали MessageBus, мы можем использовать его для публикации стоп-сообщения в приложении Go следующим образом:

// JavaScript
// The bootstrap returns an object with a "willBeRemoved" callback
resolve({
    willBeRemoved: () => {
        console.info('Ummounting Go WebAssembly app');
        // Send stop signal
        messageBus.publish(`stop_${hostElementId}`, {});
    }
});
// Go
func onStop(this js.Value, inputs []js.Value) interface{} {
   c <- true
   return js.Null()
}
func main() {
    messageBusObjName = os.Args[2]
    messageBus := Window.Get(messageBusObjName) 
   
    messageBus.Call("subscribe", "stop_" + hostElementId, js.FuncOf(onStop))
    // ...
    <-c
    Println("Go app execution stopped")
}

Возможные варианты использования

Но является ли это просто академическим упражнением или есть реальные примеры использования такого подхода? В настоящее время существует всего несколько сценариев, в которых это может быть полезно:

  • Если у вас есть библиотека C / C ++ или Go, которую вы хотите (повторно) использовать в пользовательском интерфейсе
  • Когда вам не нужно взаимодействовать с DOM, а просто хотите нарисовать какие-то 2D или 3D вещи. Например с библиотекой типа go-canvas.

Но я почти уверен, что в ближайшем будущем появится множество веб-фреймворков на основе WebAssembly, и тогда это совсем другая история.

Исходный код

Исходный код полного примера доступен здесь: https://github.com/nonblocking/mashroom-portal-demo-go-wasm-app. Проверить это!