62
0
0
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
Назад

Введение в Handlebars.NET

Время чтения 16 минут
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
62
0
0
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники

Шаблоны нужны всегда. Ваше приложение отправляет email-уведомления? Там явно есть динамические переменные. Вы формируете pdf-документы? Туда надо подставлять Ф. И. О. и кучу других параметров — зависит от предметной области. А если вы даете клиентам/аналитикам кастомизировать эти шаблоны, вам точно нужен простой шаблонизатор.

Меня зовут Дмитрий Бахтенков, и я ведущий .NET-разработчик. В этой статье мы разберем, что такое Handlebars и как он помогает работать с шаблонами.

Введение в Handlebars.NET

Что такое Handlebars

Handlebars — это так называемый logic-less шаблонизатор. Ваши шаблоны не содержат бизнес-логики: не лезут в базу данных, не вычисляют скидки и не принимают решений. Его задача — взять готовые данные и красиво их отобразить.

Изначально Handlebars появился в мире JavaScript как развитие Mustache — еще более минималистичного шаблонизатора. Со временем его портировали на разные языки, и для .NET появилась библиотека Handlebars.NET (пакет Handlebars.Net на NuGet).

Ключевая идея проста: вы пишете шаблон с плейсхолдерами вида {{name}}, передаете в него объект с данными и на выходе получаете готовую строку. Вот минимальный пример:

Здравствуйте, {{clientName}}!

Ваш заказ №{{orderNumber}} на сумму {{totalAmount}} оформлен.

Почему Handlebars?

Для нашей задачи — дать аналитикам возможность самостоятельно править шаблоны документов — Handlebars выигрывает по нескольким причинам:

  • Простой синтаксис: {{name}} понятен даже не-разработчику. 
  • Logic-less с возможностью расширения: шаблон сам по себе не может выполнить произвольный код. Но если нужна дополнительная логика (форматирование даты, склонение слов), ее можно добавить через хелперы.
  • Переносимость: тот же синтаксис Handlebars работает и в JavaScript. Если у вас есть фронтенд, который тоже генерирует документы, — шаблоны можно переиспользовать.
  • Безопасность: шаблон не может выполнить произвольный C#-код.

Синтаксис шаблонов

Самая базовая конструкция — подстановка значения:

Имя клиента: {{clientName}}

Номер договора: {{contract.number}}

Вложенные свойства доступны через точку — {{contract.number}} достанет свойство Number из объекта Contract.

