Войти с помощью github
Форум /

До Яндекса я 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.

Сначала провел небольшой эксперимент - отрендерил простой шаблон в консольном приложении.

Output

Основа всего - вот такой класс:

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;
  • написать более сложный пример (с более сложными серверными данными и шаблонами);
  • потестировать производительность.

Также в планах - адаптировать этот текст для людей, ничего не знающих про БЭМ и запостить на хабр.

Спасибо за внимание!