В последнее время фронтенд все больше и больше обрастает новыми технологиями, одна из них — 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. Надеюсь, этот опыт поможет вам в дальнейшем.
«Всё, что должно быть сказано, уже сказано. Но поскольку этого никто не услышал, можно всё что угодно сказать ещё раз.» — Андре Жид
ссылка на оригинал статьи https://habr.com/ru/articles/775852/
Добавить комментарий