До Яндекса я 9 лет работал веб-разработчиком на C#. Мне всегда очень нравилась идея БЭМ и я очень огорчался, что не могу использовать БЭМ-инструменты в проектах на .NET.
Пару раз я пытался подружить .NET и БЭМ, но этому мешало отсутствие инфраструктуры для интеграции JS-инструментов с .NET бэкендом, а создать свою инфраструктуру мешало отсутствие основательных знаний, как это всё должно работать. Сейчас некоторые из этих знаний уже появились в моей голове и текст ниже - описание еще одно попытки создать БЭМ инфраструктуру для .NET.
Итак...
Задача
По сути, задача состоит из двух относительно независимых частей:
- сделать сборку БЭМ-бандлов в Visual Studio (т.е. под Windows);
- сделать серверную шаблонизацию во время работы приложения.
Сборка
Простой вариант: запускаем enb.
В мире .NET есть такой сборщик - MsBuild. Именно с помощью него собирается проект, когда вы нажимаете Ctrl + F5 в Visual Studio. По сути, файлы .sln
и .csproj
, с которыми работает Visual Studio - это конфиги MsBuild. Среди прочего, MsBuild может запускать консольные приложения. Таким образом, мы можем добавить руками в файл проекта вызов enb make
и он будет запускаться при сборке. Достоинства: это просто сделать + сборка на enb хорошо работает. Недостатки: нет интеграции с UI => неудобно настраивать и использовать.
Продвинутый вариант: сборка на gulp.
На последнем хакатоне по БЭМ (2-3 апреля 2016) уже был более-менее рабочий пример сборки БЭМ проектов на gulp, а в Visual Studio как раз есть интеграция с gulp с помощью плагина Task Runner Explorer (в последних двух версиях VS этот плагин устанавливается по умолчанию). Достоинства: интеграция с UI + gulp настраивается гибче и удобнее, чем enb + идея с потоками значительно меньше взрывает мозг, чем идея декларативного описания сборки. Ну и gulp намного более распространен. Недостатки: плагины для БЭМ-сборки на gulp еще довольно сырые (об этом ниже).
Что получилось
Файловая структура проекта:
<Project>
├─ Bem
├─ desktop.blocks
├─ block1
└─ block2
└─ desktop.bundles
├─ bundle1
└─ bundle.bemjson.js
└─ bundle2
├─ Controllers
...
└─ gulpfile.js
Сборка шаблонов
var gulp = require('gulp'),
...
bemhtml = require('bem-xjst').bemhtml,
bem = require('@bem/gulp');
// Создаём хелпер для сборки проекта
var project = bem({
bemconfig: { ... } // уровни переопределения
});
// Создаём хелпер для сборки бандла
var bundle = project.bundle({
path: 'Bem/desktop.bundles/index',
decl: 'index.bemjson.js'
});
gulp.task('bemhtml', function() {
return bundle.src({ tech: 'bemhtml', extensions: ['.bemhtml', '.bemhtml.js'] })
.pipe(concat(bundle.name() + '.bemhtml.js'))
.pipe(through2(function(file, encoding, callback) {
var src = file.contents.toString(encoding),
bundle = bemhtml.generate(src);
file.contents = new Buffer(bundle, encoding);
callback(null, file);
}))
.pipe(gulp.dest('Bem/desktop.bundles/index'));
});
Вся магия в bundle.src
- это как gulp.src
, только возвращает файлы из всех уровней переопределения в правильном порядке.
Обнаруженные особенности:
- если нет файлов deps, то
bundle.src
падает c неинформативной ошибкой;
- мне так и не удалось запустить nodejs с поддержкой ES6 из Visual Studio, поэтому не смог воспользоваться нормальным плагином для сборки bemhtml (и написал руками).
Спасибо @tadatuta и @zxqfox за помощь и ответы на глупые вопросы.
Сборка js и стилей - аналогично. Весь код можно посмотреть здесь.
Таким образом, получился проект ASP.NET MVC, с которым мы работаем через Visual Studio. В проекте есть папка с БЭМ блоками и при сборке проекта через VS вместе с компиляцией кода на C# происходит сборка БЭМ-бандлов: серверные шаблоны, клиентский js и стили.
Серверная шаблонизация
Чего хотелось иметь в проекте на .NET:
- возможность переопределять любые куски шаблонов;
- возможность использования одних и тех же шаблонов на клиенте и на сервере;
- возможность реиспользовать инфраструктуру без танцев с бубном в каждом проекте.
Хочется иметь библиотеку, которую можно подключить к проекту из NuGet и чтобы внутри у нее был полноценный шаблонизатор BEMHTML.
Решено было попробовать запустить bemhtml внутри .NET приложения с помощью какой-нибудь .NET обертки над node. Выбрал EDGE.js. Он умеет запускать ноду внутри .NET процесса (на текущий момент, только на .NET 4.5 под Windows, но написано, что будет и поддержка .NET Core). Созданный экземпляр можно кэшировать, чтобы не создавать несколько раз контекст v8.
Сначала провел небольшой эксперимент - отрендерил простой шаблон в консольном приложении.
Основа всего - вот такой класс:
public class Bemhtml
{
private readonly Func<object, Task<object>> func;
public Bemhtml(string template)
{
this.func = Edge.Func(template + "; return function (data, cb) { cb(null, exports.apply(data));}");
}
public async Task<string> Apply(object data)
{
return await func(data) as string;
}
}
Ему в конструктор нужно передать текст bemhtml-бандла, сгенерированного с помощью bemhtml.generate(...)
(это ровно то, что мы делали во время сборки шаблонов). Содержимое бандла примерно такое:
// код шаблонизатора BEMHTML
...
// шаблоны, обернутые в вызов функции compile
var api = new BEMHTML({});
api.compile(function(...) {
// тут шаблоны, например:
block('my-block').content()('123');
});
// помещаем скомпилированные шаблоны в переменную exports
api.exportApply(exports);
Далее мы доклеиваем к этому коду возврат функции, которая будет выполняться при вызове шаблонизации из .NET. Она получает данные и callback, шаблонизирует данные и результат передает входным параметром в callback:
return function (data, callback) { callback(null, exports.apply(data));}
Сгенерированный код мы передаем в Edge.Func("сгенерированный код")
и получаем экземпляр .NET функции Func<object, Task<object>>
, которую можно использовать из программы на C#. Функция - асинхронная (она возвращает Task<object>
) и мы вызываем ее через await
, либо используем примерно таким образом:
var obj = func(data); // вызываем функцию
obj.Wait(); // ждем окончания выполнения
Console.Write(obj.Result) // в obj.Result результат шаблонизации
После того, как в консольном приложении шаблонизация отработала успешно, я написал небольшую библиотеку для использования в веб-приложениях ASP.NET MVC. Там есть:
- класс-обертка над js шаблоном (описанный чуть выше),
- менеджер шаблонов, отвечающий за создание экземпляров шаблонов и управляющий их кэшированием
- ActionResult с помощью которго можно передать данные из серверного контроллера в BEMHTML.
Что получилось
Сейчас есть небольшое ASP.NET MVC приложение, которое состоит из одной странички, отрендеренной с помощью BEMHTML. В приложение подключены bem-core и bem-components. Страничка открывается и, кажется, всё работает.
Посмотреть код можно здесь, а потыкать мышкой (и посмотреть в инспекторе) можно здесь.
Дальнейшие планы
- параметризация и вынос параметров в конфиг .NET проекта;
- отладка шаблонов на сервере (вроде EDGE.js это позволяет; идеальный вариант - если в качестве отладчика получится подключить Visual Studio, в которой открыт код проекта);
- выложить в NuGet библиотеку с классами для ASP.NET MVC;
- написать более сложный пример (с более сложными серверными данными и шаблонами);
- потестировать производительность.
Также в планах - адаптировать этот текст для людей, ничего не знающих про БЭМ и запостить на хабр.
Спасибо за внимание!