Пишем свой JavaScript шаблонизатор. Пишем свой JavaScript шаблонизатор Зачем нужны шаблоны

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

  • для совместного использования разметки на страницах;
  • для включения отдельных блоков с функциональностью (при необходимости);
  • в общем и целом для упрощения процесса построения в меру сложных макетов страниц c минимумом прилагаемых усилий.
Сравнение шаблонизаторов

Существует ряд популярных шаблонизаторов для JavaScript, и мы рассмотрим некоторые из них:

EJS

EJS (Встроенный JavaScript) очень напоминает PHP или JSP, очень простые функции – слишком простые для того, чтобы быть хоть в чем-то полезными. Сразу исключаем.

Jade

Наиболее характерной особенностью Jade является то, что он обозначает пробел без кавычек (как Coffeescript),что или является чудом (если вы верите в подобные вещи), или катастрофической неисправностью, если вы реалист (как я). По своим характеристикам этот шаблонизатор стоит на одном уровне с большинством других систем, но его стилевое оформление заставляет нас почти сразу его отбросить.

Mustache / Handlebars

Использует знакомую { { систему токенов } } для встраивания логики в существующие шаблоны (и не только HTML). Обеспечивает хороший набор циклов, логику и управление переменными. Не очень хорошо работает с частичными шаблонами и блоками, а также другими элементами, необходимыми для более легкого создания умеренно сложных структур страниц, что является ключевым требованием даже для самой простой системы CMS, которую я бы хотел видеть. Идем дальше…

Dust

В настоящее время популярен из-за его применения в LinkedIn. На первый взгляд, очень похож на Mustache / Handlebars, но дополнен рядом полезных функций, таких как именованные блоки. Однако процесс загрузки шаблона и рендеринг выглядят довольно неуклюже, и он до сих пор не поддерживает довольно широкий спектр функций для построения страницы. Хорош, но недостаточно…

Nunjucks

Еще одна { { система на основе токенов } } , обеспечивающая (как и другие движки) логику, выполнение циклов и возможность управления переменными. Но кроме этого, шаблонизатор предоставляет продвинутые элементы построения страницы, например, наследование блоков, включая наследование макетов, пользовательские теги и макросы. Это все наилучшим образом подходит для системы CMS, где страницы представляют собой серию «строительных блоков».

Итак, у нас есть победитель!

Для справки Основные примеры рендеринга

Давайте рассмотрим некоторые примеры шаблонизатора Nunjucks в действии…

Динамический рендеринг на платформе Node при использовании nunjucks.render

Во-первых, мы рассмотрим пример, в котором будем использовать Nunjucks для рендеринга страницы в ответ на запрос веб-службы, который обрабатывается сервером Node. Этот сценарий очень похож на тот, который происходит при запросе PHP или СoldАusion или страницы ASP .NET на обычном веб-сайте или в веб-приложении.

Предположим, что Node уже установлен.

Устанавливаем Express и Nunjucks:

Npm install express --save npm install nunjucks --save

Создаем нашу базовую структуру app.js:

Var express = require("express") ; var nunjucks = require("nunjucks") ; var app = express();

Настраиваем Nunjucks :

Var PATH_TO_TEMPLATES = "." ; nunjucks.configure(PATH_TO_TEMPLATES, { autoescape: true, express: app } );

Простой путь:

App.get("/home.html", function(req, res) { return res.render("index.html") ; } ); app.listen(3000);

Создаем шаблон index.html:

Привет всем

Запуск с:

Node app.js

и переходим на http://localhost:3000 . Готово!

Добавление динамических данных

Давайте быстро расширим этот пример, чтобы показать, как можно передать данные вашему шаблону в Nunjucks.

Для начала давайте передадим некоторые данные функции render:

App.get("/home.html", function(req, res) { var data = { имя: "Энди", фамилия: "Нил" } ; return res.render("index.html", data) ; } );

Во-вторых, мы будем ссылаться на эти данные в нашем шаблоне index.html:

Привет { { data.имя } }

Угадали, что произойдет, когда мы перейдем на http://localhost:3000 ?

Предварительная компиляция при помощи Gulp

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

