Главная » Хабрахабр » [Перевод] Разработка веб-приложения на Rust

[Перевод] Разработка веб-приложения на Rust

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

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

Код проекта, о котором здесь пойдёт речь, можно найти на GitHub. Клиентская и серверная части приложения расположены в одном и том же репозитории, это сделано для упрощения сопровождения проекта. Надо отметить, что Cargo понадобится компилировать фронтенд и бэкенд приложения с разными зависимостями. Здесь можно взглянуть на работающее приложение.

Он позволяет войти в систему с выбранным именем пользователя и паролем (они должны быть одинаковыми). Наш проект представляет собой простую демонстрацию механизма аутентификации.

После успешной аутентификации токен JWT (JSON Web Token) сохраняется и на стороне клиента, и на стороне сервера. Если имя и пароль различаются, аутентификация окажется неудачной. Это, например, может быть использовано, для того, чтобы узнать о том, сколько пользователей вошли в систему. Хранение токена на сервере в подобных приложениях обычно не требуется, но я поступил именно так в демонстрационных целях. Вот как выглядит стандартный код этого файла для нашего приложения. Всё приложение можно конфигурировать посредством единственного файла Config.toml, например, указывая учётные сведения для доступа к базе данных, или адрес и номер порта сервера.

[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. Это — современный Rust-фреймворк, разработчиков которого вдохновили Elm, Angular и React. Он предназначен для создания клиентских частей многопоточных веб-приложений с использованием WebAssembly (Wasm). В настоящий момент этот проект находится в стадии активной разработки, пока имеется не особенно много его стабильных релизов.

Фреймворк yew полагается на инструмент cargo-web, который предназначен для кросс-компиляции кода в Wasm.

Вот три основных цели компиляции Wasm, доступных в рамках этого средства: Инструмент cargo-web — это прямая зависимость yew, которая упрощает кросс-компиляцию Rust-кода в Wasm.

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

WebAssembly

В настоящий момент ведётся огромная работа, связанная с кросс-компиляцией Rust в Wasm и с интеграцией его в экосистему Node.js (с использованием npm-пакетов). Я решил использовать последний вариант, который требует использования «ночной» сборки компилятора Rust, но в лучшем виде демонстрирует нативные Wasm-возможности Rust.
Если говорить о WebAssembly, то в разговорах о Rust сегодня это — самая горячая тема. Я решил реализовать проект без каких-либо JavaScript-зависимостей.

Затем cargo-web запускает локальный веб-сервер, который позволяет взаимодействовать с приложением для целей разработки. При запуске фронтенда веб-приложения (в моём проекте это делается командой make frontend), cargo-web выполняет кросс-компиляцию приложения в Wasm и упаковывает его, добавляя некоторые статические материалы. Вот что происходит в консоли при запуске вышеупомянутой команды:

> 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> веб-сайта. RootComponent. Если, при первом входе на страницу, найден токен JWT, он пытается обновить этот токен, связавшись с серверной частью приложения. Он принимает решение о том, какой дочерний компонент должен быть загружен следующим. Если сделать это не удаётся, осуществляется переход к компоненту LoginComponent.

Этот компонент является потомком компонента RootComponent, он содержит форму с полями для ввода учётных данных. LoginComponent. Кроме того, если пользователя удалось аутентифицировать, он осуществляет переход к компоненту ContentComponent. Кроме того, он осуществляет взаимодействие с бэкендом приложения для организации простой схемы аутентификации, основанной на проверке имени пользователя и пароля, и, в случае успешной аутентификации, сохраняет JWT в куки-файле.

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

Данный компонент является ещё одним потомком компонента RootComponent. ContentComponent. Доступ к нему можно получить через RootComponent (если приложению, при запуске, удалось найти действительный токен сессии), или через LoginComponent (в случае успешной аутентификации). Он содержит то, что выводится на главной странице приложения (в настоящий момент это — лишь заголовок и кнопка для выхода из системы). Этот компонент обменивается данными с бэкендом тогда, когда пользователь нажимает на кнопку выхода из системы.

Компонент ContentComponent

Данный компонент хранит все возможные маршруты между компонентами, содержащими контент. RouterComponent. Он напрямую подключён к RootComponent. Кроме того, он содержит исходные состояния приложения loading и error.

Они позволяют повторно использовать одну и ту же логику в различных компонентах. Одной из следующих ключевых концепций yew, которую мы сейчас обсудим, являются сервисы. Сервисы не хранят некое глобальное состояние, они создаются при инициализации компонентов. Скажем, это могут быть интерфейсы логирования или средства для поддержки работы с куки. Их можно использовать для организации совместного использования данных различными компонентами, для поддержки общего состояния приложения, как например того, которое нужно для агента, отвечающего за маршрутизацию. Помимо сервисов yew поддерживает концепцию агентов. В yew нет стандартного маршрутизатора, но в репозитории фреймворка можно найти пример реализации маршрутизатора, который поддерживает самые разные операции с URL. Для организации системы маршрутизации нашего приложения, охватывающей все компоненты, здесь были реализованы собственные агент и сервис маршрутизации.

Это даёт возможность разрабатывать на Rust браузерные приложения с высокой степенью многопоточности. С удовольствием отмечаю, что yew использует API Web Workers для запуска агентов в различных потоках и использует локальный планировщик, прикреплённый к потоку для решения параллельных задач.

Каждый компонент реализует собственный типаж Renderable, который позволяет нам включать HTML-код напрямую в исходный код на Rust, используя макрос html!.

Вот код реализации 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> } }
}

