Что такое html препроцессор
HTML препроцессор — это скрипт, который преобразует определенный формат языка разметки в обычный HTML. Зачем же он нужен? Чем не устраивает обычный html? Основная причина — это удобно, это ускоряет процесс верстки сайта.
В большинстве случаев, на каждой странице сайта присутствуют одинаковые блоки (хедер, футер, сайтбар и др). Что бы заменить надпись, например, в хедере, нужно вручную вносить изменения на каждой странице сайта. HTML шаблонизатор позволяет вынести все повторяющееся элементы в один базовый шаблон. При этом изменить надпись в хедере на всем сайте займет пару минут.
Также, представьте, что у вас на странице список из более 10 карточек товара, и нужно в каждой внести изменения. HTML препроцессор позволяет хранить информацию в массиве объектов и выводить список элементов в цикле. Это гораздо удобнее чем менять в разметке HTML.
Основные преимущества использования HTML препроцессоров:
- Использование базовых шаблонов (layout)
- Вставка шаблонов в другие шаблоны (include)
- Переменные
- Циклы
- Миксины
- И многое другое
Список html препроцессоров:
Вот список из наиболее известных HTML препроцессоров:
- Nunjucks
- Pug
- Slim
- HAML
- Macdom
Шаблонизатор nunjucks
По сути, Nunjucks — это мощный шаблонизатор для JavaScript. Его можно использовать как в браузере, на стороне клиента, так и на стороне сервера Node.js. Но также, Nunjucks очень удобно применять для верстки сайтов.
Почему именно Nunjucks?
- Это полноценный язык, в котором есть все, что может понадобиться для frontend разработки
- В отличии от других html препроцессоров, синтаксис в nunjucks не отличается от обычного HTML
Как пользоваться шаблонизатором html nunjucks (gulp nunjucks)
Для этого проще всего использовать сборщик проектов gulp и модуль gulp-nunjucks. Устанавливать их будем с помощью утилиты npm (для этого предварительно нужно установить Node.js себе на компьютер).
Заходим с консоли в папку с проектом и инициализируем npm:
npm i
В результате получим файл package.json, в котором описываются все зависимости и настройки.
Далее устанавливаем gulp и gulp-nunjucks:
npm install -D gulp gulp-nunjucks
Создаем файл gulpfile.js. В нем находятся наборы команд для gulp. Пример простой конфигурации для nunjucks:
const gulp = require('gulp');
const nunjucks = require('gulp-nunjucks');
function njk(){
return gulp.src('src/*.html')
.pipe(nunjucks.compile())
.pipe(gulp.dest('dist'))
};
function watch(){
gulp.watch('src/**/*.html', njk);
}
exports.default = gulp.series(njk, watch);
Чтобы бы не устанавливать gulp глобально, открываем файл package.json, в раздел scripts добавляем строчку:
"gulp": "gulp"
В итоге получаем такой файл package.json:
{
"name": "nunjucks",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"gulp": "gulp",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"gulp": "^4.0.2",
"gulp-nunjucks": "^5.1.0"
}
}
Это простой пример настройки gulp для демонстрации работы nunjucks. Более подробно, как работать с gulp, мы рассмотрим в следующих статьях.
Далее создадим в корне проекта папку src и dist. В src находится исходники nunjucks. Gulp будет отслеживать изменение в этом каталоге и сохранять готовые html файлы в папку dist.
Чтобы запустить gulp, в командной строке пишем:
npm run gulp
Пример работы с nunjucks
И так, мы подготовили окружение, настроили gulp, и можем пробовать nunjucks в деле. Для примера, возьмем простой bootstrap шаблон, состоящий из 3-х страниц, написан на обычном html, и переделаем его на шаблонизаторе nunjucks.
Наследование шаблонов
Nunjucks дает возможность наследовать шаблоны. Другими словами, вы можете создавать страницы сайта по заранее созданным шаблонам. Наследование работает через конструкции block и extends.
Посмотрим на наш пример:
Все страницы имеют общий каркас (верхняя часть, футер, сайтбар) и изменяемую часть. На странице контакты присутствует дополнительный блок в сайтбаре. Также на всех страницах, кроме главной, есть навигационная цепочка (к этому мы вернемся позже).
Создаем в каталоге src папку layouts. В ней создаем файл нашего основного шаблона default.html. Копируем туда все общее, что есть на всех страницах, а вместо изменяемой части пропишем конструкцию block:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home title</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-inverse">
<div class="container">
<a class="navbar-brand" href="">(123) 456-7890</a>
<a class="navbar-brand" href="mail:name@example.com">name@example.com</a>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li class="active"><a href="index.html">Home</a></li>
<li><a href="services.html">Service</a></li>
<li><a href="contact.html">Contact</a></li>
</ul>
</div>
</div>
</nav>
<div class="container">
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">Home</h1>
<ol class="breadcrumb">
<li><a href="index.html">Home</a></li>
<li class="active">Service</li>
</ol>
</div>
</div>
<div class="row">
<div class="col-md-3">
{% block aside %}
<div class="list-group">
<a href="index.html" class="list-group-item active">Home</a>
<a href="services.html" class="list-group-item">Service</a>
<a href="contact.html" class="list-group-item">Contact</a>
</div>
{% endblock %}
</div>
<div class="col-md-9">{% block content %}{% endblock %}</div>
</div>
<footer style="padding: 50px 0; margin-top:20px; border-top: 1px solid #e3e3e3;">
<div class="row">
<div class="col-lg-12">
<p>Copyright © 2021</p>
</div>
</div>
</footer>
</div>
</body>
</html>
И так, мы добавили {% block content %}{% endblock %} в то место, где подставляется основная информация страницы. Также добавили {% block aside %} {% endblock %} для вывода сайтбара, но между этими тегами вставили блок по умолчанию, так как на большинстве страницах он одинаковый.
В папке src создаем файл index.html, в нем наследуем наш базовый шаблон и в блоке content мы можем подставить содержимое страницы сайта:
{% extends "layouts/default.html" %}
{% block content %}
<h1>Hello world!</h1>
{% endblock %}
В итоге получим:
Теперь мы можем изменить меню или футер в одном месте, и изменения применятся на всем сайте.
Создаём все недостающие страницы и подставим актуальную информацию на всех страницах.
Вернемся к странице контакты. Помимо контентной части, в ней необходимо переопределить сайтбар:
{% extends "layouts/default.html" %}
{% block content %}
<!—- Контент страници -->
{% endblock %}
{% block aside %}
<div class="list-group">
<a href="index.html" class="list-group-item active">Home</a>
<a href="services.html" class="list-group-item">Service</a>
<a href="contact.html" class="list-group-item">Contact</a>
</div>
<h3>Contact Details</h3>
<p><abbr title="Phone">Phone</abbr>:<a href="">(123) 456-7890</a></p>
<p><abbr title="Email">Email</abbr>:<a href="mailto:name@example.com">name@example.com</a></p>
{% endblock %}
Включение одного шаблона в другой (nunjucks include)
Nunjucks позволяет вставлять содержимое одного файла в другой. Это дает возможность выносить в отдельные файлы разметку повторяющихся блоков. Такой подход сделает наш код более удобным и простым в поддержке. Вставка одного файла в другой осуществляется конструкцией include:
{% include “filename” %}
Давайте на нашем сайте header, footer и sitebar вынесем в отдельные файлы. В папке src создадим каталог parts, в которой будем сохранять все различные части кода.
Создаем файлы header.html, footer.html и sidebar.html. С основного шаблоны вырезаем необходимые части, а в место них прописываем конструкцию include. В итоге файл default.html будет выглядеть так:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home title</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
{% include 'parts/header.html' %}
<div class="container">
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">Home</h1>
<ol class="breadcrumb">
<li><a href="index.html">Home</a></li>
<li class="active">Service</li>
</ol>
</div>
</div>
<div class="row">
<div class="col-md-3">
{% block aside %}
{% include 'parts/sidebar.html' %}
{% endblock %}
</div>
<div class="col-md-9">{% block content %}{% endblock %}</div>
</div>
{% include 'parts/footer.html' %}
</div>
</body>
</html>
Так же, на странице контакты нам необходимо подключить файл sidebar.html, так как мы переопределяем на ней сайтбар:
{% block aside %}
{% include 'parts/sidebar.html' %}
<h3>Contact Details</h3>
<p><abbr title="Phone">Phone</abbr>:<a href="">{{ data.phone }}</a></p>
<p><abbr title="Email">Email</abbr>:<a href="mailto:{{ data.email }}">{{ data.email }}</a></p>
{% endblock %}
Переменные (nunjucks Variables)
Очень часто возникает ситуация, когда одни и те же данные выводятся в разных местах сайта. Например, контактные данные, заголовки страниц и др. Для этого используют переменные. Они позволяют задавать необходимые значения в начале кода и выводить их в нужных участках кода.
В nunjucks переменные задаются следующей конструкцией:
{% set username = "Ivan" %}
Вывод содержимого переменной:
{{ Ivan }}
Посмотрим на шаблон default.html. В нем title и заголовок страниц жестко прописаны в коде. Это нам не подходит, так как эти значения разные на всех страницах. Поэтому, на каждой странице заведем переменные title и pageName и добавим их в начале страниц:
{% set title = "Home title" %}
{% set pageName = "Home" %}
{% extends "layouts/default.html" %}
...
В шаблоне выведем эти значения:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
{% include 'parts/header.html' %}
<div class="container">
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">{{ pageName }}</h1>
...
В итоге, у каждой страницы свой title и заголовок.
Nunjucks import
Конструкция import позволяет загружать шаблоны и получать доступ к его экспортированным значениям (переменным и миксинам).
Давайте еще посмотрим на наш сайт. В верхней части и в сайтбаре выводится контактная информация:
Ее также удобно сохранять в переменные. Но эти переменные касаются всего сайта, а не отдельных страниц. Поэтому, создадим файл, где будем сохранять глобальные переменные для всего сайта и импортируем его в основном шаблоне. Создаем папку data в src, в ней файл data.html. В самом начале файла default.html прописываем конструкцию import:
{% import "data/data.html" as data %}
Теперь можно получить доступ ко всем значениям в data.html через переменную data.
В data.html добавим 2 переменные:
{% set phone = "(123) 456-7890" %}
{% set email = "name@example.com" %}
Теперь в файлах header.html и contact.html выводим эти переменные:
<nav class="navbar navbar-inverse">
<div class="container">
<a class="navbar-brand" href="">{{ data.phone }}</a>
<a class="navbar-brand" href="mail:{{ data.email }}">{{ data.email }}</a>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li class="active">
<a href="index.html">Home</a>
</li>
<li>
<a href="services.html">Service</a>
</li>
<li>
<a href="contact.html">Contact</a>
</li>
</ul>
</div>
</div>
</nav>
...
{% block aside %}
<div class="list-group">
<a href="index.html" class="list-group-item active">Home</a>
<a href="services.html" class="list-group-item">Service</a>
<a href="contact.html" class="list-group-item">Contact</a>
</div>
<h3>Contact Details</h3>
<p><abbr title="Phone">Phone</abbr>:<a href="">{{ data.phone }}</a></p>
<p><abbr title="Email">Email</abbr>:<a href="mailto:{{ data.email }}">{{ data.email }}</a></p>
{% endblock %}
Условия (Nunjucks if)
В nunjucks, как и в других шаблонизаторах, есть условный оператор if. If проверяет условия и позволяет выборочно отображать содержимое:
{% if variable %}
Показываем контент
{% endif %}
Также есть возможность указать альтернативные условия с помощью конструкций elseif и else:
{% if page == ‘home’ %}
Контент для главной страницы
{% elseif page==’contcat’ %}
Контент для страницы контакты
{% else %}
Контент для остальных страниц
{% endif %}
Вернемся к тестовому сайту. Как мы помним, навигационная цепочка не должны выводится на главной странице. Давайте это исправим.
На каждой странице добавим еще по одной переменной:
Главная страница:
{% set pageSlug = "home" %}
Страница услуг:
{% set pageSlug = "service" %}
Страница контактов:
{% set pageSlug = "contact" %}
Этими переменными мы задаем уникальное название (ярлык) для каждой страницы.
Далее, в основном шаблоне задаем условие для вывода навигационной цепочки:
...
{% if pageSlug != 'home' %}
<ol class="breadcrumb">
<li><a href="index.html">Home</a></li>
<li class="active">Service</li>
</ol>
{% endif %}
...
Если переменная pageSlug не равна home, то мы выводим блок с контентом. Таким образом, мы выводим хлебные крошки на всех страницах, кроме главной.
Тернарный оператор (If Expression)
Тернарный оператор — конструкция, которая возвращает второе или третье значение, в зависимости от логического выражения. Нечто подобное есть и в nunjucks:
{{ "home" if page == ’home’ else "default" }}
Если условие истинно, то выведется home, в противном случае default.
На нашем тестовом сайте в навигациях не определяется активная страница. Заходим в файл header.html, находим место где выводится меню и применим тернарный оператор:
...
<ul class="nav navbar-nav navbar-right">
<li class="{{ 'active' if pageSlug == 'home' }}"><a href="index.html">Home</a></li>
<li class="{{ 'active' if pageSlug == 'service' }}"><a href="services.html">Service</a></li>
<li class="{{ 'active' if pageSlug == 'contact' }}"><a href="contact.html">Contact</a></li>
</ul>
...
Тоже самое делаем в файле sidebar.html:
<div class="list-group">
<a href="index.html" class="list-group-item {{ 'active' if pageSlug == 'home' }}">Home</a>
<a href="services.html" class="list-group-item {{ 'active' if pageSlug == 'service' }}">Service</a>
<a href="contact.html" class="list-group-item {{ 'active' if pageSlug == 'contact' }}">Contact</a>
</div>
Циклы
Циклы — это операция, при которой одно и то же действие выполняется несколько раз. В nunjucks так же есть конструкция для организации циклов:
{% set users = [
{
'name': 'Борис',
'surname': 'Иващенко'
},
{
'name': 'Эрик',
'surname': 'Николаев'
}
] %}
{% for(item in users) %}
<p>Name: {{ item.name}}. Surname: {{item.surname }} </p>
{% endfor %}
В приведенном примере перечислены все пользователи, используя атрибуты name и surname каждого элемента в массиве users.
- Внутри цикла есть доступ к нескольким переменным:
- loop.index — текущая итерация цикла, начиная с 1
- loop.index0 — текущая итерация цикла, начиная с 0
- loop.revindex — количество итераций до конца, начиная с 1
- loop.revindex0 — количество итераций до конца, начиная с 0
- loop.first — логическое значение, указывающее первую итерацию
- loop.last — логическое значение, указывающее последнюю итерацию
- loop.length — общее количество элементов
Посмотрим как использовать циклы на примере. Вернемся к навигационной цепочке, сейчас она задана жестко в шаблоне и выводиться не корректно. Давай это исправим.
На страницах, где необходимо выводить хлебные крошки, зададим массив с необходимыми элементами:
Страница услуг:
{% set breadcrumbs = [
{
'name': 'Home',
'url': 'index.html'
},
{
'name': 'Service',
'url': 'service.html'
}
] %}
Страница контакты:
{% set breadcrumbs = [
{
'name': 'Home',
'url': 'index.html'
},
{
'name': 'Contact',
'url': 'contact.html'
}
] %}
В основном шаблоне выведем элементы навигационной цепочки:
...
{% if pageSlug != 'home' %}
<ol class="breadcrumb">
{% for item in breadcrumbs %}
{% if loop.last %}
<li class="active">{{ item.name }}</li>
{% else %}
<li><a href="{{ item.url }}">{{ item.name }}</a></li>
{% endif %}
{% endfor %}
</ol>
{% endif %}
...
В цикле мы проверяем, если элемент массива последний, то выводим его как активный.
Так же, давайте выведем наше меню в цикле. Для этого в файле data.html создадим массив с элементами навигации:
{% set menu = [
{
'title': 'Home',
'url': 'index.html',
'slug': 'home'
},
{
'title': 'Service',
'url': 'services.html',
'slug': 'service'
},
{
'title': 'Contact',
'url': 'contact.html',
'slug': 'contact'
}
] %}
Выведем меню в файле header.html:
...
<ul class="nav navbar-nav navbar-right">
{% for item in data.menu %}
<li class="{{ 'active' if pageSlug == item.slug }}"><a href="{{item.url}}">{{item.title}}</a></li>
{% endfor %}
</ul>
...
Аналогично в файле sidebar.html:
<div class="list-group">
{% for item in data.menu %}
<a href="{{item.url}}" class="list-group-item {{ 'active' if pageSlug == item.slug }}">{{item.title}}</a>
{% endfor %}
</div>
Теперь элементы меню легко изменять, добавлять новые пункты навигации и изменения применятся на всем сайте, на всех страницах.
Макросы (macro)
Макросы позволяют определить фрагменты кода, которые можно использовать многократно в любом шаблоне. Это похоже на функции в языках программирования. Пример синтаксиса макроса:
{% macro User(name, isAdmin=false) %}
Пользователь {{name}} — {{‘администратор’ if isAdmin else ‘не администратор’}}
{% endmacro %}
Теперь макрос с примера можно вызвать неограниченное количество раз:
{{ User(‘Иван’) }}
{{ User(‘Николай’, true) }}
В первом варианте мы передаем один параметр, а значение isAdmin задана по умолчанию, во втором варианте мы передаем два аргумента.
Рассмотрим работу с макросами на примере. На главной странице и странице сервисы есть одинаковый блок со списком элементов, но разным содержимым:
Выведем этот блок с помощью макроса. Создаем в папке src каталог macro. В нем файл macro.html, в котором будем хранить все наши макросы. Как и в случае с файлом переменных, импортируем его в основной шаблон:
{% import "macro/macro.html" as macro %}
Далее создаем наш макрос, назовем его ItemList, в который передается массив с необходимыми данными:
{% macro ItemList(data) %}
{% for item in data %}
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h4><i class="fa fa-fw fa-check"></i>{{ item.title }}</h4>
</div>
<div class="panel-body">
<p>{{ item.text }}</p>
<a href="{{ item.url }}" class="btn btn-default">Learn More</a>
</div>
</div>
</div>
{% endfor %}
{% endmacro %}
После этого вызываем макрос в нужных местах, передавая в него необходимые значения. Файл index.html:
{% set title = "Home title" %}
{% set pageName = "Home" %}
{% set pageSlug = "home" %}
{% extends "layouts/default.html" %}
{% block content %}
<div class="row">
<div class="col-lg-12">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer sit amet egestas sapien, nec ornare diam. Maecenas non arcu libero. Mauris eu leo efficitur, vehicula elit vitae, vulputate mauris. Vestibulum libero mi, iaculis et lorem id, tempus vehicula metus. Nullam ultrices nibh sem, id fermentum mi ullamcorper sit amet. Duis luctus sagittis metus, quis auctor libero semper non.</p>
<p>Etiam congue pharetra lorem in vulputate. Pellentesque pulvinar ligula vel fringilla consequat. Donec nec nisi elit. Vivamus varius est vel sodales consequat. Praesent et scelerisque eros.</p>
</div>
{{ macro.ItemList([
{
'title': 'Advantages #1',
'text': 'Advantages #1 text',
'url': 'Advantages_#1_url'
},
{
'title': 'Advantages #2',
'text': 'Advantages #2 text',
'url': 'Advantages_#2_url'
},
{
'title': 'Advantages #3',
'text': 'Advantages #3 text',
'url': 'Advantages_#3_url'
}
]) }}
</div>
{% endblock %}
Файл service.html:
{% set title = "Service title" %}
{% set pageName = "Service" %}
{% set pageSlug = "service" %}
{% set breadcrumbs = [
{
'name': 'Home',
'url': 'index.html'
},
{
'name': 'Service',
'url': 'service.html'
}
] %}
{% extends "layouts/default.html" %}
{% block content %}
<div class="row">
<div class="col-lg-12">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Soluta, et temporibus, facere perferendis veniam beatae non debitis, numquam blanditiis necessitatibus vel mollitia dolorum laudantium, voluptate dolores iure maxime ducimus fugit.</p>
<p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Quasi, provident! Sed laboriosam deleniti eius. Consequatur corporis enim quisquam, maxime unde impedit dolorem voluptatem nulla saepe sint vero et omnis. Voluptatum necessitatibus autem cum officiis quam porro error molestias cumque laudantium.</p>
</div>
<div class="col-lg-12">
<h2 class="page-header">Service List</h2>
</div>
{{ macro.ItemList([
{
'title': 'Service #1',
'text': 'Service #1 text',
'url': 'Service_#1_url'
},
{
'title': 'Service #2',
'text': 'Service #2 text',
'url': 'Service_#2_url'
},
{
'title': 'Service #3',
'text': 'Service #3 text',
'url': 'Service_#3_url'
}
]) }}
</div>
{% endblock %}
Теперь у нас есть макрос, который можно вызывать неограниченное количество раз, передавая нужные данные.
Фильтры (Nunjucks filters)
Фильтры — это конструкция, с помощью которой можно выполнять разного рода манипуляции над передаваемыми данными. В nunjucks есть большое количество встроенных фильтров, их можно посмотреть на официальной документации.
На пример, фильтр abs возвращает абсолютное значение аргумента:
{{ -3|abs }}
Вернет 3
Фильтры можно выстраивать в цепочки для последовательной обработки данных:
{{ data | filter_1 | filter_2 … | filter_n }}
Пример использования фильтра посмотрим на нашем сайте. Ранее мы создали переменную phone и присвоили ей значение (123) 456-7890. Далее необходимо вывести телефон ссылкой в таком формате:
<a href=”1234567890”>(123) 456-7890</a>
То есть, вырезать с переменной все лишние символы и оставить только числа. Для этого применим фильтр replace:
<a href="tel:{{ data.phone | replace('(', '') | replace(')', '') | replace(' ', '') | replace('-', '') }}">{{ data.phone }}</a>
Выводим эту конструкцию в файлах header.html:
<a class="navbar-brand" href="tel:{{ data.phone | replace('(', '') | replace(')', '') | replace(' ', '') | replace('-', '') }}">{{ data.phone }}</a>
и conatc.html:
<p><abbr title="Phone">Phone</abbr>:<a href="tel:{{ data.phone | replace('(', '') | replace(')', '') | replace(' ', '') | replace('-', '') }}">{{ data.phone }}</a></p>
Выводы
Мы рассмотрели основные моменты использования шаблонизатора nunjucks. Для примеры создали небольшой сайт на этом препроцессоре. Готовый результат можно посмотреть в репозиторие. Для более подробного изучения можно воспользоваться официальной документацией по nunjucks.