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


Каждый разработчик на свое усмотрение организовывает файловую структуру для хранения CSS и JavaScript кода, для HTML представлений у Rails есть кое-какие соглашения. Возможно кому-то приходилось сталкиваться с приложениями в которых CSS и JavaScript ограничиваются чуть ли не единственными application файлами, тогда содержимое этих файлов может представлять из себя даже не "портянки", а целые "простыни" кода. С представлениями тоже не все просто, особенно это касается partials, когда они используются для создания отдельных UI компонент: модальных окон, меню, подвалов и т.д. Порой надо сильно исхитриться, чтобы использовать такой UI компонент в разных представлениях с разными параметрами и/или логикой.


В итоге получается, что CSS, JavaScript, HTML представления и UI компоненты (на основе partials) лежат в разных каталогах и каждая технология имеет свою файловую структуру. При таком подходе, фронтенд в средних и больших приложениях тяжело поддерживать, из всех этих технологий получается очень гремучая смесь, которая может просто рвануть в любой неподходящий момент.


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


Bemer позволяет:


  • создавать переиспользуемые UI компоненты
  • использовать BEMHTML шаблоны для UI компонент
  • разрабатывать приложения по методологии БЭМ

Более подробную информацию можно найти здесь.


Файловая структура для UI компонент отличается от той, которая используется в методологии БЭМ. Каждый UI компонент имеет свой каталог, но файлы технологий называются index (индексные файлы):


Пример файловой структуры при использовании Sprockets.


app/
  +-- assets/
  |     +-- javascripts/
  |     |     L-- application.coffee
  |     L-- stylesheets/
  |           L-- application.scss
  +-- bemer_components/
  |     +-- modal/
  |     |     +-- index.html.slim
  |     |     +-- index.bemhtml.slim
  |     |     +-- index.coffee
  |     |     L-- index.scss
  |     L-- ...
  L-- ...  

Пример файловой структуры при использовании Webpacker.


app/
  +-- javascript/
  |     +-- bemer_components/
  |     |     +-- modal/
  |     |     |     +-- index.html.slim
  |     |     |     +-- index.bemhtml.slim
  |     |     |     +-- index.coffee
  |     |     |     L-- index.scss
  |     |     L-- ...
  |     +-- packs/
  |     |     +-- application.js
  |     |     L-- ...
  |     L-- ...
  L-- ...  

С помощью Bemer-a можно создавать различного вида UI компоненты. В этом примере будет рассмотрен способ, при котором можно использовать BEMHTML шаблоны. За основу будет взят компонент Modal из Bootstrap, со следующей HTML структурой:


<div class="modal fade" tabindex="-1" role="dialog">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal">
          <span aria-hidden="true">&times;</span>
        </button>
        <h4 class="modal-title">Modal title</h4>
      </div>
      <div class="modal-body">
        <p>Modal body</p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
      </div>
    </div>
  </div>
</div>

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


/ Содержимое файла app/bemer_components/modal/index.html.slim
= define_component bem_cascade: false, tag: :div do |component|
  = component.block :modal, cls: 'modal fade', role: :dialog, tabindex: -1 do |modal|
    = modal.elem :dialog, role: :document, cls: 'modal-dialog'
      = modal.elem :content, cls: 'modal-content'
        = modal.elem :header, cls: 'modal-header'
          = component.block :close, type: :button, tag: :button, cls: 'close', 'data-dismiss': :modal
            span aria-hidden='true' &times;
          = modal.elem :title, tag: :h4, cls: 'modal-title'
        = modal.elem :body, cls: 'modal-body'
        = modal.elem :footer, cls: 'modal-footer'

Структура (дерево) компонента состоит из блоков и элементов (узлов), они выступают в роли меток, используя которые, можно изменять компонент с помощью BEMHTML шаблонов, а также получать css классы из методологии БЭМ. В данном случае, генерация данных из методологии БЭМ не нужна, поэтому ее можно отключить с помощью параметра bem_cascade: false.


Теперь, в любом представлении можно вызвать компонент Modal и применить к нему необходимые BEMHTML шаблоны:


/ Кнопка для показа модального окна
button.btn.btn-primary.btn-lg data-target="#my-modal" data-toggle="modal" type="button" 
  | Launch demo modal

/ Вызов компонента Modal и применение BEMHTML шаблонов
= render_component :modal do |template|
  = template.block :modal do |modal|
    = modal.add_attrs id: 'my-modal'
    = modal.specify(elem: :dialog).add_cls 'modal-lg'
    = modal.specify(elem: :title).content 'Modal title'
    = modal.specify(elem: :body).content
      p Modal body
    = modal.specify(elem: :footer).content
      button.btn.btn-default data-dismiss="modal" type="button" Close
      button.btn.btn-primary type="button" Save changes

/ Эквивалентный результат
= render_component :modal do |template|
  = template.block(:modal).add_attrs id: 'my-modal'
  = template.elem(:dialog).add_cls 'modal-lg'
  = template.elem(:title).content 'Modal title'
  = template.elem(:body).content
    p Modal body
  = template.elem(:footer).content
    button.btn.btn-default data-dismiss="modal" type="button" Close
    button.btn.btn-primary type="button" Save changes

Заключение


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


Возможно кому-нибудь Bemer поможет:


  • навести порядок на фронтенде в старых проектах (Rails >= 3.2.22)
  • реорганизовать фронтенд в виде UI компонент
  • создавать отдельные коллекции UI компонент (как bemer-bootstrap) и использовать их в разных проектах
  • сделать фронтенд приложений более удобным для работы
  • более удобно вести разработку по методологии БЭМ

В заключении, хочу сказать спасибо всей команде БЭМ-а за быструю обратную связь и в частности tadatuta и miripiruni за развернутые ответы на вопросы по методологии БЭМ и шаблонизатору bem-xjst.


Ссылки


  1. Исходный код
  2. Документация
  3. bemer-bootstrap — Переиспользуемые UI компоненты Bootstrap
  4. Пример приложения
  5. Organizing large Rails projects with namespaces

Комментарии (1)


  1. Mox
    08.04.2018 13:07

    На мой взгляд, не очень удачное решение.

    Переиспользуемые компоненты идут лучше уж тогда в React/Vue — там действительно компоненты со своей логикой.

    А так, я не могу сказать что получилось меньше кодинга + в данном случае про модалку при привлечении нового разработчика в проект ему придется осваивать данную систему, а так — мог бы сразу включится.

    Я считаю что лучше определять какие-то правила кодинга и подходы на проекте.