Обзор проекта

[server] ip = "127.0.0.1" port = "30080" tls = false [log] actix_web = "debug" webapp = "trace" [postgres] host = "127.0.0.1" username = "username" password = "password" database = "database"

Разработка клиентской части приложения

yew

yew

asmjs-unknown-emscripten — использует asm.js через Emscripten.

— использует asm.js через Emscripten. wasm32-unknown-emscripten — использует WebAssembly через Emscripten

— использует WebAssembly через Emscripten wasm32-unknown-unknown — использует WebAssembly с помощью нативного бэкенда Rust для WebAssembly

WebAssembly

make frontend

cargo-web

cargo-web

> make frontend Compiling webapp v0.3.0 (file:///home/sascha/webapp.rs) Finished release [optimized] target(s) in 11.86s Garbage collecting "app.wasm"... Processing "app.wasm"... Finished processing of "app.wasm"! If you need to serve any extra files put them in the 'static' directory in the root of your crate; they will be served alongside your application. You can also put a 'static' directory in your 'src' directory. Your application is being served at '/app.js'. It will be automatically rebuilt if you make any changes in your code. You can access the web server at `http://0.0.0.0:8000`.

yew

<body>

LoginComponent

RootComponent

ContentComponent

Внешний вид компонента LoginComponent

RootComponent

RootComponent

LoginComponent

Компонент ContentComponent

loading

error

RootComponent

yew

yew

yew

yew

Renderable

LoginComponent

impl Renderable<LoginComponent> for LoginComponent { fn view(&self) -> Html<Self> { html! { <div class="uk-card uk-card-default uk-card-body uk-width-1-3@s uk-position-center",> <form onsubmit="return false",> <fieldset class="uk-fieldset",> <legend class="uk-legend",>{"Authentication"}</legend> <div class="uk-margin",> <input class="uk-input", placeholder="Username", value=&self.username, oninput=|e| Message::UpdateUsername(e.value), /> </div> <div class="uk-margin",> <input class="uk-input", type="password", placeholder="Password", value=&self.password, oninput=|e| Message::UpdatePassword(e.value), /> </div> <button class="uk-button uk-button-default", type="submit", disabled=self.button_disabled, onclick=|_| Message::LoginRequest,>{"Login"}</button> <span class="uk-margin-small-left uk-text-warning uk-text-right",> {&self.error} </span> </fieldset> </form> </div> } } }

yew

Протокол Cap’n Proto

@0x998efb67a0d7453f; struct Request { union { login :union { credentials :group { username @0 :Text; password @1 :Text; } token @2 :Text; } logout @3 :Text; # The session token } } struct Response { union { login :union { token @0 :Text; error @1 :Text; } logout: union { success @2 :Void; error @3 :Text; } } }

LoginComponent

RootComponent

UIkit — компактный модульный фронтенд-фреймворк для разработки быстрых и мощных веб-интерфейсов

▍Тестирование фронтенда

yew

Разработка серверной части приложения

/ws — основной ресурс для WebSocket-коммуникаций.

— основной ресурс для WebSocket-коммуникаций. / — основной обработчик, который даёт доступ к статическому фронтенд-приложению.

actix-web

СУБД PostgreSQL и проект Diesel

actix-web

UpdateSession

actix-web

impl Handler<UpdateSession> for DatabaseExecutor { type Result = Result<Session, Error>; fn handle(&mut self, msg: UpdateSession, _: &mut Self::Context) -> Self::Result { // Обновить сессию debug!("Updating session: {}", msg.old_id); update(sessions.filter(id.eq(&msg.old_id))) .set(id.eq(&msg.new_id)) .get_result::<Session>(&self.0.get()?) .map_err(|_| ServerError::UpdateToken.into()) } }

actix-web

▍Тестирование бэкенда

Развёртывание проекта

Docker

make deploy

webapp

Config.toml

make run

Технологии, использованные при разработке проекта

Технологии, использованные при разработке веб-приложения на Rust

Итоги. Готов ли Rust к веб-продакшну?

actix-web

Автор материала, перевод которого мы сегодня публикуем, говорит, что его самым свежим экспериментом в области архитектуры программных проектов стало создание рабочего веб-приложения с использованием исключительно языка Rust и с минимально возможным применением шаблонного кода. В этом материале он хочет поделиться с читателями тем, что он выяснил, разрабатывая приложение и отвечая на вопрос о том, готов ли уже Rust к применению его в различных сферах веб-разработки.Код проекта, о котором здесь пойдёт речь, можно найти на GitHub . Клиентская и серверная части приложения расположены в одном и том же репозитории, это сделано для упрощения сопровождения проекта. Надо отметить, что Cargo понадобится компилировать фронтенд и бэкенд приложения с разными зависимостями. Здесь можно взглянуть на работающее приложение.Наш проект представляет собой простую демонстрацию механизма аутентификации. Он позволяет войти в систему с выбранным именем пользователя и паролем (они должны быть одинаковыми).Если имя и пароль различаются, аутентификация окажется неудачной. После успешной аутентификации токен JWT (JSON Web Token) сохраняется и на стороне клиента, и на стороне сервера. Хранение токена на сервере в подобных приложениях обычно не требуется, но я поступил именно так в демонстрационных целях. Это, например, может быть использовано, для того, чтобы узнать о том, сколько пользователей вошли в систему. Всё приложение можно конфигурировать посредством единственного файла Config.toml , например, указывая учётные сведения для доступа к базе данных, или адрес и номер порта сервера. Вот как выглядит стандартный код этого файла для нашего приложения.Для разработки клиентской части приложения я решил использовать yew . Это — современный Rust-фреймворк, разработчиков которого вдохновили Elm, Angular и React. Он предназначен для создания клиентских частей многопоточных веб-приложений с использованием WebAssembly (Wasm). В настоящий момент этот проект находится в стадии активной разработки, пока имеется не особенно много его стабильных релизов.Фреймворкполагается на инструмент cargo-web , который предназначен для кросс-компиляции кода в Wasm.Инструмент cargo-web — это прямая зависимость, которая упрощает кросс-компиляцию Rust-кода в Wasm. Вот три основных цели компиляции Wasm, доступных в рамках этого средства:Я решил использовать последний вариант, который требует использования «ночной» сборки компилятора Rust, но в лучшем виде демонстрирует нативные Wasm-возможности Rust.Если говорить о WebAssembly, то в разговорах о Rust сегодня это — самая горячая тема. В настоящий момент ведётся огромная работа, связанная с кросс-компиляцией Rust в Wasm и с интеграцией его в экосистему Node.js (с использованием npm-пакетов). Я решил реализовать проект без каких-либо JavaScript-зависимостей.При запуске фронтенда веб-приложения (в моём проекте это делается командой),выполняет кросс-компиляцию приложения в Wasm и упаковывает его, добавляя некоторые статические материалы. Затемзапускает локальный веб-сервер, который позволяет взаимодействовать с приложением для целей разработки. Вот что происходит в консоли при запуске вышеупомянутой команды:Фреймворкобладает некоторыми весьма интересными возможностями. Среди них — поддержка архитектуры компонентов, подходящих для повторного использования. Эта возможность упростила разбиение моего приложения на три основных компонента: RootComponent . Этот компонент напрямую монтируется к тегувеб-сайта. Он принимает решение о том, какой дочерний компонент должен быть загружен следующим. Если, при первом входе на страницу, найден токен JWT, он пытается обновить этот токен, связавшись с серверной частью приложения. Если сделать это не удаётся, осуществляется переход к компоненту LoginComponent . Этот компонент является потомком компонента, он содержит форму с полями для ввода учётных данных. Кроме того, он осуществляет взаимодействие с бэкендом приложения для организации простой схемы аутентификации, основанной на проверке имени пользователя и пароля, и, в случае успешной аутентификации, сохраняет JWT в куки-файле. Кроме того, если пользователя удалось аутентифицировать, он осуществляет переход к компоненту ContentComponent . Данный компонент является ещё одним потомком компонента. Он содержит то, что выводится на главной странице приложения (в настоящий момент это — лишь заголовок и кнопка для выхода из системы). Доступ к нему можно получить через(если приложению, при запуске, удалось найти действительный токен сессии), или через(в случае успешной аутентификации). Этот компонент обменивается данными с бэкендом тогда, когда пользователь нажимает на кнопку выхода из системы. RouterComponent . Данный компонент хранит все возможные маршруты между компонентами, содержащими контент. Кроме того, он содержит исходные состояния приложения. Он напрямую подключён кОдной из следующих ключевых концепций, которую мы сейчас обсудим, являются сервисы. Они позволяют повторно использовать одну и ту же логику в различных компонентах. Скажем, это могут быть интерфейсы логирования или средства для поддержки работы с куки . Сервисы не хранят некое глобальное состояние, они создаются при инициализации компонентов. Помимо сервисовподдерживает концепцию агентов. Их можно использовать для организации совместного использования данных различными компонентами, для поддержки общего состояния приложения, как например того, которое нужно для агента, отвечающего за маршрутизацию. Для организации системы маршрутизации нашего приложения, охватывающей все компоненты, здесь были реализованы собственные агент и сервис маршрутизации . Внет стандартного маршрутизатора, но в репозитории фреймворка можно найти пример реализации маршрутизатора, который поддерживает самые разные операции с URL.С удовольствием отмечаю, чтоиспользует API Web Workers для запуска агентов в различных потоках и использует локальный планировщик, прикреплённый к потоку для решения параллельных задач. Это даёт возможность разрабатывать на Rust браузерные приложения с высокой степенью многопоточности.Каждый компонент реализует собственный типаж Renderable , который позволяет нам включать HTML-код напрямую в исходный код на Rust, используя макрос html!{} Возможность это замечательная, и, конечно, её правильное использование контролирует компилятор. Вот код реализациив компонентеСвязь между фронтендом и бэкендом реализована на базе WebSocket -соединений, которые используются каждым клиентом. Сильной стороной технологии WebSocket является тот факт, что она подходит для передачи бинарных сообщений, а также то, что сервер, при необходимости, может отправлять пуш-уведомления клиентам. Весть стандартный сервис WebSocket, однако я решил создать его собственную версию в демонстрационных целях, преимущественно из-за «ленивой» инициализации соединений прямо внутри сервиса. Если сервис WebSocket создавался бы в ходе инициализации компонента, мне пришлось бы отслеживать множество соединений.Я решил использовать, в качестве слоя передачи данных приложения, протокол Cap’n Proto (вместо чего-то наподобие JSON MessagePack или CBOR ) из соображений скорости и компактности. Тут стоит отметить, что я не использовал интерфейс протокола RPC , который есть в Cap’n Proto, так как его Rust-реализация не компилируется для WebAssembly (из-за Unix-зависимостей tokio-rs ). Это несколько усложнило выделение запросов и ответов правильных типов, но эту проблему можно решить с помощью чётко структурированного API . Вот объявление протокола Cap’n Proto для приложения.Вы можете видеть, что тут у нас имеются два различных варианта запроса на вход в систему.Один — для(тут, для получения токена, используются имя и пароль), и ещё один — для(он применяется для обновления уже существующего токена). Всё, что нужно для работы протокола, упаковано в сервисе protocol , благодаря которому соответствующие возможности удобно переиспользовать в различных частях фронтенда.Пользовательский интерфейс клиентской части приложения основан на фреймворке UIkit , его версия 3.0.0 выйдет в ближайшем будущем. Специально подготовленный скрипт build.rs автоматически загружает все необходимые зависимости UIkit и компилирует итоговую таблицу стилей. Это означает, что в единственный файл style.scss можно добавлять собственные стили, которые могут быть применены в масштабах всего приложения. Это очень удобно.Я полагаю, что с тестированием нашего решения имеются некоторые проблемы. Дело в том, что отдельные сервисы тестировать очень просто, ноне предоставляет разработчику удобного способа тестирования компонентов и агентов. Сейчас, в рамках чистого Rust, недоступно интеграционное и сквозное тестирование фронтенда. Тут можно было бы воспользоваться проектами вроде Cypress или Protractor , но при таком подходе в проект пришлось бы включить очень много шаблонного JavaScript/TypeScript кода, поэтому я решил отказаться от реализации подобных тестов.Кстати, вот вам идея для нового проекта: фреймворк для сквозного тестирования, написанный на Rust.Для реализации серверной части приложения я выбрал фреймворк actix-web . Это компактный, практичный и очень быстрый Rust-фреймворк, основанный на модели акторов . Он поддерживает все необходимые технологии, вроде WebSockets, TLS и HTTP/2.0 . Этот фреймворк поддерживает различные обработчики и ресурсы, но в нашем приложении была использована лишь пара основных маршрутов:По умолчаниюзапускает рабочие процессы в количестве, соответствующем количеству процессорных ядер, доступных на локальном компьютере. Это означает то, что если у приложения есть состояние, его надо будет безопасно разделять между всеми потоками, но, благодаря надёжным шаблонам параллельных вычислений Rust, проблемой это не является. Как бы там ни было, бэкенд должен представлять собой систему без состояния, так как множество его копий может быть развёрнуто параллельно в облачном окружении (наподобие Kubernetes ). В результате данные, формирующие состояние приложения, должны быть отделены от бэкенда. Например, они могут находиться внутри отдельного экземпляра контейнера Docker В качестве основного хранилища данных я решил использовать СУБД PostgreSQL . Почему? Этот выбор определило существование замечательного проекта Diesel , который уже поддерживает PostgreSQL и предлагает безопасную и расширяемую ORM-систему и средство построения запросов для неё. Всё это отлично соответствует нуждам нашего проекта, так какуже поддерживает Diesel. В результате тут, для выполнения CRUD-операций с информацией о сессиях в базе данных, можно использовать особый язык, учитывающий специфику Rust. Вот пример обработчикадля, основанного на Diesel.rs.Для установления соединения междуи Diesel используется проект r2d2 . Это означает, что у нас имеется (помимо приложения с его рабочими процессами) разделяемое состояние приложения, которое поддерживает множество подключений к базе данных в виде единого пула соединений. Это чрезвычайно упрощает серьёзное масштабирование бэкенда, делает такое решение гибким. Здесь можно найти код, ответственный за создание экземпляра сервера. Интеграционное тестирование бэкенда в нашем проекте выполняется путём запуска тестового экземпляра сервера и подключения к уже работающей базе данных. Затем можно воспользоваться стандартным WebSocket-клиентом (я пользовался tungstenite ) для отправки серверу данных, сформированных с учётом особенностей протокола Cap’n Proto, и сопоставления результатов с ожидаемыми. Эта схема тестирования отлично показала себя. Я не использовал специальные тестовые серверы actix-web , так как для настройки и запуска реального сервера не требуется намного большего объёма работы. Модульное тестирование бэкенда оказалось, как и ожидалось, довольно простым занятием, особых проблем проведение таких тестов не вызывает.Приложение очень легко развернуть, воспользовавшись образом Docker.С помощью командыможно создать образ, который называетсяи содержит статически связанные исполняемые файлы бэкенда, текущий файл, TLS-сертификаты и статический контент фронтенда. Сборка полностью статически связанных исполняемых файлов в Rust реализуется с помощью модифицированного варианта Docker-образа rust-musl-builder . Готовое веб-приложение можно испытать, воспользовавшись командой, которая запускает контейнер с поддержкой сети. Контейнер PostgreSQL, для обеспечения работы системы, должен быть запущен параллельно с контейнером приложения. В целом, процесс развёртывания нашей системы довольно прост, кроме того, благодаря использованным здесь технологиям, можно говорить о его достаточной гибкости, упрощающей его возможную адаптацию к нуждам развивающегося приложения.Вот схема зависимостей приложения.Единственный компонент, которым пользуется и фронтенд, и бэкенд — это Rust-версия Cap’n Proto, для создания которой требуется локально установленный компилятор Cap’n Proto.Это — большой вопрос. Вот что я могу на него ответить. С точки зрения серверов я склоняюсь к ответу «да», так как экосистема Rust, помимо, имеет весьма зрелый HTTP-стек и множество самых разных фреймворков для быстрой разработки серверных API и сервисов.Если же говорить о фронтенде, то тут, благодаря всеобщему вниманию к WebAssembly, сейчас идёт огромная работа. Однако, проекты, создаваемые в этой области, должны достичь той же зрелости, которой достигли серверные проекты. В особенности это касается стабильности API и возможностей по тестированию. Поэтому сейчас я говорю «нет» использованию Rust во фронтенде, однако не могу не отметить, что он движется в правильном направлении.