История одного шаблонизатора, от xml к xtpl и data-binding
<div>
{{if expr }}
Вот поэтому Smarty и подобные плохи, {{$username}}.
</span>
{{/if}}
Очень велика степень ошибки, а сам шаблонизатор никак не проверяет валидность получаемого html.


Что у них общего?
#!+ <!-- XML -->
<xtpl:if test="expr"> .. </xtpl:if>
#!- <xtpl:value> expr </xtpl:value>
#!+ <!-- Smarty -->
{{if expr }} .. {{/if}}
#!- {{ expr }}
new Parser({
#! left: "<"
#! right: ">"
#!+ c_open: "<!--"
c_close: "-->"
tags: false
trim: true
#!- firstLine: true
});
new Parser({
#! left: "{{"
#! right: "}}"
#! tags: "foreach include extends block ..."
});
<!-- XML -->
<div>
<xtpl:if test="expr">OK</xtpl:if>
</div>
<!-- Smarty -->
<div>
{{if expr}}OK{{/if}}
</div>
[{
#! name: "div", type: 1, __open: true // <div>
}, {
#! name: "if", ns: "xtpl", type: 1, __open: true, // <xtpl:if
#!+ attributes: [{
#! name: "test", type: 2, value: "expr" // test="expr">
#!- }]
}, {
#! name: "#text", type: 3, value: "OK" // текст в if
}, {
#! name: "if", ns: "xtpl", type: 1, __close: true // </xtpl:if>
}, {
#! name: "div", type: 1, __close: true // </div>
}]
[{
#! name: "#text", type: 3, value: "<div>" // <div>
}, {
#! name: "if", type: 1, __open: true, // {{if
#! attributes: "expr" // expr}}
}, {
#! name: "#text", type: 3, value: "OK" // текст в if
}, {
#! name: "if", type: 1, __close: true // {{/if}}
}, {
#! name: "#text", type: 3, value: "</div>" // </div>
}]
_trans: function (node, input, stack) {
var val;
switch (node.name) {
#!+ case "if": // if test="expr"
#! val = node.__open
#! ? "__XIF=0;" + _try("__XIF=(" + node.attr("test") + ")", node) + "if(__XIF){";
#! : "}"
#!- break;
#!+ case "foreach": // foreach data="array" item="val"
val = node.__open
#! ? ("__XFOR=0;" + _try("__XFOR=" + node.attr("data"), node)
#!+ + "__buf.each(__XFOR, function (" + (node.attrAny("item", "__v")) + "){"
#!- )
#! : "});"
#!- break;
}
// ..
}
var xtpl = require("AsyncTpl").engine("XML");
xtpl.NS = "xtpl"; // namespace
xtpl.ASYNC = true; // async include templates
xtpl.STREAM = false; // streaming
xtpl.ESCAPE = true; // html escape all variables
xtpl.DEBUG = true; // compile & run-time errors (console.log)
xtpl.ROOT_DIR = "./tpl/";
xtpl.COMPILE_DIR = "./tpl_c/";
var smarty = require("AsyncTpl").engine("Smarty");
smarty.LEFT = "{{";
smarty.RIGHT = "}}";
#!+ smarty.modifiers({ // {{ctx.expr|lower}}
lower: function (val) {
return val.toLowerCase();
}
#!- });
<div> <xtpl:if test="ctx.expr"> Hello! </xtpl:if> </div>
function (ctx, __buf) {
var __XIF;
#! __buf.w("<div>");
#!+ __XIF = 0;
try { __XIF = ctx.expr; } // <xtpl:if test="ctx.expr">
#!- catch (e) { xtpl.error(e, 1, "path/to.xml"); }
#!+ if (__XIF) {
__buf.w("Hello!");
#!- }
#! __buf.w("</div>");
return __buf.toStr();
}
Я начал пробовать шаблонизатор на реальных задача. Взял Backbone, написал простенький SPA и...
<ul>
#!+ <x:each data="ctx.users" as="user">
#!+ <li>
#! <x:value>user.get("name")</x:value>
#!- </li>
#!- </x:each>
</ul>
var UserList = new Backbone.View.extend({
#! template: xtpl.compile("users.xml"),
#!+ initialize: function () {
this.listenTo(this.collection, "change", this.render);
#!- },
#!+ render: function () {
#! var html = this.template({ users: this.collection.models });
#! this.$el.html(html);
#!- }
});
// Где-то в приложении
users.fetch({
success: function () {
#!+ var list = new UserList({
el: "#users",
collection: users
#!- });
#! list.render();
}
});
#!+ // это? #!- this.listenTo(this.collection, "change", this.render);
#!+ <x:bind data="ctx.items" as="list">
<ul>
#!+ <x:each data="list" as="item">
#!+ <li>
<x:value>item.get("name")</x:value>
#!- </li>
#!- </x:each>
</ul>
#!- </x:bind>
<x:bind/> — создает замыкание (микро шаблон), подписывается на все события коллекции,
при изменении которой, заново рендерил связанный блок.
#!+ __buf.w("<div id='uniqId'>"); // <x:bind
#! try { __xbind = ctx.items; } catch (e) {} // data="ctx.items"
#!+ __buf.bind(__xbind, "uniqId", function (list) { // as="list"
__buf.w("<ul>"); // <ul>
#!+ __buf.each(list, function (item) { // <x:each/>
// вывод элементов
#!- });
__buf.w("</ul>"); // </ul>
#!- });
#!- __buf.w("</div>"); // </x:bind>
Это был очень долгий и сложный путь, как внедрить data binding в обычный js шаблон, чтобы он работал как на сервере так и браузере.
<ul> <x:each bind="ctx.list" as="item"> #!+ <li> <x:value bind>item.name</x:value> #!- </li> </x:each> </ul> #! <b>total: <x:value bind>ctx.list.length</x:value></b>
<ul> <x:each bind/> // [0] <li> <x:value bind/> // [0, $idx, 0] </li> </x:each> </ul> <b>total: <x:value bind/></b> // [1, 1]
__buf.w("<ul>");
#!+ __buf.bind("each", [0], // <x:each bind/>
#!+ function () { // getter
try { return ctx.list; } catch (e) {}
#!- },
#!+ function (__xbind0) { // микро шаблон
__buf.w("<li>");
#!+ __buf.bind("value", [0], function () { // <x:value bind/>
#! try { return __xbind0.name; } catch (e) {} // getter
}, function (__xbind0) { // микро шаблон
#! __buf.v(__xbind0);
#!- });
__buf.w("</li>");
#!- }
#!- );
__buf.w("</ul>");
#! // и так далее
input.btn.btn-success.xlarge[type="button"] {
on-tap: ctx.counter++;
value: ctx.counter || "Click me"
}
&btn = button.btn { // Обявляем элемент «btn»
#!+ class: "btn-{{ctx.mod || 'default'}}"
| {{ctx.text}} // это текстовая нода
if ctx.icon {
#! &icon { x-name: ctx.icon }
#!- }
}
#! &btn { x-text: "Fine", x-mod: "success", type: "reset" }
Если вы решили создать свой синтаксис, то позаботьтесь о поддержки им хоть в одной IDE, иначе оно не имеет смысла. Конечно это только мое мнение, но без этого будет крайне трудно использовать подобную поделку.
xtpl.tag({
"nodeName": {
#!+ // Препроцессин
#!- pre: function (node, attrs) { },
#!+ // JavaScript код
code: function (node, attrs) {
#! return ["begin", "end"];
#!- }
}
});
xtpl.tag({ // loop[from=1][to=3] | {{$i}}
#!+ "loop": function () {
return [
#! "for (var i = $from; i < $to; i++) {",
#! "}"
];
#!- }
});
xtpl.decl("name", {
#!+ compile: function (node, expr) {
// Вызывается в момент компиляции
#!- },
#!+ init: function (el, value) {
// Инициализация элемента (добавление в DOM)
#!- },
#!+ update: function (el, newValue, oldValue) {
// Изменение значения
#!- },
#!+ remove: function (el) {
// Удаления из DOM
#!- }
});
xtpl.decl("x:show", {
#!+ compile: function (node, expr) {
var attrs = node.attrs;
attrs.style = (attrs.style || "")
+ ";{{" + expr + " ? '' : 'display: none;'}}";
#!- }
});
// init(el, state) & update(el, state)
xtpl.decl("x:autofocus", function (node, state) {
#!+ if (state) {
setTimeout(function () {
#! el.focus();
#!+ el.selectionStart =
#!- el.selectionEnd = (el.value ? el.value.length : 0);
}, 1);
#!- }
});
xtpl.mod("name", function (value, ...rest) {
// Модификатор значения
});
// Нужно: <b>Всего 2 письма</b>
#! // Шаблон: b | Всего {{ctx.total | plural:"letters"}}
#!+ xtpl.mod("plural", function (total, name) {
#! // total — значение пеменной
#! // name — название языковой константы
#! return total + " " + i18n.plural(total, name);
#!- });
// Нужно: <pre>{ "foo": "bar" }</pre>
// Шаблон: pre | {{ctx.data | pretty }}
#!+ xtpl.mod("pretty", function (value) {
return JSON.stringify(value, null, "\t");
#!- });
Как я говорил раньше, основное отличие в том, что теперь не нужно считать ноды, а шаблон всё также представляет из себя фунцию.
#! input { x-model: **ctx.name**, placeholder: "Enter your name" }
#! h2 | Hello **{{ctx.name}}**!
var el = document.getElementById("target");
xtpl.bind(el, templateStr, { name: "" });
<div id="target"> #!+ <!--attrs--> <input placeholder="Enter your name"/> #!- <!--/attrs--> #! <h2>Hello <!--x:value--><!--/x:value-->!</h2> </div>
__buf.bindOpen("attrs"); // <!--attrs-->
#! __buf.s("<input");
#!+ __buf.bindEvent("input", // x-model="ctx.name"
#!+ function (el, evt) { // слушатель
try { ctx.name = el.value } // получение значения
catch (e) { __xerr(e, "hello.xtpl", 2); }
#!- }
#!- );
#!+ __buf.bindAttr("value", // x-model="ctx.name"
#!+ function (__xargs) { // getter
try { __xargs[0] = ctx.name }
catch (e) { __xerr(e, "hello.xtpl", 2) }
#!- },
#!+ function (__xbind0) { // setter
__buf.w(__xbind0)
#!-}
#!- );
#! __buf.s(" placeholder='Enter your name'/>")
__buf.bindClose("attrs"); // <!--/attrs-->
__buf.s("<h2>Hello ")
#!+ __buf.b("x:value", // {{ctx.name}}
#!+ function (__xargs) { // getter
try { __xargs[0] = ctx.name }
catch (e) { __xerr(e, "hello.xtpl", 3) }
#!- },
#!+ function (__xbind0) { // setter
try { __xval0 = __xbind0 }
catch (e) { __xval0 = void 0; __xerr(e, "hello.xtpl", 3) }
__buf.w(__xval0);
#!- }
#!- );
__buf.s("!</h2>")
| total, ms | avg. per loop, ms | |
|---|---|---|
| Reactive | 69 984 | 23.33 |
| React | 52 950 | 17.65 |
| AngularJS | 49 815 | 16.61 |
| Vue | 43 141 | 14.38 |
| xtpl | 33 458 | 11.15 |
По-большому счету, это не готовый продукт, а исследование на тему, возможно ли доработать обычную js-функцию-шаблон до поддержки data binding, заставив всё это работать, как на сервере, так и клиенте. Как оказалось такой подход достаточно производительный и не уступает таким проектам как Angular или React, единственно что ему не хватает, это довести до ума :]