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

Опубликовано: 2019-11-16

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

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

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

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

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


Что мы решаем для

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

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

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

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

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

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

.card__header
  .card__header-left
    %h2 Backups

  .card__header-right
    = link_to "#" do
      = icon("plus_small")

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

...
  .card__header-left
    = icon("arrow_backup", color: "gray25")
    %h2 Backups
...

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

...
  .card__header-left
    = icon("arrow_backup", color: "gray25")
    %h2.--ml-10 Backups
...

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

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

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

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

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

.card__header.--borderless
  .card__header-left
    %h2.--ml-10 Backups

  .card__header-right
    = link_to "#" do
      = icon("plus_small")

  %hr.--mt-0.--mb-0

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


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

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

Приложение Flywheel очень тяжелое с точки зрения серверной части, в основном это HTML, отображаемый на сервере, но, к счастью для нас, компоненты могут быть разных форм. В конце концов, компонент пользовательского интерфейса — это инкапсуляция стилей и правил проектирования, которая выводит разметку в браузер. С этой реализацией мы можем использовать тот же подход к компонентам, но без накладных расходов, связанных с фреймворком JavaScript.

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

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

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

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

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

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

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

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

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

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

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

%div
  %h1 Hello, world!

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

= cell("hello_world")

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

Построение ячеек

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

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

// cells/ui/slat_cell.rb

module UI
  class SlatCell < ViewModel
    def show
    end
  end
end

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

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

// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      %h4= options[:title]
      %p= options[:subtitle]
      = icon(options[:icon], color: "blue")

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

// cells/ui/slat_cell.rb
def title
  return unless options[:title]
  content_tag :h4, options[:title]
end

def subtitle
  return unless options[:subtitle]
  content_tag :p, options[:subtitle]
end
// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      = title
      = subtitle

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

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

= ui "slat", title: "Title", subtitle: "Subtitle", label: "Label"

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

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

= ui “slat", title: “Title”, subtitle: “Subtitle”, label: “Label”, link: “#”, tertiary_title: “Tertiary”, disabled: true, checklist: [“Item 1”, “Item 2”, “Item 3”]

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

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

module UI
  class SlatCell < ViewModel
    def show
      OptionProxy.new(self).yield!(options, &block)
      super()
    end
  end
end

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

= ui "slat" do |slat|
  - slat.title = "Title"
  - slat.subtitle = "Subtitle"
  - slat.label = "Label"
  - slat.link = "#"
  - slat.tertiary_title = "Tertiary"
  - slat.disabled = true
  - slat.checklist = ["Item 1", "Item 2", "Item 3"]

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

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

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

Приведенный ниже метод вызывает вспомогательную функцию link_to или content_tag в зависимости от наличия параметров [:link] .

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

def container(&block)
  tag =
    if options[:link]
      [:link_to, options[:link]]
    else
      [:content_tag, :div]
    end

  send(*tag, class: “slat__inner”, &block)
end

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

.slat
  = container do
  ...

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

= ui "slat" do |slat|
  ...
  - slat.disabled = true

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

.slat{ class: possible_classes("--disabled": options[:disabled]) }
  .slat__inner
    .slat__content
      %h4{ class: possible_classes("--alt": options[:disabled]) }= options[:title]
      %p{ class: possible_classes("--alt": options[:disabled]) }=
      options[:subtitle]
      = icon(options[:icon], color: "gray")

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

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


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

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

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

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


Наш вывод

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

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

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


Приходите работать в «Маховик»!

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

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