В статье речь пойдет о сборке БЭМ-проектов с помощью бандлера 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. Надеюсь, данный пример поможет всем, кто начинает своё знакомство с БЭМ, лучше понять базовые принципы сборки БЭМ-проектов.
Полезные ссылки
- Репозиторий проекта на GitHub
- Полное руководство по БЭМ
- Рекомендации по сборке БЭМ-проектов
- Ещё один способ собрать БЭМ-проект при помощи Webpack
Автор: Ortophius