javascript-шаблонизация для начинающих на примере lodash template

декабрь 25 , 2018

В своих постах я часто упоминаю javascript-шаблонизацию. Underscore и lodash-шаблоны, может, видели. И недавно понял свою ошибку. Я пишу про шаблоны так, как будто прям каждый обязан знать, что это такое. А если не знает, то легко загуглит. А давайте-ка вместе и погуглим. Чуть не первая статья, с хабра, цитирую
— Лучшим выбором оказываются шаблоны, потому что это приводит к более чистому базовому коду и лучшему процессу работы с ними.

Хм, ну окееей. Погуглим еще. Сайт developer.mozilla.org, серьезные ребята, почитаем.
— Шаблонными литералами называются строковые литералы, допускающие использование выражений внутри. С ними вы можете использовать многострочные литералы и строковую интерполяцию.

Че-то это вообще не то. Это что-то из es6. Ну и третья попытка:
— Неплохо бы использовать механизм шаблонизации на стороне клиента, чтобы отделить поведение приложения от его внешнего вида

Я в свое время читал похожие умные слова и ни фига не понимал. Если Вы понимаете, то читать дальше будет не интересно. Зайдите на хабр и прочитайте лучше там. А вот те, кто по этим фразам с трудом улавливает, о чем речь, но хочет понять, оставайтесь. Попробуем вместе разобраться, что такое javascript-шаблонизация, зачем она нужна и когда применяется.

Люблю случаи из жизни. Давай представим. Ты работаешь в веб-студии фронтендщиком. На пару с бекендщиком пилите сайты. Пусть его зовут Серега. Ты верстаешь шаблоны, отдаешь ему html-файлы, а он их натягивает на cms-ки или фреймворки. Какой-нибудь битрикс или laravel, не суть. Серега твой бро, он не воротит нос от html-тегов и вообще отличный парень. Нормально относится к тому, что ты просишь в зависимости от разных условий проставлять тегам нужные классы или не выводить определенные блоки. И все у вас хорошо, спокойно работаете, а по пятницам вместе бухаете в баре.

