Рано или поздно шаблонизация перемещается на клиент. На данный момент существует несколько общепринятых клиентских шаблонизаторов ими являются mustache и underscore.template или что-нибудь подобное (в доклад(ах) Сергея Бережного можно найти ещё). Несмотря на огромное количество клиентских шабнизаторов большинство, за редким исключением, являются строчными.
Проблемы строчной шаблонизации — работа со строками
Это действительно ужасно т.к.
Во-первых, шаблоны избыточны (про отношение к html будет раскрыто ниже) и кашеобразны. Даже в рекламных примерах
var template = '<div class="entry"><h1>{{title}}</h1><div class="body">{{body}}</div></div>';
В шаблонах мы наблюдаем неформатированный html, и некий метаязык, поддержку которого нужно как-то внедрить в нашу среду (спасибо и не спасибо JetBrains за своевременную поддержку хипстерских метаязыков). А так же после прогона через регулярочки всё это вставляется с помощью innerHTML, причём, если вы писали более менее большое приложение, вы понимаете что вставки шаблонов происходят постоянно в разных местах дерева, и свести это к одной вставке всех нагенерёных шаблонов через documentFragment попросту невозможно. А частые вставки через innerHTML это ад (в будущем планирую выложить интересные тесты по этому поводу).
Хотя стоит признаться, что проблема форматирования отчасти решена вставкой шаблона в неопределённый тег script, что в свою очередь создаёт новую проблему: все шаблоны вашего приложения оказываются на index.html(php?), и когда их набирается много приходится писать хитросборщики, ибо phpStorm'у печально даже с 32Гб оперативки.
Во-вторых, проблемы работы с логикой
где-то так
{{#repo}}
<b>{{name}}</b>
{{/repo}}
{{^repo}}
No repos :({
{/repo}}
где-то в строках (надеюсь среды уже умеют работать с таким, иначе мне искрение жаль тех, кто с этим работает)
var list = "<% _.each(people, function(name) { %> <li><%= name %></li> <% }); %>";
_.template(list, {people: ['moe', 'curly', 'larry']});
// "<li>moe</li><li>curly</li><li>larry</li>"
где-то в отдельных функциях, собирающих строки
{{#list people}}{{firstName}} {{lastName}}{{/list}}
{
people: [
{firstName: "Yehuda", lastName: "Katz"},
{firstName: "Carl", lastName: "Lerche"},
{firstName: "Alan", lastName: "Johnson"}
]
}
Handlebars.registerHelper('list', function(items, options) {
var out = "<ul>";
for(var i=0, l=items.length; i<l; i++) {
out = out + "<li>" + options.fn(items[i]) + "</li>";
}
return out + "</ul>";
});
(невольно вспоминается мой первый опыт клиентского шаблонизирования по методу от Степана Резникова)
В-третьих, поиск элементов. Проблема не настолько страшна как две предыдущих, но мне как-то непонятно, почему я должен искать то, что только что сделал?
Обычно это выглядит так
var templateWrapper = document.getElementById('templateWrapper');
something.render(template, templateWrapper);
var templateElemet = templateWrapper.getElementsByClassName('templateElement');
//особо не приятно в старых ие
_small_(Отчасти расстраивает отсутсвие возможности нормально использовать шаблон в шаблоне, хотя возможно недоизучил эту тему по причине глубокого расстройства, вызванного изложенным выше.)_/small_
cnCt
Основным плюсам cnCt являются построение DOM методами DOM и описание шаблона в виде json.
//описание шаблона
var template = function(data){
//если 'e' не указан, будет сделан div
return {c: 'user', C: [
{e: 'a', h: 'http://facebook.com/' + data.facebookId, c: 'user-avatar-wrapper', C:
{e: 'img', c: 'user-avatar', S: 'http://graph.facebook.com/' + data.facebookId + '/picture?width=200&height=200'}
},
{c: 'user-name', t: data.fristName + ' ' + data.lastName, n: 'userName'},
]};
};
//собираем шаблон. метод tp возвращает нам результат сбоки
var build = cnCt.tp(template, {fristName: 'swf', lastName: 'dev', facebookId: '100005155868851'}, document.body);
//работаем и навешиваем события на найденные элементы
build.r.addEventListener('click', function(){console.log('hello')}); //build.r (r сокращённо от root) это div.user
build.userName.addEventListener('click', function(){console.log(this.textContent)}); // элемент найденный по n: 'userName'
При разработке сложных веб интейрфейсов мы стараемся отойти от тегов в принципе, т.к. понимаем html как структуру блоков вьюшки, поэтому используем только теги a/input/textarea/(...) как основу необходимого функционала для блока вьюшки, в следствии чего был выработан такой подход.
В бою это выглядит так (jsfiddle)
//для работы с датами
function dataToStr(_date){
var date = _date instanceof Date ? _date : new Date(_date);
return date.getDate() + '/' + date.getMonth() + '/' + date.getFullYear();
}
//псевдолокализация
function l10n(key){
return l10Keys[key] || key;
}
var l10Keys = {
youFriendsList: 'список ваших друзей',
sendMessage: 'отправить сообщение'
},
//описываем шаблоны
templates = {
//основа
basis: function(){
return [
//локализация
{c: 'header', n: 'header', t: l10n('youFriendsList')},
{c: 'content-view', n: 'contentView'},
{c: 'footer', t: '© SoftWear LLC'}
];
},
//статический микрошаблон используемый другими шаблонами
icon: {c: 'icon'},
//кнопка
button: function(data){
return {c: 'button', t: data.t, n: data.n, C: templates.icon, a: data.a};
},
//список друзей
friendsList: function(friends){
var items = [],
i = 0,
iMax = friends.length;
//чистый js в шаблонах (можно и посортировать, но но не будем усложнять)
for(; i < iMax; i += 1){
items[i] = {c: 'friend', C: [
{e: 'a', h: 'http://facebook.com/' + friends[i].facebookId, c: 'friend-avatar-wrapper', C:
//максимум результата из минимум данных
{e: 'img', c: 'friend-avatar', S: 'http://graph.facebook.com/' + friends[i].facebookId + '/picture?width=200&height=200'}
},
{c: 'friend-name', t: friends[i].firstName + ' ' + friends[i].lastName},
//работа с датами
{c: 'friend-birthday', t: dataToStr(friends[i].birthday)},
//шаблоны в шаблонах
templates.button({n: 'sendMessage', t: l10n('sendMessage'), a: {'data-friend-id': friends[i].facebookId}})
]};
}
return {c: 'friend-list', C: items};
}
},
// модель данных
friends = [
{
facebookId: 1,
firstName: 'Иван',
lastName: 'Иванов',
birthday: 1
},
{
facebookId: 2,
firstName: 'Василий',
lastName: 'Васин',
birthday: 111111111
},
{
facebookId: 3,
firstName: 'Пётр',
lastName: 'Петров',
birthday: 123123123
}
],
build,
$contentView,
$buttons,
i;
//строим объект шаблонов для более удобной работы (они же описаны на уровне вьюшек эппа)
cnCt.bindTemplates(templates);
//строим основу
$contentView = cnCt.tp('basis', document.body).contentView;
function getByFacebookId(id){
var i = friends.length;
id = +id;
for (; i-- ;){
if (friends[i].facebookId === id){
return friends[i];
}
}
return null;
}
$buttons = cnCt.tp('friendsList', friends, $contentView).sendMessage; // а тут массив т.к. элементов много
function sendMessage(){
var friend = getByFacebookId(this.getAttribute('data-friend-id'));
console.log('hello ' + friend.firstName);
}
for (i = friends.length; i-- ;){
$buttons[i].addEventListener('click', sendMessage);
}
Плюсы:
- Чистый js
- Абстрагирование от html
- Намного быстрее строчных на мобильных, быстрее везде кроме оперы и ие на десктопах (визуализацию тестов делаю), и на телике и киосках быстрее :)
- Шаблоны в шаблонах
- Поиск внутри и не завязан на имена классов или id
- Самодостаточна
- Автоподсветка и cc во всём что понимает js без плагинов
- поддерживает inline SVG и VML, т.к. поддерживает .createElementNS()
- XML покидает вашу жизнь
- Работает в жизни(доступ к последнему), правда не в выделенном виде, соответсвенно будем поддерживать
Минусы:
- Медленнее в ие (нам важнее мобилки, киоски (мы переводим их на Chrome) и телики)
- Кажется непонятной (через неделю использования, html кажется непонятным)
- Слабо документирована (скоро всё будет)
- Бесполезна на ноде (потому что не для него / хотя можно написать ветку cnCt json to str, но зачем?)
Будем документировать и развивать.
PS: Решили открыть т.к. коллеги взвыли от underscore при работе с проектом, который делался не у нас.
Автор: DmitryMakhnev