Мне ранее не доводилось использовать в своей работе такую возможность HTML5 как History API. И вот настал тот час, разобраться в этом и провести небольшой эксперимент. Результатом этого эксперимента я решил поделиться с Вами.
И так что мы хотим:
— Навигация по сайту с использованием history api
— Получения данных с сервера в виде json объекта с последующим рендером на клиенте
— При прямом переходе рендер должен происходить на сервере
— Что бы все было легко и просто
С кругом потребностей определились, теперь определимся с технологиями:
— На сервере будет трудиться expressjs под nodejs
— В качестве шаблонитизатора jade
— Для клиента History.js
Сервер
Для тех кто никогда не работал с nodejs для начала стоит ее установить. Как это сделать быстро под Ubuntu можно посмотреть тут. Создадим себе папку для проекта и перейдем в нее. Далее установим необходимые модули:
npm i express jade
И создадим две директории:
— view — тут будут лежать шаблоны
— public — тут будет статичный контент
Далее напишем сервер и остановимся лишь на основных моментах.
Первое чем я хотел себе облегчить жизнь, это не задумываться о том как пришел к нам запрос по ajax или нет. Для этого мы перехватим стандартный res.render
app.all('*', function replaceRender(req, res, next) {
var render = res.render,
view = req.path.length > 1 ? req.path.substr(1).split('/'): [];
res.render = function(v, o) {
var data;
res.render = render;
//тут мы должны учесть что первым аргументом может придти
//имя шаблона
if ('string' === typeof v) {
if (/^/.+/.test(v)) {
view = v.substr(1).split('/');
} else {
view = view.concat(v.split('/'));
}
data = o;
} else {
data = v;
}
//в res.locals располагаются дополнительные данные для рендринга
//Например такие как заголовок страницs (res.locals.title)
data = merge(data || {}, res.locals);
if (req.xhr) {
//Если это аякс то отправляем json
res.json({ data: data, view: view.join('.') });
} else {
//Если это не аякс, то сохраняем текущее
//состояние (понадобиться для инициализации history api)
data.state = JSON.stringify({ data: data, view: view.join('.') });
//И добавляем префикс к шаблону. Далее я расскажу для чего он нужен.
view[view.length - 1] = '_' + view[view.length - 1];
//Собственно сам рендер
res.render(view.join('/'), data);
}
};
next();
});
res.render перегрузили, теперь мы можем спокойно вызывать в наших контроллерах res.render(data) или res.render('view name', data), и сервер сам либо отрендрит либо вернет json на клиента в зависимости от типа запроса.
Посмотрим на код еще раз, а я попробую объяснить зачем нужен префикс '_' к шаблонам в случае «рендринга на сервере».
Проблема заключается в следующем. В jade отсутствуют layout'ы, в место них используются блоки, блоки могут расширять, заменять или дополнять друг друга (все это хорошо описано в документации).
Рассмотрим пример.
Предположим у нас есть вот такая структура отображений:
!!! 5
html
head
title Page title
body
#content
block content
index.jade
extends layout
block content
hello world
Если мы сейчас отрендрим index.jade то он отрендриться вместе с layout.jade. Это не доставляет проблем до тех пор пока мы не хотим экспортировать index.jade на клиента и рендрить его там, но уже без layout.jade. Поэтому я решил добавить еще один шаблон, который бы позволял это делать легко и просто.
!!! 5
html
head
title Page title
body
#content
block content
_index.jade
extends layout
block content
include index
index.jade
hello world
Теперь если мы хотим отрендрить блок с layout'ом, то мы рендрим файл _index.jade, если нам не нужен layout, то рендрим index.jade. Мне показался такой способ наиболее простым и понятным. Если придерживаться правила что только шаблоны с префиксом "_" расширяют layout.jade то можно безболезненно экспортировать все остальное на клиента. (Несомненно есть и другие способы сделать такое, можете рассказать о них в комментариях, будет интересно узнать)
Следующий момент на котором я остановлюсь, это экспорт шаблонов на клиента. Для этого напишем функцию которая будет на вход получать путь к шаблону относительно viewdir, а на выход будет возвращать скомпилированную функцию приведенную к строке.
function loadTemplate(viewpath) {
var fpath = app.get('views') + viewpath,
str = fs.readFileSync(fpath, 'utf8');
viewOptions.filename = fpath;
viewOptions.client = true;
return jade.compile(str, viewOptions).toString();
}
Теперь напишем контроллер который будет собирать javascript файл с шаблонами.
app.get('/templates', function(req, res) {
var str = 'var views = { '
+ '"index": (function(){ return ' + loadTemplate('/index.jade') + ' }()),'
+ '"users.index": (function(){ return ' + loadTemplate('/users/index.jade') + ' }()),'
+ '"users.profile": (function(){ return ' + loadTemplate('/users/profile.jade') + ' }()),'
+ '"errors.error": (function(){ return ' + loadTemplate('/errors/error.jade') + ' }()),'
+ '"errors.notfound": (function(){ return ' + loadTemplate('/errors/notfound.jade') + ' }())'
+ '};'
res.set({ 'Content-type': 'text/javascript' }).send(str);
});
Теперь когда клиент запросит /template, в ответ он получит такой объект:
var view = {
'имя шаблона': <функция>
};
И на клиенте что бы отрендрить нужный шаблон, достаточно будет вызвать view['имя шаблона'](data);
Закончим рассматривать серверную часть, т.к. все остальное особо к делу не относится и на прямую не связано с нашей задачей. Тем более код можно посмотреть тут.
Клиент
Так как мы экспортируем на клиента уже скомпилированные шаблоны, нам нет нужды подключать сам шаблонитизатор, достаточно подключить его runtime и не забываем подгружать наши шаблоны, подключив их как обычный javascript файл.
Следующая библиотека из списка это History.js, название которой говорит само за себя. Я выбрал версию только для html5 браузеров, это все современные браузеры, хотя библиотека может работать в старых браузерах через url hash.
Осталось совсем немного клиентского кода.
Первое напишем функцию render(). Она достаточно простая и выполняет рендер заданного шаблона в блок content.
var render = (function () {
return function (view, data) {
$('#content').html(views[view](data));
}
}());
Теперь код инициализирующий работу с History.js
$(function () {
var initState;
if (History.enabled) {
$('a').live('click', function () {
var el = $(this),
href = el.attr('href');
$.get(href, function(result) {
History.pushState(result, result.data.title, href);
}, 'json');
return false;
});
History.Adapter.bind(window,'statechange', function() {
var state = History.getState(),
obj = state.data;
render(obj.view, obj.data);
});
//init
initState = $('body').data('init');
History.replaceState(initState, initState.data.title, location.pathname + location.search);
}
});
Код достаточно простой. Первое что мы делаем, это смотрим поддерживает ли браузер history api. Если нет, то ничего не меняем и клиент работает по старинке.
А если поддерживает, мы перехватываем все клики по a, посылаем аякс запрос на сервер.
Не забываем навесить обработчик события «statechange», в этот момент нам нужно перерисовывать наш content блок, и добавить инициализацию начального состояния, я решил хранить его в теге body, атрибут data-init, сюда пишутся начальные значения при рендере на сервере.
Строчка data.state = JSON.stringify({ data: data, view: view.join('.') }); в функции replaceRender
Вот собственно и все.
Рабочий пример тут (Если умрет, значит хаброэффект его накрыл :))
Код можно посмотреть тут
Автор: zxcabs