Связь между фронтендом и бэкендом реализована на базе WebSocket-соединений, которые используются каждым клиентом. Сильной стороной технологии WebSocket является тот факт, что она подходит для передачи бинарных сообщений, а также то, что сервер, при необходимости, может отправлять пуш-уведомления клиентам. В yew есть стандартный сервис WebSocket, однако я решил создать его собственную версию в демонстрационных целях, преимущественно из-за «ленивой» инициализации соединений прямо внутри сервиса. Если сервис WebSocket создавался бы в ходе инициализации компонента, мне пришлось бы отслеживать множество соединений.

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

Тут стоит отметить, что я не использовал интерфейс протокола RPC, который есть в Cap’n Proto, так как его Rust-реализация не компилируется для WebAssembly (из-за Unix-зависимостей tokio-rs). Я решил использовать, в качестве слоя передачи данных приложения, протокол Cap’n Proto (вместо чего-то наподобие JSON, MessagePack или CBOR) из соображений скорости и компактности. Вот объявление протокола Cap’n Proto для приложения. Это несколько усложнило выделение запросов и ответов правильных типов, но эту проблему можно решить с помощью чётко структурированного API.

@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; } }
}

Вы можете видеть, что тут у нас имеются два различных варианта запроса на вход в систему.

Всё, что нужно для работы протокола, упаковано в сервисе protocol, благодаря которому соответствующие возможности удобно переиспользовать в различных частях фронтенда. Один — для LoginComponent (тут, для получения токена, используются имя и пароль), и ещё один — для RootComponent (он применяется для обновления уже существующего токена).

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

0. Пользовательский интерфейс клиентской части приложения основан на фреймворке UIkit, его версия 3. Специально подготовленный скрипт build.rs автоматически загружает все необходимые зависимости UIkit и компилирует итоговую таблицу стилей. 0 выйдет в ближайшем будущем. Это очень удобно. Это означает, что в единственный файл style.scss можно добавлять собственные стили, которые могут быть применены в масштабах всего приложения.

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

Я полагаю, что с тестированием нашего решения имеются некоторые проблемы. Дело в том, что отдельные сервисы тестировать очень просто, но yew не предоставляет разработчику удобного способа тестирования компонентов и агентов. Сейчас, в рамках чистого Rust, недоступно интеграционное и сквозное тестирование фронтенда. Тут можно было бы воспользоваться проектами вроде Cypress или Protractor, но при таком подходе в проект пришлось бы включить очень много шаблонного JavaScript/TypeScript кода, поэтому я решил отказаться от реализации подобных тестов.

Кстати, вот вам идея для нового проекта: фреймворк для сквозного тестирования, написанный на Rust.

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

Для реализации серверной части приложения я выбрал фреймворк actix-web. Это компактный, практичный и очень быстрый Rust-фреймворк, основанный на модели акторов. Он поддерживает все необходимые технологии, вроде WebSockets, TLS и HTTP/2.0. Этот фреймворк поддерживает различные обработчики и ресурсы, но в нашем приложении была использована лишь пара основных маршрутов:

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

По умолчанию actix-web запускает рабочие процессы в количестве, соответствующем количеству процессорных ядер, доступных на локальном компьютере. Это означает то, что если у приложения есть состояние, его надо будет безопасно разделять между всеми потоками, но, благодаря надёжным шаблонам параллельных вычислений Rust, проблемой это не является. Как бы там ни было, бэкенд должен представлять собой систему без состояния, так как множество его копий может быть развёрнуто параллельно в облачном окружении (наподобие Kubernetes). В результате данные, формирующие состояние приложения, должны быть отделены от бэкенда. Например, они могут находиться внутри отдельного экземпляра контейнера Docker.

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

Почему? В качестве основного хранилища данных я решил использовать СУБД PostgreSQL. Всё это отлично соответствует нуждам нашего проекта, так как actix-web уже поддерживает Diesel. Этот выбор определило существование замечательного проекта Diesel, который уже поддерживает PostgreSQL и предлагает безопасную и расширяемую ORM-систему и средство построения запросов для неё. Вот пример обработчика UpdateSession для actix-web, основанного на Diesel.rs. В результате тут, для выполнения CRUD-операций с информацией о сессиях в базе данных, можно использовать особый язык, учитывающий специфику Rust.

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 и Diesel используется проект r2d2. Это означает, что у нас имеется (помимо приложения с его рабочими процессами) разделяемое состояние приложения, которое поддерживает множество подключений к базе данных в виде единого пула соединений. Это чрезвычайно упрощает серьёзное масштабирование бэкенда, делает такое решение гибким. Здесь можно найти код, ответственный за создание экземпляра сервера.

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

Интеграционное тестирование бэкенда в нашем проекте выполняется путём запуска тестового экземпляра сервера и подключения к уже работающей базе данных. Затем можно воспользоваться стандартным WebSocket-клиентом (я пользовался tungstenite) для отправки серверу данных, сформированных с учётом особенностей протокола Cap’n Proto, и сопоставления результатов с ожидаемыми. Эта схема тестирования отлично показала себя. Я не использовал специальные тестовые серверы actix-web, так как для настройки и запуска реального сервера не требуется намного большего объёма работы. Модульное тестирование бэкенда оказалось, как и ожидалось, довольно простым занятием, особых проблем проведение таких тестов не вызывает.

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

Приложение очень легко развернуть, воспользовавшись образом Docker.

Docker

Сборка полностью статически связанных исполняемых файлов в Rust реализуется с помощью модифицированного варианта Docker-образа rust-musl-builder. С помощью команды make deploy можно создать образ, который называется webapp и содержит статически связанные исполняемые файлы бэкенда, текущий файл Config.toml, TLS-сертификаты и статический контент фронтенда. Контейнер PostgreSQL, для обеспечения работы системы, должен быть запущен параллельно с контейнером приложения. Готовое веб-приложение можно испытать, воспользовавшись командой make run, которая запускает контейнер с поддержкой сети. В целом, процесс развёртывания нашей системы довольно прост, кроме того, благодаря использованным здесь технологиям, можно говорить о его достаточной гибкости, упрощающей его возможную адаптацию к нуждам развивающегося приложения.

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

Вот схема зависимостей приложения.

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

Единственный компонент, которым пользуется и фронтенд, и бэкенд — это Rust-версия Cap’n Proto, для создания которой требуется локально установленный компилятор Cap’n Proto.

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

Это — большой вопрос. Вот что я могу на него ответить. С точки зрения серверов я склоняюсь к ответу «да», так как экосистема Rust, помимо actix-web, имеет весьма зрелый HTTP-стек и множество самых разных фреймворков для быстрой разработки серверных API и сервисов.

Однако, проекты, создаваемые в этой области, должны достичь той же зрелости, которой достигли серверные проекты. Если же говорить о фронтенде, то тут, благодаря всеобщему вниманию к WebAssembly, сейчас идёт огромная работа. Поэтому сейчас я говорю «нет» использованию Rust во фронтенде, однако не могу не отметить, что он движется в правильном направлении. В особенности это касается стабильности API и возможностей по тестированию.

Пользуетесь ли вы Rust в веб-разработке? Уважаемые читатели!


Оставить комментарий

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

*

x

Ещё Hi-Tech Интересное!

На все компьютеры в России хотят предустанавливать российские антивирусы

В правительство РФ внесён национальный проект «Цифровая экономика», в паспорте которого указано интересное предложение от Министерство цифрового развития, связи и массовых коммуникаций России: законодательно обеспечить предустановку отечественных антивирусных программ на все персональные компьютеры, ввозимые и создаваемые на территории РФ, начиная ...

Когда нужны скорость и масштабирование: сервер распределенных iOS-устройств

В Badoo прогоняется более 1400 end-to-end тестов для iOS-приложений на каждый запуск регрессии. Многим разработчикам UI-тестов под iOS наверняка знакома проблема времени тестового прогона. Это более 40 машинных часов тестов, которые проходят за 30 реальных минут. Николай Абалов из Badoo ...