Навигация без перезагрузки используя expressjs, jade и History.js

от автора

Мне ранее не доводилось использовать в своей работе такую возможность 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’ы, в место них используются блоки, блоки могут расширять, заменять или дополнять друг друга (все это хорошо описано в документации).

Рассмотрим пример.
Предположим у нас есть вот такая структура отображений:

вариант А

layout.jade

!!! 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. Поэтому я решил добавить еще один шаблон, который бы позволял это делать легко и просто.

вариант Б

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

Вот собственно и все.

Рабочий пример тут (Если умрет, значит хаброэффект его накрыл :))
Код можно посмотреть тут

ссылка на оригинал статьи http://habrahabr.ru/post/161943/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *