Navigation without rebooting using expressjs, jade and History.js

  • Tutorial
I have never been able to use in my work such an HTML5 feature as the History API. And then the hour has come, to figure it out and conduct a small experiment. I decided to share the result of this experiment with you.

And so what we want:
- Website Navigation with history api
- Retrieves data from a server in a json object, followed by the renderer on the client
- When a direct transition renderer should occur on the server
- Whatever it was quick and easy

with circles to identify the needs, Now let's decide on the technologies:
- Expressjs will work on the server under nodejs
- As a jade template engine
- For the client History.js

Server

For those who have never worked with nodejs, you should first install it. You can see how to do this quickly under Ubuntu here . Create a folder for the project and go to it. Next, install the necessary modules:
npm i express jade

And create two directories:
- view - here the templates will lie
- public - there will be static content

Next, we will write the server and focus only on the main points.
The first thing I wanted to make my life easier was not to think about how the ajax request came to us or not. To do this, we will intercept the standard res.render
The code
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 is overloaded, now we can safely call res.render (data) or res.render ('view name', data) in our controllers , and the server itself will either render or return json to the client, depending on the type of request.

Let's look at the code again, and I'll try to explain why we need the prefix '_' to templates in case of “rendering on the server”.
The problem is as follows. There are no layouts in jade, instead of them blocks are used, blocks can expand, replace or complement each other (all this is well described in the documentation ).

Consider an example.
Suppose we have this mapping structure:
option a
layout.jade
!!! 5
html
	head
		title Page title
	body
		#content
			block content


index.jade
extends layout
block content
	hello world


If we now render index.jade, then it will render together with layout.jade. This does not cause problems until we want to export index.jade to the client and render it there, but without layout.jade. Therefore, I decided to add another template that would allow this to be done easily and simply.

option B
layout.jade
!!! 5
html
	head
		title Page title
	body
		#content
			block content


_index.jade
extends layout
block content
	include index


index.jade
hello world


Now if we want to render a block with a layout, then we will render the _index.jade file, if we do not need layout, then index.jade will be rendered. This method seemed to me the most simple and understandable. If you adhere to the rule that only templates with the prefix "_" extend layout.jade, then you can safely export everything else to the client. (There are undoubtedly other ways to do this, you can tell about them in the comments, it will be interesting to know)

The next point I will focus on is the export of templates to the client. To do this, we write a function that will receive the path to the template relative to the viewdir at the input, and the compiled function reduced to the string will be returned to the output.
The code
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();	
}


Now let's write a controller that will collect javascript file with templates.
The code
(Please do not pay attention to the fact that everything with your hands, this is just an experiment, of course, in a real project it’s not worth doing)
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);
});


Now when the client requests / template, in response he will receive the following object:
var view = {
		'имя шаблона': <функция>
	};

And on the client to render the desired template, it will be enough to call view ['template name'] (data);

Finish considering the server side, because everything else is not particularly relevant and is not directly related to our task. Moreover, the code can be found here .

Client

Since we export already compiled templates to the client, we do not need to connect the template engine itself, just connect it to runtime and do not forget to load our templates by connecting them as a regular javascript file.

The next library from the list is History.js , the name of which speaks for itself. I chose the version only for html5 browsers, these are all modern browsers, although the library can work in older browsers via hash url.

There is very little client code left.
First, we write the render () function . It is quite simple and renders the given template into the content block.
var render = (function () {
	return function (view, data) {
		$('#content').html(views[view](data));
	}
}());


Now the code initializing work with History.js
The code
$(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);
	}
});


The code is quite simple. The first thing we do is to see if the browser supports history api. If not, then do not change anything and the client works the old fashioned way.
And if it supports, we intercept all clicks on a , send an ajax request to the server.

Do not forget to hang up the “statechange” event handler, at this moment we need to redraw our content block and add initialization of the initial state, I decided to store it in the body tag , the data-init attribute , here the initial values ​​are written when rendering on the server.
The line data.state = JSON.stringify ({data: data, view: view.join ('.')}); in replaceRender function That

's all.

Working example here(If he dies, then the habroeffect has covered him :)) You
can see the code here

Also popular now: