Вёрстка по БЭМу в Ruby on Rails

Введение


В этой статье я хотел бы рассказать о технике вёрстки по БЭМу в рельсовых проектах. Я ещё не видел подобных руководств(кроме, может быть, этого, но оно мало подходит в качестве руководства и о нём ещё расскажу дальше), поэтому решил написать эту статью. Кроме того, я создал гем, который упростит интеграцию БЭМ и рельс, о нём и как его использовать я тоже напишу дальше.

Подготовка


Для начала вам нужно установить собственно руби и рельсы. Я предпочитаю rvm. Следуйте инструкции по установке rvm, а затем установите рельсы через команду:

gem install rails


Ну или можете установить рельсы отсюда, если у вас Windows.

Затем создаём новый проект через команду:

rails new some_cool_project


Далее добавим гем «bem» в файл Gemfile, который находится в корне только что созданного проекта. Также я рекомендую вам добавить гемы high_voltage и slim-rails. Первый позволит создавать для верстальщика типовые страницы без каких-либо особых знаний рельс и руби, второй — великолепный шаблонизатор, который может значительно ускорить вёрстку, а также минифицировать выходной html.

Теперь выполним установку необходимых файлов для гема bem при помощи команды:

rails g bem:install


Установится конфиг config/initializers/bem.rb, в котором вы можете менять технологии(изначально это scss и js) и шаблоны для генерации файлов для каждой технологии lib/bem/templates/scss.tt и lib/bem/templates/js.tt. Допустим, мы хотим использовать less в нашем проекте, тогда эту технологию нужно прописать в конфиг config/initializers/bem.rb вместо scss:

BEM.configure do |config|
  config.technologies = [
    { :group => 'stylesheets', :extension => '.less', :name => 'less',
      :css_directive => '@import', :css_prefix => "'", :css_postfix => "';" },
    { :group => 'javascripts', :extension => '.js', :name => 'js' }
  ]
end


Также я очень рекомендую установить конфиг для spring через команду:

rails g bem:spring


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

Если вы установили конфиг для spring, то выполните следующую команду:

spring stop


Это команда нужна для последующей перезагрузки spring.

Теперь можно уже приступать к вёрстке.

Вёрстка


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

Блоки, уровни и манифесты создаются с помощью команды bem create. Это команда принимает на вход следующие флаги(значение флагов нужно указывать через пробел после них самих):

  • -b создаёт или использует блок.
  • -e создаёт или использует элемент.
  • -m создаёт модификатор.
  • -v значение модификатора.
  • -l создаёт или использует уровень.
  • -a создаёт или использует манифест.
  • -js флаг для создания файлов javascripts технологий.
  • -css флаг для создания файлов stylesheets технологий.


Вы также можете прочитать информацию о флагах и их использовании выполнив команду:

bem help create


Команда create будет создавать такуе же структуру файлов(как и тут).

Далее создадим наш будущий уровень, в котором будут подключаться наши блоки и манифест через команду:

spring bem create -l shared -a application


Можно и без spring:

bem create -l shared -a application


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

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

Если вы заглянете в файл манифеста, то увидете, что уровень shared подключен в него строкой
@import 'shared/shared';

Действует простое правило — в манифестах подключаются уровни, в уровнях подключаются блоки.

Удаляем прошлый манифест app/assets/application.css, так как мы будем использовать теперь манифест app/assets/application.less

Далее создадим нужные нам блоки|элементы|модификаторы через подобные команды:

spring bem create -l shared -b clear --no-js
spring bem create -l shared -b page --no-js
spring bem create -l shared -b page -e head-line --no-js
spring bem create -l shared -b page -e line --no-js
spring bem create -l shared -b link -m menu -v active --no-js
...


Как видно, я указываю уровень shared, в котором будут находится блоки и ещё флаг --no-js, так как для этих блоков не будут использоваться js файлы.

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

Если вы ошиблись и написали не тот класс или включили блок не в тот уровень/манифест, всегда можно откатиться выполнив обратную команду удаления блока|элемента|модификатора|уровня через команду bem destroy. Она принимает все те же флаги, кроме --js и --css.

Далее, когда мы создадим нужные нам блоки|элементы|модификаторы, переходим к вёрстке html. Если вы установили гем high_voltage, о котором я уже писал, то достаточно будет просто создать папку:

mkdir app/views/pages


В которой будут находится нужные нам вьюхи. Если вы прочитали документацию к этому гему, то вам наверняка понравилась простота подключения и отображения вьюх. Например, вам нужна вёрстка для страницы «welcome». Создаём вьюху app/views/pages/welcome.html.slim и прописываем там нужный нам html. После запуска вебсервера через команду

rails s


Вы можете увидеть полученный результат по адресу http://localhost:3000/pages/welcome

Складываем все картинки в папку app/assets/images и все теги заменяем на рельсовые хэлперы image_tag. Адреса картинок в less файлах заменяем на image-url (хэлпер, который предоставляет гем less-rails).

