Как мы создаем компоненты пользовательского интерфейса в Rails

Опубликовано: 2024-06-28

Поддержание визуальной согласованности в большом веб-приложении является общей проблемой во многих организациях. Основное веб-приложение, лежащее в основе нашего продукта Flywheel, построено на Ruby on Rails, и в любой день у нас есть около нескольких разработчиков Rails и три разработчика внешнего интерфейса, фиксирующих для него код. Мы также уделяем большое внимание дизайну (это одна из наших основных ценностей как компании), и в наших Scrum-командах есть три дизайнера, которые очень тесно сотрудничают с разработчиками.

два человека совместно работают над дизайном веб-сайта

Наша главная цель — обеспечить, чтобы любой разработчик мог создать адаптивную страницу без каких-либо препятствий. Препятствия обычно заключались в незнании того, какие существующие компоненты использовать для создания макета (что приводило к раздуванию кодовой базы очень похожими, избыточными компонентами) и незнании того, когда обсуждать возможность повторного использования с дизайнерами. Это способствует непоследовательному опыту клиентов, разочарованию разработчиков и разным языкам дизайна между разработчиками и дизайнерами.

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

бородатый мужчина улыбается в камеру, сидя перед монитором компьютера, на котором отображаются строки кода

В этой статье я углублюсь в:

  • Что мы решаем
  • Ограничивающие компоненты
  • Рендеринг компонентов на стороне сервера
  • Где мы не можем использовать серверные компоненты

Для чего мы решаем

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

Традиционный подход к компонентам заключался в нашем руководстве по стилю, в котором перечислялась вся разметка, необходимая для создания данного компонента. Например, вот как выглядела страница руководства по стилю для нашего компонента планки:

Страница руководства по стилю для компонента планки

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

Зачастую дизайнеры вносили небольшие дополнения или изменения в тот или иной компонент. Поскольку руководство по стилю не совсем поддерживало это, альтернативные приемы для правильного отображения этой настройки (например, ненадлежащее использование части другого компонента) стали раздражающе распространенными.

Пример неограниченного компонента

Чтобы проиллюстрировать, как с течением времени проявляются несоответствия, я буду использовать простой (и надуманный), но очень распространенный пример одного из наших компонентов в приложении Flywheel: заголовков карточек.

Вот как выглядел заголовок карточки, начиная с макета дизайна. Это было довольно просто: заголовок, кнопка и нижняя граница.

 .card__header
  .card__header-left
    %h2 резервных копий
  .card__header-право
    = link_to "#" сделать
      = значок("plus_small")

Представьте себе, что после написания кода дизайнер хочет добавить значок слева от заголовка. Изначально между значком и заголовком не будет никакого поля.

 ...
  .card__header-left
    = icon("arrow_backup", цвет: "gray25")
    %h2 резервных копий
...

В идеале мы должны решить эту проблему в CSS для заголовков карточек, но в этом примере, скажем, другой разработчик подумал: «О, я знаю! У нас есть несколько помощников по марже. Я просто напишу в названии класс помощников».

 ...
  .card__header-left
    = icon("arrow_backup", цвет: "gray25")
    %h2.--ml-10 Резервные копии
...

Ну, технически это выглядит так же, как и макет, верно?! Конечно, но предположим, что через месяц другому разработчику нужен заголовок карты, но без значка. Они находят последний пример, копируют/вставляют его и просто удаляют значок.

Опять же, это выглядит правильно, не так ли? Конечно, вне контекста, для человека, не разбирающегося в дизайне! Но посмотрите на это рядом с оригиналом. Это левое поле в заголовке все еще существует, потому что они не осознавали, что вспомогательное поле слева нужно удалить!

Продолжая этот пример, предположим, что в другом макете заголовок карточки должен быть без нижней границы. Можно найти в руководстве по стилю состояние под названием «без границ» и применить его. Идеальный!

Другой разработчик может затем попытаться повторно использовать этот код, но в этом случае ему действительно нужна граница. Предположим гипотетически, что они игнорируют правильное использование, описанное в руководстве по стилю, и не понимают, что удаление класса без границ даст им их границу. Вместо этого они добавляют горизонтальное правило. В конечном итоге между заголовком и границей появляется дополнительное отступы, поэтому они применяют вспомогательный класс к hr и вуаля!

Со всеми этими изменениями в исходном заголовке карты у нас теперь есть беспорядок в коде.

 .card__header.-без полей
  .card__header-left
    %h2.--ml-10 Резервные копии
  .card__header-право
    = link_to "#" сделать
      = значок("plus_small")
  %hr.--mt-0.--mb-0

Имейте в виду, что приведенный выше пример просто иллюстрирует, как неограниченные компоненты со временем могут стать беспорядочными. Если кто-то из нашей команды пытался выпустить вариант заголовка карты, это должно быть обнаружено при проверке дизайна или проверке кода. Но подобные вещи иногда ускользают от внимания, поэтому нам необходимо делать вещи пуленепробиваемыми!


Ограничивающие компоненты

Вы можете подумать, что перечисленные выше проблемы уже явно решены с помощью компонентов. Это правильное предположение! Фронтенд-фреймворки, такие как React и Vue, очень популярны именно для этой цели; это потрясающие инструменты для инкапсуляции пользовательского интерфейса. Однако у них есть один недостаток, который нам не всегда нравится: они требуют, чтобы ваш пользовательский интерфейс отображался с помощью JavaScript.

Наше приложение «Маховик» очень тяжелое с точки зрения серверной части и в основном содержит HTML, отображаемый на сервере, но, к счастью для нас, компоненты могут иметь множество форм. В конце концов, компонент пользовательского интерфейса — это инкапсуляция стилей и правил дизайна, которая выводит разметку в браузер. Благодаря этой реализации мы можем применить тот же подход к компонентам, но без накладных расходов, связанных с инфраструктурой JavaScript.

Ниже мы рассмотрим, как создавать ограниченные компоненты, но вот несколько преимуществ, которые мы обнаружили, используя их:

  • На самом деле никогда не бывает неправильного способа собрать компонент.
  • Компонент выполняет всю работу по проектированию за вас. (Вы просто передаете параметры!)
  • Синтаксис создания компонента очень последователен и понятен.
  • Если в компоненте необходимо изменить дизайн, мы можем изменить его один раз в компоненте и быть уверенными, что он будет обновлен повсюду.

Рендеринг компонентов на стороне сервера

Итак, о чем мы говорим, ограничивая компоненты? Давайте копаться!

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

Частички спешат на помощь (или мы так думали)

Первым шагом в этом направлении, который мы пробовали в прошлом, было использование партиалов Rails. Частички — единственный инструмент, который Rails предоставляет вам для повторного использования в шаблонах. Естественно, это первое, к чему все стремятся. Но у их использования есть существенные недостатки, поскольку, если вам нужно объединить логику с шаблоном многократного использования, у вас есть два варианта: дублировать логику на каждом контроллере, который использует частичный код, или встроить логику в сам частичный код.

Частичные версии ДЕЙСТВИТЕЛЬНО предотвращают ошибки копирования/вставки и нормально работают в первые пару раз, когда вам нужно что-то повторно использовать. Но, по нашему опыту, частичные версии вскоре загромождаются поддержкой все большей функциональности и логики. Но логика не должна жить в шаблонах!

Введение в клетки

К счастью, есть лучшая альтернатива партиалам, которая позволяет нам повторно использовать код и скрывать логику. Он называется Cells, драгоценный камень Ruby, разработанный Trailblazer. Ячейки появились задолго до того, как возросла популярность интерфейсных фреймворков, таких как React и Vue, и они позволяют писать модели инкапсулированных представлений, которые обрабатывают как логику , так и шаблоны. Они предоставляют абстракцию модели представления, которой в Rails просто нет «из коробки». На самом деле мы уже некоторое время используем ячейки в приложении «Маховик», но не в глобальном, сверхмногоразовом масштабе.

На самом простом уровне Cells позволяет нам абстрагировать фрагмент разметки следующим образом (в качестве языка шаблонов мы используем Haml):

 %дел
  %h1 Привет, мир!

В многоразовую модель представления (на данный момент очень похожую на частичные) и превратите ее в следующее:

 = ячейка("привет_мир")

В конечном итоге это помогает нам ограничить компонент так, чтобы вспомогательные классы или неправильные дочерние компоненты не могли быть добавлены без изменения самой ячейки.

Создание клеток

Мы помещаем все наши ячейки пользовательского интерфейса в каталог app/cells/ui. Каждая ячейка должна содержать только один файл Ruby с суффиксом _cell.rb. Технически вы можете писать шаблоны прямо в Ruby с помощью помощника content_tag, но большинство наших ячеек также содержат соответствующий шаблон Haml, который находится в папке, названной компонентом.

Супербазовая ячейка без логики выглядит примерно так:

 // ячейки/ui/slat_cell.rb
пользовательский интерфейс модуля
  класс SlatCell <ViewModel
    определенно шоу
    конец
  конец
конец

Метод show — это то, что отображается при создании экземпляра ячейки и автоматически ищет соответствующий файл show.haml в папке с тем же именем, что и ячейка. В данном случае это app/cells/ui/slat (мы ограничиваем все наши ячейки пользовательского интерфейса модулем пользовательского интерфейса).

В шаблоне вы можете получить доступ к параметрам, переданным в ячейку. Например, если ячейка создана в представлении типа = cell("ui/slat", title: "Title", subtitle: "Subtitle", label: "Label"), мы можем получить доступ к этим параметрам через объект параметров.

 // ячейки/ui/slat/show.haml
.планка
  .slat__inner
    .slat__content
      %h4= опции[:title]
      %p= опции[:subtitle]
      = значок(опции[:значок], цвет: "синий")

Часто мы перемещаем простые элементы и их значения в метод в ячейке, чтобы предотвратить отображение пустых элементов, если опция отсутствует.

 // ячейки/ui/slat_cell.rb
определённый титул
  return, если только options[:title]
  content_tag :h4, параметры[:title]
конец
чёткий субтитр
  return if options[:subtitle]
  content_tag :p, параметры[:subtitle]
конец
 // ячейки/ui/slat/show.haml
.планка
  .slat__inner
    .slat__content
      = название
      = субтитры

Обертывание ячеек с помощью утилиты пользовательского интерфейса

Доказав, что это может работать в больших масштабах, я захотел разобраться с лишней разметкой, необходимой для вызова ячейки. Это просто не совсем правильно, и его трудно запомнить. Поэтому мы сделали для этого маленького помощника! Теперь мы можем просто вызвать = ui «имя_компонента» и передать параметры в строке.

 = ui "планка", title: "Название", субтитр: "Субтитр", метка: "Ярлык"

Передача параметров блоком, а не строкой

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

 = ui «slat», title: «Title», подзаголовок: «Subtitle», метка: «Label», ссылка: «#», tertiary_title: «Tertiary», отключено: true, контрольный список: [»Item 1», «Item 2», «Пункт 3»]

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

Вот пример использования класса OptionProxy внутри нашей ячейки:

 пользовательский интерфейс модуля
  класс SlatCell <ViewModel
    определенно шоу
      OptionProxy.new(self).yield!(параметры, &блок)
      супер()
    конец
  конец
конец

Теперь, когда все это готово, мы можем превратить наши громоздкие встроенные параметры в более приятный блок!

 = ui "планка" до |планка|
  - slat.title = "Название"
  - slat.subtitle = "Субтитры"
  - slat.label = "Метка"
  - slat.link = "#"
  - slat.tertiary_title = "Третичный"
  - slat.disabled = правда
  - slat.checklist = ["Пункт 1", "Пункт 2", "Пункт 3"]

Знакомство с логикой

До этого момента примеры не содержали никакой логики относительно того, что отображает представление. Это одна из лучших вещей, которые предлагает Cells, так что давайте поговорим об этом!

Придерживаясь нашего компонента планки, нам нужно иногда отображать весь объект как ссылку, а иногда — как элемент div, в зависимости от того, присутствует ли опция ссылки или нет. Я считаю, что это единственный имеющийся у нас компонент, который можно отобразить как элемент div или ссылку, но это довольно хороший пример возможностей Cells.

