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
Теперь проверьте, успешно ли установлен 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
Вот, что вы должны увидеть:
Поздравляем!
Шаг 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.