S.Tominoff

Fullstack JavaScript разработчик

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 &copy; {{ 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, думаю те, кого я заинтересовал, найдут все остальные нюансы самостоятельно прочитав документацию. Если у Вас возникли вопросы - смело задавайте их в комментариях - попробуем разобраться вместе.