- PVSM.RU - https://www.pvsm.ru -
— Сначала вы его отрицаете, потом вы его ненавидите, а потом вы не можете без него жить.
из доклада Артема Курбатова «БЭМ: мастер-класс»
Методология БЭМ существует достаточно долго и принята на вооружение в Google, EPAM Systems, BBC, Альфа-Банке. При этом она все еще вызывает беспокойство у типичного разработчика и менеджера проектов среднего звена.
У некоторых смельчаков изучение БЭМ не ушло дальше ограничения возможностей CSS для получения более предсказуемых результатов. И хотя БЭМ давно вышел за пределы верстки, до сих пор на вопрос «Знаете ли вы БЭМ?» можно услышать: «Конечно, это про подчеркивания в классах».
Если ваше представление о БЭМ близко к этому, я отвечу вам словами работодателя при приеме на работу новоиспеченного выпускника: «Забудьте о том, что вы слышали о БЭМ ранее». Методология БЭМ настолько интересна, насколько большинству о ней ничего не известно. Чтобы понять всю прелесть БЭМ, необходимо иметь представление обо всех технологиях, библиотеках, фреймворках и инструментах, которые БЭМ предоставляет. Изучите их, оставайтесь инопланетянином, ребенком, который удивляется тому, с чем взрослые смирились.
Что для меня БЭМ?
Наверняка вы слышали истории, как дерзкие вундеркинды, закрывшись в гараже, изобрели очередной стартап. Первые успехи позволили им привлечь инвестиции и выйти на рынок с потрясающим продуктом. Как это им удалось, как удалось выдержать пиковые нагрузки и не погрязнуть в вечном рефакторинге? Как надо, чтобы не как обычно?
БЭМ — это лайфхак для стартаперов. По сравнению со многими другими фреймворками и технологиями начало работы с БЭМ требует определенных усилий, но это окупается архитектурой, обеспечивающей устойчивую расширяемость. Во все технологии, фреймворки и библиотеки БЭМ изначально заложены принципы декларативного подхода. И именно это делает их такими непонятными на первый взгляд и любимыми в итоге.
Прочитав данную статью вы не постигните дзен БЭМ, но точно сможете ответить взрослым, что научились создавать полноценные динамические проекты по БЭМ, используя не только CSS. И если завтра ваше приложение постигнет участь в +10 К пользователей в сутки, будьте спокойны — у вас останется времени отметить это событие.
Сегодня мы расскажем о новом шаблонном репозитории для динамических проектов bem-express [15], который не только позволяет развернуть БЭМ-проект в один клик, но и делает автоматическую пересборку проекта и перезагрузку браузера. На его основе разработаем динамическое приложение и опишем процесс взаимодействия различных БЭМ-технологий. Мы сознательно не будем рассматривать вопросы верстки и клиентского JavaScript, чтобы избежать участи «подчеркивания в классах».
Что мы будем разрабатывать?
Поисковое приложение Social Services Search Robot (сокр. SSSR), которое по запросу покажет последние твиты и видео с YouTube.
Будем использовать:
Для начала потребуется установить:
Важно! Пользователям операционной системы Windows необходимо дополнительно установить Git Bash [24].
Чтобы статья получилась ярче, мы немного порисуем:
В БЭМ нет разделения технологий на главные и второстепенные. Есть набор, а выбор применения определяется индивидуально:
Подробнее о BEMJSON-формате [30] входных данных.
Давайте рассмотрим их подробнее.
Определяет список БЭМ-сущностей [31] для страницы.
Такой список в БЭМ называется декларацией [32]. Задача декларации — определить, что и в каком порядке подключать в сборку.
Декларации описываются в файлах с расширением .bemdecl.js.
Пример
exports.blocks = [
{ name: 'page' },
{ name: 'header' },
{ name: 'body' },
{ name: 'footer' }
];
Когда количество блоков переходит рубеж «легко запомнить», возникает проблема их перечисления в нужном порядке. Поэтому обычно декларируют какой-то определенный блок, который следует рассматривать как центральную «точку входа».
Все остальные БЭМ-сущности попадают в сборку по зависимостям [26].
Определяет зависимости [18] между БЭМ-сущностями, которые разнесены по файловой структуре проекта и не отражены в декларации.
Зависимости описываются в виде JavaScript-объекта в файлах с расширением .deps.js.
Пример
/* page зависит от header */
({
block: 'page',
shouldDeps: [
{ block: 'header' }
]
})
Название технологии происходит от английского слова dependence и обозначает желание подключить в сборку какую-то БЭМ-сущность. Изучающие БЭМ порой забывают о декларативности технологий и недоумевают: почему для описанного в шаблоне блока, не собираются его стили и скрипты.
Помните: когда вы описываете шаблон (BEMHTML [28] или BEMTREE [27]) с каким-то блоком внутри (дочерний узел), вы просто добавляете новый HTML-элемент. Чтобы стили и скрипты этого блока попали в сборку, необходимо описать зависимость от него.
Например, для того чтобы добавить в сборку блоки header, body и footer, необходимо описать зависимость от них:
/* page зависит от header, body, footer */
({
block: 'page',
shouldDeps: [
'header',
'body',
'footer'
]
})
Нижеследующая схема показывает логику сборки по зависимостям:
index(DECL) # Декларация блока page
|
└──> page(DEPS) # Зависимость блока page от header, body, footer
|
├──> header(DEPS)
| |
| └──> ...
|
├──> body(DEPS)
| |
| └──> ...
|
└──> footer(DEPS)
|
└──> ...
Является частью шаблонизатора bem-xjst [17] и преобразует данные в BEMJSON.
Шаблоны описываются в BEMJSON-формате в файлах с расширением .bemtree.js.
Вход и выход шаблонизатора:
Является частью шаблонизатора bem-xjst [17] и преобразует BEMJSON-описание страницы в HTML.
Шаблоны описываются в файлах с расширением .bemhtml.js.
Вход и выход шаблонизатора:
Клиентский JavaScript-фреймворк [16] для веб-разработки в рамках БЭМ-методологии.
JavaScript-код описывается в файлах с расширением .js.
Позволяет:
У программистов есть традиция: начинать программирование на новом языке или фреймворке с приложения Hello, World. Приложение обычно выводит слова «Hello, World» в выходной поток, демонстрируя тем самым, что оно запускается и выполняет операции ввода/вывода.
Давайте создадим его, а затем расширим до желаемого SSSR.
Нам потребуется локальная копия шаблонного репозитория bem-express [4]. Ее можно сделать с помощью Git.
Примечание. Для пользователей OS X или Linux все команды выполняются в терминале. Пользователям Windows потребуется Git Bash. Убедитесь, что Git Bash запущен от имени администратора.
При решении задач по разработке динамических приложений в рамках БЭМ создан шаблонный репозиторий bem-express [15]. Он содержит необходимый минимум конфигурационных файлов и решает целый класс задач, таких как сборка проекта, настройка линтеров, подключение библиотек и др.
В него по умолчанию подключены основные БЭМ-библиотеки:
Чтобы создать приложение Hello, World:
Склонируйте bem-express:
git clone https://github.com/bem/bem-express.git sssr-project
Примечание. В данном примере используется
bem-expressверсии 2.00.
Перейдите в директорию проекта:
cd sssr-project
Удалите историю версионирования исходного репозитория:
rm -rf .git
Инициализируйте собственный Git-репозиторий:
git init
Установите зависимости:
npm install
Примечание. Не используйте права суперпользователя
rootпри установке npm-зависимостей.
Соберите проект и запустите сервер:
npm run dev
Примечание. За сборку отвечает ENB [35].
При запуске приложения в терминале выводится сообщение о том, что сервер выполняется на порте 3000:
Server is listening on 3000.
Примечание. Если порт
3000используется другой программой, его можно переназначить. Например, на8000:Способ 1. Изменение значения при запуске приложения.
PORT=8000 npm run devСпособ 2. Изменение значения по умолчанию в файле
server/config.js.defaultPort: 8000,
На компьютере запустился:
*.blocks/ и перестраивает структуру проекта;
Откройте браузер и введите адрес localhost:3000 [40].
Должна открыться страница со следующим контентом:
Index page content
footer content
Примечание. Если при запуске приложения в Windows, выводится уведомление от Брандмауэра:
- Отключите опцию Общественные сети (Public Network).
- Установите опцию Частные сети (Private Network).
- Разрешите доступ.
Откройте файл server/index.js и внесите следующие изменения (см. комментарии) в код начинающегося строкой app.get('/', function(req, res):
/**
* Функция обрабатывает все GET-запросы с главной страницы приложения
* @function
* @param {object} req - Запрос.
* @param {object} res - Ответ.
*/
app.get('/', function(req, res) {
var hello = 'Hello'; // Инициализируем переменную `hello`
var world = 'World'; // Инициализируем переменную `world`
render(req, res, {
view: 'page-index',
title: 'Main page',
meta: {
description: 'Page description',
og: {
url: 'https://site.com',
siteName: 'Site name'
}
},
hello: hello, // Передаем переменную `hello` в `this.data.hello`
world: world // Передаем переменную `world` в `this.data.world`
})
});
Откройте файл common.blocks/page-index/page-index.bemtree.js и замените его содержимое на следующее:
block('page-index').content()(function() {
// Получаем данные из глобального объекта `this`
var data = this.data;
// Возвращаем полученные данные: `data.hello: 'Hello'`, `data.world: 'World'`
return data.hello + ', ' + data.world;
});
После сохранения сервер автоматически перезапустится и контент страницы изменится на:
Hello, World
footer content
Приложение Hello, World готово.
Не получилось?
Если при создании приложения возникли сложности, поищите решение на форуме [41]. Если готового ответа не нашлось, задайте вопрос.
Собственно мы дошли до шага разработки приложения SSSR. Напомню, что по запросу приложение будет выводить последние твиты и видео с YouTube.
Сразу забежим в недалекое будущее — выглядеть приложение будет так:

Пользователь отправляет запрос на сервер.
Приложение обращается за данными к Twitter Search API [21] и YouTube Data API [20] в соответствии с полученным запросом.
Приложение передает полученные данные BEMTREE-шаблонизатору [27], который преобразует данные в BEMJSON.
Приложение передает BEMJSON BEMHTML-шаблонизатору [28], который преобразует BEMJSON в HTML.
Приложение возвращает результат (HTML-страницу) пользователю.
Базовая реализация Node остается настолько простой, насколько это возможно. Вместо того, чтобы встраивать все возможные компоненты прямо в Node, разработчики предоставляют дополнительную функциональность в виде отдельных модулей (пакетов).
Система модулей Node построена по образцу системы CommonJS [42], механизма создания взаимодействующих модулей. Центральное место в системе занимает контракт, который должен выполняться разработчиками, чтобы их модули нормально взаимодействовали с другими.
Все пакеты установленные с помощью менеджера пакетов npm находятся в директории node_modules.
Подключение модулей происходит при помощи команды require. Если пакет установлен с использованием npm, указывать путь не нужно. Достаточно указать имя:
var express = require('express');
При подключении собственного локального модуля, необходимо указать к нему путь:
var someModule = require('./somefolder/somemodule');
Важной особенностью любого модуля является то, что он должен быть рассчитан на взаимодействие с Node. Для этого модуль нужно экспортировать с помощью module.exports:
module.exports = {
// some module
};
Для работы приложения потребуются следующие модули:
Установить их можно одной командой:
npm install express passport passport-youtube-v3 twitter googleapis moment --save
Прежде чем начать писать код, немного изменим структуру взятого за основу приложения Hello, World.
Изменения для:
Директория static
Создайте поддиректорию images.
images.Директория common.blocks
Отредактируйте файл root/root.bemtree.js.
Измените:
favicon: '/favicon.ico',
На:
favicon: '/images/favicon.ico',
Полный код root.bemtree.js [52].
Директория server
Отредактируйте файл index.js.
Измените:
.use(favicon(path.join(staticFolder, 'favicon.ico')))
На:
.use(favicon(path.join(staticFolder, '/images/favicon.ico')))
Полный код index.js [53].
Директория server
Создайте поддиректории:
controllers — контроллеры;helpers — хелперы;middleware — модули промежуточного звена.
Создайте пустые JS-файлы для будущих модулей:
app.js — модуль монтирования промежуточных модулей (делает их доступными в приложении);auth.js — модуль аутентификации на YouTube;routes.js — модуль маршрутизации веб-запросов.
Добавьте следующий код [54] в файл app.js.
Добавьте следующий код [55] в файл routes.js.
Измените расширение файла config:
config.js —> config.json
Отредактируйте файл config.json.
Измените:
module.exports = {
staticFolder: 'static',
defaultPort: 3000,
cacheTTL: 30000,
sessionSecret: 'REPLACE_ME_WITH_RANDOM_STRING'
};
На:
{
"staticFolder": "static",
"defaultPort": 3000,
"cacheTTL": 30000,
"sessionSecret": "REPLACE_ME_WITH_RANDOM_STRING"
}
Измените весь текущий контент файла index.js на следующий [56].
Примечание. В
index.jsостается только функциональность, отвечающая за запуск приложения и прослушивание запросов на порте.
Директория controllers
Создайте пустой JS-файл:
index.js — контроллер обработки запросов и рендеринга HTML.
index.js.Директория helpers
Создайте пустые JS-файлы:
index.js — входная точка для хелперов;twitter.js — модуль-хелпер для работы с Twitter Search API;youtube.js — модуль-хелпер для работы с YouTube Data API.
Директория middleware
Создайте пустой JS-файл:
auth.js — модуль проверки прохождения аутентификации на YouTube.
Сервисы Twitter и Google хранят различные данные пользователей — твиты, видео на Youtube, письма в Почте, фотографии и так далее. Чтобы обеспечить удобный доступ к этим данным из других приложений или сторонних сервисов, они используют открытый протокол авторизации OAuth 2.0 [58].
Согласно протоколу, разработчик регистрирует приложение на OAuth-сервере и запрашивает доступ к определенным данным. Авторизованный пользователь разрешает или запрещает его.
Twitter предлагает приложениям возможность выдавать аутентифицированные запросы от имени самого приложения.
С чего начать?
Consumer Key:Consumer Secret методом Base64 [63].Используйте полученные токен и ключи в запросах к Twitter Search API.
Примечание. Postman поможет получить OAuth-токен с помощью POST-запроса в обмен на код, полученный методом Base64.
Чтобы закодировать строку методом Base64:
Сформируйте строку вида: Consumer Key:Consumer Secret.
Пример
xvz1evFS4wEEPTGEFPHBog:L8qq9PZyRg6ieKGEKhZolGC0vJWLw8iEJ88DRdyOg
Примечание. Получить ключи Consumer Key и Consumer Secret можно, перейдя на вкладку Keys and Access Tokens вашего приложения [60].
echo -n "xvz1evFS4wEEPTGEFPHBog:L8qq9PZyRg6ieKGEKhZolGC0vJWLw8iEJ88DRdyOg" | base64.Скопируйте полученный код.
Пример
eHZ6MWV2RlM0d0VFUFRHRUZQSEdFS2hab2xHQzB2SldMdzhpRUo4OERSZHlPZw==
Примечание. Если возникли сложности, воспользуйтесь онлайн-ресурсом base64encode.org [65].
Чтобы получить токен в обмен на код:
Запустите Postman.
Примечание. По умолчанию открывается вкладка, в которой необходимо сформировать POST-запрос к OAuth-серверу Twitter.
https://api.twitter.com/oauth2/token.Введите в поле Key заголовок Authorization со значением (поле Value) Basic <закодированная строка Consumer Key:Consumer Secret>.
Пример
Authorization: Basic eHZ6MWV2RlM0d0VFUFRHRUZQSEdFS2hab2xHQzB2SldMdzhpRUo4OERSZHlPZw==
Примечание. Basic указывает на базовый метод авторизации.
Введите второй заголовок Content-Type со значением application/x-www-form-urlencoded;charset=UTF-8.
Пример
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
x-www-form-urlencoded.grant_type со значением client_credentials.Нажмите кнопку Send.
OAuth-сервер вернет токен в JSON-формате:
{
"token_type": "bearer",
"access_token": "AAAAAAAAAAAAAAAAAAAAAA%2FAAAAAAAAAA%3DAAAAAAAAAAAAAAAAAA"
}
Важно! Сохраните полученные токен и ключи (Consumer Key и Consumer Secret). Они потребуются для конфигурационного файла [11] приложения.
Google предлагает приложениям возможность выдавать аутентифицированные запросы от имени самого приложения.
Примечание. За получение и обновление OAuth-токена с помощью POST-запроса в обмен на код авторизации отвечает модуль passport-youtube-v3 [45].
С чего начать?
http://localhost:3000) в учетной записи вашего приложения.Важно! Сохраните полученные ключи (Client ID и Client Secret). Они потребуются для конфигурационного файла [11] приложения.
После того как все ключи и токены получены, их необходимо добавить в конфигурационный файл приложения:
Добавьте в файл server/config.json поле services.
"services": {
"twitter": {
"consumer_key": "",
"consumer_secret": "",
"bearer_token": ""
},
"youtube": {
"client_id": "",
"client_secret": "",
"redirect_url": "http://localhost:3000"
}
}
Полный код config.json [67].
Скройте файл server/config.json от системы контроля версий Git, чтобы случайно не добавить личные ключи в репозиторий файлов.
# файл .gitignore
server/config.json
Полный код .gitignore [69].
Twitter Search API [21] позволяет найти последние или популярные твиты, опубликованные на сайте Twitter.com за последние 7 дней.
Для успешного вызова API необходимо сделать следующие изменения:
Директория controllers
index.js на следующий [70].Директория helpers
Добавьте в файл index.js следующий контент:
module.exports = {
twitter: require('./twitter')
};
twitter.js.YouTube Data API [72] позволяет найти видеоролики, опубликованные на сайте Youtube.com. По умолчанию в набор результата поиска включены следующие ресурсы: видео, каналы, списки воспроизведения.
Для успешного вызова API необходимо сделать следующие изменения:
Директория server
Добавьте следующий код [73] в файл auth.js.
Отредактируйте файл routes.js.
Измените:
var router = require('express').Router(),
controllers = require('./controllers');
router
.get('/ping/', function(req, res) {
res.send('ok');
})
.get('/', controllers.getContent);
module.exports = router;
На:
var router = require('express').Router(),
controllers = require('./controllers'),
passportYouTube = require('./auth'),
middleware = require('./middleware/auth'),
isAuthenticated = middleware.isAuthenticated;
router
.get('/auth/youtube', passportYouTube.authenticate('youtube'))
.get('/auth/youtube/callback', passportYouTube.authenticate('youtube', { failureRedirect: '/error', failureFlash: true }), (req, res) => {
res.redirect('/');
})
.get('/', isAuthenticated, controllers.getContent);
module.exports = router;
Директория controllers
index.js на следующий [74].Директория helpers
Добавьте в файл index.js следующий контент (см. комментарий):
module.exports = {
twitter: require('./twitter'),
youtube: require('./youtube') // Подключаем модуль `youtube.js`
};
youtube.js.Директория middleware
Добавьте в файл auth.js следующий контент:
module.exports = {
isAuthenticated: function(req, res, next) {
if (req.isAuthenticated()) return next();
return res.redirect('/auth/youtube');
}
};
Мы сознательно не рассматривали вопросы верстки и клиентского JavaScript. Это привело бы к большему объему, а, значит, и к меньшей практической ценности данной статьи.
Процесс верстки сведен к следующим шагам:
common.blocks.common.blocks.static/images.npm run dev.Приложение Social Services Search Robot готово.
Не получилось?
Если при создании приложения возникли сложности, поищите решение на форуме [41]. Если готового ответа не нашлось, задайте вопрос.
Автор: godfreyd
Источник [78]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/263346
Ссылки в тексте:
[1] Используемые обозначения: #used-names
[2] Используемые технологии: #used-technologies
[3] Приложение Hello, World: #app-hello-world
[4] Шаблонный репозиторий: #template
[5] Быстрый старт: #faststart
[6] Приложение Social Services Search Robot: #app-sssr
[7] Схема работы приложения: #scheme
[8] Используемые модули Node: #node-modules
[9] Подготовка структуры проекта: #preparing
[10] Получение OAuth-токенов: #oauth-tokens
[11] Конфигурация приложения: #config-file
[12] Работа с Twitter Search API: #twitter-api-work
[13] Работа с YouTube Data API: #youtube-api-work
[14] Верстка: #verstka
[15] bem-express: https://github.com/bem/bem-express
[16] i-bem.js: https://ru.bem.info/platform/i-bem/
[17] bem-xjst: https://ru.bem.info/platform/bem-xjst/
[18] DEPS: https://ru.bem.info/platform/deps/
[19] Express.js: http://expressjs.com
[20] YouTube Data API: https://developers.google.com/youtube/v3/docs/search/list
[21] Twitter Search API: https://dev.twitter.com/rest/public/search
[22] Node.js 4+: https://nodejs.org
[23] Git: https://git-scm.com
[24] Git Bash: https://git-for-windows.github.io
[25] BEMDECL: #bemdecl
[26] DEPS: #deps
[27] BEMTREE: #bemtree
[28] BEMHTML: #bemhtml
[29] i-bem.js: #i-bemjs
[30] BEMJSON-формате: https://ru.bem.info/platform/bemjson/
[31] БЭМ-сущностей: https://ru.bem.info/methodology/key-concepts/#%D0%91%D0%AD%D0%9C-%D1%81%D1%83%D1%89%D0%BD%D0%BE%D1%81%D1%82%D1%8C
[32] декларацией: https://ru.bem.info/methodology/declarations/
[33] bem-core: https://ru.bem.info/platform/libs/bem-core/
[34] bem-components: https://ru.bem.info/platform/libs/bem-components/
[35] ENB: https://ru.bem.info/toolbox/enb/
[36] nodemon: https://github.com/remy/nodemon/
[37] файловой структуре: https://ru.bem.info/methodology/filestructure/
[38] chokidar: https://github.com/paulmillr/chokidar
[39] livereload: https://github.com/napcs/node-livereload
[40] localhost:3000: http://localhost:3000
[41] форуме: https://ru.bem.info/forum/
[42] CommonJS: https://en.wikipedia.org/wiki/CommonJS
[43] express: http://expressjs.com/ru/
[44] passport: http://passportjs.org
[45] passport-youtube-v3: https://github.com/yanatan16/passport-youtube-v3
[46] OAuth 2.0: https://oauth.net/2/
[47] twitter: https://www.npmjs.com/package/twitter
[48] googleapis: http://google.github.io/google-api-nodejs-client/
[49] moment: http://momentjs.com
[50] статических файлов: #static-changes
[51] серверного кода: #server-changes
[52] Полный код root.bemtree.js: https://gist.github.com/godfreyd/fba71361207a95134982579c13b0050d
[53] Полный код index.js: https://gist.github.com/godfreyd/fdc6e2d7f1f42deac4dcfc0dde36bd11
[54] следующий код: https://gist.github.com/godfreyd/a584cee1191833afae70fc059ba1f200
[55] следующий код: https://gist.github.com/godfreyd/f6de1c33a83dda708a0e3ba9312f0c78
[56] следующий: https://gist.github.com/godfreyd/37d903c73f863619e2e1be1cd946d4c3
[57] следующий код: https://gist.github.com/godfreyd/4bda7da3db029890378e15bcc38f32de
[58] OAuth 2.0: https://oauth.net
[59] документацию: https://dev.twitter.com/oauth
[60] приложение: https://apps.twitter.com
[61] Postman: https://www.getpostman.com
[62] Закодируйте строку: #codestring
[63] Base64: https://en.wikipedia.org/wiki/Base64
[64] Получите OAuth-токен: #gettoken
[65] base64encode.org: https://www.base64encode.org
[66] приложение: https://console.developers.google.com/
[67] Полный код config.json: https://gist.github.com/godfreyd/3697d1ba5fe5ac298a9b471fe943340f
[68] полученными данными: #%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5-oauth-%D1%82%D0%BE%D0%BA%D0%B5%D0%BD%D0%BE%D0%B2
[69] Полный код .gitignore: https://gist.github.com/godfreyd/71a35fde50f54205fa395230bc97358f
[70] следующий: https://gist.github.com/godfreyd/3420597de46509b02c69707d596c8dc4
[71] следующий код: https://gist.github.com/godfreyd/e48b6831d785e51ee6ce0892151e3395
[72] YouTube Data API: https://developers.google.com/youtube/v3/
[73] следующий код: https://gist.github.com/godfreyd/68af82df0bc171da54971990f442dddb
[74] следующий: https://gist.github.com/godfreyd/60d5d123c45c067b3fb675688dc74835
[75] следующий код: https://gist.github.com/godfreyd/e103013e1fe480965cd84b3e7040d04b
[76] следующие блоки: https://github.com/godfreyd/bem-in-dynamic/tree/master/common.blocks
[77] logo.svg: https://github.com/godfreyd/bem-in-dynamic/blob/master/static/images/logo.svg
[78] Источник: https://habrahabr.ru/post/337166/
Нажмите здесь для печати.