Привет, друзья!
На днях прочитал интересную статью, в которой демонстрируется возможность использования WebAssembly-модулей
(далее — Wasm
), скомпилированных из Rust
, в React-приложении
.
Так вот, статья интересная, но автор толком ничего не объясняет, видимо, исходя из предположения, что читатели, как и он, владеют обоими языками программирования (JavaScript
и Rust
).
Поскольку я не отношусь к этой категории (пока не знаю Rust
), но люблю как следует разбираться в интересующих меня вещах, представляю вашему вниманию собственную версию.
Если вам это интересно, прошу под кат.
Если вы впервые слышите о Wasm
, вот статья, в которой освещаются некоторые связанные с ним общие вопросы.
Предполагается, что вы знакомы с React.js
, имеете общее представление о Node.js
и хотя бы раз настраивали какой-нибудь сборщик модулей типа Webpack
(я буду использовать Snowpack
).
Разумеется, на вашей машине должен быть установлен Node.js
и Rust
.
На Mac
это делается так:
# устанавливаем Node.js
brew install node@16 # lts версия
# устанавливаем Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Подготовка проекта
Создаем шаблон React-проекта
с помощью Snowpack
:
# react-rust - название проекта
# --template @snowpack/app-template-react - название используемого шаблона
# --use-yarn - использовать yarn вместо npm для установки зависимостей, опционально
yarn create snowpack-app react-rust --template @snowpack/app-template-react --use-yarn
# или
npx create-snowpack-app ...
Переходим в созданную директорию и инициализируем Rust-проект
:
# переходим в директорию
cd react-rust
# инициализируем проект
cargo init --lib
Cargo
— это пакетный менеджер (package manager) Rust
(аналог npm
, входит в состав Rust
). Он устанавливает зависимости, компилирует пакеты, создает распространяемые пакеты и загружает их в crates.io
(реестр пакетов Rust
, аналог npmjs.com
).
Команда cargo init
создает новый пакет в существующей директории. Флаг --lib
создает пакет с целевой библиотекой (src/lib.rs
, файлы с кодом на Rust
, как правило, имеют расширение rs
). Целевая библиотека — это "библиотека", которая может быть использована другими библиотеками и исполняемыми файлами (executables). Один пакет может иметь только одну библиотеку.
cargo
не умеет компилировать Rust
в Wasm
. Для этого нам потребуется пакет wasm-bindgen
(данный пакет входит в состав wasm-pack
).
Он, в частности, позволяет импортировать "вещи" из JavaScript
в Rust
и экспортировать "вещи" из Rust
в JavaScript
(цитата из документации).
Также нам необходимо сообщить компилятору, что типом пакета является cdylib
. Указание cdylib
приводит к генерации динамической системной библиотеки (dynamic system library). Этот тип используется "при компиляции динамической библиотеки, загружаемой из другого языка программирования".
Редактируем Cargo.toml
(аналог package.json
, создается при инициализации Rust-проекта
):
[package]
name = "react-rust"
version = "1.0.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
Выполняем сборку Rust-приложения
:
cargo build # данная команда компилирует пакеты и все их зависимости
Это приводит к генерации директории target
. В ней пока нет ничего интересного, но скоро мы это исправим.
Для того, чтобы сборка содержала Wasm-файл
, необходимо явно определить цель сборки:
rustup target add wasm32-unknown-unknown
rustup
— это установщик набора инструментов (toolchain installer) Rust
. target add
позволяет определить цель компиляции.
Что означает wasm32-unknown-unknown
? Первый unknown
означает систему, в которой выполняется компиляция, второй — систему, для которой выполняется компиляция. wasm32
означает, что адресное пространство имеет размер 32 бита (источник).
Редактируем src/lib.rs
(возьмем пример из документации wasm-bindgen
):
// импорт пакета
// https://doc.rust-lang.org/beta/reference/names/preludes.html
// https://stackoverflow.com/questions/36384840/what-is-the-prelude
use wasm_bindgen::prelude::*;
// импорт функции `window.alert` из "Веба"
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
// экспорт функции `greet` в JavaScript
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
Выполняем сборку Rust-приложения
, указывая нужную цель:
cargo build --target wasm32-unknown-unknown
Это приводит к генерации интересующего нас файла target/wasm32-unknown-unknown/debug/react_rust.wasm
. debug
означает, что мы выполнили сборку для разработки. Для создания продакш-сборки используется команда cargo build --release
(выполнение этой команды приводит к генерации директории target/wasm32-unknown-unknown/release
).
Устанавливаем плагин @emily-curry/snowpack-plugin-wasm-pack
. Данный плагин генерирует обертку для Wasm
, состоящую из набора JS
и TS-файлов
, в частности, index.js
, экспортирующего функцию greet
, которую мы будем использовать в React-приложении
.
Редактируем snowpack.config.mjs
:
export default {
mount: {
public: { url: '/', static: true },
src: { url: '/dist' },
// это позволяет импортировать файлы из директории pkg,
// находящейся за пределами директории src
pkg: { url: '/pkg' }
},
plugins: [
'@snowpack/plugin-react-refresh',
'@snowpack/plugin-dotenv',
// плагин для создания обертки
[
'@emily-curry/snowpack-plugin-wasm-pack',
{
// директория проекта, содержащая файл Cargo.toml
projectPath: '.'
}
]
],
// ...
Для работы плагина требуется cargo-watch
и wasm-pack
. wasm-pack
устанавливается как зависимость wasm-bindgen
.
cargo-watch
выполняет соответствующие команды cargo
при изменении файлов проекта (аналог nodemon
). Устанавливаем его:
cargo install cargo-watch
Теперь займемся React-приложением
.
Редактируем src/App.jsx
:
import React, { useState } from 'react'
// импортируем функцию инициализации и
// нашу функцию `greet`
import init, { greet } from '../pkg'
function App() {
// состояние для имени
const [name, setName] = useState('')
// функция изменения имени
const changeName = ({ target: { value } }) => setName(value)
// функция приветствия
const sayHello = async (e) => {
e.preventDefault()
const trimmed = name.trim()
if (!trimmed) return
// выполняем инициализацию
await init()
// вызываем нашу функцию
greet(name)
}
return (
<div className='app'>
<h1>React Rust</h1>
<form onSubmit={sayHello}>
<fieldset>
<label htmlFor='name'>Enter your name</label>
<input
type='text'
id='name'
value={name}
onChange={changeName}
autoFocus
/>
</fieldset>
<button>Say hello</button>
</form>
</div>
)
}
export default App
Запускаем проект в режиме разработки:
yarn start
# or
npm start
Обратите внимание: здесь может возникнуть ошибка 404 Not Found
, связанная с тем, что сервер для разработки запускается до генерации директории pkg
, в которую помещаются файлы, скомпилированные с помощью плагина @emily-curry/snowpack-plugin-wasm-pack
(из этой директории импортируется функция greet
). В этом случае просто перезапустите сервер и все будет ок ????
Вводим имя и нажимаем кнопку Say hello
:
Функция greet
, написанная на Rust
и скомпилированная в Wasm
, работает в JS
. Круто!
Выполняем сборку для продакшна:
yarn build
# or
npm run build
Это приводит к генерации директории build
со всеми файлами проекта (настройка сборки для продакшна выполняется с помощью раздела buildOptions
файла snowpack.config.js
).
Поскольку типом скрипта, подключаемого в index.html
, является module
, запустить проект с помощью расширения VSCode
типа Live Server
не получится — сработает блокировка CORS
.
Что делать? Писать сервер? Есть вариант получше.
Устанавливаем serve
глобально:
yarn global add serve
# or
npm i -g serve
Запускаем проект:
# флаг -s или --single означает, что отсутствующие пути
# будут перенаправляться к index.html
serve -s build
# or without install
npx serve -s build
Получаем адрес сайта, переходим по нему, видим наше приложение.
Вводим имя, нажимаем Say hello
, получаем приветствие. Да, мы сделали это!
Пожалуй, это все, чем я хотел поделиться с вами в этой статье.
Надеюсь, вам было интересно и вы не зря потратили время.
Благодарю за внимание и happy coding!
john_samilin
А зачем инициализация происходит несколько раз?