?

Log in

No account? Create an account

Previous Entry | Next Entry

Что это за текст


Текст написан для себя. По ходу дела я подумал, что он может быть интересен и другим (новичкам). Но редактирование поста в LiveJournal через медленную мобильную сеть не доставляет особой радости, поэтому оформление явно подкачало. И кое-где остались другие мелкие огрехи. Во всяком случае, я выполнил правило «если что-то непонятно, разберись и напиши об этом пост - когда забудешь - нагуглишь его».

Завязка


Прежде всего, о названиях. В семейство входят (как минимум) три имени: Mustache, Handlebars и Hogan.

В целом синтаксис mustache объяснён в руководстве по mustache (перевод тут, правда они не осилили перевод и перевели partials как "обособленные фрагменты" и тут же - как "частные значения"). Но есть две проблемы:



  • partials непонятны

  • не написано о наследовании шаблонов, которое, впрочем, является нестандартным расширением


В конце концов я пришёл к выводу, что partials - это вставляемые из другого места фрагменты шаблона (аналог #include в Си), которые тоже могут содержать расширяемые элементы, такие, как {{имя}}. В зависимости от реализации mustache, эти куски могут браться из файлов (по имени файла), или же они должны быть предоставлены при расширении (render) шаблона.



Наши цели



  • сделать простейший работающий пример с partials

  • сделать, чтобы имя в шаблоне, отсутствующее в данных выдавало ошибку, а не просто расширялось в пустую строку - иначе не поймать опечатки

  • компилировать partials (в документации по mustache говорится, что они расширяются во время выполнения)

  • избавиться от необходимости явно передавать partials - это дикость

  • понять варианты использования шаблонизатора и как их реализовывать



Теория


Mustache, по определению - это препроцессор, который на вход получает три вещи:



  • данные, которые по структуре представимы в виде json

  • шаблон - текст с разметкой для подстановки данных

  • реализацию всех partials, упомянутых в шаблоне


и на выходе даёт текст - расширенный шаблон. Препроцессор имеет много реализаций на разных языках программирования и много интеграций с различными средами и может называться mustache, hogan, handlebars (и ещё как нибудь?).



Процесс (на примере hogan) таков:


 Компиляция (Hogan.compile) (нужен только шаблон)
   лексический разбор (Hogan.scan)
   синтаксический разбор (Hogan.parse)
   генерация кода (Hogan.generate)
 Расширение (КомпилированныйШаблон.render)  (на входе - данные и partials)




В простейшем случае компиляция и расширение шаблона производятся клиентом. Но! Данные - каждый раз разные (допустим, это товары или люди), а шаблон - одинаковый. Поэтому логично делать компиляцию пореже, запоминать и повторно использовать её результаты. Обычный Mustache может генерировать код расширения шаблона на Ruby (с помощью утилиты mustache). Hogan (с помощью утилиты hulk) генерирует код на JavaScript. Этот код может выполняться как на клиенте, так и на сервере (в случае node.js). Для PHP я нашёл библиотеку lightncandy, которая декларирует возможность генерировать PHP файлы по шаблону - так достигается серверное кеширование результатов компиляции. GRMustache для Swift не декларирует возможность сгенерировать код расширения шаблона. CL-mustache для common lisp порождает функцию, но по сути внутри неё - интерпретация, partial-ы не прекомпилируются. Есть реализация и для моего любимого tcl.



Так получилось, что для моей текущей работы был выбран Hogan.js - и это хорошо. В нём нет лишнего и он позволяет сохранять результат компиляции.


Пишем «Привет, мир!»



Скачиваем hogan.



> mkdir mu
> cd mu
> npm init 
> npm install hogan.js






Эти строки уже не совсем тривиальны. Если собираетесь использовать express, прочитайте все ответы на этот вопрос на stackoverflow - может быть, вам нужен hogan-express.



Создадим два файла - для шаблона и для фрагмента (partial).



шаблон.хш:



{{! это - файл шаблон.хш }}
Подставляемый говорит: '{{> подставляемый}}' 





подставляемый.хш:



Я - подставляемый и говорю: "{{Приветствие}}"





Это - файлы шаблонов. Обычно они имеют расширение .mustache или .hgs. Но мы, квасные патриоты, всему пытаемся дать русские названия. PhpStorm можно научить, что .хш - это файл mustache, для этого открываем файл, щёлкаем правой кнопкой мыши по его "табу" в списке файлов, далее - Associate with file type и выбираем Handlebars/Mustache.



Прекомпилируем шаблоны и сохраняем результат в файл «скомпилированный-шаблон.js»



> node_modules/.bin/hulk шаблон.хш подставляемый.хш > скомпилированный-шаблон.js





Заглянем в этот «скомпилированный-шаблон.js»:



if (!!!templates) var templates = {};
templates["шаблон"] = new Hogan.Template({
    code: function (c, p, i) {
        var t = this;
        t.b(i = i || "");
        t.b("Подставляемый говорит: '");
        t.b(t.rp("<подставляемый0", c, p, ""));
        t.b("'");
        return t.fl();
    }, partials: {"<подставляемый0": {name: "подставляемый", partials: {}, subs: {}}}, subs: {}
});
templates["подставляемый"] = new Hogan.Template({
    // и т.п.
});






Забавно, что вместо достаточно компактного шаблона получился гораздо больший по размеру код (который клиент должен скачать, скомпилировать и выполнить). На знаю-не знаю, всегда ли так уж хороши их рекомендации по предварительной компиляции.



Собираем всё воедино, в файл «тест-хогана.html»:



<html><head><meta charset=utf-8>
<script src="node_modules/hogan.js/dist/hogan-3.0.2.js"></script>
<script src="скомпилированный-шаблон.js"></script>
<script>
  let Данные = { "Приветствие": "Привет, мир!" };
  let РезультатРасширения = templates["шаблон"].render(Данные, templates);
  console.log(`РезультатРасширения: ${РезультатРасширения}`);
</script>
</head><body>Смотри сообщения консоли разработчика</body></html>





Открываем файл, переходим в инструменты разработчика (F12) и видим в консоли сообщение:



РезультатРасширения: Подставляемый говорит: 'Я - подставляемый и говорю: "Привет, мир!"'





Ловим ошибки


Теперь наша задача - выбрасывать исключение, если имя не существует. Мы запретим отсутствующие имена вообще. Если мы хотим пустоту, то нужно будет явно передать Null.



Во-первых, опции описаны на github. Среди них опции отлова ошибок нет. Смотрим исходник и видим недокументированную опцию doModelGet. Но суть не в том. Судя по коду, можно либо модифицировать код "на горячую", либо форк. Форк плох, т.к. ломается npm install и прочая. Попробуем горячую замену. Вставим в начало нашего скрипта следующие строки:



    // спасибо https://learn.javascript.ru/decorators
  let Старая_rp = Hogan.Template.prototype.rp;
  let Старая_d = Hogan.Template.prototype.d;
  let Старая_f = Hogan.Template.prototype.f;

  Hogan.Template.prototype.rp = function Декорированная_rp() {
    let фр = Старая_rp.apply(this,arguments);
    if (!!!фр) {
      throw new Error(`Не найден подставляемый фрагмент (partial) - ищите ключ отладчиком`);
    }
    return фр;
  };

  Hogan.Template.prototype.d = function Декорированная_d() {
      let зн = Старая_d.apply(this,arguments);
      if (!!!зн) {
          let key = arguments[0];
          throw new Error(`Не найдено имя «${key}»`);
      }
      return зн;
  };


  Hogan.Template.prototype.f = function Декорированная_f() {
      let зн = Старая_f.apply(this,arguments);
      if (!!!зн) {
          let key = arguments[0];
          throw new Error(`Не найдено имя «${key}»`);
      }
      return зн;
  };





Вроде оно работает для нашего простейшего примера, дальше пока не тестировал. Для локализации ошибки нужно с помощью dev tools поставить точку прерывания на место, откуда вылетает исключение, перегрузить страницу. Точка прерывания сработает. Тогда в стеке находим сгенерированный hulk-ом код и по его виду пытаемся понять, в каком месте у нас проблема, а по контексту смотрим, чего не хватает. Не слишком удобно, но лучше, чем ничего. Что делать, если шаблон представлен в виде строки - я не знаю, но, вероятно, где-то на стеке можно будет увидеть текст шаблона.



Наследование шаблонов


Это - ещё одна возможность, не упомянутая в «man 5 mustache», но описанная в непринятом предложении (pull request) для спецификации mustache. Попробую перевести суть сопроводительного письма своими словами:
«Мы часто хотим иметь базовый шаблон, в котором хранится скелет страницы, а каждая настоящая страница просто заменяет секции в этом скелете своим содержимым. Скелет может включать все скрипты аналитики и общую для всех страниц разметку, при этом остаётся возможность заменить заголовок на любой другой. Возможные решения здесь - копировать скелет в каждую страницу или генерировать страницу за два прохода, оба не идеальны. Поэтому мы хотим `наследование' страниц в стиле ООП»

Детали смотрите в самом  предложении - на мой взгляд, очень полезно с точки зрения компактности, но может привести к дальнейшему затруднению отладки. Я не проверял, помогают ли мои декораторы искать ошибки в наследуемых шаблонах - думаю, нужно что-то к ним добавить.

Варианты использования

С этим мы толком не разобрались и примеры не были разработаны. Ясны следующие варианты:

  • Шаблоны передаются текстом на клиента, где и расширяются. В SPA могут кешироваться.

  • Шаблоны компилируются во время сборки сайта и превращаются в Javascript, который загружается клиентом и исполняется на им (это вариант нашего "приветмира"). По заверениям Twitter, это лучше, чем передавать шаблоны в виде текста и разбирать их на стороне клиента, но я сомневаюсь - как минимум, размер передаваемых данных увеличивается. Нужно проверять.

  • Шаблоны полностью расширяются на сервере, при возможности результаты расшерения кешируются с помощью утилит, генерирующих код на языке серверной стороны. Это возможно для PHP, Node.js, Swift.

Чего не хватает


  • В проекте https://github.com/kupriyanenko/jsttojs обещают предкомпиляцию в js для нескольких разных шаблонизаторов, включая mustache, но я не смотрел.

  • Плохо то, что сообщения об ошибке не показывают точное место в шаблоне, где ошибка произошла. Конечно, наличие хоть какого-то сообщения вместо молчаливой вставки пустоты - это большой шаг вперёд. Но я слёту не вижу, как можно выводить контекст без серьёзных изменений кода.

  • Предложенный патч толком не протестирован


Полезные ссылки по теме: