Портирование приложения JavaScript в WebAssembly с помощью Rust

Портирование приложения JavaScript в WebAssembly с помощью Rust

TL; DR

Мы покажем, как сделать полный порт веб-приложения из React+Redux, написанного на JavaScript, в WebAssembly (WASM) с Rust.

Мотивация

Обслуживание программного обеспечения, написанного на динамически типизированном языке, таком как JavaScript, является дорогостоящим. Поддержка интерфейса JavaScript, созданного с помощью React, NPM, WebPack и Babel, еще дороже. Часто возникали ситуации, когда мы просто хотели обновить отдельную зависимость или плагин WebPack, что приводило к часам исправления проблем совместимости. Более того, из-за особенностей динамически типизированных языков вы никогда не узнаете, вызывает ли обновление библиотеки ошибки во время выполнения.

В прошлом у нас не было альтернативы JavaScript/NPM. Конечно, есть замечательный, чисто функциональный язык elm, который дает возможность писать надежные веб-интерфейсы. Но как только вы захотите покинуть мир функционального elm (например, для взаимодействия с JavaScript API), вам придется потратить много времени на построение «мостов».

Появление WebAssembly (WASM) – отличная возможность объединить мощь мира JavaScript с гарантиями времени компиляции и производительностью Rust.

За последние два года сообщество Rust создало более 10 веб-фреймворков, которые можно использовать для создания веб-интерфейсов с помощью WASM. Большинство из них пробными версиями, но некоторые – довольно серьезными проектами.

Мы хотим проверить, насколько далеко мы могли бы использовать Rust в качестве языка интерфейса. Для этого мы выбрали Seed в качестве одного из наиболее зрелых фреймворков.

Более того, мы хотим продемонстрировать реальный вариант использования вместо реализации еще одного приложения с TODO cписком. Вот почему мы собираемся перенести полный интерфейс kartevonmorgen.org на Rust. Karte von morgen – это проект с открытым исходным кодом для картирования устойчивых инициатив и организаций.

Давайте начнем 🙂

Шаг 1: Подготовка

Мы предполагаем, что вы уже знакомы с Rust и его экосистемой. Тем не менее, мы стараемся сделать это как можно проще для новичков и разработчиков JavaScript. Если вы знакомы с Rust, вы можете пропустить этот раздел.

Установка RUST

Для большинства пользователей rustup должен работать.

В этом руководстве мы используем Ubuntu Linux.

Сначала установите несколько основных инструментов:

sudo apt-get install git curl build-essential pkg-config libssl-dev

Затем запустите следующее в своем терминале:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Портирование приложения JavaScript в WebAssembly с помощью Rust

Теперь проверьте, успешно ли установлен Rust:

rustc -V
rustc 1.40.0 (73528e339 2019-12-16)

Установка wasm-pack

Чтобы иметь возможность упаковать наш веб-проект, нам нужен wasm-pack:

cargo install wasm-pack

Установка cargo-watch

Во время разработки вы можете запускать компилятор при каждом изменении файла. Для этого и нужен cargo-watch.

cargo install cargo-watch

Установка microserver

Для локального обслуживания вашего веб-приложения вы можете использовать ваш любимый веб-сервер. В этом руководстве мы используем microserver

cargo install microserver

Шаг 2: Инициализация проекта Seed

В рамках нашего унаследованного JavaScript проекта мы собираемся создать базовый проект Rust на основе фреймворка Seed.

git clone https://github.com/kartevonmorgen/kartevonmorgen
cd kartevonmorgen/
git checkout -b rust
cargo init --lib

Теперь нам нужно изменить файл Cargo.toml. Помимо имени мы должны добавить зависимости и сказать Rust, что эта библиотека – контейнер cdylib.

[package]
name = "kartevonmorgen"

[dependencies]
seed = "0.5"
wasm-bindgen = "0.2"

[lib]
crate-type = ["cdylib"]

Затем мы перемещаем существующий index.html в корень проекта и модифицируем его.

mv src/index.html .

Поскольку мы больше не будем использовать WebPack и его плагины, мы должны заменить все шаблонные части, которые соответствуют модели <% = …%>:

- <title><%= htmlWebpackPlugin.options.title %></ title>
+ <title>Porting JS на Rust</ title>

Дополнительно убедитесь, что кодировка документа установлена в utf-8:

<meta charset="utf-8" />

Теперь нам нужно добавить наш скрипт, который инициализирует модуль WASM. Обычно уже существует определенный контейнер приложения, например

  <div id="app"></ div>

В этом случае добавьте следующий тег script:

 <div id="app"></div>
+ <script type="module">
+   import init from '/pkg/kartevonmorgen.js';
+   init('/pkg/kartevonmorgen_bg.wasm').catch(console.error);
+ </script>

Примечание. Старые браузеры не поддерживают ES-модули (здесь мы используем Firefox 71).

Чтобы проверить, все ли работает как надо, начнем с простого приложения hello world.

#[macro_use]
extern crate seed;
use seed::prelude::*;

#[derive(Default)]
struct Mdl {
    // TODO
}

#[derive(Clone)]
enum Msg {
    // TODO
}

fn update(_: Msg, _: &mut Mdl, _: &mut impl Orders<Msg>) {
    // TODO
}

fn view(_: &Mdl) -> impl View<Msg> {
    div![h1!["Hello Rust"],]
}