К сожалению, в приведённом примере не используется js. Тем не менее шаблоны, которые предоставляются для него, позволяют легко написать нужную функцию и запустить её:

function your_function_name_initializer() {
// some code
}

$(function() {
  your_function_name_initializer();
});

$(window).bind('page:load', function() {
  your_function_name_initializer();
})


Почему так? Дело в том, что начиная с 4 версии рельс, используется гем turbolinks, который значительно ускоряет отображение страниц, но добавляет некоторые особенности в инициализации js, поэтому приходится оформлять такие инициализации в виде функций и затем вызывать при этих двух событиях.

Конечный вариант перенесённой вёрстки и самого рельсового проекта можно посмотреть на гитхабе.

Альтернативы


Есть также гем bem-on-rails, о статье про который я говорил в начале. Я уже было хотел использовать его, но у него есть множество минусов на данный момент, которые вынудили меня написать свой гем. Среди минусов — невозможность нормально работать с уровнями и манифестами(мне так и не удалось создать два манифеста со своими уровнями и раскидывать блоки по ним), плохочитаемый код(где-то 4 пробела в качестве таба, где-то два) и вообще невысокое качество кода(нет ни одного теста, некрасивая архитектура, такое чувство, что гем делался совсем впопыхах и туда просто накидан код и вообще мне не удалось его запустить на рельсах версии 4.1 — пришлось отправить патч), плохая документация, ну а также, если использовать вьюшные хэлперы этого гема, то может получится такое например(простой футер):

= b 'footer', content: [{ elem: 'el', elemMods: ['left'], content: ['ОАО «фирма»'] },
   { elem: 'el', elemMods: ['center'], content: [{ elem: 'nav-link', tag: 'a', content: ['ссылка'] },
   { elem: 'nav-link', tag: 'a', content: ['ещё сслыка']},
   { elem: 'nav-link', tag: 'a', content: ['и ещё ссылка'] }] },
   { elem: 'el', elemMods: ['right'], 
   content: ['<a href="http://ya.ru/">такие дела</a>'.html_safe] }]


Что явно не идёт на пользу читаемости и может запутать верстальщика.

Тем не менее, я считаю, что в целом это неплохая попытка подстраивания bem-tools под рельсы.

Есть ещё некоторые экзотические варианты встраивания ноды в рельсы, чтобы использовать нативно bem-tools через grunt, например half-pipe. Но я посчитал этот вариант весьма громоздким и непростым, поэтому не стал его рассматривать всерьёз.

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

Заключение


Итак, верстальщику достаточно будет просто клепать вьюхи и блоки с css|js не обязательно зная, что там творится внутри. А бекэнд разработчик вставит нужную логику. То есть процесс вёрстки сильно упрощается.

Библиотека(гем) bem, которую я написал, полностью открытая и использует самую демократичную лицензию MIT. Библиотеку я написал совсем недавно и буду рад вашим отзывам и предложениям, а также пуллреквестам на гитхаб.

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

Ссылки


https://github.com/gkopylov/bem
https://github.com/gkopylov/bem_with_rails_and_less_example_app

P.S.

На момент написания статьи использовался гем bem версии 1.0.0. На данный момент версия этого гема 1.1.0 — в ней добавлена возможность подключения блоков|элементов|модификаторов прямо в манифест. Сделано это для того, чтобы верстальщику можно было самому менять порядок подключения стилей прямо в манифесте, вместо того, чтобы подключать туда уровни.

Чтобы воспользоваться данной опцией, достаточно добавить флаг -i в команду генерации. Таким образом созданные стили подключатся сразу в манифест без создания стилей для уровня.

