В статье речь пойдет о сборке БЭМ-проектов с помощью бандлера Webpack. Я покажу один из примеров конфигурации, не нагружая читателей лишними сущностями.
Материал подойдет тем, кто только начинает знакомство с БЭМ. Сначала коснемся теоретических аспектов методологии, а в разделе «Практика» я покажу, как их можно применить.
Немного теории
Если вы впервые слышите о БЭМ и хотите познакомиться с ним самостоятельно, держите документацию.
БЭМ — методология, которая применяется для организации проектов любого масштаба. Её разработали Яндекс и сперва использовали только в работе своих сервисов, но позже опубликовали в общем доступе.
БЭМ расшифровывается как “Блок, Элемент, Модификатор”.
Блок — сущность с автономной архитектурой, которая может повторно использоваться. Блок может содержать собственные элементы.
Элемент — составная часть блока. Элемент может использоваться только внутри родительского блока.
Модификатор — сущность, которая изменяет отображение, состояние или поведение блока.
Эти компоненты лежат в основе методологии. Они обеспечивают красоту и удобное разделение кода. Более подробно об их устройстве написано в документации.
Документация по БЭМ написана обстоятельно. Однако есть одно “но”: высокий порог вхождения в материал. Если с основами верстки можно разобраться, прочитав одну страницу документации, то вопрос сборки проекта обстоит сложнее.
Почему речь зашла о сборке проекта? При работе над масштабным проектом каждый сталкивается с проблемой организации кода. Хранить весь код большого проекта в одном файле неудобно. Разбивать код на несколько файлов, затем собирать его вручную — тоже не лучший выход. Для решения этой проблемы используются сборщики, или бандлеры, которые автоматизируют преобразование исходного кода проекта в код, готовый к отправке в продакшн.
Напомню: далее подразумевается, что у читателей есть базовые навыки работы с Webpack. Если прежде вы с ним не работали, рекомендую для начала познакомиться с этим инструментом.
В документации БЭМ даются рекомендации по сборке проектов. В качестве примеров предлагаются только два варианта: сборка при помощи ENB и Gulp.
ENB — утилита, разработанная специально для сборки БЭМ-проектов. Она способна работать с сущностями БЭМа из коробки. Но взгляните на код. Он с первого взгляда может демотивировать неподготовленного разработчика:
const techs = { // essential fileProvider: require('enb/techs/file-provider'), fileMerge: require('enb/techs/file-merge'), // optimization borschik: require('enb-borschik/techs/borschik'), // css postcss: require('enb-postcss/techs/enb-postcss'), postcssPlugins: [ require('postcss-import')(), require('postcss-each'), require('postcss-for'), require('postcss-simple-vars')(), require('postcss-calc')(), require('postcss-nested'), require('rebem-css'), require('postcss-url')({ url: 'rebase' }), require('autoprefixer')(), require('postcss-reporter')() ], // js browserJs: require('enb-js/techs/browser-js'), // bemtree // bemtree: require('enb-bemxjst/techs/bemtree'), // bemhtml bemhtml: require('enb-bemxjst/techs/bemhtml'), bemjsonToHtml: require('enb-bemxjst/techs/bemjson-to-html') }, enbBemTechs = require('enb-bem-techs'), levels = [ { path: 'node_modules/bem-core/common.blocks', check: false }, { path: 'node_modules/bem-core/desktop.blocks', check: false }, { path: 'node_modules/bem-components/common.blocks', check: false }, { path: 'node_modules/bem-components/desktop.blocks', check: false }, { path: 'node_modules/bem-components/design/common.blocks', check: false }, { path: 'node_modules/bem-components/design/desktop.blocks', check: false }, 'common.blocks', 'desktop.blocks' ]; module.exports = function(config) { const isProd = process.env.YENV === 'production'; config.nodes('*.bundles/*', function(nodeConfig) { nodeConfig.addTechs([ // essential [enbBemTechs.levels, { levels: levels }], [techs.fileProvider, { target: '?.bemjson.js' }], [enbBemTechs.bemjsonToBemdecl], [enbBemTechs.deps], [enbBemTechs.files], // css [techs.postcss, { target: '?.css', oneOfSourceSuffixes: ['post.css', 'css'], plugins: techs.postcssPlugins }], // bemtree // [techs.bemtree, { sourceSuffixes: ['bemtree', 'bemtree.js'] }], // bemhtml [techs.bemhtml, { sourceSuffixes: ['bemhtml', 'bemhtml.js'], forceBaseTemplates: true, engineOptions : { elemJsInstances : true } }], // html [techs.bemjsonToHtml], // client bemhtml [enbBemTechs.depsByTechToBemdecl, { target: '?.bemhtml.bemdecl.js', sourceTech: 'js', destTech: 'bemhtml' }], [enbBemTechs.deps, { target: '?.bemhtml.deps.js', bemdeclFile: '?.bemhtml.bemdecl.js' }], [enbBemTechs.files, { depsFile: '?.bemhtml.deps.js', filesTarget: '?.bemhtml.files', dirsTarget: '?.bemhtml.dirs' }], [techs.bemhtml, { target: '?.browser.bemhtml.js', filesTarget: '?.bemhtml.files', sourceSuffixes: ['bemhtml', 'bemhtml.js'], engineOptions : { elemJsInstances : true } }], // js [techs.browserJs, { includeYM: true }], [techs.fileMerge, { target: '?.js', sources: ['?.browser.js', '?.browser.bemhtml.js'] }], // borschik [techs.borschik, { source: '?.js', target: '?.min.js', minify: isProd }], [techs.borschik, { source: '?.css', target: '?.min.css', minify: isProd }] ]); nodeConfig.addTargets([/* '?.bemtree.js', */ '?.html', '?.min.css', '?.min.js']); }); };
Код из публичного репозитория project-stub.
Код конфига ENB явно будет сложным для тех, кто только начинает использовать БЭМ.
В документации представлены готовые настройки сборщика, и их вполне можно использовать, не вникая в детали сборки. Но что если вам как и мне хотелось бы иметь полное представление о том, что происходит с проектом во время сборки?
В документации БЭМ хорошо объясняется процесс сборки в теории, однако практических примеров немного и они не всегда подходят для наглядного понимания процесса. Чтобы решить эту проблему я попробую собрать элементарный БЭМ-проект с помощью Webpack.
Практика
До этого я упоминал, что разделение кода и организация сборки упрощают работу с проектом. В примере ниже мы обеспечим разделение кода с помощью БЭМ и его сборку при помощи Webpack.
Мы хотим получить наипростейший конфиг, логика сборки должна быть линейной и интуитивно понятной. Давайте соберём страницу с одним БЭМ-блоком, у которого будут две технологии: CSS и JS.
Можно написать HTML-код с одним DIV с классом "block" и вручную подключить все его технологии. Используя БЭМ-именование классов и соответствующую файловую структуру, мы не нарушаем принципы методологии.
У меня получилось вот такое дерево проекта:
├── desktop # Уровень переопределения "desktop" │ └── block # Блок "block" │ ├── block.css # CSS-технология блока "block" │ └── block.js # JS-технология блока "block" ├── dist # Каталог, где мы увидим собранную страницу ├── pages # Каталог, с исходной вёрсткой страниц и их JS-скриптами │ ├── index.html # Файл, содержащий вёрстку будущей страницы │ └── index.js # Входная точка для сборки страницы index.html └── webpack.config.js # Конфиг-файл Webpack
В первой строке упоминается уровень переопределения “desktop”. В терминологии БЭМ, уровни переопределения — директории, которые содержат собственные реализации блоков. При сборке проекта в итоговый бандл попадают реализации со всех уровней переопределения в определённом порядке.
Например, у нас есть уровень переопределения "desktop", в котором хранятся реализации блоков для настольных устройств. Если нам понадобится дополнить проект вёрсткой для мобильных устройств, нам будет достаточно создать новый уровень переопределения "mobile" и наполнить его новыми реализациями тех же блоков. Удобство такого подхода в том, что на новом уровне переопределения нам не нужно будет дублировать код, уже существующий в "desktop", так как он подключится автоматически.
Перед вами конфиг Webpack:
// webpack.config.js // Подключаем внешние модули const path = require('path'); const сopy = require('copy-webpack-plugin'); module.exports = { // Указываем entry и output - входную точку и имя конечного бандла entry: path.resolve(__dirname, "pages", "index.js"), output: { filename: 'index.js', path: path.join(__dirname, 'dist') }, module: { rules: [ // Добавляем загрузчики для CSS-технологий { test: /\.css$/, loader: 'style-loader!css-loader' } ] }, plugins: [ new сopy([ // Копируем HTML-файл с версткой в конечную директорию { from: path.join(__dirname, 'pages'), test: /\.html$/, to: path.join(__dirname, "dist") } ]) ] }
Здесь мы указываем файл /pages/index.js как входную точку, добавляем загрузчики для стилей CSS и копируем /pages/index.html в /dist/index.html.
<html> <body> <div class="block">Hello, World!</div> <script src="index.js"></script> </body> </html>
.block { color: red; font-size: 24px; text-align: center; }
document.getElementsByClassName('block')[0].innerHTML += " [This text is added by block.js!]"
В примере использован один уровень переопределения и один блок. Задача — собрать страницу так, чтобы к ней были подключены технологии (css, js) нашего блока.
Для подключения технологий воспользуемся require():
// index.js require('../desktop/block/block.js'); require('../desktop/block/block.css');
Запустим Webpack и посмотрим, что получилось. Откроем index.html из папки ./dist:

Стили блока подгрузились, javascript успешно отработал. Теперь к нашему проекту по праву можно добавить заветные буквы "БЭМ".
Прежде всего БЭМ создавался для работы с большими проектами. Давайте представим, что наш дизайнер постарался и на страничке теперь находится не один блок, а сто. Действуя по предыдущему сценарию, мы будем вручную подключать технологии каждого блока, используя require(). То есть в index.js появится как минимум сто дополнительных строк кода.
Лишние строки кода, которых можно было избежать, — это плохо. Неиспользуемый код — ещё хуже. Что если на нашей странице будет всего 10 из имеющихся блоков, или 20, или 53? У разработчика появится дополнительная работа: ему придется фокусировать внимание на том, какие именно блоки используются на странице, а также подключать и отключать их вручную, чтобы избежать лишнего кода в итоговом бандле.
К счастью, эту работу можно поручить Webpack.
Оптимальный алгоритм действий для автоматизации этого процесса:
- Выделить из имеющегося HTML-кода классы, соответствующие именованию БЭМ;
- На основании классов получить список БЭМ-сущностей, используемых на странице;
- Проверить, есть ли на уровнях переопределения директории используемых блоков, элементов и модификаторов;
- Подключить технологии этих сущностей в проект, добавив соответствующие выражения
require().
Для начала я решил проверить, нет ли готовых загрузчиков для данной задачи. Модуля, который предоставлял бы весь нужный функционал в одном флаконе, я не обнаружил. Но наткнулся на bemdecl-to-fs-loader, который преобразует декларации БЭМ в выражения require(). Он основывается на уровнях переопределения и технологиях, которые имеются в файловой структуре проекта.
Декларация в БЭМ — список БЭМ-сущностей, используемых на странице. Подробнее о них в документации.
Не хватает одного звена — преобразования HTML в массив БЭМ-сущностей. Эту задачу решает модуль html2bemjson.
bemjson — данные, которые отражают структуру будущей страницы. Обычно они используются шаблонизатором bem-xjst для формирования страниц. Синтаксис bemjson схож с синтаксисом деклараций, но декларация содержит только список используемых сущностей, в то время как bemjson также отражает их порядок.
bemjson не является декларацией, поэтому предварительно преобразуем его в формат decl для передачи в bemdecl-to-fs-loader. Для этой задачи используем модуль из SDK: bemjson-to-decl. Так как это обычные NodeJS-модули, а не загрузчики Webpack, придется сделать загрузчик-обертку. После этого мы сможем использовать их для преобразований в Webpack.
Получаем такой код загрузчика:
let html2bemjson = require("html2bemjson"); let bemjson2decl = require("bemjson-to-decl"); module.exports = function( content ){ if (content == null && content == "") callback("html2bemdecl requires a valid HTML."); let callback = this.async(); let bemjson = html2bemjson.convert( content ); let decl = bemjson2decl.convert( bemjson ); console.log(decl); // Проверим корректность формирования декларации callback(null, decl); }
Чтобы упростить установку загрузчика и сэкономить время в дальнейшем, я загрузил модуль на NPM.
Давайте установим загрузчик в наш проект и внесём изменения в конфигурацию Webpack:
const webpack = require('webpack'); const path = require('path'); const сopy = require('copy-webpack-plugin'); module.exports = { entry: path.resolve(__dirname, "pages", "index.js"), output: { filename: 'index.js', path: path.join(__dirname, 'dist') }, module: { rules: [ { test: /\.html$/, use: [ { // Передаем результат в bemdecl-to-fs-loader loader: 'bemdecl-to-fs-loader', // Указываем уровни переопределения и расширения технологий options: { levels: ['desktop'], extensions: ['css', 'js'] } }, // Для начала передаем файл в html2bemdecl-loader { loader: 'html2bemdecl-loader' } ] }, { test: /\.css$/, loader: 'style-loader!css-loader' } ] }, plugins: [ new сopy([ { from: path.resolve(__dirname, 'pages'), test: /\.html$/, to: path.resolve(__dirname, "dist") } ]) ] }
Параметр levels загрузчика bemdecl-to-fs-loader указывает, какие уровни переопределения использовать и в каком порядке. В extensions даны расширения файлов-технологий, которые используются в нашем проекте.
В итоге вместо подключения технологий вручную, мы подключаем лишь HTML-файл. Все необходимые преобразования будут выполнены автоматически.
Давайте заменим содержимое index.js строкой:
require('./index.html');
Теперь запустим Webpack. При сборке выводится строка:
[ BemEntityName { block: 'block' } ]
Это значит, что формирование декларации прошло успешно. Смотрим непосредственно вывод Webpack:
Entrypoint main = index.js [0] ./pages/index.js 24 bytes {0} [built] [1] ./pages/index.html 74 bytes {0} [built] [2] ./desktop/block/block.css 1.07 KiB {0} [built] [3] ./node_modules/css-loader/dist/cjs.js!./desktop/block/block.css 217 bytes {0} [built] [7] ./desktop/block/block.js 93 bytes {0} [built] + 3 hidden modules

Мы получили результат, идентичный предыдущему, с той разницей, что все технологии блока подключились автоматически. Сейчас нам достаточно добавить в HTML БЭМ-именованный класс, подключить этот HTML с помощью require() и создать соответствующий каталог с технологиями для подключения.
Итак, у нас есть файловая структура, которая соответствует методологии БЭМ, а также механизм автоматического подключения технологий блоков.
Абстрагируясь от механизмов и сущностей методологии, мы создали предельно простой, но эффективный конфиг Webpack. Надеюсь, данный пример поможет всем, кто начинает своё знакомство с БЭМ, лучше понять базовые принципы сборки БЭМ-проектов.