Предположим, у вас есть простой сайт, состоящий из двух страниц со схожей разметкой:

  • src/index.html
  • src/contact-us.html
  • src/_layout.html

Ваши странички могут выглядеть так:

{ % set title = "Главная" %} { % extends "_layout.html" %} { % block content %} Добро пожаловать { % endblock %} { % set title = "Контакты" %} { % extends "_layout.html" %} { % block content %} Контакты

Звоните нам на 0800 000 0000 или отправьте письмо на эл. адрес [email protected].

{ % endblock %}

{ { title } } { % block content %} { % endblock %}

Устанавливаем плагин gulp-nunjucks:

Npm install gulp-nunjucks --save

Создаем простой gulpfile.js в корневой папке проекта:

Var gulp = require("gulp") ; var nunjucks = require("nunjucks") ; var COMPILE = { SRC: "/src/**.html", DEST: "/dist’ } ; gulp.task("render", function() { return gulp.src(COMPILE.SRC) .pipe(nunjucks()) .pipe(gulp.dest(COMPILE.DEST)) ; } );

Запускаем процесс рендеринга:

Gulp render

Полученные в результате страницы HTML будут выглядеть так:

Главная Добро пожаловать

Разве это не самый лучший сайт в мире?

Контакты Контакты

Позвоните нам на 0800 000 0000 или отправьте письмо на эл. адрес [email protected].

Особенности Nunjucks

Полную документацию можно посмотреть здесь:

В этом разделе мы затронем некоторые из основ для иллюстрации некоторых ключевых функций Nunjucks.

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

Наследование шаблона - это самый простой путь к многократному использованию шаблонов. При написании шаблона можно определить «блоки», которые дочерние шаблоны могут переопределить. (Это то, что мы делали выше в разделе { % содержание блока %} ).

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

Включения

Обратная сторона расширений – это то, что они включают шаблон внутрь текущего шаблона. Это полезно для «вытягивания» блоков контента, которые используются в нескольких местах на сайте или в приложении.

Импортирование и макросы

Импортирование позволяет загружать шаблон и открывать любые переменные и макросы (функции), определенные в нем. Это может быть полезно для таких вещей, как создание функций для последовательного отображения полей формы... немного похоже на написание пользовательских тегов с помощью ColdFusion, например. Для наглядности давайте рассмотрим шаблон под названием forms.html , который выглядит так:

{ % macro field(name, value = "", type = "text") %} { % endmacro %} { % macro label(text) %} { { text } } { % endmacro %}

Мы можем импортировать этот шаблон и использовать его для быстрого рендеринга серии полей формы:

{ % import "forms.html" as forms %} { { forms.label("Username") } } { { forms.field("user") } } { { forms.label("Password") } } { { forms.field("pass", type = "password") } }

Это особенно полезно при использовании таких фреймфорков, как Foundation или Bootstrap , которые требуют разумного количества HTML и CSS сред - если у вас это получилось путем рендеринга в макросе, то код вашего шаблона будет гораздо проще. (Плюс, если ваш шаблон делает весь рендеринг через макросы, то он напрямую не привязан к фреймворку, что облегчает переключение фреймфорков, а также позволяет делать изменения, общие для всех полей формы определенного типа).

Логика – If / For / While

if проверяет состояние и используется для выборочного отображения контента или выполнения других операций:

{ % if variable %} Переменная есть, ура { % endif %} { % if user.authorised %} { % extends "logged-in.html" %} { % else %} { % extends "logged-out.html" %} { % endif %}

for зацикливает массивы:

Var messages = ["Необходимо ввести адрес эл. почты", "Пароль должен быть не короче 8 знаков"];

{ % for message in messages %} { { message } }
{ % endfor %}

Следующие шаги

Советую изучить официальную документацию Nunjucks для более ясного представления о свойствах и возможностях шаблонизатора.
В будущих обзорах я покажу, как можно скомбинировать возможности шаблонизатора Nunjucks с генератором статичных сайтов Wintersmith для построения своей собственной простой и многоязычной системы CMS.


Сейчас я вижу всего два варианта шаблонов в js приложениях шаблонизаторы которые встроены в фреймворк и jquery лапшу (последнее самое распространённое) Я полагаю что это из-за того что многие не понимают как работают шаблонизаторы вообще (я о их внутренностях, а не о функции).
В этой статья я покажу как сделать простой но мощный шаблонизатор на основе регулярных выражений.

Но для тех кто не в теме немного о том что же это такое и что оно нам даст.

Шаблонизатор (в web) — программное обеспечение, позволяющее использовать html-шаблоны для генерации конечных html-страниц. Основная цель использования шаблонизаторов — это отделение представления данных от исполняемого кода. Часто это необходимо для обеспечения возможности параллельной работы программиста и дизайнера-верстальщика. Использование шаблонизаторов часто улучшает читаемость кода и внесение изменений во внешний вид, когда проект целиком выполняет один человек.

И так мы хотим рендить шаблоны в 1 проход и иметь представление отдельно от логики. Обычно шаблонизаторы имеют подобный интерфейс.
template.Run("template url", { VarName:"VarValue" });
Ниже код с подробными комментариями (что и почему)
window.template = { //заружает представление TemplateGet: function (TemplateUrl) { // 1. Создаём новый объект XMLHttpRequest var xhr = new XMLHttpRequest(); // 2. Конфигурируем его: GET-запрос на URL "TemplateUrl" xhr.open("GET", TemplateUrl, false); // 3. Отсылаем запрос xhr.send(); if (xhr.status >= 200 && xhr.status < 400) { // вернуть результат return xhr.responseText; } else { // обработать ошибку alert(xhr.status + ": " + xhr.statusText); // пример вывода: 404: Not Found return ""; } }, //класс функций для рендинга Render:{ //а в этом классе мы напишем все функции рендера func:{ //рендит переменные vars: function(html, vars){ //ищем все переменные в шаблоне var $ = html.match(/{{var.(.*?)}}/g); //проверяем нашли ли что то if (Array.isArray($)) { //мы нашли переменные в шаблоне. Необходимо их все распарсить. $.forEach(function (item) { //item содержит найденую строку {{var.VarName}} //по этому мы должны распарсить ету строку дабы получить только VarName item = item.replace("{{var.", ""); item = item.replace("}}", ""); //у нас есть имя переменной. Пора проверить передали ли мы такую //и обработать ошибки if (vars === undefined){ //перенную не передали. //пишем об ошибке в консоль и заменяем её значение в шаблоне на null console.warn("Переменная "+item+" не найдена"); html = html.replace("{{var." + item + "}}", "null"); } else { //переменную мы нашли. Вставим ка её в шаблон html = html.replace("{{var." + item + "}}", vars); } }); } //удалим обьект с переменными delete vars; //вернём html return html; } }, //точка входа в рендер. Run:function(html, vars){ html = this.func.vars(html, vars); return html; } }, //точка входа Run: function (TemplateUrl, vars) { //загружаем шаблон var html = this.TemplateGet(TemplateUrl); //отправляем шаблон рендеру html = this.Render.Run(html, vars); //удаляем обьект с переменными. Он нам больше не нужен, а может занимать много места delete vars; //результат шаблонизатор вставит в document.getElementById("page").innerHTML = html; } }; //вызовем представление template.Run("test.tpl", { VarName: "test" });
А в test.tpl просто напишем
{{var.VarName}}

Я надеюсь теперь меньше станет jquery лапши)

На тему шаблонизаторов статей написано великое множество, в том числе и здесь, на хабре.
Раньше мне казалось, что сделать что-нибудь своё - «на коленке» - будет очень сложно.
Но, случилось так, что прислали мне тестовое задание.
Напиши, мол, JavaScript шаблонизатор, вот по такому сценарию, тогда придёшь на собеседование.
Требование, конечно, было чрезмерным, и поначалу я решил просто игнорить.
Но из спортивного интереса решил попробовать.
Оказалось, что не всё так сложно.

Собственно, если интересно, то под катом некоторые заметки и выводы по процессу создания.

Для тех, кому только глянуть: the result , the cat .

Исходный шаблон - это JS String(), а данные это JS Object().
Блоки вида {% name %} body {% / %} , возможна неограниченная вложенность .
Если значение name является списком , то выводятся все элементы, иначе если не undefined, выводится один элемент.
Подстановки вида: {{ name }} .
В блоках и подстановках возможно использование точек в качестве имени, например {{.}} или {%.%} , где точка будет текущим элементом объекта верхнего уровня.
Есть ещё комментарии - это {# any comment w\wo multiline #} .
Для самих значений возможны фильтры, задаются через двоеточие: {{ .:trim:capitalize… }} .

Работать оно должно как:

Var str = render (tpl, obj);

Доказать:
+1 к самооценке .

UPD 2 : Сразу скажу, чтобы «чётко понять» что там и зачем, нужно начать это делать, желательно вместе с debugger"ом.
UPD 3 : Оно разобрано «на пальцах». Там есть ещё где «оптимизнуть». Но будет гораздо менее наглядно.

Приступим.

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

Для начала можно убрать комментарии, чтобы не отсвечивали:

// to cut the comments tpl = tpl.replace (/\{#[^]*?#\}/g, "");

Hint: [^] означает любой символ, * - сколько угодно раз.

Теперь можно подумать над тем, как будем парсить «чистый» результат.
Так как блоки возможны вложенные, предлагаю хранить всё в виде дерева.
На каждом уровне дерева будет JS Array (), элементы которого могут содержать аналогичную структуру.

Чтобы создать этот массив нужно отделить мух от котлет.
Для этого я воспользовался String.split() и String.match() .

Ещё нам понадобится глубокий поиск по строковому val имени внутри объекта obj .

Применённый вариант getObjDeep:

var deeps = function (obj, val) { var hs = val.split("."); var len = hs.length; var deep; var num = 0; for (var i = 0; i < len; i++) { var el = hs[i]; if (deep) { if (deep) { deep = deep; num++; } } else { if (obj) { deep = obj; num++; } } } if (num == len) { return deep; } else { return undefined; } };

Итак, разделим строку на части parts и элементы matches:

// регулярка для парсинга: // цифробуквы, точка, подчеркивание, // двоеточие, слеш и минус, сколько угодно раз var ptn = /\{\%\s*+?\s*\%\}/g; // строковые куски var parts = tpl.split (ptn); // сами спички var matches = tpl.match (ptn);

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

// все блоки var blocks = ; // вложенности var curnt = ; if(matches){ // т.к. м.б. null var len = matches.length; for (var i = 0; i < len; i++) { // выкидываем {% и %}, и попутно делаем trim var str = matches[i].replace (/^\{\%\s*|\s*\%\}$/g, ""); if (str === "/") { // finalise block // ... } else { // make block // ... } // ...

Тут blocks - итоговый массив с выделенными блоками, а curnt - массив с текущей вложенностью.

На каждом шаге цикла мы определяем, что сейчас в str, начало блока или завершение.
Если начало блока, т.е. str !== "/" , то создаём новый элемент и push его в массив.
И ещё push его в curnt, т.к. нам необходимо понимать на каком мы уровне.
Попутно заносим в блок сами строки.
Соответственно, если у нас пустой curnt, то мы на нулевом уровне дерева.
Если curnt не пустой, то нужно заносить в nested элемент последнего curnt.

// длина текущей вложенности var cln = curnt.length; if (cln == 0) { // т.к. это верхний уровень, то просто в него и кладём текущий элемент blocks.push (struct); // пишем текущую вложенность, она же нулевая curnt.push (struct); } else { // нужно положить в nested текущего вложенного блока curnt.nest.push (struct); // теперь взять этот "последний" элемент и добавить его в curnt var last = curnt.nest.length - 1; curnt.push (curnt.nest [ last ]); }

Соотвественно, каждый элемент массива это, минимум:

Var struct = { // текущий obj для блока cnt: deeps(obj, str), // вложенные блоки nest: , // строка перед всеми вложенными блоками be4e: parts[ i + 1 ], // str -- строка, идущая после завершения данного // cnt -- блок-родитель, парсить строку будем в его рамках af3e: { cnt: null, str: "" } };

Т.к. у нас может быть ситуация, когда после блока есть что-нибудь ещё, то здесь af3e.str и должно быть строкой, идущей сразу после {% / %} текущего блока. Все необходимые ссылки мы проставим в момент завершения блока, так наглядней.
В этот же момент мы удаляем последний элемент элемент curnt.

If (str === "/") { // предыдущий элемент curnt // является родителем // завершившегося сейчас блока curnt .af3e = { cnt: (curnt [ cln - 2 ] ? curnt [ cln - 2 ].cnt: obj), str: parts[ i + 1 ] }; curnt.pop();

Теперь мы можем собрать одномерный массив, в котором будут все нужные подстроки с их текущими obj.
Для этого нужно «разобрать» получившийся blocks, учитывая что могут быть списки.
Понадобится немного рекурсии, но в целом это будет уже не так сложно.

// массив строк для парсинга элементарных частей блоков var stars = [ [ parts, obj ] ]; parseBlocks(blocks, stars);

Примерный вид parseBlocks()

var parseBlocks = function (blocks, stars) { var len = blocks.length; for (var i = 0; i < len; i++) { var block = blocks [i]; // если определён текущий obj для блока if (block.cnt) { var current = block.cnt; // найдём списки switch (Object.prototype.toString.call(current)) { // если у нас массив case "": var len1 = current.length; for (var k = 0; k < len1; k++) { // кладём в stars текущий элемент массива и его строку stars.push ([ block.be4e, current[k] ]); // парсим вложенные блоки parseBlocks(block.nest, stars); } break; // если у нас объект case "": for (var k in current) { if (current.hasOwnProperty(k)) { // кладём в stars текущий элемент объекта и его строку stars.push ([ block.be4e, current[k] ]); // парсим вложенные блоки parseBlocks(block.nest, stars); } } break; // у нас не массив и не объект, просто выведем его default: stars.push ([ block.be4e, current ]); parseBlocks(block.nest, stars); } // кладём в stars то, что было после текущего блока stars.push ([ block.af3e.str, block.af3e.cnt ]); } } };

Var pstr = ; var len = stars.length; for (var i = 0; i < len; i++) { pstr.push(parseStar (stars[i], stars[i])); } // Результат: return pstr.join ("");

Примерный вид parseStar()

var parseStar = function (part, current) { var str = ""; // убираем лишнее var ptn = /\{\{\s*.+?\s*\}\}/g; var parts = part.split (ptn); var matches = part.match (ptn); // начинаем собирать строку str += parts; if (matches) { var len = matches.length; for (var i = 0; i < len; i++) { // текущий элемент со значением var match = matches [i]; // убираем лишнее и делаем trim var el = match.replace(/^\{\{\s*|\s*\}\}$/g, ""); var strel = ""; // находим элемент в текущем объекте var deep = deeps(current, el); // если нашли, то добавляем его к строке deep && (strel += deep); str += strel; } if (len > 0) { str += parts[ len ]; } } return str; }

Приведённый код немного меньше финального результата.
Так, например, я не показал что делать с текущим элементом, если он задан ка точка.
Так же я не привёл обработку фильтров.
Кроме того в итоговом варианте, я «от себя» добавил в обработку ситуаций, когда «текущий элемент» или «значение для» являются функциями.

Но моей целью было показать саму концепцию…

А результат, как уже было сказано в начале статьи, можно найти .
Итоговый пример .

Надеюсь, кому-нибудь пригодится.
Спасибо за внимание!

Есть несколько случаев, когда вам может понадобиться шаблонизатор на JavaScript , среди них как необходимость формирования содержимого на клиенте, так и на сервере, если используется JavaScript среды, такие как NodeJS или Rhino . Сразу скажу, что для себя рассматривал многие имеющиеся шаблонизаторы, от простых до экзотических. Более всего интересовали простые, однако позволяющие использовать сложную логику в шаблонах, и одним из таковых оказался EJS . Однако, штука эта была написана несколько не так, как мне бы хотелось, с одной стороны было много лишнего, с другой основная функциональность уж слишком усложнена. Я увидел возможность реализовать компиляцию и рендеринг его шаблонов гораздо проще.

Применение EJS

То, что делает шаблонизатор : беря шаблон и данные , возвращает экземпляр содержимого , полученный путём обработки шаблона на основе этого конкретного экземпляра данных .

В случае EJS суть проста: в составе шаблона мы можем совмещать выводимое как есть содержимое с кусочками встроенного JavaScript , аналогично тому, как в файлах сценариев PHP объединяется HTML с кодом. В коде видны как глобальные переменные, так и те, что мы позаботимся передать рендереру при вызове. Ещё там видны так называемые функции-помощники, которые упрощают формирование часто используемых конструкций, например, тегов ссылок или картинок.

Код обрамляется в тег или в тег .

Вот как это может выглядеть:

Я намеренно написал конструкции в разных стилях, чтобы показать, что наличие пробелов значения не имеет. Формируя вывод с использованием шаблона, мы передаём объект со свойствами, выступающими в качестве переменных, с которыми оперирует код внутри нашего шаблона. Смысл в том, что с помощью логических конструкций на самом же JavaScript можно гибко менять вид формируемого в результате содержимого. Как вариант возможно использование тегов вида [% %] , если шаблон предполагается рендерить уже после того, как он включён в дерево DOM . В свете моих паттернов использования, оно мне показалось не очень полезным.

Улучшаем EJS

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

Разбор исходных шаблонов

Компиляция всегда начинается собственно с разбора исходного материала по заведомо заданным правилам. Наш набор правил сводится к различению содержимого в специальных тегах и всего остального. Глядя на структуру наших специальных тегов, можно понять, что достаточно просто захватывать их целиком с помощью несложного регулярного выражения. Помним, что кроме собственно кода, нам понадобится вытаскивать открывающий тег, чтобы понять, что делать с кодом внутри. Вот как будет выглядеть RegExp:

/(?:\n\s*)?(])+)%>/gm

Итак, что мы здесь делаем:

  • (?:\n\s*)? - убираем перевод строки и все пробелы от него до тега кода, если таковое вообще присутствует. Это, конечно, делать было вовсе не обязательно, но мы же эстеты, так предотвратим появление множества пустых строк в формируемом содержимом. Спецификатор ?: в начале подпаттерна означает, что его содержимое не захватывается, а просто пропускается.
  • (] - наш код - это любые символы, не являющиеся % , или символ % , за которым не следует символ > .
  • (?:[^%]|[%][^>])+ - наш код - эти последовательности могут присутствовать более одного раза, но не должны захватываться.
  • ((?:[^%]|[%][^>])+) - наш код - а захватываем мы всю последовательность целиком.
  • %> - закрывающий тег не захватываем.
  • gm - включаем глобальную полнотекстовую (многострочную) обработку.

Как можно видеть, всё достаточно тривиально, осталось разобраться, что делать с этим регулярным выражением. А здесь нам поможет очень полезный метод String.split , который разбивает строку с использованием указанного разделителя, возвращая массив получившихся подстрок. Особенность данного метода в том, что в качестве разделителя он может принимать регулярное выражение, причём, если в нём присутствуют захватываемые подпаттерны, подстроки, которые им соответствуют, также попадут в результирующий массив в обычном порядке. Это и даёт нам идею нативной оптимизации компилятора шаблонов: разбиваем строку регулярным выражением, получаем последовательность из кусочков строчного контента и спецификаторов кода со следующими за ними кусочками этого кода, далее достаточно просто обработать получившийся массив.

Формирование исполняемого кода

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

Организуем это следующим нехитрым образом:

  • функцию будем формировать в виде строки, пригодной для вызова eval
  • в теле функции организуем последовательное присоединение кусочков содержимого к некой переменной результата
  • обычный код на JS вставим как есть

Однако нам потребуется решить следующие проблемы:

  • передача переменных и функций-помощников в тело исполняемой функции шаблона
  • организация переменной результата так, чтобы это не пересекалось с переменными, используемыми в шаблоне
Решение первой проблемы

Для того, чтобы решить первую проблему, проще всего использовать конструкцию with . Да, это именно та вещь, которая является извечной темой бурных дискуссий разработчиков и головной болью стандартизаторов w3c, именно её так ненавидят всякие валидаторы типа JSLINT и компиляторы вроде Closure Compiler . То, что делает данная конструкция, проще всего представить как предоставление объекта в качестве уровня видимости локальных переменных, то есть вы сможете обратиться к свойствам объекта по имени как к обычным переменным, то есть без префикса имени объекта с точкой, вот, как это выглядит:

Var obj = { foo: "It is foo.", bar: 42 }; with(obj){ foo += " And bar is " + bar; }

При чтении значений всё довольно просто, не так тривиально при записи, особенно при использовании вложенных конструкций with , однако в шаблонах нам потребуется в основном читать значения, поэтому использование нативного подхода вместо того, чтобы городить очередной костыль, более чем оправданно. Если быть точным, то на самом деле создаются два взаимовложенных уровня видимости локальных переменных: один из объекта, другой внутри тела with, то есть при создании локальных переменных там, они не попадают в исходный объект.

Решение второй проблемы

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

Посмотрим, что у нас есть, а вариантов в общем то не много: как-то использовать объект this или объект arguments , которые, ввиду особенностей языка, пользователь всё равно не должен использовать в шаблонах, а если и может, то лишь неким особым образом. Рассмотрев все за и против, я решил использовать arguments , массив неименованных аргументов, переданных функции, в один из них и будем класть всё, что нужно для корректного формирования содержимого.

Особенности реализации

Теперь у нас есть всё, чтобы реализовать законченный движок шаблонизатора. Как я уже говорил, он не будет полностью совместим с оригинальным EJS, однако сможет использовать созданные для него шаблоны. Также не будет реализации автоматического загрузчика шаблонов, его нужно прикрутить самостоятельно, опираясь на принципы используемого вами фреймворка в данной конкретной среде исполнения.

Самое время перейти непосредственно к коду, рассмотрим всё по частям, начиная с главного и основного, а именно, с объекта шаблона:

// Конструктор var EJS = function(src){ if(typeof src == "string"){ // Если передан шаблон this.compile(src); // Сразу компилируем его } }; // Прототип EJS.prototype = { regexp: /(?:\n\s*)?(])+)%>/gm, helper: {} // Функции-помощники };

EJS.prototype.compile = function(src){ delete this.method; delete this.error; // удаляем следы предыдущего вызова компилятора var p = src.split(this.regexp), // Результат разбора r = , // Результат сборки i, o; this.parsed = p; // Сразу же сохраняем результат разбора // Выполняем сборку функции генерации содержимого for(i = 0; i < p.length; i++){ if(p[i] == "/g, match; while(match = re.exec(tpl)) { console.log(match); }

Запустив данный код мы найдем обе переменные и .

Теперь самое интересное, нам необходимо заменить найденные переменные их значениями. Самое простое что приходит на ум это сделать простой.replace(). Но это бы работало с простыми json объектами с одним уровнем вложенности. На практике же мы имеем дело с объектами, которые имеют многоуровневую вложенность:

{ name: "John", profile: { age: 23 } }

И tpl.replace(match, data) будет уже не достаточным решением. Потому что когда мы напишем , код заменится на data[«profile.age»] и будет undefined. Так как способ замены нам не подходит, было бы очень круто, если бы между тегами можно было выполнять реальный js код.

Var template = "

Hello, my name is . I\"m years old.

";

Как избавиться от this я расскажу ниже.

Как же это реализовать? В статье Джона Резига он использует new Function для создания функции из строки.

Var fn = new Function("arg", "console.log(arg + 1);"); fn(2); // outputs 3

Для понимания, данный код можно рассмотреть как:

Var fn = function(arg) { console.log(arg + 1); } fn(2); // outputs 3

fn — реальная функция, которая выполняет другую функцию, которая передана как текстовый параметр.

Это именно то, что нам необходимо, нам необходимо шаблон преобразовывать в вид:

Return "

Hello, my name is " + this.name + ". I\"m " + this.profile.age + " years old.

";

Но наш код будет работать только для вывода переменных между текстом верстки. Но нам ведь необходим шаблонизатор с циклами и условиями, и если мы будем иметь шаблон типа

Return "My skills:" + for(var index in this.skills) { + "" + this.skills + "" + }

мы конечно будем хватать ошибки. Именно для решения этой задачи Джон разбивает строку на элементы массива а в конце объединяет в строку.

Var r = ; r.push("My skills:"); for(var index in this.skills) { r.push(""); } return r.join("");

Следующим логическим шагом будет находить наши js строки и выполняя их добавлять в нужной части шаблона результат выполнения js циклов или иных функций. Для этого введем дополнительную переменную cursor, которая будет знать в какой части шаблона мы сейчас находимся и куда вставлять код.

Var TemplateEngine = function(tpl, data) { var re = /]+)?%>/g, code = "var r=;\n", cursor = 0, match; var add = function(line) { code += "r.push("" + line.replace(/"/g, "\\"") + "");\n"; } while(match = re.exec(tpl)) { add(tpl.slice(cursor, match.index)); add(match); cursor = match.index + match.length; } add(tpl.substr(cursor, tpl.length - cursor)); code += "return r.join("");"; // /g, reExp = /(^()?(var|if|for|else|switch|case|break|{|}|;))(?:(?=\()|(?=)|$)/g, code = "var r=;\n", cursor = 0, match; var add = function(line, js) { js? (code += line.match(reExp) ? line + "\n" : "r.push(" + line + ");\n") : (code += line != "" ? "r.push("" + line.replace(/"/g, "\\"") + "");\n" : ""); return add; };

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

Var r=; r.push("My skills:"); for(var index in this.skills) { r.push(""); r.push(this.skills); r.push(""); } r.push(""); return r.join("");

И конечно же все это будет успешно скомпилировано.

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

В данном случае mime тип text/html для браузера будет неизвестным и он пропустит выполнение его. Но содержимое этих тегов мы легко можем получить с помощью.innerHTML.

Добавим теперь в начало нашего шаблонизатора проверку, если строка начинается с #, то это id нашего шаблона.

Var html = tpl.charAt(0) === "#" ? document.getElementById(tpl.substring(1)).innerHTML: tpl;

Если строка начинается не с #, значит мы передали сразу шаблон как строку.

И завершающим этапом для нас будет избавление от обращения к this перед каждой переменной, а для этого мы передадим имена переменных scope в параметре new Function, а значения их мы присвоим с помощью метода.apply().

Return new Function(name, code.replace(/[\r\t\n]/g, "")).apply(this,value);

Теперь наш шаблонизатор может принять шаблон типа:

min

при этом js будет выглядеть так:

Var scope = [ { name: "item 1", price: "10$", time: "30" }, { name: "item 1", price: "10$", time: "30" }, { name: "item 1", price: "10$", time: "30" } ]; var template = tpl("#tpl_id",scope);

Теперь содержимое переменной template будет скомпилированной версткой, которую мы можем вставить в любой участок кода.

В итоге мы получим финальную версию нашего шаблонизатора, который может получать шаблоны по их id и компилировать в верстку.

Var tpl = function (str, data) { var name = , value = ; var html = str.charAt(0) === "#" ? document.getElementById(str.substring(1)).innerHTML: str; if (typeof(data) === "object") { for (var k in data) { name.push(k); value.push(data[k]); } } var re = /]+)?%>/g, reExp = /(^()?(var|if|for|else|switch|case|break|{|}|;))(?:(?=\()|(?=)|$)/g, code = "var r=;\n", cursor = 0, match; var add = function(line, js) { js? (code += line.match(reExp) ? line + "\n" : "r.push(" + line + ");\n") : (code += line != "" ? "r.push("" + line.replace(/"/g, "\\"") + "");\n" : ""); return add; }; while(match = re.exec(html)) { add(html.slice(cursor, match.index))(match, true); cursor = match.index + match.length; } add(html.substr(cursor, html.length - cursor)); code += "return r.join("");"; return new Function(name, code.replace(/[\r\t\n]/g, "")).apply(this,value); };