#[wasm_bindgen(start)]
pub fn render() {
    seed::App::builder(update, view).build_and_start();
}

Постройте проект с

wasm-pack build --target web

и затем используйте

microserver

Вот, что вы должны увидеть:

Портирование приложения JavaScript в WebAssembly с помощью Rust

Поздравляем!

Шаг 3: переместить существующий код и очистить

Поскольку мы не хотим переписывать интерфейс с нуля, а портировать существующий код, мы сначала должны переименовать все файлы JavaScript (.js) и JSX (.jsx) в файлы Rust (.rs). Вместо того, чтобы делать это вручную, вы можете написать и запустить небольшой вспомогательный скрипт:

#!/bin/bash
for f in `find src/ -type f \( -iname \*.js -o -iname \*.jsx \)`
do
  git mv `echo $f` `echo $f | sed -e "s/\.jsx\?/\.rs/g"`
done
./rename-js-and-jsx-to-rs.sh

Вы также можете удалить устаревшие файлы, такие как .eslintrc, package-lock.json и т.д. В файле package.json мы найдем информацию, которую мы можем использовать повторно или, по крайней мере, оставить в качестве напоминания.

Большинство метаданных можно напрямую переместить в Cargo.toml, например, в следующие поля:

  • name (имя)
  • version (версия)
  • description (описание)
  • repository (хранилище)
  • author (автор)
  • license (лицензия)
  • homepage (домашняя страница)

Другая информация, такая как dependencies (зависимости) или scripts (сценарии), не может быть использована повторно. Но вместо того, чтобы убрать их, мы перемещаем их как комментарии в Cargo.toml, помеченный как TODO. Позже они послужат нам напоминанием о том, какие эквивалентные библиотеки Rust нам могут понадобиться в качестве замены этих JS-зависимостей.

Вот пример того, как это может выглядеть:

[dependencies]
seed = "0.5"
wasm-bindgen = "*"

### JS DEPENDENCIES ###

# TODO: "@fortawesome/react-fontawesome": "^0.1.3",
# TODO: "i18next": "^10.6.0",
# TODO: "leaflet": "^1.4.0",
# TODO: "normalize.css": "^8.0.1",
# TODO: "purecss": "^1.0.0",
# TODO: "react": "^16.8.2",
# TODO: "react-dom": "^16.8.2",
# TODO: "react-i18next": "^7.13.0",
# TODO: "react-leaflet": "^2.2.0",
# TODO: "react-redux": "^6.0.0",
# TODO: "redux": "^4.0.1",
# TODO: "redux-form": "^8.1.0",
# TODO: "redux-thunk": "^2.3.0",

Когда мы находим подходящую библиотеку Rust, мы можем заменить TODO фактической зависимостью или иным образом удалить ее полностью, если это не требуется для новой реализации Rust, например, babel, webpack и т.д.

В нашем случае все сценарии устарели:

  • lint сейчас cargo fmt
  • test сейчас cargo test
  • watch-test сейчас cargo watch -xt
  • pack сейчас wasm-pack build –release –target web

Не забудьте обновить README.md и CONTRIBUTING.md.

Шаг 4: Создание модулей

В начале мы сохраним структуру и имена оригинального приложения JS. Не поддавайтесь искушению переименовать файлы и модули в соответствии с соглашениями об именах Rust, то есть заменить CamelCase на snake_case. Позже мы сделаем это, но не сейчас.

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

#!/bin/bash
for f in `find src/ -type f -iname \*.rs`
  do
    awk -i inplace '{print "// TODO: " $0}' $f
    git add $f
done
./comment-out-rust-files.sh

Затем мы рекурсивно определяем наши модули. В lib.rs мы указываем модули верхнего уровня:

mod Actions;
mod GeoLocation;
mod Store;
mod WebAPI;
mod components;
mod constants;
mod i18n;
mod index;
mod rating;
mod reducers;
mod route;
mod util;
mod widgets;

В каждой папке мы должны создать файл mod.rs, в котором перечислены все содержащиеся субмодули.

В качестве примера наш src/components/mod.rs выглядит так:

pub mod App;
pub mod EntryForm;
pub mod Flower;
pub mod LandingPage;
pub mod Map;
pub mod SearchBar;
pub mod SelectTags;
pub mod Sidebar;
pub mod pure;
pub mod stories;
pub mod styling;

Вот результат структуры файла:

$ tree src
src
├── Actions
│   ├── client.rs
│   ├── mod.rs
│   └── server.rs
├── components
│   ├── App.rs
│   ├── EntryForm.rs
│   ├── Flower
│   │ ├── FlowerLeaf.rs
│   │ ├── index.rs
│   │ └── mod.rs
│   ├── LandingPage.rs
│   ├── Map.rs
│   ├── mod.rs
│   ├── pure
│   │ ├── AddressLine.rs
│   │ ├── BusinessCard.rs
│   │ ├── CityList.rs
│   │ ├── Contact.rs
│   │ ├── EntryDetails.rs

Вывод и следующие шаги

Мы создали базовое веб-приложение Seed в рамках существующего проекта JavaScript. Мы переместили существующий код JavaScript в закомментированные файлы Rust. Мы определили модули Rust, которые изначально отражают устаревшую структуру приложения JavaScript.


.

  • December 23, 2019