Вопреки слухам на пространствах девелоперских комьюнити, Rails не становится устаревшей технологией, он не собирается умирать, и остается отличным инструментом для разработки вашего нового проекта. И одна из причин заключается в том, что у Rails имеется достаточно инструментов, чтобы покрыть базовый функционал типичного веб-приложения. Вам не нужно думать о том, как обрабатывать НТТР запросы, что использовать для ввода и получения данных из базы, как отрисовать HTML, который пользователи увидят в своих браузерах, и даже как "вдохнуть жизнь" в пользовательский интерфейс.  Я работаю в продуктовой студии и к нам часто обращаются за разработкой MVP для различных продуктов. Инструменты и подходы для построения пользовательского интерфейса от команды Rails прекрасно подходят для этой задачи. В этой статье я немного расскажу о них и покажу, как все работает. Давным давно, когда я только пытался сверстать свою первую HTML страничку, у Rails уже был крутой инструмент jquery-ujs (unobtrusive javascript), который теперь называется rails-ujs. Он отлично работает с рельсовым бэкендом, когда вам нужно добавить парочку AJAX запросов малой ценой. Можете попробовать сделать что-то вроде этого: app/controllers/money_controller.rb  views/money/show.html.erb views/money/destroy.js Итак, вы сделали AJAX запрос, используя всего несколько HTML атрибутов и один JS файл с одной строчкой кода. Круто, правда? Еще один старожил в мире Rails - Turbolinks. Эта библиотека не находится в стадии активной разработки, но о ее преемнике мы поговорим немного позже. В двух словах, Turbolinks приносит вам SPA опыт почти без клиентского кода. Если подробно, то эта библиотека: загружает содержимое новых страниц с помощью JS и заменяет его на странице без перезагрузки браузера;  она кэширует страницы, чтобы повторные посещения казались мгновенными;  позволяет сохранять элементы на странице неизменными во время навигации. Первые две фичи просто работают из коробки, а последняя должна быть явно определена в вашем коде. Я покажу небольшой и надуманный пример того, что мы можем достичь с ее помощью. Предположим, что где-то на странице есть счетчик уведомлений. app/helpers/application_helper.rb app/views/layouts/application.html.erb Подсчет количества уведомлений может занять некоторое время, но это цена, которую вы платите за поддержку актуальности данных.  Позже, возможно, вы также захотите обновлять количество уведомлений, подписавшись на такие обновления в режиме реального времени. У Rails даже есть встроенный Action Cable для этого. Поскольку эта работа проделана на фронтенде, вам не нужно подсчитывать общее количество страниц между переходами, обработанными Turbolinks. Конечно, вся проблема может быть решена с помощью простого кэширования, но знаете… есть только две сложные вещи в CS… инвалидация кэша… и мы все равно говорим о Turbolinks. Таким образом, мы можем просто не выполнять код, если страница запрашивалась Turbolinks и и запретить Turbolinks обновлять часть страницы. Вот как это выглядит: app/helpers/application_helper.rb app/views/layouts/application.html.erb Старенькие фичи Rails делают свою работу хорошо и многие приложения успешно строили на них сложные юзерские интерфейсы без использования сложного JS фреймворка. Несмотря на это, нам все еще не хватает фич, которые сделают наши приложения более удобными для обслуживания и упростят разработку интерфейса. В начале 2021 года DHH объявил о появлении альтернативного подхода Hotwire, нового способа Rails для построения пользовательских интерфейсов. Несмотря на то, что Hotwire является собирательным названием для семейства библиотек, эта семья довольно мала. По состоянию на октябрь 2021 года было всего две библиотеки: Turbo и Stimulus. Они обе разработаны командой Rails и могут без проблем интегрироваться в ваш величественный монолит. Я расскажу больше о Turbo, так как эта библиотека относительно новая и заменит уже существующую Turbolinks. Если вы думали, что Turbolinks потеряли свою часть "links", потому что это теперь больше чем навигация, вы на 100% правы. Библиотека Turbo разделена на несколько частей, где каждая служит единой цели - доставить в ваше приложение HTML, отрисованный на сервере, с разницей в том, когда и как это делается: Turbo Drive - тот старый добрый Turbolinks, с которым мы знакомы.  Turbo Frames - “отдельные” фреймы, которые могут быть загружены асинхронно и обновлены, когда сервер возвращает фрейм с тем же id.  Turbo Streams - другой тип фреймов, который обновляется в результате HTTP запроса или с помощью сервера через Websocket.  Turbo Native - обёртка вашего “турбированного” веб-приложения, которая интегрирует его в мобильное приложение. Итак, теперь обо всем по порядку. Как упоминалось ранее, Turbo Drive просто заменяет Turbolinks и берет на себя навигацию между страницами. Поскольку почти ничего не изменилось, миграция довольно проста. Вам нужно просто добавить пакет npm  Заменить Turbolinks на Turbo в вашем javascript коде app/javascript/packs/application.js Зменить data-turbolinks... атрибуты с data-turbo… app/views/layouts/application.html.erb Важный момент, на который нужно обратить внимание -  заполнение формы. Turbo drive берет на себя и это. Прежде всего, он ожидает, что redirect после отправки формы будет со статусом 303, чтобы позволить Fetch API автоматически следовать за редиректом. Это правильный статус НТТР для неиндемпотентных (умное слово для описания HTTP методов помимо GET и HEAD 🤓) запросов, если вы хотите, чтобы переадресация осуществлялась с помощью метода GET. В противном случае правильно перенаправлены будут лишь POST запросы, поскольку они также предусматривают статусы 301 и 302. Так что вам следует явно указать код статуса для редиректа. И вот как это сделать: app/controllers/any_controller.rb Так или иначе в рельсовых формах все равно используется метод POST и добавляется <input type="hidden" name="_method" value="patch">, чтобы определить какое действие контроллера использовать. Это означает, что ваши формы все еще будут работать, а о необходимости правильного кода статуса уже велись бурные дискуссии.  Следующее, на что стоит обратить внимание это то, что Turbo не поддерживает параметр local: true, который вы могли использовать для отключения JS-контроля над формой. Если это ваш случай, необходимо внести еще одно небольшое изменение:  app/views/_any_form.html.erb Наконец мы подобрались к чему-то новенькому в рельсе. Turbo Frame - это простой инструмент для создания контейнера с контентом, который может загружаться и обновляться отдельно. Так же, как и в геме render_async или .ejs от Rails, но с меньшим количеством кода.  Давайте посмотрим пример того, как мы можем разложить нашу страницу на несколько частей, которые асинхронно подгружаются только когда пользователь их видит. Представьте страницу продукта с общей информацией, свойствами продукта и отзывами клиентов. Нет гарантии, что пользователь посетит каждый из этих разделов, поэтому мы можем загрузить их только если он действительно добрался до этой части страницы. Я пропущу часть примера, относящуюся к настройке моделей, маршрутов, установке Bootstrap и добавлению CSS. Полагаю, вас не интересуют такие базовые вещи. Вот так выглядит наш app/views/products/show.html.erb Это обычные табы из Bootstrap. Но самое интересное в элементах .tab-page. Мы добавили тег turbo-frame, который является нашим контейнером для загрузки и обновления. У каждого фрейма должен быть свой собственный атрибут идентификатора (id), а хелпер dom_id будет хорошим инструментом, чтобы освободить нас от необходимости думать над именами. Для асинхронной загрузки фрейма мы должны добавить атрибут src, и ответ из этого пути должен вернуть фрейм с таким же идентификатором (id). Поскольку мы хотим загружать только видимую часть, мы добавляем loading="lazy" и фрейм будет загружаться только тогда, когда этот элемент появится на странице. Обратите внимание, что не важно, как этот элемент стал видимым. Пользователь может просто проскролить страницу к этому тегу и его содержимое загрузится, стили родительского элемента могут измениться с display: none на display: block, приложение может вставить этот тег на страницу с помощью Javascript или вы даже можете рекурсивно рендерить один фрейм из другого (но не забудьте как-нибудь выйти из рекурсии). Спиннер в примере - это просто div с CSS анимацией. Вам ничего не нужно с этим делать. Он просто будет вращаться, пока содержимое фрейма не загрузится и появится на странице. app/views/common/_spinner.html.erb В качестве альтернативы можно использовать атрибут busy, который добавляется во фрейм при загрузке, и добавить свой CSS, чтобы показать состояния загрузки.  Наш контроллер довольно простой: app/controllers/products_controller.rb app/views/products/_general.html.erb app/views/products/_properties.html.erb app/views/products/_review.html.erb Тут мы рендерим отдельные фрагменты, но это может быть и вся страница с макетом. Главное - отрендерить тег turbo-frame с тем же идентификатором, что и у тега куда контент будет вставлен. В целом, это все, что вам нужно, чтобы получить “лениво загруженную страницу”. К сожалению, я не смог найти удобный способ обработки ошибок при работе с turbo frames, но набросал решение, которое может быть вам в помощь: app/controllers/any_controller.rb app/views/common/_turbo_error.html.erb Ещё одна замечательная вещь, которую мы можем сделать с turbo frames, это заменять части страницы в ответ на заполнение формы. Идея очень похожая. Действие контроллера должно вернуть тег turbo-frame и Turbo заменит его на странице. Давайте расширим предыдущий пример, чтобы получить возможность добавлять и удалять товары в корзине. app/controllers/products_controller.rb app/views/products/_general.html.erb Вы видите, что теперь у контроллера есть экшены для добавления и удаления товаров. Оба этих метода просто рендерят фрагмент general и он волшебным образом обновляется на странице. Это очень похоже на то, что обычно делается в шаблонах .js.erb. Тем не менее, turbo - более предпочтительный вариант, чтобы избежать дополнительного JS кода, который, к тому же, лежит в папке views. Turbo дал нам еще один интересный инструмент для изменения HTML на странице - Turbo Streams. Он дает больше возможностей для обновления интерфейса DOM и вы не ограничены заменой только одного фрейма, как это происходит с Turbo frames. Этa манипуляция с DOM называются action и она должна выполняться на элементах targets, полученных из какого-либо селектора. Turbo streams дает вам 7 действий для выполнения: append - добавить html в начало цели.  prepend - добавить html в конец цели.  replace - заменить всю цель на html.  update - обновить html внутри цели.  remove - удалить всю цель.  before - добавить html после цели.  after - добавить html перед целью Вы обычно можете услышать/прочитать о Turbo Streams, когда речь заходит про обновления в режиме реального времени и создание еще одного приложения для чата. Но мы можем начать с примера попроще и посмотреть, как Turbo Streams помогает отобразить заполнение формы в пользовательском интерфейсе. Давайте продолжим с предыдущим примером: добавим возможность разместить новый отзыв и показать общее количество отзывов. В начале я просто добавлю количество отзывов и форму для добавления нового отзыва:  app/views/products/show.html.erb app/views/products/_reviews.html.erb app/views/products/reviews/_count_badge.html.erb app/views/products/reviews/_form.html.erb app/views/products/reviews/_card.html.erb И вот как это будет выглядеть: Теперь мы можем добавить немного интерактивности, используя Turbo Streams:  app/controllers/products_controller.rb app/views/products/add_review.turbo_stream.erb Самый интересный файл вот здесь _add_review.turbo_stream.erb. Формат turbo_stream может быть новым для вас, если вы впервые сталкиваетесь с Turbo Streams. Turbo требует, чтобы HTTP ответ имел контент-тип text/vnd.turbo-stream.html, поэтому вы должны либо передать content_type: "text/vnd.turbo-stream.html" в метод render  в действии контроллера, либо добавить расширения .turbo_stream.erb для вашего шаблона. Второй вариант мне кажется более практичным. Главный субъект в _add_review.turbo_stream.erb это хэлпер turbo_stream. Мы используем его для вызова ранее упомянутых действий. А если точнее, он генерирует XML теги, которые описывают, какие манипуляции DOM должны быть сделаны. Этот файл делает три вещи: Обновляет счетчик отзывов - обновляет содержимое тега с идентификатором dom_id(@product, 'reviews_count')  Сбрасывает форму обзора - заменяет весь тег на id dom_id(@product, 'reviews_count').  Показывает новый обзор на странице - добавляет контент в начало тега с id dom_id(@product, 'reviews_list') Это все, что вам нужно для создания действительно интерактивного веб-приложения. Без единой строки JS кода! И этого будет достаточно для большинства приложений.  С Turbo Streams вы также можете изменять содержимое страницы с помощью WebSocket. Это не потребует много действий с нашей стороны. Предлагаю вернуться к нашему примеру и обновить отзывы во всех открытых браузерах, когда будет добавлен новый отзыв. Перед тем, как мы начнем, вы должны добавить в свой Gemfile гем turbo-rails и запустить эту команду bundle exec rails turbo:install Он установит @hotwired/turbo-rails и заменит адаптер Action Cable с async(по умолчанию) на redis. Теперь мы готовы к работе в режиме реального времени. Первое, что нам нужно сделать, это подписаться на обновления продукта. Это очень просто благодаря хэлперу  turbo_stream_from. Вот как это выглядит: app/views/products/show.html.erb И теперь, вместо того, чтобы возвращать теги turbo-frame, которые показывают, какие действия должны быть выполнены на пользовательском интерфейсе, мы отправим эти действия всем слушателям (всем открытым страницам продукта) app/controllers/products_controller.rb И чтобы не выполнять некоторые действия дважды, удалим их из НТТР ответа app/views/products/add_review.turbo_stream.erb Обновление формы все еще останется в HTTP ответе, так как форма должна быть очищена после отправки, и мы не хотим очищать ее для всех пользователей. Вот и всё, что вам нужно, если вы хотите добавить в Rails приложение немного коммуникации в режиме реального времени. Магия рельсов во всей её красе! Команда Rails проделала большую работу, чтобы свести к минимуму взаимодействие со всем этим большим и страшным миром JS, оставив фреймворк отличным инструментом для создания современных веб-приложений. Конечно, реальный мир может (и, скорее всего, будет) требовать больше, чем может дать Turbo. И команда Rails разработала Stimulus и request.js, чтобы сделать вашу жизнь легче, когда вам все-таки придется писать JS код в Rails приложении. Впрочем, это уже совсем другая история. Fullstack developer at Railsware