Устали ждать пока пересобирется Jade (Pug) при сборке front-end’а? Есть решение.

Некоторое время назад в компании White Label, в которую я только пришел, для проектов уже существовала система сборки для frontend’а. На популярном таск-менеджере Gulp.

Для удобной работы c HTML использовался Jade, ныне Pug. Хотя при интеграции с backend в качестве шаблонизатора выходит Twig и PHP (см. Примечание 1).

Gulp таск для Jade был самый банальный, другого, по крайней мере, не находил. Примерно такой:

gulp.task('jade', function(cb) {
return gulp.src('<src path>/*.jade')
.pipe(changed(<dest path>, { extension: '.html' }))) // gulp-changed
.pipe(jadeInherit({ basedir: <src path> })) // gulp-jade-inheritance
.pipe(jade({ pretty: true })) // gulp-jade
.pipe(gulp.dest(<dest path>))
});

По ходу разработки, количество шаблонов может возрасти, а значит время на то, чтобы скомпилировать их из Jade в HTML, тоже возрастет. К примеру, 30 шаблонов будут собираться примерно 7 секунд.

Для решения этой проблемы в интернете предлагают комбинировать два Gulp плагина gulp-changed и gulp-jade-inheritance. Первый смотрит был ли изменен файл, второй определяет, где конкретный файл используется, в контексте Jade.

Эффекта, лично я, не увидел.

Я предположил, что что-то в таске идет не так, но не стал долго копать, так как показалось, что вся проблема в самом Jade и заменил его на Twig. Есть соответствующий плагин — gulp-twig (обертка над Twig.js). Скорость слегка прибавилась, но проблема, что пересобирается все, осталась.

При обдумывании решения меня осенило — почему бы не поднять локальный сервер, где, по запросу на статику, мне будет отдаваться конкретный шаблон. Тогда надобность запускать целый таск для бессмысленной сборки шаблона отпадет.