Ещё немного хотел написать насчёт использования директив import и require. У обеих директив есть свои плюсы и минусы. Среди плюсов у import:
— можно подключать миксины и переменные
— всё сливается в один файл и быстрее отдаётся браузеру, вместо множество файлов, который генерирует require (но это и минус, см. ниже)
среди минусов:
— трудно отлаживать css — если возникнет ошибка препроцессора, будет трудно найти место этой ошибки
— не обновляются стили при изменении вложенных css файлов (для того, чтобы обновились стили нужно выполнить команду rm -rf tmp/assets/* и перезапустить рельсы)
Среди плюсов require:
— проще отлаживать, так как на каждый require он компилирует отдельный файл(в development среде)
— возможность livereload injection(теоретически это сделать проще чем с импортом)
среди минусов:
— невозможность использования миксинов или переменных
— может долго отдаваться скомпилированные файлы стилей браузеру, если директив 'require' много

Так что решение использовать ту или иную директиву нужно принимать обдуманно и в зависимости от проекта.

Ещё немного хотел написать почему по-дефолту я решил использовать scss. Дело в том, что в сообществе рельс sass препроцессор пользуется большей популярностью и даже по умолчанию при создании нового приложения включается гем с этим препроцессором. А ещё там(как впрочем, верно подметил faost и в less http://lesscss.org/features/#parent-selectors-feature) есть отличная фича, которая как раз подходит для БЭМ — это использование амперсанда. Уже многие в БЭМ сообществе отходят от написания отдельного файла под модификатор(если он небольшой) и пишут модификатор прямо в файле с блоком, а при помощи этого сассного амперсанда можно как раз делать такие штуки прямо в стилях для блока.
Метки:
  • +10
  • 10,2k
  • 9
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 9
  • 0
    Уже многие в БЭМ сообществе отходят от написания отдельного файла под модификатор и пишут модификатор прямо в файле с блоком,

    некоторые принципы диктуются особенностями ихнего (BEM) инструментария. тут основной прикол в тамошнем сборщике, который работает не с BEM-сущностями как таковыми, а с файлами. именно раскидывание кода по файлам дает возможность тулзам собирать результат в правильном порядке (с учетом уровней переопределения, порядка применения модификаторов, явных зависимостей и прочей хиромантии). поэтому файлы и четкое следование правилам их именования вообще-то являются очень важной частью разработки. когда вы складываете все в один файл, результат сборки становится значительно менее предсказуемым.
    • 0
      Ну да, в бемтулсах это всё имеет значение. Тем не менее, команда bem create создаёт структуру файлов, полностью идентичную той, что создают бемтулсы. Так что, если не хотите использовать эти амперсанды, можно делать всё по правилам БЭМа.

      Насчёт переопределения и порядка — для этого я как раз добавил возможность вставки css ассетса прямо в манифест через опцию -i, чтобы верстальщик смог контролировать переопределение и порядок таким образом.
      • 0
        И ещё — конечно можно использовать разные манифесты и не складывать все стили в один файл. Например, для разных лэйаутов можно использовать разные манифесты. А если вы хотите использовать свой манифест для каждого контроллера(что правда убивает преимущества turbolinks), то нужно будет прописать пару строк и не забыдь добавить в прекомпиляцию соответствующие манифесты. В последней версии рельс это добавляется в файле config/initializers/assets.rb.
        • 0
          а можно какой-нибудь пример, который бы проиллюстрировал ситуацию, когда результат сборки «все в одном файле» окажется непредсказуемым?
          • 0
            например, когда у нас есть библиотека (common), мы пытаемся переопределять что-то на своем уровне (desktop):

            common/
              block/
               block.css
               block_mod.css
            
            desktop/
              block/
                block.css
            


            предположим, что я положил все свои переопределения в один desktop/block/block.css:

            .block {}
            .block_mod {}
            


            в сборке у меня часто получалась вот такая прелесть:

            @import url(common/block/block.css);
            @import url(desktop/block/block.css);
            @import url(common/block/block_mod.css);
            


            т.е. common/block/block_mod.css перекрывает все старания desktop/block/block.css. если же модификаторы класть в отдельный файл, тогда эффект в сборке получается правильным:

            @import url(common/block/block.css);
            @import url(desktop/block/block.css);
            @import url(common/block/block_mod.css);
            @import url(desktop/block/block_mod.css);
            


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

              в процессе сборки происходит достаточно простая штука.

              есть 2 сущности, задекларированные в БЭМ-дереве: блок и модификатор.
              {
                  block: 'block',
                  mods: { mod: true }
              }
              


              на файловой системе на одном из уровней сущности описаны в отдельных файлах, на другом — в одном:
              common/ block/ block.css _mod/ block_mod.css desktop/ block/ block.css

              сборщик на основе дерева строит граф зависимостей, представляющий из себя массив. зависимости попадают туда строго в том порядке, в котором они встречаются в БЭМ-дереве:
              exports.deps = [
                  {
                      "block": "block"
                  },
                  {
                      "block": "block",
                      "mod": "mod"
                  }
              ];
              


              далее при сборке конкретной технологии (в нашем примере — CSS), сборщик обходит все задекларированные уровни и пушит в массив пути к каждому найденному файлу.
              соответственно, для первого элемента массива найдутся ['common/block/block.css', 'desktop/block/block.css'], затем перейдет к модификатору и допушит туда 'common/block/block_mod.css'.

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

                такое возможно, лишь когда весь код находится под контролем одной команды (крайность какая-то?). в общем же случае, надежнее придерживаться общих соглашений.
        • 0
          А ещё там есть отличная фича, которая как раз подходит для БЭМ — это использование амперсанда.


          В LESS также:

          .nav {
            background: white;
            &__link {
              font-size: 1.2em;
              &_active {
                color: blue;
              }
            }
          }


          Превратится в

          .nav {
            background: white;
          }
          .nav__link {
            font-size: 1.2em;
          }
          .nav__link_active {
            color: blue;
          }
          • 0
            Да, действительно, есть такое. Спасибо, добавил ссылку на документацию и ваш комментарий в статью.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.