cnCt — клиентский js шаблонизатор

в 16:39, , рубрики: javascript, шаблонизаторы, метки: , ,

Рано или поздно шаблонизация перемещается на клиент. На данный момент существует несколько общепринятых клиентских шаблонизаторов ими являются 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.

пример на jsfiddle

//описание шаблона
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);    
}

Плюсы:

  1. Чистый js
  2. Абстрагирование от html
  3. Намного быстрее строчных на мобильных, быстрее везде кроме оперы и ие на десктопах (визуализацию тестов делаю), и на телике и киосках быстрее :)
  4. Шаблоны в шаблонах
  5. Поиск внутри и не завязан на имена классов или id
  6. Самодостаточна
  7. Автоподсветка и cc во всём что понимает js без плагинов
  8. поддерживает inline SVG и VML, т.к. поддерживает .createElementNS()
  9. XML покидает вашу жизнь
  10. Работает в жизни(доступ к последнему), правда не в выделенном виде, соответсвенно будем поддерживать

Минусы:

  1. Медленнее в ие (нам важнее мобилки, киоски (мы переводим их на Chrome) и телики)
  2. Кажется непонятной (через неделю использования, html кажется непонятным)
  3. Слабо документирована (скоро всё будет)
  4. Бесполезна на ноде (потому что не для него / хотя можно написать ветку cnCt json to str, но зачем?)

Будем документировать и развивать.

PS: Решили открыть т.к. коллеги взвыли от underscore при работе с проектом, который делался не у нас.

Автор: DmitryMakhnev

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js