В сборщике уже использовался Browser Sync, который, по сути, выполнял мои требования: обращаешься к HTML файлу (https://localhost:3000/index.html) и он выдается, если такой есть на диске. Но, к сожалению, он умеет работать только со статикой. Поэтому нужно было, что-то покруче — Express, например. А чтобы не велосипедить с live-reload и socket’ами Browser Sync тоже пригодился как proxy-сервер.

Шепотка документаций, прогулка по stackoverflow, депрессия из-за изучения исходников, тестирование и вуаля — получилось то, что хотел. Работаем только с тем шаблоном, который нужен.

По сути, все осталось как прежде. Таск, который собирает HTML и кладет в нужную папку, никуда не делся и также работает. Просто при разработке мы не запускаем его.

Решения подходит также для Jade и для других шаблонизаторов, у которых есть JS реализация.


Note: далее я начну использовать новое название Jade - Pug, так как теперь официальным считается именно он.

Далее прилагаю ход действий на примере Pug, заголовок статьи же вещает о нем (как использовать с Gulp будет в конце).

Давайте обусловимся, что у нас следующая структура:

src
--templates
----pages
-------partials
---------header.pug
-------home.pug
----index.pug
public
--assets
-------styles.css
-------scripts.js
--static - скомпилированные шаблоны
-----pages
-------home.html
-----index.html

Файл src/templates/pages/home.pug:

<!DOCTYPE html>
html(lang="en")
head
meta(charset="UTF-8")
title Document
body
include partials/header
h1 Hello World!

Файл src/templates/index.pug:

<!DOCTYPE html>
html(lang="en")
head
meta(charset="UTF-8")
title Document
body
ol
li
a(href="/static/pages/home.html") Главная страница
  1. устанавливаем Express и Browser Sync:
npm install express browser-sync --save-dev

2. создаем в корне проекта файлик server.js (или любое другое название);

3. устанавливаем Pug:

npm install pug --save-dev

4. открываем server.js в любимом редакторе и копируем следующее:

var express = require('express');
var app = express();
var fs = require('fs');
// отключаем кеширования
app.disable('view cache');
// указываем какой шаблонизатор использовать
app.set('view engine', 'pug');
// расположение шаблонов ('src/templates')
app.set('views', './');
// путь до наших стилей, картинок, скриптов и т.д.
app.use('/assets', express.static('./public/assets/**/*'));
// роутинг на наши страницы
app.get('/static/*.*', function(req, res) {
// регулярка для получения пути до шаблона
var fileName = req.url.replace(/static\/|\..*$/g, '') || 'index';
  res.render('src/templates/' + fileName, {}); (1)
});
// редирект на главную страницу
app.get('/static', function(req, res) {
res.redirect('/static/index.html');
});
var listener    = app.listen();
var port = listener.address().port;
var browserSync = require('browser-sync').create();
// proxy на локальный сервер на Express
browserSync.init({
proxy: 'http://localhost:' + port,
startPath: '/static/',
notify: false,
tunnel: false,
host: 'localhost',
port: port,
logPrefix: 'Proxy to localhost:' + port,
});
// обновляем страницу, если обновились assets файлы
browserSync.watch('./public/assets/**/*').on('change', browserSync.reload);
// обновляем страницу, если был изменен исходник шаблона
browserSync.watch('./src/templates/**/*').on('change', browserSync.reload);

(1) вторым аргументом можно передать объект с данными, которые Вы хотите использовать в шаблоне, например:

{
title: ' ... ',
foo: function( ... ) {
...
}
}

5. запускаем:

node server.js

В браузере откроется хост с нужным портом.

Чтобы закрыть используйте сочетание Ctrl + C (применимо для всех платформ).


Для Twig это выглядело бы так:

var express = require('express');
var app = express();
var fs = require('fs');
var Twig = require('twig');
Twig.cache(false);
// Если хотите использовать какую-то функцию в шаблоне
// Twig.extendFunction(<навзание>, function(<аргументы>) { ... });
app.set('view engine', 'twig');
app.set('twig options', {
base: './', (1)
strict_variables: false (2)
});
app.set('views', './');
app.use('/assets', express.static('./public/assets/**/*'));
app.get('/static/*.*', function(req, res) {
var fileName = req.url.replace(/static\/|\..*$/g, '') || 'index';
res.render('src/templates/' + fileName);
});
app.get('/static', function(req, res) {
res.redirect('/static/index.html');
});
var listener    = app.listen();
var port = listener.address().port;
var browserSync = require('browser-sync').create();
browserSync.init({
proxy: 'http://localhost:' + port,
startPath: '/static/',
notify: false,
tunnel: false,
host: 'localhost',
port: port,
logPrefix: 'Proxy to localhost:' + port,
});
browserSync.watch('./public/assets/**/*').on('change', browserSync.reload);browserSync.watch('./src/templates/**/*').on('change', browserSync.reload);

(1) директория от которой будут искаться шаблоны при include и extend

(2) честно, сам не знаю для чего это


Интеграция с Gulp

Все очень просто:

  1. устанавливаем плагин (см. Примечание 2):
npm install gulp-pug --save-dev

2. создаем Gulp таск (см. Примечание 3):

gulp.task('server', function() {
// Содержимое файла server.js
});

3. запускаем (см. Примечание 3):

gulp server watch

Если нам нужно собрать все, то все еще можно запустить таск, который за это у Вас отвечает, к примеру:

gulp pug

Ну вот, как-то так.

Если у Вас появились вопросы, то можно задать их мне в твиттере.


Примечание:

  1. Логично использовать тот синтаксис, который используется при интеграции с backend. Если Jade, то Jade, если HTML-подобный, то HTML-подобный. Все очень просто. Так будет легче и интегрировать, и править баги. Вы должно быть видели какой HTML Jade выплевывает. С таким кошмаром приходится работать backend’рам при интеграции. Каждый раз выкашивать header, footer и массу другого лишнего контента, чтобы найти нужный блок, когда как, если бы была одна система, взять его из исходников.
  2. Устанавливать само ядро шаблонизатора нет необходимости, так как такие Gulp плагины “gulp-<название технологии>“ тянут их сами как зависимости.
  3. Лично я считаю, что watch должен отвечать только за слежение и запуск тасков, но не за обновление страницы, поэтому последнее вынесено в таск server.