nunjucks - html на диете
Посмотрим как с помощью шаблонизатора nunjucks можно значительно ускорить процесс верстки сайтов.
А при чем тут ящики?
Каждый раз при слове HTML, у меня невольно возникает (т.е. возникала раньше) именно такая ассоциация. Берем дизайн одной страницы, верстаем. Берем дизайн второй страницы, копипастим и верстаем. И каждая страница это как бы заколоченный ящик, а html код - это запертое в нем содержимое.
Ниже картинка, делающая этот текст бессмысленным:)
Каждая ваша страница (ну ок, каждая вторая) чуть менее чем полностью описана дублирующимся кодом.
За примерами дублирующихся элементов далеко ходить не нужно - как минимум это шапка, навигация и подвал которые вполне можно было бы переиспользовать.
А теперь представьте (хотя чего уж там, сталкивались уже и сами, я думаю), что чтобы заменить паршивую надпись в футере и добавить ссылку на страничку в навигационную панель, нужно вскрыть каждый из этих ящиков (страниц), вручную найти нужный код, вручную добавить код (слава великому ctrl+c, ctrl+v), вручную убрать лишний код (как-то многовато этих "вручную", не кажется?), ну и протестировать все страницы перед отправкой к клиенту.
Чем больше эйч-ти-эм-эла - тем больше геморроя ©
Всего этого можно было бы избежать, применяя принцип DRY к процессу верстки. К сожалению, сам по себе HTML не имеет механизмов (пока что, см. HTML Imports), способствующих построению документа из независимых модулей и DRY к нему неприменим совершенно, поэтому для построения “модульного” html, мы будем применять шаблонизатор Nunjucks.
Nunjucks
Nunjucks представляет собой javascript шаблонизатор, построенный в духе jinja2 - чертовски удобного шаблонизатора для Python (ну т.е. если верстали шаблоны под django, то вы уже в курсе всего что будет описано далее).
Nunjucks можно использовать как для своих Node.js проектов в качестве серверного компонента, так и для рендеринга шаблонов на стороне клиента (это, кстати, фишка многих node.js библиотек).
Как мы собираемся применять Nunjucks для верстки html страниц? Это решается применением потокового сборщика проектов - gulp вкупе с модулем gulp-nunjucks-render, но обо всем по порядку.
Подготовка рабочего окружения
Предупреждение
Этот раздел посвящен настройке gulp для рендеринга nunjucks шаблонов, его можно пропустить. Если вы еще не знакомы с gulp, то рекомендую сперва ознакомиться с предыдущей статьей цикла - "gulp, npm и bower - враги рутины"
И gulp и nunjucks являются node пакетами, поэтому для их установки воспользуемся утилитой npm, входящей в состав node.js.
В npm есть два способа установки пакетов - глобальный и локальный. При глобальной установке пакета - он будет доступен в системе повсеместно, и его можно будет использовать как обычную консольную утилиту. Локальная установка подразумевает загрузку пакетов в каталог node_modules внутри проекта и хорошо подходит для установки зависимостей.
Заходим в папку с проектом и выполняем инициализацию npm:
npm init
Результатом выполнения команды будет файл package.json, в котором будут храниться все необходимые зависимости проекта.
Далее нужно установить gulp и gulp-nunjucks-render.
Помимо этого мы воспользуемся плагином gulp-html-prettify, чтобы красиво отформатировать html файлы после сборки.
Gulp устанавливаем глобально (если он еще не установлен):
npm install -g gulp
gulp-nunjucks-render и gulp-prettify соответственно - локально. Помимо этого, нужно также установить еще и gulp, чтобы иметь возможность использовать его в gulpfile:
npm install --save gulp gulp-nunjucks-render gulp-html-prettify
Ключ --save используется для автоматического добавления пакета в список зависимостей в файле package.json.
Далее сформируем gulpfile.js - набор команд для gulp. Вот пример простого gulpfile для использования nunjucks.
Пользователей, не работавших до этого с gulp, прошу не пугаться при виде gulpfile - в основном он пишется один раз для определенных задач, после чего его можно копировать из проекта в проект с минимальными правками.
var gulp = require('gulp'),
njkRender = require('gulp-nunjucks-render'),
prettify = require(‘gulp-html-prettify’);
// создаем gulp задачу на компиляцию всех nunjucks шаблонов в текущей директории
gulp.task('nunjucks', function() {
return gulp.src('./*.njk')
.pipe(njkRender())
.pipe(prettify({
indent_size : 4 // размер отступа - 4 пробела
})
.pipe(gulp.dest('./'));
});
// используем gulp.watch для автоматической перекомпиляции шаблонов после изменения
gulp.task('watch', function() {
gulp.watch('./**/*.njk', [‘nunjucks’]);
});
// при запуске выполняем компиляцию и начинаем следить за изменениями
gulp.task('default', ['nunjucks', 'watch']);
Мы будем использовать twitter bootstrap в качестве css фреймворка для наших экспериментов, поэтому нужно его добавить в проект (подробнее основы работы с gulp изложены в статье "gulp, npm и bower - враги рутины").
Проинициализируем bower(если это еще не сделано) и установим пакет bootstrap с помощью него:
bower init
bower install bootstrap
Bower подтянет последнюю версию bootstrap, а также все его зависимости (в нашем случае только jquery).
На этом настройка завершена, теперь оформляем следующую структуру директорий для шаблонов:
- /njk - каталог шаблонов nunjucks
- /njk/parts - директория куда будем складывать различные куски страниц
- /njk/layout - директория куда положим общие шаблоны
Nunjucks это весело!
Nunjucks предоставляет нам обширные возможности управления содержимым шаблонов, и даже если чего-то не хватает для конкретного проекта - он оставляет за Вами возможность самостоятельно запрограммировать необходимый функционал.
Возможно, на первый взгляд, это выглядит несколько сложно и запутанно, после использования обычного html, поэтому сейчас остановимся подробнее на матчасти по шаблонам nunjucks.
Блоки и наследование
Блоки представляют собой именованные площадки в шаблоне, которые мы можем заполнять контентом. (Например, {content area} на рисунке отражает блок, который заполняется контентом index page content, или services content)
Наследование используется для расширения одних шаблонов другими. К примеру, мы можем задать общий каркас для всех страниц в одном шаблоне, а затем унаследоваться от него в другом шаблоне и заполнить необходимыми данными с помощью переопределения блоков.
Если сказанное выше пока непонятно, взгляните еще раз на изображение и перечитайте текст.:)
Фильтры
Фильтры - это просто функции, применяемые к переменным или тексту перед выводом. К примеру, с помощью фильтра можно перевести текст в верхний регистр перед его выводом на странице.
Код в шаблоне nunjucks | Результирующий html |
<h1>{{ “This is title” | upper }}</h1> | <h1>THIS IS TITLE</h1> |
Синтаксис
Nunjucks предоставляет нам 3 синтаксические конструкции:
{# Текст комментария #} | Используется для комментирования в шаблонах. Текст заключенный внутри тега не будет отображен в выхлопном html файле |
{{ Переменная_или_текст }} | Используется для применения фильтров и вывода содержимого переменной, либо текста. Фильтры перечисляются с помощью символа “палочка” (pipeline) | фильтр1 | фильтр2 |
{% Управляющая_конструкция %} {% endУправляющая_конструкция %} | Этот тег позволяет добавить в шаблон логику, циклы и прочее. Как правило есть открывающая часть {% block myBlock %} и закрывающая {% endblock %} |
Для отключения обработки управляющих конструкций можно заключить их внутри тега {% raw %}:
{% raw %} {% if true %} {{ hello }} {% endif %} {% endraw %} | {% if true %} {{ hello }} {% endif %} |
На заметку
Почитать подробные инструкции по всем директивам и фильтрам, доступным в nunjucks можно на официальном сайте шаблонизатора, мы же попробуем сверстать несколько страниц с использованием bootstrap, чтобы наглядно убедиться в удобстве применения nunjucks.
Пробуем в деле
Наконец со скучным введением покончено и мы можем приступить непосредственно к разработке!
Итак, я набросал структурный шаблон main.njk (он будет основой для наших страниц):
{% set company = "My random company" %}
{% if not(title) %}
{% set title = "Default title" %}
{% endif %}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css">
</head>
<body>
{% include "../parts/header.njk" %}
<div class="container">
<div class="row">
{% block content %}{% endblock %}
</div>
</div>
{% include "../parts/footer.njk" %}
<script type="text/javascript" src="bower_components/jquery/jquery-2.2.2.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
</body>
</html>
Давайте разберем его по порядку:
{% set company = "My random company" %} - задает переменной company значение “My random company”, в зависимых шаблонах мы будем использовать это значение.
{{ title }} - выводит содержимое переменной title, которую мы будем определять в дочерних шаблонах.
Конструкция {% include “filename” %} позволяет включить один шаблон в другой, как если бы они были определены в одном файле.
Конструкция {% block blockName %}{% endblock %} в контексте базового шаблона задает расположение блока. Можно также указать внутри него какой либо текст, который будет отображаться по умолчанию (если не переопределить блок).
Подвал и шапка
Как видим в шаблон включаются footer.njk и header.njk - подвал и шапка соответственно.
footer.njk - ничего сверхъестественного, просто выводим название компании, определенное в переменной company:
<hr>
<footer>
<div class="container">
<p>Copyright © {{ company }}</p>
<p>All rights reserved.</p>
</div>
</footer>
header.njk - в этом шаблоне будет храниться навигационная панель. Тут мы будем использовать логические блоки для автоматического выставления классов active в элементах меню.
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="index.html">{{ title | upper }}</a>
<button type="button" class="navbar-toggle btn btn-default collapsed " data-toggle="collapse" data-target="#nav-menu">
<i class="glyphicon glyphicon-menu-hamburger"></i>
</button>
</div>
<div id="nav-menu" class="navbar-collapse collapse">
<ul id="" class="nav navbar-nav navbar-right">
<li {% if navActiveItem == 0 %}class="active"{% endif %}>
<a href="index.html">Главная</a>
</li>
<li {% if navActiveItem == 1 %}class="active"{% endif %}>
<a href="feedback.html">Обратная связь</a>
</li>
</ul>
</div>
</div>
</nav>
Блок {% if statement %} content {% endif %} выводит содержимое в случае если statement - возвращает истинное значение. В нашем случае класс active в навигационной панели будет регулироваться значением переменной navActiveItem.
Верстка страниц
Каркас для страниц готов, теперь мы можем набросать нашу главную страницу - index.njk:
{% set navActiveItem = 0 %}
{% set title = "Nunjucks + Gulp!" %}
{% extends "njk/layout/main.njk" %}
{% block content %}
<div class="col-md-12">
<div class="page-header">
<h2>Это главная страница!</h2>
</div>
<div class="page-content">
<p>А здесь можно разместить текст-рыбу!:)</p>
</div>
</div>
{% endblock %}
Мы наследуем index.njk от main.njk, присваиваем объявленным ранее переменным необходимые значения и переопределяем содержимое блока content.
Теперь запустим gulp для компиляции шаблонов. Пока gulp запущен, мы можем изменять содержимое шаблонов, перекомпиляция и обновление страницы в браузере будут осуществляться автоматически.
На заметку
Причина по которой мы запускаем gulp только сейчас в том, что по некоторым причинам функция watch не обрабатывает создание новых файлов, а оперирует только с измененными. Ну чтож и на том спасибо, как говорится.:)
Макросы
Шаблон для страницы “Обратная связь” можно сделать по подобию главной, однако я предлагаю разобраться на его примере с директивой macro.
Дело в том, что мы можем определять макросы, которые затем можно многократно использовать в любых nunjucks шаблонах. В некотором смысле они похожи на миксины в less.
Синтаксис макроса следующий:
{% macro MacroName(param=value) %}
marco content here
{% endmacro %}
Макросы nunjucks обладают фичей, которую я называю - чертовски крутые параметры. Нет, правда, взгляните на эти чудесные примеры определения макросов:
<!-- Макрос с обязательным параметром(скучно):-->
{% macro test1(name) %}
<!-- Макрос с параметром со значением по умолчанию(лучше):-->
{% macro test2(name=’Name’) %}
<!-- Макрос с параметром со значением по умолчанию равным значению другого параметра (взрыв мозга!):-->
{% macro test3(name=’Name’, id=name) %}
Перед использованием макроса, его необходимо импортировать (если он определен в другом файле). Делается это с одним из способов, описанных ниже:
{% import “filename” as Variable %}
{% from “filename” import MacrosName %}
{% from “filename” import MacrosName as Variable %}
Отличие директивы import от from заключается в том, что import импортирует все макросы определенные в файле filename в виде переменной Variable, таким образом вызов этих макросов будет выглядеть так:
{{ Variable.macroName() }}
Директива from импортирует только указанные макросы, причем каждому из них можно назначить псевдоним с помощью оператора as, например:
{% from “form-macros.njk” import Form, TextField as Field %}
Страница “Обратная связь”
Разобравшись в матчасти nunjucks макросов, давайте создадим файл macro.njk в директории njk/, в котором поместим следующий код:
{# Макрос для добавления bootstrap-style элементов формы #}
{% macro form_input(name, value='', title=name, type='text') %}
<div class="form-group">
<label class="form-label" for="{{ name }}_id">{{ title }}</label>
<input class="form-control" id="{{ name }}_id" name="{{ name }}" type="{{ type }}" value="{{ value | escape }}">
</div>
{% endmacro %}
{% macro form_textarea(name, value='', title=name) %}
<div class="form-group">
<label class="form-label" for="{{ name }}_id">{{ title }}</label>
<textarea class="form-control" name="{{ name }}" id="{{ name }}_id" cols="20" rows="5"></textarea>
</div>
{% endmacro %}
{# Макрос для создания формы обратной связи #}
{% macro feedback_form(action="", method="POST") %}
<form class="form" action="{{ action }}" method="{{ method }}">
{{ form_input("Name", '', "Имя") }}
{{ form_input("Email", '', "Email", 'email') }}
{{ form_textarea("Message", '', 'Сообщение') }}
<button type="submit" class="btn btn-primary">Отправить</button>
</form>
{% endmacro %}
ОК! Теперь у нас есть несколько макросов для облегчения создания bootstrap форм в будущем. Теперь посмотрим на шаблон feedback.njk:
{% set navActiveItem = 1 %}
{% set title = "Обратная связь" %}
{% extends "njk/layout/main.njk" %}
{% from "njk/macro.njk" import feedback_form %}
{% block content %}
{{ feedback_form('feedback.php', 'POST') }}
{% endblock %}
Циклы
Отлично! С макросами мы разобрались, но это не все чем нас может порадовать nunjucks. В нем также имеется директива для организации циклов.
Синтаксис циклов в nunjucks, опять же схож с синтаксисом в python:
{% for(i in list) %}
Вывод в цикле
{% endfor %}
list - это либо переменная со списком элементов, либо функция, генерирующая список. Если вы используете nunjucks только для сборки в html, как подразумевает этот туториал, то скорее всего для работы с циклами Вам понадобится только одна функция - range.
Синтаксис range:
range(start, stop, step)
start - первое число,
stop - последнее число,
step - шаг между числами
Давайте рассмотрим их использование на небольшом примере. Скажем мы хотим сверстать страничку для показа списка проектов.
Добавим в macro.njk новый макрос - project - он будет выводить представление проектов на странице:
{% macro project(name, description = '', src=’img/pic.jpg’) %}
<div class="row">
<div class="col-md-2">
<img class="img-responsive" src="{{ src }}" alt="">
</div>
<div class="col-md-10">
<h3>{{ name }}</h3>
<p>{{ description }}</p>
<a href="#" class="btn btn-default">Подробнее</a>
</div>
</div>
{% endmacro %}
Теперь набросаем шаблон projects.njk:
{% set title = "Наши проекты" %}
{% extends "njk/layout/main.njk" %}
{% from "njk/macro.njk" import project %}
{% block content %}
<div class="row">
<div class="col-md-9">
{% for i in range(0, 6) %}
{{ project("Проект №" + i) }}
{# Не добавляем <hr> к последнему проекту #}
{% if not loop.last %}
<hr>
{% endif %}
{% endfor %}
</div>
</div>
{% endblock %}
Здесь используется цикл для отображения шести (range(0, 6)) проектов. Стоит отметить, что внутри цикла создается переменная loop, позволяющая отслеживать выполнение цикла. Ознакомиться с аттрибутами loop можно на сайте nunjucks, мы же ограничимся использованием last, указывающим является ли текущая итерация последней.
На этом предлагаю закончить рассмотрение nunjucks, думаю те, кого я заинтересовал, найдут все остальные нюансы самостоятельно прочитав документацию. Если у Вас возникли вопросы - смело задавайте их в комментариях - попробуем разобраться вместе.