WebAssembly: короткий обзор ассемблера для фронтенда
В последнее время фронтенд все больше и больше обрастает новыми технологиями, одна из них — WebAssembly. На ней можно написать полноценное web‑приложение и почти все на что способна ваша фантазия. В статье мы рассмотрим, что такое WebAssembly, как работает и с чем её едят. И конечно же я хотел поделиться рецептом приложения. Возьмем наш любимый React
и добавим Yew
, приправим всё это webpack
-ом и добавим щепоточку module‑federation
. Статья будет интересна всем, кто хочет познакомиться с WebAssembly и добавить разнообразия в список используемых фреймворков и библиотек для написания пользовательских компонентов.
Что такое Wasm и зачем он был создан
WebAssembly (Wasm) — это новый открытый формат байт-кода, исполняемого современными браузерами. Сейчас он поддерживается во всех браузерах. Подробнее можно почитать тут.
Wasm позволяет переносить код, написанный на языках C, C++, C#, Rust и других (поддерживаемый список можно посмотреть тут), для использования в браузерной среде с производительностью, близкой к нативной. Важно понимать, максимальная производительность WebAssembly равна максимальной производительности JavaScript, но написать оптимизированный по памяти код иногда проще на WebAssembly. Подробнее можно почитать в статье Вячеслава Егорова.
Язык WebAssembly предназначен для дополнения и работы вместе с JavaScript, что позволяет веб-разработчикам использовать преимущества обоих языков:
JavaScript — это язык высокого уровня, гибкий и достаточно выразительный для написания веб-приложений. У него много преимуществ: он динамически типизирован, не требует компиляции и обладает огромной экосистемой, которая предоставляет мощные фреймворки, библиотеки и другие инструменты.
WebAssembly — это низкоуровневый язык, похожий на ассемблер, с компактным двоичным форматом, который работает с почти естественной производительностью и предоставляет низкоуровневые модели памяти для таких языков как C++ и Rust, чтобы компилировать их для работы в интернете. Обратите внимание, что у WebAssembly на будущее есть высокоуровневая цель по поддержке языков со сборщиками мусора в модели памяти.
Как WebAssembly работает в браузерах
Сейчас во всех браузерах уже есть поддержка WebAssembly: его код исполняется в специальной виртуальной машине. Выглядит это так. Код на низкоуровневом языке компилируется с помощью мощных компиляторов (например, LLVM
) или набора инструментов таких как Emscripten
и превращается в бинарный формат. Но у него есть и текстовое представление:
(module
(func import “imports” “imported_func”) (param i32))
(func (export “exported_func”)
i32.const 42
call $i)
)
Затем модуль JavaScript вызывает Wasm-модуль. Пример кода:
WebAssembly.instantiateStreaming(
fetch("simple.wasm"),
{
imports: {
imported_func: console.log,
},
},
).then((module) =>
module.instance.exports.exported_func(),
);
В браузере модуль скачивается, парсится и исполняется.
Кажется, что всё довольно просто. Благодаря всей этой магии открылась возможность запускать десктопные приложения в браузере или использовать код на низкоуровневых языках.
Не будем сильно углубляться в эту тему, уж очень много статей написано. Подробнее ознакомиться можно тут.
Достоинства и недостатки WebAssembly
Достоинства
? Стандарт. Сейчас он широко распространяется в отрасли и поддерживается многими языками, в том числе и со сборкой мусора (Garbage Collection). Совсем недавно поддержка стандарта появилась в Google Chrome и Mozilla Firefox. Также Wasm можно использовать не только в браузере, но и на серверах, используя специальный интерфейс WASI.
? Возможность использовать библиотеки, написанные на низкоуровневых языках. Индустрия системных языков программирования накопила большой опыт в написании сложных алгоритмов с преобразованиями данных с оптимальной скоростью. И, о чудо, теперь этот опыт можно использовать при написании приложений в браузере.
? Доступ к DOM и Browser API. WebAssembly напрямую не может обращаться к DOM или браузерному API. Все вызовы API внутри Wasm происходят через JavaScript (компиляторами добавляется специальная прослойка для исполнения JavaScript-кода). Значит, мы можем использовать почти все возможности современных браузеров с помощью специальных полифилов.
? Быстрота и эффективность. Благодаря тому, что Wasm это бинарный формат, длительность его парсинга и компиляции заметно сокращается. Это позволяет ему исполняться в браузере на скорости, близкой к нативной (но тут есть некоторые нюансы, в основном зависящие от размера файла). И можно кешировать распарсенный и скомпилированный Wasm-модуль, используя специальный заголовок Content-Type: application/wasm.
⛓️ Типизация. В Wasm есть система типов. Это помогает корректно выполнять код. Но несмотря на всю привлекательность типизации, она может существенно замедлить работу, потому что каждый тип нужно конвертировать в понятный Wasm. Подробнее о типах в WebAssembly.
? Изоляция. По умолчанию Wasm-модуль запускается в изолированной браузерной среде, и всё, что он может делать, это читать выделенную ему область памяти. Но изоляция не означает безопасность. Для безопасного исполнения лучше использовать последние стандарты в индустрии, к примеру, Content Security Policy (CSP).
Недостатки
? Исполняется в основном потоке. Так как Wasm исполняется внутри основного потока бразера, стоит бережно следить и профилировать используемый код. Основные проблемы, которые могут возникнуть: накладные расходы на копирование данных, общение между потоками, блокировка потока и всё, что относится к работе с основным потоком в JavaScript. Мы тут опускаем взаимодействие с любыми типами Worker
'ов.
? Большие размеры файлов. Портируемость в браузер кода, написанного на системном языке программирования, требует добавления своего рода полифилов. Большая часть проблем такого рода решается оптимизацией сборки или прогоном с усиленной оптимизаций через компилятор. Выше я уже говорил, что файлы можно кешировать.
⌛️ Время на изучение технологии. Wasm — молодая, развивающая технология, которая только частично связана с использованием JS
. Для написания Wasm-модулей нужно будет погрузиться в новый язык программирования, найти или написать плагины для используемых сборщиков или бандлеров. И, что немаловажно, обучиться грамотно отлаживать и профилировать новую для вас технологию.
Особенности (не смог отнести к достоинствам или недостаткам)
? JS необходим для старта Wasm-модуля. Это связано с тем, что Wasm призван не заменить JS, а стать ещё одним инструментом в руках веб-разработчика.
? Сложно отлаживать. Сейчас можно это делать с помощью devtools
. Но возможностей меньше того, к чему привык JavaScript-разработчик. В то же время уже сейчас можно отлаживать исходники файлов, написанных на низкоуровневых языках.
Пишем web-приложение c использованием WebAssembly
Кого-то удивит, но на WebAssembly можно писать и полноценные фронтенд-приложения. Вот список ингредиентов для приготовления проекта:
Yew. Фреймворк для написания веб-приложений на WebAssembly, использующий язык Rust.
React. Одна из популярных библиотек для создания пользовательских интерфейсов.
Yarn . Менеджер пакетов, который одновременно выполняет функции менеджера проекта.
Webpack. Сборщик приложений.
Typescript. Надстройка над JavaScript c добавлением типизации.
Prettier. Форматер кода.
Eslint. Статический Анализатор кода, помогающий придерживаться единого стиля кода и находить некоторые типы ошибки.
Что за проект мы будем создавать? Небольшое приложение с микрофронтендной архитектурой с использованием Yew
и React
.
Для начала установим Yarn. Нам потребуется версия 3.6.3. После успешной установки настроим своё рабочее пространство.
Затем инициализируем конфигурацию проекта. Скачиваем репозиторий:
git clone https://github.com/easymikey/wasm-example/tree/init-project
…и переключаемся на ветку init-project
. Создадим необходимые директории:
mkdir packages && mkdir packages/shell && mkdir packages/react-counter && mkdir packages/yew-counter
Разработка входной точки
Начнем с подготовки окружения:
cd packages/shell && yarn init -y && touch webpack.config.ts && tsconfig.json && mkdir public && touch public/index.html && mkdir src && touch src/App.tsx
Добавим dev-зависимости:
yarn add -D css-loader \
esbuild \
esbuild-loader \
html-webpack-plugin \
style-loader \
typescript \
typescript-plugin-css-modules \
webpack \
webpack-cli \
webpack-dev-server \
Добавим React
:
yarn add react react-dom
Получим такой package.json
:
{
"name": "shell",
"version": "0.1.0",
"sideEffects": false,
"scripts": {
"serve": "webpack serve --color --open"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.8",
"@types/webpack": "^5.28.3",
"@types/webpack-dev-server": "^4.7.2",
"css-loader": "^6.8.1",
"esbuild": "^0.19.4",
"esbuild-loader": "^4.0.2",
"esbuild-register": "^3.5.0",
"html-webpack-plugin": "^5.5.3",
"style-loader": "^3.3.3",
"typescript": "4.9.5",
"typescript-plugin-css-modules": "4.1.1",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
},
"packageManager": "yarn@3.6.3",
"volta": {
"node": "20.8.1"
}
}
Следом настроим TypeScript:
{
"extends": "../../tsconfig.base.json",
"include": [
"./src/**/*",
],
"compilerOptions": {
"outDir": "dist",
"baseUrl": ".",
"paths": {
// Дадим возможность IDE найти наши модули
"react_counter/*": [ "../react-counter/dist/@mf-types/*" ],
"yew_counter/*": [ "../yew-counter/core/pkg/index.d.ts" ]
},
"plugins": [ { "name": "typescript-plugin-css-modules" } ],
},
"exclude": [
"node_modules",
"./dist/**/*"
],
"references": [ ]
}
Настроим сборку:
import path from 'path';
import webpack from 'webpack';
import 'webpack-dev-server';
import HtmlWebpackPlugin from 'html-webpack-plugin';
const config: webpack.Configuration = {
entry: './src/index.tsx',
mode: 'development',
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
},
headers: {
'Access-Control-Allow-Origin': '*',
},
port: 3001,
},
resolve: {
extensions: ['.js', '.ts', '.tsx', '.css'],
},
module: {
rules: [
// Use esbuild to compile JavaScript & TypeScript
{
// Match `.js`, `.jsx`, `.ts` or `.tsx` files
test: /\.[jt]sx?$/,
exclude: /yarn/,
loader: 'esbuild-loader',
options: {
// JavaScript version to compile to
target: 'es2015',
},
},
{
test: /\.module.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName:
'[name]__[local]--[hash:base64:16]',
},
},
},
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
export default config;
Если хочется детально изучить настройки, то почитайте документацию используемых плагинов.
Инициализируем входной HTML-файл в папке public:
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Yew + React </title>
</head>
<style>
body {
margin: 0;
padding: 0;
}
</style>
<body>
<div id="react-counter"></div>
</body>
</html>
И можно приступать к разработке.
Откроем файл src/App.tsx и напишем оболочку нашего приложения, отображающее счётчик с состоянием по умолчанию, равному нулю. Добавим колбеки для обновления состояния onIncrement
и onDecrement
.
import {FC, useState} from 'react';
// Стили можно посмотреть https://github.com/easymikey/wasm-example/blob/main/packages/shell/src/App.module.css
import styles from './App.module.css';
const App: FC = () => {
const [counter, setCounter] = useState(0);
const onIncrement = () => setCounter((c) => c + 1);
const onDecrement = () => setCounter((c) => c - 1);
return (
<div className={styles.root}>
<div className={styles.layout}>
<div className={styles.counter}>{counter}</div>
</div>
</div>
);
};
export default App;
Затем создадим файлы src/index.tsx и src/bootstrap.tsx со следующим содержанием:
// bootstrap.tsx
import {createRoot} from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('react-counter')!).render(<App />);
index.tsx
import('./bootstrap');
Теперь можно запускать наше приложения из корня командой yarn serve
.Можно увидеть текущее состояние приложения, открыв браузер на http://localhost:3001
Отлично, с входной точкой закончили, время начать разрабатывать модуль для уменьшения и увеличения счётчика на React
.
Разработка React-cчетчика
Как и в первом случае начнем с настройки окружения.
cd packages/react-counter && yarn init -y && touch webpack.config.ts && tsconfig.json && mkdir src && touch src/ReactConter.tsx && src/index.tsx
Добавим dev-зависимости:
yarn add -D css-loader \
esbuild \
esbuild-loader \
html-webpack-plugin \
style-loader \
typescript \
typescript-plugin-css-modules \
webpack \
webpack-cli \
webpack-dev-server \
@module-federation/typescript \
И добавим React
:
yarn add react react-dom
Итоговый package.json
:
{
"name": "react-counter",
"version": "0.1.0",
"sideEffects": false,
"scripts": {
"serve": "webpack serve --color"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@module-federation/typescript": "^3.0.1",
"@types/react": "^18.2.21",
"@types/webpack": "^5.28.3",
"@types/webpack-dev-server": "^4.7.2",
"css-loader": "^6.8.1",
"esbuild": "^0.19.4",
"esbuild-loader": "^4.0.2",
"esbuild-register": "^3.5.0",
"html-webpack-plugin": "^5.5.3",
"style-loader": "^3.3.3",
"typescript": "4.9.5",
"typescript-plugin-css-modules": "4.1.1",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
},
"volta": {
"node": "20.8.1"
}
}
Настроим TypeScript.
{
"extends": "../../tsconfig.base.json",
"include": [
"src",
],
"compilerOptions": {
"outDir": "dist",
"baseUrl": ".",
"plugins": [ { "name": "typescript-plugin-css-modules" } ],
},
"exclude": [
"node_modules",
"dist/**/*"
],
"references": [ ]
}
Настроим сборку.
import path from 'path';
import webpack from 'webpack';
import 'webpack-dev-server';
import {FederatedTypesPlugin} from '@module-federation/typescript';
import {ModuleFederationPluginOptions} from '@module-federation/typescript/src/types';
const federationConfig: ModuleFederationPluginOptions = {
name: 'react_counter',
filename: 'remoteEntry.js',
exposes: {
'./ReactCounter': './src/ReactCounter.tsx',
},
shared: {
react: {singleton: true},
'react-dom': {singleton: true},
},
};
const config: webpack.Configuration = {
entry: './src/index.tsx',
mode: 'development',
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
},
hot: true,
port: 3002,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
output: {
publicPath: 'auto',
},
resolve: {
extensions: ['.js', '.ts', '.tsx', '.css'],
},
module: {
rules: [
// Use esbuild to compile JavaScript & TypeScript
{
// Match `.js`, `.jsx`, `.ts` or `.tsx` files
test: /\.[jt]sx?$/,
exclude: /yarn/,
loader: 'esbuild-loader',
options: {
// JavaScript version to compile to
target: 'es2015',
},
},
{
test: /\.module.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName:
'[name]__[local]--[hash:base64:16]',
},
},
},
],
},
],
},
plugins: [
new FederatedTypesPlugin({
federationConfig,
}),
new webpack.container.ModuleFederationPlugin(federationConfig),
],
};
export default config;
Приступаем к написанию нашего микрофронтенда на React
.
Откроем файл src/ReactCounter.tsx и добавим возможность обновлять счётчик. Это очень простой компонент, думаю вы писали сотни или даже тысячи таких. Код можно увидеть ниже.
import {FC} from 'react';
// Стили можно посмотреть https://github.com/easymikey/wasm-example/blob/main/packages/react-counter/src/ReactCounter.module.css
import styles from './ReactCounter.module.css';
const ReactCounter: FC<{
onIncrement: () => void;
onDecrement: () => void;
}> = ({onDecrement, onIncrement}) => {
return (
<div className={styles.layout}>
<button className={styles.button} onClick={onIncrement}>
+
</button>
<button className={styles.button} onClick={onDecrement}>
-
</button>
</div>
);
};
export default ReactCounter;
Теперь нам нужно добавить новоиспеченный микрофонтенд.
Переходим к файлу packages/shell/webpack.config.ts и обновляем его.
...
const config: webpack.Configuration = {
...
plugins: [
new webpack.container.ModuleFederationPlugin({
name: 'shell',
remotes: {
// Used modules
react_counter:
'react_counter@http://localhost:3002/remoteEntry.js',
},
shared: {
react: {singleton: true},
'react-dom': {singleton: true},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
...
};
export default config;
Теперь у нас есть возможность загружать модуль счётчик на React. Давайте это используем и добавим обработку в нашу оболочку. Код изменится следующим образом:
import {FC, Suspense, lazy, useState} from 'react';
import styles from './App.module.css';
const ReactCounter = lazy(() => import('react_counter/ReactCounter'));
const App: FC = () => {
const [counter, setCounter] = useState(0);
const onIncrement = () => setCounter((c) => c + 1);
const onDecrement = () => setCounter((c) => c — - 1);
return (
<div className={styles.root}>
<div className={styles.layout}>
<Suspense fallback="Loading ReactCounter">
<ReactCounter
onIncrement={onIncrement}
onDecrement={onDecrement}
/>
</Suspense>
<div className={styles.counter}>{counter}</div>
</div>
</div>
);
};
export default App;
Снова запустим проект из корня командой yarn serve. Проект изменился, теперь он выглядит так:
Отличной, второй кусочек нашего приложения готов. Теперь мы можем добавить модуль на Wasm.
Разработка Yew-счётчика
Как и в первых двух случаях начнем с подготовки окружения.
cd packages/yew-counter && yarn init -y && touch webpack.config.ts
Для разработки Wasm-модуля нам потребуется установить Rust
и wasm-pack
.
Сначала установим Rust
, следуя официальной документации. Затем установим wasm-pack
снова следуя официальной документации.
Установим dev-зависимости:
yarn add -D webpack \
webpack-cli \
webpack-dev-server \
esbuild \
esbuild-register \
@wasm-tool/wasm-pack-plugin \
Итоговый package.json
:
{
"name": "yew-counter",
"version": "0.1.0",
"sideEffects": false,
"scripts": {
"serve": "webpack serve --color"
},
"devDependencies": {
"@types/webpack": "^5.28.3",
"@types/webpack-dev-server": "^4.7.2",
"@wasm-tool/wasm-pack-plugin": "^1.7.0",
"esbuild": "^0.19.4",
"esbuild-register": "^3.5.0",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
},
"packageManager": "yarn@3.6.3",
"volta": {
"node": "20.8.1"
}
}
Настроим сборку.
import path from 'path';
import webpack from 'webpack';
import 'webpack-dev-server';
import WasmPackPlugin from '@wasm-tool/wasm-pack-plugin';
const config: webpack.Configuration = {
entry: './core/pkg/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
mode: 'development',
experiments: {asyncWebAssembly: true},
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
},
headers: {
'Access-Control-Allow-Origin': '*',
},
port: 3003,
},
resolve: {
extensions: ['.js', '.ts'],
},
plugins: [
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, 'core'),
outDir: path.join(__dirname, 'core/pkg'),
}),
new webpack.container.ModuleFederationPlugin({
name: 'yew_counter',
filename: 'remoteEntry.js',
exposes: {
'./yew': './core/pkg',
},
}),
],
};
export default config;
Инициализируем Wasm-репозиторий c Yew:
wasm-pack new core --template https://github.com/yewstack/yew-wasm-pack-minimal
У нас появляется создатся папкаcore
со всеми необходимыми нам файлами.
Для начала обновим файл Cargo.toml
. Это аналог package.json
и в нём сосредоточны все наши зависимости.
[package]
name = "core"
version = "0.1.0"
edition = "2021"
readme = "README.md"
[lib]
crate-type = ["cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
yew = { version = "0.21", features = ["csr"] }
serde-wasm-bindgen = "0.6.1"
wasm-bindgen = "0.2.78"
web-sys = "0.3.64"
gloo = "0.10.0"
js-sys = "0.3.64"
stylist = { version = "0.13.0", features = ["yew_integration"] }
Начнем с файла src/app.rs и переименуем его в src/yew_counter.rs.
Начнём реализовывать счётчик. Для начала импортируем необходимые зависимости.
// Добавляем возможность использовать css
use stylist::css;
// Добавляем возможность стилизации компонентов
use stylist::yew::styled_component;
// Своего рода полифил для wasm
use wasm_bindgen::prelude::*;
// Инициализация фреймворка
use yew::prelude::*;
Затем опишем типы для нашего компонента. Вы скорее всего заметите, что это напоминает описание интерфейсов в TypeScript
, за исключением использования декоратора derive. Хотя кого я обманываю, большинство из нас так или иначе пробовали использовать декораторы в TypeScript.
#[derive(Properties, Clone, PartialEq)]
pub struct Props {
pub on_increment: js_sys::Function,
pub on_decrement: js_sys::Function,
}
// Если это перенести на Typescript
export interface Props {
onIncrement: (...args: any[]) => any,
onDecrement: (...args: any[]) => any,
};
Следует отметить, что функции и типы данных не равны в Rust и JavaScript/TypeScript. В частности потому, что это разные языки. Если немного уточнить, то для перевода типов нужно сначала замапить типы из JS/TS в Wasm, а только потому уже в Rust. По этой причине мы видим тут тип any
. Поэтому любые функции, передаваемые в модуль на Rust
мы будем вызывать не таким образом как все привыкли.
С типами разобрались, давайте начнем писать компонент счётчика. И он удивительным образом похож на React
.
#[styled_component]
pub fn YewCounter(props: &Props) -> Html {
html! {
<div>
<button>{"+"}</button>
<button>{"-"}</button>
</div>
}
}
Стилизуем наш компонент. Получилось что-то очень похожее на styled-components
, за исключением того, что библиотека на Rust только генерирует стили без обертки в компонент через шаблонные строки. Обратим особое внимание на функцию clone
. Если простыми словами, то это концепция в Rust, которая явным образом позволяет создавать копию переменной.
#[styled_component]
pub fn YewCounter(props: &Props) -> Html {
let on_increment = {
let on_increment = props.on_increment.clone();
Callback::from(move |_| {
let _ = on_increment.call0(&JsValue::NULL);
})
};
let on_decrement = {
let on_decrement = props.on_decrement.clone();
Callback::from(move |_| {
let _ = on_decrement.call0(&JsValue::NULL);
})
};
let layout = css!(
r#"
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;"#
);
let button = css!(
r#"
height: 65px;
width: 65px;
padding: 0;
border: none;
background-color: #aee18b;
color: #e16541;
font-size: 40px;
text-align: center;
box-shadow:
rgb(50 50 93 / 25%) 0px 2px 5px -1px,
rgb(0 0 0 / 30%) 0px 1px 3px -1px;
cursor: pointer;
& + & {
margin-left: 8px;
}"#
);
html! {
<div class={layout}>
<button class={button.clone()} onclick={on_increment}>{"+"}</button>
<button class={button} onclick={on_decrement}>{"-"}</button>
</div>
}
}
У нас нет никаких обработчиков, сейчас есть просто статический модуль, который загружает кнопки. Добавим немного магии в YewCounter
:
#[styled_component]
pub fn YewCounter(props: &Props) -> Html {
...
let on_increment = {
let on_increment = props.on_increment.clone();
Callback::from(move |_| {
let _ = on_increment.call0(&JsValue::NULL);
})
};
let on_decrement = {
let on_decrement = props.on_decrement.clone();
Callback::from(move |_| {
let _ = on_decrement.call0(&JsValue::NULL);
})
};
...
}
Согласитесь же, снова очень похоже на React
? Да есть некоторые отличия, но всё же похоже.
Давайте немного разберём создание функции. В Rust нет функций с переменным количеством параметров, поэтому для связи с JavaScript используется обертка над функцией call с передачей контекста и аргументов.
// on_decrement.call0(&JsValue::NULL) В Rust === on_decrement() в JavaScript
let on_decrement = {
let on_decrement = props.on_decrement.clone();
// Тут мы используем анонимную функцию и ключевое слово move.
// Подробнее про move https://doc.rust-lang.org/book/ch13-01-closures.html?highlight=move#capturing-references-or-moving-ownership
Callback::from(move |_| {
// Присваиваем к неиспользуемой переменной, чтобы вызвать панику. Подробнее про unwrap https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html?highlight=unwrap#shortcuts-for-panic-on-error-unwrap-and-expect
let _ = on_decrement.call0(&JsValue::NULL);
})
};
Компонент нашего приложения мы написали, теперь нужно добавить для него точку монтирования, и это будет lib.rs.
// Указываем имя модуля. Подробнее про модули в Rust. https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html
mod yew_counter;
// Добавляем возможность использовать Document API
use gloo::utils::document;
// И снова добавляем полифил для Wasm
use wasm_bindgen::prelude::*;
// Импортируем компонент и тип как JS
use yew_counter::{YewCounter, Props};
// Указываем, входную точку модуля
#[wasm_bindgen]
pub fn run_app(
root_id: String,
on_increment: js_sys::Function,
on_decrement: js_sys::Function,
// Небольшой след алгебраических типов данных
) -> Result<(), JsValue> {
// Добавляем проверку, что элемент с нужным id существует.
let root = match document().get_element_by_id(&root_id) {
Some(v) => v,
None => return Ok(()),
};
// Указываем, куда отрисовывать элемент и какие пропсы для это нужны
yew::Renderer::<App>::with_root_and_props(
root,
Props {
on_increment,
on_decrement,
},
)
.render();
// Указываем, что инициализация модуля произошла успешно
Ok(())
}
Очень похоже на вызов функции ReactDOM.createRoot
, хоть и более многословно.
Дальше нам нужно добавить возможность вызвать наш новоиспеченный модуль. Обновим поле plugins
в shell/webpack.config.ts:
...
const config: webpack.Configuration = {
...
plugins: [
new webpack.container.ModuleFederationPlugin({
name: 'shell',
remotes: {
// Used modules
react_counter:
'react_counter@http://localhost:3002/remoteEntry.js',
yew_counter: 'yew_counter@http://localhost:3003/remoteEntry.js',
},
shared: {
react: {singleton: true},
'react-dom': {singleton: true},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
...
};
export default config;
Добавим ленивую инициализацию модуля YewCounter
в shell/src/App.tsx и возможность обрабатывать ошибку загрузку модуля:
Код
import {FC, Suspense, lazy, useEffect, useState} from 'react';
import styles from './App.module.css';
/**
* Общие пропсы для любого счётчика
*/
type CounterProps = {
onIncrement: () => void;
onDecrement: () => void;
};
// Обработчик ошибки при импорте модуля
const onRejectedComponentHandler = (
error: unknown,
): {
default: FC<CounterProps>;
} => {
console.error('Component Failed Loading:', error);
return {
default: () => <div>Failed to Load</div>,
};
};
// Добавим возможность обрабатывать ошибку импорта модуля
const ReactCounter = lazy(() =>
import('react_counter/ReactCounter').catch(onRejectedComponentHandler),
);
const YEW_COUNTER = 'yew-counter';
// Добавим возможность ленивой загрузки YewCounter и обработку ошибки
const YewCounter = lazy(() =>
// импортируем модуль и инициализируем модуль
import('yew_counter/yew')
.then((module) => {
const YewCounter: FC<CounterProps> = ({
onDecrement,
onIncrement,
}) => {
useEffect(() => {
// Используя доступное из lib.rs API монтируем компонент
module.run_app(YEW_COUNTER, onIncrement, onDecrement);
}, []);
return <div id={YEW_COUNTER} />;
};
return {default: YewCounter};
})
.catch(onRejectedComponentHandler),
);
const App: FC = () => {
const [counter, setCounter] = useState(0);
const onIncrement = () => setCounter((c) => c + 1);
const onDecrement = () => setCounter((c) => c - 1);
return (
<div className={styles.root}>
<div className={styles.layout}>
<Suspense fallback="Loading ReactCounter">
<ReactCounter
onIncrement={onIncrement}
onDecrement={onDecrement}
/>
</Suspense>
<div className={styles.counter}>{counter}</div>
<Suspense fallback="Loading YewCounter">
<YewCounter
onIncrement={onIncrement}
onDecrement={onDecrement}
/>
</Suspense>
</div>
</div>
);
};
export default App;
Снова запускаем из корня yarn serve
.
Ихууу. Теперь мы можем менять состояние своего приложения и через React
, и через Yew
. Мы попробовали новую технологию, и, кажется, у нас получилось построить простое, но в то же время необычное приложения c использование Wasm. Полная версия кода.
Заключение
Как я уже говорил, в последнее время WebAssembly стремительно развивается и позволяет творить настоящую магию. Мы с вами разработали небольшое web-приложение, используя WebAssembly. Надеюсь, этот опыт поможет вам в дальнейшем.
«Всё, что должно быть сказано, уже сказано. Но поскольку этого никто не услышал, можно всё что угодно сказать ещё раз.» — Андре Жид