История одного шаблонизатора, от 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, единственно что ему не хватает, это довести до ума :]