Блок {{#each}} — перебор коллекций

Список товаров:


{{#each items}}
  - {{this.name}}: {{this.price}} руб.
{{/each}}

 

Внутри {{#each}} контекст переключается на текущий элемент коллекции. Через {{this}} можно обратиться к самому элементу, а через {{@index}} — к его индексу.

Блоки {{#if}} и {{#unless}}

{{#if}} в Handlebars проверяет только «правдивость» значения (не null, не пустая строка, не false, не 0, не пустая коллекция).


{{#if discount}}
  Скидка: {{discount}}%
{{else}}
  Скидка не предоставляется
{{/if}}
{{#unless paid}}
  Счет не оплачен
{{/unless}}

 

{{#unless}} — это просто инвертированный {{#if}}. Удобно для случаев, когда нужно показать что-то при отсутствии значения.

Быстрый старт

С теорией разобрались, теперь перейдем к практике. Для начала сделаем консольный проект и подключим библиотеку.


dotnet new console -n HandlebarsDemo
cd HandlebarsDemo
dotnet add package Handlebars.Net

 

Теперь напишем минимальный пример: скомпилируем шаблон, передадим в него данные и получим результат:


using HandlebarsDotNet;
 
// Компилируем шаблон — это делается один раз
var template = Handlebars.Compile(
    "Здравствуйте, {{{name}}}! Ваш заказ №{{{orderNumber}}} оформлен.");
 
// Передаем данные и получаем результат
var result = template(new
{
    name = "Иван Петров",
    orderNumber = "12345"
});
 
Console.WriteLine(result);
// Вывод: Здравствуйте, Иван Петров! Ваш заказ №12345 оформлен.

 

Шаблон компилируется один раз, а вызывается многократно с разными данными. Метод Handlebars.Compile() возвращает делегат HandlebarsTemplate<object, object>, который можно сохранить и переиспользовать. Не стоит компилировать один и тот же шаблон на каждый вызов — это лишняя нагрузка.

В реальном приложении скомпилированные шаблоны стоит кешировать — например, в Dictionary<string, HandlebarsTemplate> или через DI-контейнер как синглтон. Об этом мы еще поговорим в разделе про best practices.

Хелперы

Хелпер — это функция, которую можно вызвать прямо из шаблона. Встроенных конструкций Handlebars хватает для базовых вещей: показать значение, перебрать список, проверить наличие. Но как только нужно отформатировать дату, сравнить два значения, вывести сумму прописью — без хелперов не обойтись.

В Handlebars.NET есть два вида хелперов:

Inline-хелперы вызываются как функции и возвращают строку:

Дата: {{formatDate date "dd.MM.yyyy"}}

Сумма: {{formatCurrency amount "RUB"}}

Block-хелперы оборачивают блок контента и решают, показывать его или нет:


{{#ifEquals status "paid"}}
  ✅ Оплачено
{{else}}
  ❌ Не оплачено
{{/ifEquals}}

 

Пример: пишем inline-хелпер formatDate

При генерации документов форматирование дат — одна из самых частых задач. Давайте напишем хелпер, который принимает дату и формат, а возвращает отформатированную строку.


using HandlebarsDotNet;
 
var handlebars = Handlebars.Create();
 
// Регистрируем хелпер formatDate
handlebars.RegisterHelper("formatDate", (output, context, arguments) =>
{
    if (arguments.Length < 2)
    {
        output.Write("(формат не указан)");
        return;
    }
 
    var dateValue = arguments[0];
    var format = arguments[1]?.ToString() ?? "dd.MM.yyyy";
 
    if (dateValue is DateTime date)
    {
        output.Write(date.ToString(format));
    }
    else if (DateTime.TryParse(dateValue?.ToString(), out var parsedDate))
    {
        output.Write(parsedDate.ToString(format));
    }
    else
    {
        output.Write("(дата не указана)");
    }
});

 

Теперь используем его в шаблоне:

Дата договора: {{formatDate contractDate "dd MMMM yyyy"}}

Срок оплаты: {{formatDate dueDate "dd.MM.yyyy"}}

Попробуем скомпилировать и проверить:


var template = handlebars.Compile(
    "Дата договора: {{formatDate contractDate "dd MMMM yyyy"}}n" +
    "Срок оплаты: {{formatDate dueDate "dd.MM.yyyy"}}"
);
 
var result = template(new
{
    contractDate = new DateTime(2025, 3, 15),
    dueDate = new DateTime(2025, 4, 1)
});
 
Console.WriteLine(result);
// Вывод:
// Дата договора: 15 марта 2025
// Срок оплаты: 01.04.2025

 

Пример: пишем block-хелпер ifEquals

Помните, мы говорили, что {{#if}} не умеет сравнивать значения? Вот хелпер, который это исправляет:


handlebars.RegisterHelper("ifEquals", (output, options, context, arguments) =>
{
    if (arguments.Length < 2)
    {
        options.Inverse(output, context);
        return;
    }
 
    var left = arguments[0]?.ToString();
    var right = arguments[1]?.ToString();
 
    if (string.Equals(left, right, StringComparison.OrdinalIgnoreCase))
    {
        options.Template(output, context);
    }
    else
    {
        options.Inverse(output, context);
    }
});

 

Здесь появляются два важных метода: options.Template() — отрисовывает содержимое между {{#ifEquals}} и {{else}}, а options.Inverse() — содержимое после {{else}}. Это и есть главное отличие block-хелпера от inline: он управляет рендерингом целого блока контента.

Использование:


{{#ifEquals documentType "invoice"}}
  СЧЕТ НА ОПЛАТУ
{{else}}
  ДОКУМЕНТ
{{/ifEquals}}

 

Best practices в работе с Handlebars

Мы написали несколько хелперов, и теперь самое время поговорить о том, как делать это правильно. Вот практики, к которым я пришел на реальных проектах.

Хелпер делает одну вещь

Принцип единственной ответственности работает и здесь. formatDate форматирует дату — и больше ничего. Он не проверяет, просрочена ли дата, не логирует вызовы и не лезет в настройки локализации.

Плохой пример — хелпер-комбайн:


// Так делать не стоит
handlebars.RegisterHelper("smartDate", (output, context, arguments) =>
{
    var date = (DateTime)arguments[0];
    Logger.Log($"Formatting date: {date}"); // логирование
    if (date < DateTime.Now) // бизнес-логика
    {
        output.Write("ПРОСРОЧЕНО: " + date.ToString("dd.MM.yyyy"));
    }
    else
    {
        output.Write(date.ToString("dd.MM.yyyy"));
    }
});

 

Лучше разделить: formatDate форматирует, а данные можно подготовить заранее и передать в модель.

Обработка входных данных

Всегда проверяйте входные аргументы.


// Хороший подход — всегда проверять аргументы
handlebars.RegisterHelper("formatDate", (output, context, arguments) =>
{
    if (arguments.Length == 0 || arguments[0] == null
        || arguments[0] is UndefinedBindingResult)
    {
        output.Write("(дата не указана)");
        return;
    }
    // ... форматирование
});

 

Без такой проверки вы получите NullReferenceException в рантайме и вместо красивого документа — стектрейс в логах.

Организация работы с шаблонизатором

  • Camel case для имен хелперов: formatDate, formatCurrency, ifEquals, pluralize.
  • Группировка по назначению: хелперы форматирования отдельно, условные — отдельно, работа с коллекциями — отдельно.
  • Регистрация в одном месте: соберите все хелперы в метод расширения (extension method).

public static class HandlebarsHelpers
{
    public static IHandlebars RegisterAllHelpers(this IHandlebars handlebars)
    {
        RegisterFormattingHelpers(handlebars);
        RegisterConditionalHelpers(handlebars);
        RegisterCollectionHelpers(handlebars);
        return handlebars;
    }
 
    private static void RegisterFormattingHelpers(IHandlebars handlebars)
    {
        handlebars.RegisterHelper("formatDate", /* ... */);
        handlebars.RegisterHelper("formatCurrency", /* ... */);
    }
 
    private static void RegisterConditionalHelpers(IHandlebars handlebars)
    {
        handlebars.RegisterHelper("ifEquals", /* ... */);
    }
}

 

Теперь регистрация выглядит чисто:

var handlebars = Handlebars.Create();

handlebars.RegisterAllHelpers();

Производительность

Несколько вещей, о которых стоит помнить:

  • Компилируйте шаблон один раз: Handlebars.Compile() парсит шаблон и строит AST. Делать это на каждый запрос — расточительно.
  • Не создавайте IHandlebars на каждый вызов: создайте один экземпляр, зарегистрируйте все хелперы и переиспользуйте его.
  • Количество хелперов почти не влияет на производительность: они хранятся в словаре, поиск по имени — O(1).

Заключение

Handlebars.NET — отличный выбор, когда шаблоны должны быть понятны не только разработчикам. Простой синтаксис с двойными фигурными скобками осваивается за считаные минуты, а система хелперов позволяет расширять функциональность без усложнения самих шаблонов.

Рекомендую несколько ресурсов для более глубокого изучения темы:

Спасибо за прочтение статьи! Кроме того, я веду телеграм-канал Flexible Coding, где рассказываю о своем опыте в программировании.

Комментарии0
Тоже интересно
Комментировать
Поделиться
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники