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(), );

В браузере модуль скачивается, парсится и исполняется. 

Wasm в браузере

Wasm в браузере

Кажется, что всё довольно просто. Благодаря всей этой магии открылась возможность запускать десктопные приложения в браузере или использовать код на низкоуровневых языках.

Не будем сильно углубляться в эту тему, уж очень много статей написано. Подробнее ознакомиться можно тут. 

Достоинства и недостатки 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 можно писать и полноценные фронтенд-приложения. Вот список ингредиентов для приготовления проекта:

  1. Yew. Фреймворк для написания веб-приложений на WebAssembly, использующий язык Rust.

  2. React. Одна из популярных библиотек для создания пользовательских интерфейсов.

  3. Yarn . Менеджер пакетов, который одновременно выполняет функции менеджера проекта.

  4. Webpack. Сборщик приложений.

  5. Typescript. Надстройка над JavaScript c добавлением типизации.

  6. Prettier. Форматер кода.

  7. 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. Проект изменился, теперь он выглядит так:

Версия приложения с добавлением ReactCounter.

Версия приложения с добавлением ReactCounter.

Отличной, второй кусочек нашего приложения готов. Теперь мы можем добавить модуль на 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

Версия приложения с ReactCounter и YewCounter.

Версия приложения с ReactCounter и YewCounter.

Ихууу. Теперь мы можем менять состояние своего приложения и через React, и через Yew. Мы попробовали новую технологию, и, кажется, у нас получилось построить простое, но в то же время необычное приложения c использование Wasm. Полная версия кода.

Заключение

Как я уже говорил, в последнее время WebAssembly стремительно развивается и позволяет творить настоящую магию. Мы с вами разработали небольшое web-приложение, используя WebAssembly. Надеюсь, этот опыт поможет вам в дальнейшем.

«Всё, что должно быть сказано, уже сказано. Но поскольку этого никто не услышал, можно всё что угодно сказать ещё раз.» — Андре Жид


ссылка на оригинал статьи https://habr.com/ru/articles/775852/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *