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

https://medium.com/@saschagrunert/a-web-application-completely-in-rust-6f6bdb6c4471
  • Перевод
Автор материала, перевод которого мы сегодня публикуем, говорит, что его самым свежим экспериментом в области архитектуры программных проектов стало создание рабочего веб-приложения с использованием исключительно языка 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.

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

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


WebAssembly

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

При запуске фронтенда веб-приложения (в моём проекте это делается командой make frontend), cargo-web выполняет кросс-компиляцию приложения в Wasm и упаковывает его, добавляя некоторые статические материалы. Затем 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 обладает некоторыми весьма интересными возможностями. Среди них — поддержка архитектуры компонентов, подходящих для повторного использования. Эта возможность упростила разбиение моего приложения на три основных компонента:

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

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


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

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


Компонент ContentComponent

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

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

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

Каждый компонент реализует собственный типаж 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

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


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

Пользовательский интерфейс клиентской части приложения основан на фреймворке UIkit, его версия 3.0.0 выйдет в ближайшем будущем. Специально подготовленный скрипт build.rs автоматически загружает все необходимые зависимости UIkit и компилирует итоговую таблицу стилей. Это означает, что в единственный файл 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. Почему? Этот выбор определило существование замечательного проекта Diesel, который уже поддерживает PostgreSQL и предлагает безопасную и расширяемую ORM-систему и средство построения запросов для неё. Всё это отлично соответствует нуждам нашего проекта, так как actix-web уже поддерживает Diesel. В результате тут, для выполнения CRUD-операций с информацией о сессиях в базе данных, можно использовать особый язык, учитывающий специфику Rust. Вот пример обработчика UpdateSession для actix-web, основанного на Diesel.rs.

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

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

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


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


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

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

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


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

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

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

RUVDS.com

699,00

RUVDS – хостинг VDS/VPS серверов

Поделиться публикацией
Комментарии 12
    +3
    Rust — язык в который влюбляешься без оглядки) думаю он взлетит и так взлетит, как никто другой) Когда язык не тянут на дно всякие корпорации добра) развитие действительно радует.
    Спасибо за инетересную статью, побольше бы их про Rust на Хабре.
      +1
      Вот код реализации Renderable в компоненте LoginComponent.

      impl Renderable<LoginComponent> for LoginComponent {
          fn view(&self) -> Html<Self> {
              html! {
      стена html
              }
          }
      }
      

      Выглядит так, как будто о template engine автор статьи и не слышал.

      Или слышал?

      P.S. Я вижу, что перевод, тем не менее, раз статью перевели — наверное автор поста хоть сколько-то шарит в теме. Или нет?
        +1
        Есть различные темплейт-движки для раста, но лично я например ими не пользовался — делать фронт на каком-нибудь реакте или вью мне кажется логичнее.
          0

          Поясните, а зачем нам template engine, если мы можем компонетны с кусками html определять прямо в коде?

            0
            Разъясните пожалуйста вашу мысль.
              0

              Есть два разных подхода к разработке фронтэнда: первый — это статически описывать разметку в шаблоне и добавлять вкрапления исполняемого кода; второй — это создавать фронтенд программно, а для упрощения кода по созданию отдельных элементов использовать вкрапления DSL-синтаксиса, описывающего структуру элементов. Вот как раз второй подход и используется в yew, как и в React/JSX.

                0
                Да, это выглядит рационально в 2к18 (дискуссию о неоптимальности и быстродействии фтопку).

                Дело в том, что в приведенном примере я увидел что-то похожее на спагетти-код. Что и породило вопрос.
          0
          Скажите, я правильно понимаю, что Diesel работает синхронно?
          Т.е. поток исполнения будет остановлен пока не придёт ответ от БД?
            +1
            Да, но можно использовать пул соединений, например, через r2d2-diesel.
              +1

              Работа с базой происходит в отдельном потоке, обработчик http запроса асинхронный и останавливаться не будет

              0
              Rust — действительно классный язык. Его концепции заставляют обратить на себя внимание с первого знакомства. И соглашусь с автором первого комментария, что этот язык однозначно станет очень востребованным. Правда он отнюдь не для новичков в программировании. Я сразу же попытался использовать его на production проектах и… возникли некоторые сложности. Почти все названные технологии были опробованы мною. Я пробовал создать на Rust rest api, web приложение и работал с базой PostgreSQL.

              После некоторого времени, я пришел к выводу, что использовать его на production WEB проекте будет затруднительно. Доступные фреймворки для Rust — активно развиваются, что означает что они сильно меняются. Некоторые выпадают из саппорта (Iron), появляются новые (Rocket, который пока что использует Rust Nightly). То что сейчас доступно для Rust можно отслеживать на http://www.arewewebyet.org . Самые важные фрейворки и библиотеки уже доступны, спасибо за это сообществу Rust, которое стремится соблюдать Закон Парето :)

              Но той гибкости и разнообразия выбора как у «старых» языков веб программирования пока у Rust нет. И развернуть веб приложение на нем — значит быть готовым к большим переменам и дефициту. Но, ничего не мешает писать на нем внутренние системные rest api для решения проблем «узких» мест. Например системы анализа, логирования итд. Ведь скорость работы у Rust по сравнению с другими языками очень даже приличная. А ведь Rust еще только развивается.

              Время Rust на вебе придет и это ускорится в тот самый момент, когда появится полноценная IDE, например от Jetbrains.
                +1
                Я как раз планировал написать статью по сравнению экосистем Rust и моего текущего основного ЯП (в данный момент — C#).

                Использовать раст как раз в web проекте очень просто — берете actix и пользуетесь. А вот по другим фронтам все не так очевидно.

                Скорость раста — приятный бонус, но не основная его цель, по крайней мере мне так кажется. В общем, будет время — напишу обязательно.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое