📜 ⬆️ ⬇️

Navigation without rebooting using expressjs, jade and History.js

I have never had the opportunity to use the HTML5 feature like the History API in my work. And then came the time to figure it out and conduct a small experiment. The result of this experiment, I decided to share with you.

And so what we want:
- Site navigation using history api
- Receive data from the server in the form of a json object, followed by rendering on the client
- With a direct transition, the render should occur on the server
- To make everything easy and simple

We have decided on the range of needs, now we will define the technologies:
- The server will work expressjs under nodejs
- as a jade template
- For client History.js

Server

For those who have never worked with nodejs, it's worth installing it first. How to do it quickly under Ubuntu can be found here . Create a folder for the project and go into it. Next, install the necessary modules:
npm i express jade
')
And create two directories:
- view - there will be templates
- public - there will be static content

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

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

Consider an example.
Suppose we have the following 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, it will be rendered along with layout.jade. This is not a problem unless we want to export index.jade to the client and render it there, but without layout.jade. So I decided to add another template that would allow it 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 the block with the layout, we will render the _index.jade file, if we do not need the layout, then we need to render index.jade. Such a method seemed to me the most simple and understandable. If you stick to the rule that only templates with the "_" prefix expand layout.jade, 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’ll 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 output will return the compiled function reduced to the string.
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 we will write a controller that will collect javascript file with templates.
Code
(I ask you not to pay attention to the fact that everything is handled, this is just an experiment, in a real project, of course, you should not do this)
 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 consider the server part, because everything else is not particularly relevant and is not directly related to our task. Especially the code can be viewed here .

Customer

Since we export already compiled templates to the client, we do not need to connect the template itself, it is enough to connect its 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 , whose name speaks for itself. I chose the version only for html5 browsers, these are all modern browsers, although the library can work in older browsers through the url hash.

There are very few client code left.
The first is the render () function. It is quite simple and renders the specified template in the content block.
 var render = (function () { return function (view, data) { $('#content').html(views[view](data)); } }()); 


Now the code initializing work with History.js
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 see if the browser supports history api. If not, we change nothing and the client works in the old manner.
And if it supports, we intercept all the clicks on a , send an AYAX request to the server.

We do not forget to attach 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, the initial values ​​are written here when rendering on the server.
Line data.state = JSON.stringify ({data: data, view: view.join ('.')}); in the replaceRender function

That's all.

The working example is here (If it dies, it means it has covered its effect :))
Code can be found here

Source: https://habr.com/ru/post/161943/


All Articles