
Сегодня я расскажу о том, как мы переезжаем с Vue.js на Hotwire в нашем проекте, а также поделюсь причинами переезда и некоторыми промежуточными результатами.
Uscreen помогает авторам видеоконтента продавать видеоролики, предоставляя им конструктор веб-сайтов, с которым они могут запустить собственное кастомное веб-приложение и продавать видеоконтент по подписке для своей аудитории.
С точки зрения клиента наше приложение делится на три основные части:
Admin Area, в которой пользователи могут загружать свой видеоконтент и настраивать веб-сайт.
Storefront, представляющий собой сайт, которым пользуются клиенты контент креаторов.
Нативные приложения, доступные как на мобильных, так и на ТВ-платформах.

В вебе наше приложение держится на достаточно популярном стеке — Rails и Vue.js. Кроме того, мы используем специальную библиотеку Inertia.js, которая позволяет писать Vue-компоненты в стиле Rails таким образом, что каждый компонент представляет собой view шаблон страницы, подвязанный к своему экшну в контроллере. При этом переходы между страницами осуществляются без перезагрузки страницы в браузере, как это делает библиотека Turbolinks.
Про Inertia.js у меня есть отдельный доклад.
Uscreen разрабатывался так несколько лет, пока у нас не накопился ряд проблем на фронтенде.
Что было не так с фронтендом:
Скопилось достаточно много легаси. Ряд неупорядоченных решений привел к тому, что нам стало сложно развивать фронтенд. В итоге мы застряли на версии Vue 2 и не смогли проапгрейдится до версии Vue 3.
Дублирование бизнес-логики и валидаций входных данных.
Компоненты на каждый чих, что привело к тому, что в базе компонентов стало сложно разбираться.
Собственный model layer на фронтенде, который еще сильнее усложнил фронтенд.
Ну и основной недостаток, который нас не устраивал, — наш фронтенд не подходил к Rails Way! Мы любим то, что предлагает Rails сообщество вместе с DHH и ценим практики компании Basecamp, поэтому стараемся не сильно отклоняться от стандартного Rails Way подхода.
В итоге мы решили переехать.
Hotwire
Мы решили переехать на Hotwire, а именно на библиотеки Turbo и Stimulus. Про эти библиотеки в интернете опубликовано достаточно много докладов и статей, поэтому мы лишь вкратце пробежимся по принципам их работы.
Turbo
В Turbo мы разбиваем HTML страницы на независимые контексты — фреймы (turbo frames). Турбо-фреймы — части HTML документа, которые в дальнейшем будут асинхронно видоизменяться.

При отправке формы или при переходе по ссылкам в турбо-фреймах мы инициируем запрос на сервер со специальным хедером:
Content-type: text/vnd.turbo-stream.html
Распарсив такой запрос, сервер вернет на клиент специальный HTML-подобный документ с описанием команд (turbo streams). В турбо стриме может описываться то, как следует поступить с тем или иным турбо фреймом. Например, мы можем заменить один турбо фрейм абсолютно новым HTML контентом, удалить его из DOM-дерева или добавить в его конец новый HTML-фрагмент.

Stimulus
Библиотека Stimulus появилась как попытка упорядочить различные подходы к написанию JavaScript в компании Basecamp и вынести их в специальные классы-контроллеры.
Контроллеры — JS-вкрапления с небольшой динамикой для страниц.

Все параметры таких контроллеров явно указываются в HTML-коде. В data-атрибутах явно указывается, какой контроллер привязан к этому фрагменту, какие атрибуты контроллера связаны с HTML-элементами, и какие экшны контроллера будут вызываться при том или ином событии. Таким образом контроллеры имеют сильную связь с HTML-разметкой, но отделены от нее.

При правильном построении таких контроллеров, в них можно выносить целые поведения — например, работу с буфером обмена или загрузку картинок.

Подведем итоги и рассмотрим плюсы, которые действительно могут быть полезны для нас в разработке:
Работа с Hotwire это 90% Ruby + erb, что упрощает разработку для Rails-инженеров.
Несложная концепция библиотек Turbo и Stimulus. Нам больше не нужно погружаться в такие понятия как виртуальный DOM, обработку вложенных vue событий, осуществление API запросов и т. д. Мы отправляем на клиент уже готовые куски HTML.
Проект достаточно активно развивается сообществом. В интернете уже опубликовано достаточно много докладов и статей, а также появляются первые best practices.
Для разработки зачастую требуется буквально один разработчик.
Все это приближает нас к концепции David Heinemeier Hansson, которую он описал в конце 2021 года — The One Person Framework, согласно которой Rails 7 в связке с Hotwire возвращает Rails былое величие, и для разработки современных веб-приложений нужен один разработчик.

Если Rails-разработчики будут все делать сами, что же тогда делать фронтендерам? Фронтендеры могут:
Создавать web component-ы.
Работать над сложными интерфейсами, сильно завязанными на js (в нашем случае это Page Builder, Video Player, Live Chat).
Оборачивать легаси Vue-компоненты в Stimulus.
Консультировать и помогать Rails-инженерам.
Web components
Web Components — технология, позволяющая инкапсулировать в HTML-теги некую верстку, стили и JavaScript. При этом их теги выглядят как обычные HTML-теги.
<emoji-rain></emoji-rain>
Веб-компоненты могут так же принимать параметры, тем самым предоставляя интерфейс.
<emoji-rain active="false"></emoji-rain>
Имея в своем арсенале ряд таких веб-компонентов, мы получаем строительные блоки, которые сильно упрощают разработку. В нашем проекте уже используются такие веб-компоненты, как <ds-calendar>
, <ds-modal>
, <ds-datepicker>
, <ds-image-gallery>
и т.д.
Интересный факт, сам тег <turbo-frame>
из библиотеки Turbo также является веб-компонентом. В этом несложно убедиться, если заглянуть в исходники.
Резюмируем наш план переезда:
Постранично переписываем наши .vue-шаблоны на .erb.
Динамические части HTML-страниц выносим в турбо-фреймы и используем Turbo для коммуникации с сервером.
Для легкой динамики на странице используем Stimulus.
Используем webcomponents для повторяющихся элементов UI.
Сложные Vue-компоненты, с которых не удается быстро переехать, оборачиваем в Stimulus-контроллеры.
Повторяем процесс до тех пор, пока не закончим.
Следующие примеры наглядно демонстрируют то, как мы производим нашу разработку.
Переход с Vue на Turbo на примере лайка поста
На сайтах наших клиентов существуют сообщества, в которых пользователи могут оставлять свои посты, комментировать их и ставить лайки.

По сути, это дерево компонентов, среди которого нас интересует компонент <BLike>
. Во vue-коде это кнопка, к которой привязан некий обработчик. При вызове обработчика вызывается событие like
, которое в дальнейшем будет обрабатываться в родителе.

Родитель вызовет метод like
на модели поста и инициирует запрос на сервер. Сервер обработает запрос, вернет в компонент некоторый JSON, компонент изменит свое состояние, т. е. выполнится стандартный флоу, знакомый для всех кто работал с Vue.js.

Теперь разберемся, как то же самое будет работать через Hotwire.

Мы разделяем страницу поста на старые добрые erb partial-ы. Тут нас интересует _post_controls.html.erb
. В нем указан турбо фрейм, в котором лежит форма с кнопкой.

Здесь важным является параметр data: { turbo: true }
. Благодаря этому параметру Turbo понимает, что на данный запрос вернется турбо-стрим, который будет необходимо асинхронно обработать и изменить контент в соответствующем турбо-фрейме.
В контроллере мы явно вернем турбо стрим. Для этого мы вызовем на объекте turbo_stream
метод update
в который передадим название турбо-фрейма и то, каким partial-ом его следует обновить. В итоге на клиент вернется документ с указанным турбо стримом.

Аналогичным образом будет выглядеть метод создания нового комментария. Следующий пример демонстрирует, что наряду с одним турбо-стримом мы можем вернуть несколько стримов.

Пример на Stimulus
В разделе сообщества на сайтах наших клиентов под каждым постом есть форма для комментария, которая должна автоматически расширяться в зависимости от контента, вводимого пользователем.

На erb это выглядит следующим образом: в шаблоне есть форма, в которой мы указываем несколько параметров:
имя контроллера —
autogrow-textarea
,target
, который связывает контроллер с текущим HTML-элементом (в данном случае — textarea),action
— метод контроллера, который будет вызываться при каждом вызове события. Событием в данном случае является ввод какого-либо символа.

Контроллер выглядит достаточно просто и содержит всего два метода:
стандартный метод
connect
в Stimulus, который вызывается в момент подсоединения контроллера к HTML-странице. В данном случае мы указываем первоначальные стилистические атрибуты для нашего элемента.метод
resize
, который будет указывать на textarea величину, соответствующую зоне скролла.

Обертка Vue в Stimulus
В нашем проекте есть достаточно сложный Live Chat, который пока не удается переписать в веб-компонент. Поэтому мы оборачиваем его в Stimulus. Упрощенный вид контроллера выглядит следующим образом:

Во-первых, в методе connect
мы загружаем необходимые нам JavaScript-модули и передаем их в метод render
. В методе render
мы создаем новый Vue-компонент и «подвязываем» его к некоему root-элементу. Этот root-элемент хранится в переменной currentTarget
.

После того, как пользователь переходит на другую страницу, и мы меняем турбо-фрейм, нам необходимо удалить наш Vue-компонент. Поэтому мы используем метод disconnect
, который уничтожает наш виджет.

Подведем промежуточные итоги нашего переезда.
Наблюдать за динамикой происходящего на фронтенде для Rails-инженеров стало проще, потому что больше не нужно понимать, как устроен виртуальный DOM, обрабатывать хитрые события, разбираться в сложной структуре веб-компонентов и API запросов. Мы возвращаемся к старой доброй HTML спеке: отправляем запросы через формы или ссылки и оперируем готовыми кусками HTML.
Web Components — технология, которая упрощает жизнь Rails-разработчикам и ускоряет разработку фронтенда. По сути, в нашем распоряжении готовые строительные блоки, которые можно напрямую указывать в наших erb-partial-ах.
Зачастую фичу (и фронтенд, и бэкенд) может разработать один человек благодаря Hotwire.
Несмотря на простоту Turbo + Stimulus, они требуют нового образа мышления и понимания того, как следует проектировать фичи, как делить страницу на турбо-фреймы и в какой момент их загружать (лениво или сразу). Кроме того, необходимо понимать, каким образом выносить в Stimulus контроллеры то или иное поведение.
За простотой использования Web Components также скрывается сложность самой технологии, которая требует от фронтенд-разработчиков времени на погружение и проектирование подходящего API. Разработчикам нужно уметь проектировать хороший интерфейс таких веб-компонентов, чтобы их можно было переиспользовать в рамках всего проекта.
Rails-инженерам приходится сильнее углубляться в HTML и CSS, что требует определенных ресурсов.