Метод ниже вызывает либо хелпер link_to, либо хелпер content_tag в зависимости от наличия опции [:link] .

 контейнер защиты (& блок)
  тег =
    если варианты[:ссылка]
      [:link_to, параметры[:link]]
    еще
      [:content_tag, :div]
    конец
  send(*tag, class: «slat__inner», &block)
конец

Это позволяет нам заменить элемент .slat__inner в шаблоне блоком-контейнером:

 .планка
  = контейнер делать
  ...

Другой пример логики в Cells, который мы часто используем, — это условный вывод классов. Допустим, мы добавляем в ячейку отключенную опцию. Ничего больше при вызове ячейки не меняется, кроме того, что теперь вы можете передать параметр Disabled: True и наблюдать, как все это переходит в отключенное состояние (серое, с неактивными ссылками).

 = ui "планка" до |планка|
  ...
  - slat.disabled = правда

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

 .slat{ класс: возможные_классы("--disabled": options[:disabled]) }
  .slat__inner
    .slat__content
      %h4{ класс: возможные_классы("--alt": options[:disabled]) }= options[:title]
      %p{ класс: возможные_классы("--alt": options[:disabled]) }=
      варианты[:subtitle]
      = значок(опции[:значок], цвет: «серый»)

Традиционно нам приходилось помнить (или ссылаться на руководство по стилю), каким отдельным элементам требовались дополнительные классы, чтобы все это работало правильно в отключенном состоянии. Ячейки позволяют нам объявить один вариант, а затем сделать за нас тяжелую работу.

Примечание: возможные_классы — это метод, который мы создали, чтобы позволить условно применять классы в Haml.


Где мы не можем использовать серверные компоненты

Хотя клеточный подход чрезвычайно полезен для нашего конкретного приложения и способа нашей работы, было бы упущением сказать, что он решил 100% наших проблем. Мы по-прежнему пишем JavaScript (много его) и создаём довольно много возможностей на Vue в нашем приложении. В 75% случаев наш шаблон Vue все еще живет в Haml, и мы привязываем наши экземпляры Vue к содержащему элементу, что позволяет нам по-прежнему использовать преимущества клеточного подхода.

Однако в тех местах, где имеет смысл полностью ограничить компонент как однофайловый экземпляр Vue, мы не можем использовать Cells. Например, все наши списки выбора — это Vue. Но я думаю, что это нормально! На самом деле мы не сталкивались с необходимостью иметь дублирующиеся версии компонентов как в Cells, так и в компонентах Vue, поэтому вполне нормально, что некоторые компоненты на 100% созданы с помощью Vue, а некоторые — с Cells.

Если компонент создан с помощью Vue, это означает, что для его создания в DOM требуется JavaScript, и для этого мы воспользуемся преимуществами платформы Vue. Однако для большинства других наших компонентов не требуется JavaScript, а если и требуется, то требуется, чтобы DOM уже был построен, и мы просто подключаемся и добавляем прослушиватели событий.

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


Наше заключение

На данный момент мы преобразовали около тридцати наиболее часто используемых визуальных компонентов в ячейки. Это дало нам огромный прирост производительности и дало разработчикам чувство уверенности в том, что создаваемый ими опыт верен и не взломан.

Наша команда дизайнеров более чем когда-либо уверена, что компоненты и возможности нашего приложения точно соответствуют тому, что они разработали в Adobe XD. Изменения или дополнения к компонентам теперь обрабатываются исключительно посредством взаимодействия с дизайнером и интерфейсным разработчиком, что позволяет остальной части команды сосредоточиться и не беспокоиться о том, как настроить компонент в соответствии с макетом дизайна.

Мы постоянно совершенствуем наш подход к ограничению компонентов пользовательского интерфейса, но я надеюсь, что методы, показанные в этой статье, дадут вам представление о том, что у нас работает хорошо!


Приходите работать к нам!

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

Готовы присоединиться к нашей команде? Мы нанимаем! Подайте заявку здесь.