Однажды к вам приходит дизайнер и говорит, а чо мы список товаров делаем всегда одинаково? Пагинация, фу, прошлый век же. Давайте замутим подгрузку товаров аяксом, когда прокручиваем до конца страницы. Вы такие с Серегой думаете, а чо, прикольно. Набрасываете схему, ты отлавливаешь, когда юзер докрутил до подвала страницы, дергаешь аяксом запрос, а он отдает тебе html со списком товаров. А ты такой ррраз и $('#products').append(response). Нормальная схема, все довольны. Ты делаешь верстку, несложную, примерно такую

    
  • Телефон Samsung
    Описание телефона бла-бла-бла
    20000 руб.
    В наличии
  • Идешь к Сереге и объясняешь, мол, верстка такая, но есть две детали. Если товара нет в наличии, то в тексте у product__store пишешь "Нет в наличии". И еще не забудь кнопке button при этом проставить атрибут disabled. Серега говорит, без проблем, все сделаю. Ты дальше возишься с прокруткой страницы и отправкой аякс-запроса, он формирует html по твоей верстке и данным из базы. Потом стыкуетесь, все отлично, начальство и дизайнер довольны.

    Проходит неделя, к тебе снова идет дизайнер. Говорит, чувак, давай когда товара нет в наличии, немного стили изменим. Ну там заголовок сереньким сделаем, а кнопку "В корзину" вообще уберем. Ты говоришь, хорошо. Думаешь, так, стили поменять, это нужно класс модификатор навесить, типа -available прямо на li.product. А кнопку можно убрать через display: none, но лучше бы вообще убрать. Идешь к Сереге, рассказываешь это все, он делает, опять все довольны.

    Магазин развивается, начальство придумывает новую фишку. Давайте сделаем рейтинги товаров. Нарисуем звездочки у каждого товара и пусть люди кликают. Вы с Серегой собираетесь на совет. Он говорит, мол, я заведу поле rating в базе, ты дергаешь post-запрос, отправляешь мне айдишник товара, а я там разберусь, в базе инкремент сделаю, рейтинг товара увеличиваю. Ты говоришь, все нормально, только звездочки в верстку не забудь добавить. И тут он отвечает, рано или поздно, он такое скажет. Мол, бро, я все понимаю, но и ты меня пойми. Мне твои звездочки до одного места. Я тут запросы в базу пишу и редисы настраиваю, давай ты версткой сам будешь заниматься. Я тебе отдам данные о товарах, в json-чике, а ты делай с ними, что хочешь. Хочешь, звездочки добавляй, хочешь, классы навешивай, хочешь, тексты меняй в зависимости от наличия. Ты немножко недоволен, нормально же все раньше было. Но в душе понимаешь, Серега прав. И говоришь, отличная идея, давай так и сделаем.

    Серега готовит запрос и тебе рассказывает. Вот смотри, теперь тебе будет отдаваться не готовый html, а json-массив такого вида.

        [{
            id: 5,
            title: 'Телефон Самсунг',
            price: 20000,
            available: true,
            rating: 4
        }, {
            ...
        }]
    

    Ты говоришь, ну нормально, чо я массив что ли распарсить не смогу и в html добавить? И начинаешь делать. Сначала у тебя получается что-то вроде этого

        var products = JSON.parse(response);
        var html = '';
        products.forEach(function(product) {
            // формируем строку html для одного товара и добавляем ее в переменную html
        });
        $('#products').append(html);
    

    Пока ничего особенного, пока не начали реализовывать вот это

        // формируем строку html для одного товара и добавляем ее в переменную html
    

    Вспомним те условия, что у нас есть: класс-модификатор -available, разный текст в зависимости от наличия и убрать кнопку корзины, если нет товара. И начинаем писать что-то такое

        html += '
  • '; html += '
    ' + product.title + '
    '; html += '
    ' + product.title + '
    '; html += '
    ' + product.price + ' руб.
    '; html += '
    ' + (product.available ? 'В наличии' : 'Нет в наличии') + '
    '; if (product.available) { html += ''; } html += '
  • ';

    Уф... Читать, конечно, тяжко, но пока еще можно. Вспомним, для чего мы это затеяли? Да, нам же звездочки рейтинга нужно добавить. Изящный код в нужное место и все.

        html += '
    • 1
    • 2
    • 3
    • 4
    • 5
    ';

    Ну так себе, выглядит диковато, но ладно. Главное, теперь не надо Серегу дергать каждый раз со всякой фигней. И всей версткой ты управляешь на клиенте. Ты гордо говоришь, что вы с Серегой перенесли рендер html с сервера на клиент. Начальство довольно.

    Но только эйфория недолго длится. Прибегает менеджер и говорит: так, у нас акция, последние 5 товаров со склада продаем со скидкой 20%. И красным выделяем такие товары. Обсуждаете с Серегой. Он говорит, слушай, давай я добавлю в респонс json-a поле count - сколько товаров на складе осталось. А ты на фронте скидку сам посчитаешь. А то сам знаешь, сегодня прибегут, скажут последние 5 товаров с 20% скидкой и цвет красный, а завтра последние 10 и скидка 15%. А цвет зеленый. А ты на фронте все разрулишь, все равно к тебе будут с этим бегать. Ты такой, ну окей, сделаю.

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

        var products = JSON.parse(response);
        var html = '';
        products.forEach(function(product) {
            var isLast = product.count <= 5;
            var price = isLast ? Math.round(product.price * 0.8) : product.price;
            
            html += '
  • '; html += '
    ' + product.title + '
    '; html += '
    ' + product.description + '
    '; html += '
    ' + price + ' руб.
    '; html += '
    ' + (product.available ? 'В наличии' : 'Нет в наличии') + '
    '; html += '
    • 1
    • 2
    • 3
    • 4
    • 5
    '; if (product.available) { html += ''; } html += '
  • '; }); $('#products').append(html);

    Как оно? Такое чувство, что с этим пора что-то делать. Это мы пока очень простые варианты рассматриваем, а что будет, если посложнее? У меня от кавычек одинарных и двойных уже в глазах рябит. Ошибиться в такой мешанине легче легкого. Чуть промахнулся, удалил кавычку лишнюю, попробуй потом найди, где косяк и почему все валится. Надо что-то делать. Для начала гуглим. А что гуглить-то? Хрен знает даже, как вопрос задать... Чешешь репу дальше.

    Вдруг ты вспоминаешь, что у Сереги же должны были быть такие же проблемы. Раньше, когда он верстку сам натягивал. Какая разница, на бекенде, на фронтенде, у него такие же условия и вычисления. Идешь к нему. Показываешь код, спрашиваешь, как он раньше делал? Он говорит, у меня шаблонизатор есть, я туда данные загоняю, а на выходе готовый html. Спрашиваешь, а как это выглядит? Серега показывает, вот, например, у тебя такая строка

        html += '
    ' + price + ' руб.
    ';

    А у меня в шаблоне была бы такая

        
    {{price}} руб.

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

        html += '
    ' + (product.available ? 'В наличии' : 'Нет в наличии') + '
    ';

    Серега отвечает, да примерно так же.

        
    {{ product.available ? 'В наличии' : 'Нет в наличии') }}

    Или еще лучше, текст записать заранее в переменную, вот так

        var availableText = product.available ? 'В наличии' : 'Нет в наличии');
    

    А в шаблоне сделать так

        
    {{ availableText }}

    Вот так точно лучше, кратко и ясно. А вычисления все отдельно. И 5 звездочек нечего лепить руками, циклом прогнал по массиву от 1 до 5 и все. А то завтра нужно будет 10 звездочек поставить, чтобы еще не копипастить, а просто заменить 5 на 10. Еще Серега что-то рассказывает про MVC, отделение логики приложения от представления, про модели и вьюхи, но ты слушаешь плохо, и так уже есть над чем подумать.

    Ты подумал и теперь, имея некий багаж знаний, гуглишь уже целенаправленно. И инфу найти легче. Возможно, ты уже пользовался библиотеками lodash или underscore. Ты удивляешься, но в них есть функция работы с шаблонами _.template. Если про нее не знать, то читая доку и внимания не обратишь. Но теперь-то ты представляешь, что это такое, и смотришь внимательнее.

    Оказывается, чтобы работать с шаблонами через lodash (да и вообще с любым шаблонизатором), нужно сделать 2 вещи: подготовить данные и верстку, то есть сам шаблон. Это и есть то самое отделение логики от представления. Или почти что MVC. Данные о товаре это условно модель, а шаблон - вьюха или представление.

    Давайте сначала простейший пример, как работает _.template() в lodash (в underscore точно так же). Заглянем в документацию.
    Там приведен код

        var compiled = _.template('hello <%= user %>!');
        compiled({ 'user': 'fred' });
        // => 'hello fred!'
    

    Такие смешные закорючки <% обязательны, lodash должен знать, где просто строка, а где нужно вставить переменную. <%= user %> - это место для переменной user. Точнее, не переменная, а поле объекта { 'user': 'fred' }. user можно и без кавычек, просто { user: 'fred' }

    Смотрим дальше. Написать в одной строке 'hello <%= user %>!' легко. Но у нас немаленькая верстка из нескольких элементов. Чтобы сделать адекватно, применяют хитрый шаг. Шаблоны не сразу записывают в строки, а предварительно прямо в html в специальном теге

        
    

    Или type="text/x-template", можно по-разному, но суть одна - содержимое этого тега не исполняется как javascript, и мы из него вытаскиваем строку шаблона через простой $('#template-name').html(). Давайте попробуем написать шаблон для нашего случая

        
    

    Ну как? Сравните с тем, что было выше, где складывали строки. Читалось это совершенно иначе. В кавычках уже не путаемся. Видим, что у элемента с классом product могут быть еще 2 класса-модификатора classAvailable и classLast. Условие if (available) then вывести кнопку button тоже логически срисовывается. И даже цикл for, где мы выводим список li, не вызывает страха. Да, немножко непривычны эти постоянные <%, но присмотреться можно. Это уже не такая жесть, как раньше.

    Наш шаблон готов. Его содержимое вытаксивается обычным jquery $('#template-product-item').html() или нативным js document.getElementById('template-product-item').innerHTML.

    Осталось передать в шаблон данные. Общая заготовка будет выглядеть так

        var products = JSON.parse(response),
            templateProductItem = document.getElementById('template-product-item').innerHTML,
            compiled = _.template(templateProductItem),
            html = '';
        
        products.forEach(function(product) {
            html += compiled(data);
        });
        $('#products').append(html);
    

    Осталось собрать объект data. Сначала заполним его тем, что приходит напрямую с бекенда, в product

        products.forEach(function(product) {
            var data = {
                id: product.id,
                title: product.title,
                description: product.description,
                available: product.available,
                rating: product.rating
            }
            html += compiled(data);
        });
    

    Теперь нужно добавить поля price с учетом скидки, классы-модификаторы classAvailable и classLast, а также текст наличие/неналичие textAvailable. Допишем код так

        products.forEach(function(product) {
            var isLast = product.count <= 5,
                price = isLast ? Math.round(product.price * 0.8) : product.price,
                classAvailable = product.available ? '-available' : '',
                classLast = isLast ? '-last' : '',
                textAvailable = product.available ? 'В наличии' : 'Нет в наличии';
            
            var data = {
                id: product.id,
                title: product.title,
                description: product.description,
                available: product.available,
                rating: product.rating,
                
                maxRating: 5, // это типа настройка, сколько звездочек выводить для рейтинга
                
                price: price, // это пошли вычисленные поля
                classAvailable: classAvailable,
                classLast: classLast,
                textAvailable: textAvailable
            }
        
            html += compiled(data);
        });
    

    Вот так. У нас довольно много вычислений, но они все стоят отдельно от верстки, ничего не пересекается. Это и называется отделение логики от представления. Или MVC, если хотите. А чтобы было еще симпатичнее, можно выделить подготовку data для одного товара в отдельную функцию. Получится так

        function getDataForProductItemTemplate(product) {
            var isLast = product.count <= 5,
                price = isLast ? Math.round(product.price * 0.8) : product.price,
                classAvailable = product.available ? '-available' : '',
                classLast = isLast ? '-last' : '',
                textAvailable = product.available ? 'В наличии' : 'Нет в наличии';
            
            return {
                id: product.id,
                title: product.title,
                description: product.description,
                available: product.available,
                rating: product.rating,
                
                maxRating: 5,
                price: price,
                classAvailable: classAvailable,
                classLast: classLast,
                textAvailable: textAvailable
            }
        }
    

    А общий код будет выглядеть так

        var products = JSON.parse(response),
            templateProductItem = document.getElementById('template-product-item').innerHTML,
            compiled = _.template(templateProductItem),
            html = '';
        
        products.forEach(function(product) {
            html += compiled(getDataForProductItemTemplate(product));
        });
        $('#products').append(html);
    

    Или еще короче

        var products = JSON.parse(response),
            templateProductItem = document.getElementById('template-product-item').innerHTML,
            compiled = _.template(templateProductItem),
            html = products.map(function(product) {
                return compiled(getDataForProductItemTemplate(product));
            }).join('');
        
        $('#products').append(html);
    

    Теперь у нас все аккуратно разделено. Вычисления в одном месте, верстка в другом. Если понадобится добавить еще один класс-модификатор к .product, нам не нужно будет щурить глаза, целясь, куда ввернуть дополнительные жуткие кавычки. Мы в шаблоне проставим <%= classNew %>, а в getDataForProductItemTemplate вычислим новое поле classNew и добавим его в return. Жизнь становится гораздо проще.

    Весь код в сборе смотрите на jsfiddle - https://jsfiddle.net/Webdevkin/q8y0ob7w/2/.

    Подведу итоги. Надеюсь, мы смогли разобраться, зачем нужна javascript-шаблонизация. Или по крайней мере поняли ее плюсы и будем пытаться применять. На мой взгляд, синтаксис шаблонов в undescore/lodash не самый симпатичный. Но есть и куча других, например, handlebars. Он лаконичнее и приятнее. Например, там переменная выводится не <%= value %>, а {{value}} Кому как, но мне симпатичнее. Плюс в handlebars есть свои плюшки, типа вычисляемых полей и прочего. Впрочем, синтаксис lodash меня вообще не напрягает, поэтому именно его я и использую в своих статьях. И скажу по секрету, его можно переопределить под себя. Например, вместо <% задать {{ - будет как в handlebars. Или вообще придумать что-то свое (не рекомендую этим увлекаться). В общем, тема большая, копать можно долго. Моей же целью было попытаться вас заинтересовать темой шаблонов и объяснить их работу на пальцах и живых примерах.

    Спасибо, что дочитали до конца и до встречи в следующих статьях!

    Что еще почитать на тему фронтенда

    Заходите в группу в контакте - https://vk.com/webdevkin
    Анонсы статей, обсуждения интернет-магазинов, vue, фронтенда, php, гита.
    Истории из жизни айти и обсуждение кода.
    Как Вам статья? Оцените!