Every day the sites are becoming more and more complex. It is not enough just to “revive” the interface - it is increasingly necessary to create a full-fledged one-page application. A striking example of such an application is any web-mail (for example,
Mail.Ru ), where links follow not to reload the page, but only to change the presentation. This means that the task of obtaining data and displaying them, depending on the route, which has always been the prerogative of the server, falls on the client. Usually, this problem is solved with the help of a simple router, based on regular expressions, and is not further developed, while the back-end of this topic is given much more attention. In this article I will try to fill this gap.
What is routing?
This is probably the most underrated part of a javascript application:]
')
On the server, routing is the process of determining the route within the application, depending on the request. Simply put, it is a search for the controller at the requested URL and the execution of appropriate actions.
Consider the following task: you need to create a one-page application "Gallery", which will consist of three screens:
- Home - choice of direction in painting
- View gallery - display pictures with page navigation and the ability to change the number of items on the page
- Detailed view of the selected work
Schematically, the application will look like this:
<div id="app"> <div id="app-index" style="display: none">...</div> <div id="app-gallery" style="display: none">...</div> <div id="app-artwork" style="display: none">...</div> </div>
Each screen will have its own URL, and the router describing them may look like this:
var Router = { routes: { "/": "indexPage", "/gallery/:tag/": "galleryPage", "/gallery/:tag/:perPage/": "galleryPage", "/gallery/:tag/:perPage/page/:page/": "galleryPage", "/artwork/:id/": "artworkPage", } };
In the `routes` object, the following routes are set: the key is the path template, and the value is the name of the controller function.
Next, you need to convert the keys of the object `Router.routes` into regular expressions. To do this, define the `Router.init` method:
var Router = { routes: { }, init: function (){ this._routes = []; for( var route in this.routes ){ var methodName = this.routes[route]; this._routes.push({ pattern: new RegExp('^'+route.replace(/:\w+/, '(\\w+)')+'$'), callback: this[methodName] }); } } };
It remains to describe the navigation method that will search for the route and call the controller:
var Router = { routes: { }, init: function (){ }, nav: function (path){ var i = this._routes.length; while( i-- ){ var args = path.match(this._routes[i].pattern); if( args ){ this._routes[i].callback.apply(this, args.slice(1)); } } } };
When everything is ready, we initialize the router and set the starting point of the navigation. It is important not to forget to intercept the `click` event from all links and redirect to the router.
Router.init(); Router.nav("/");
As you can see, nothing complicated; I think many people are familiar with this approach. Typically, all the differences in implementations are reduced to the format of the recording route and its transformation into a regular expression.
Let's return to our example. The only thing that is missing in it is the implementation of the functions responsible for processing the route. Typically, they are collecting data and drawing, for example, like this:
var Router = { routes: { }, init: function (){ }, nav: function (url){ }, indexPage: function (){ ManagerView.set("index"); }, galleryPage: function (tag, perPage, page){ var query = { tag: tag, page: page, perPage: perPage }; api.find(query, function (items){ ManagerView.set("gallery", items); }); }, artworkPage: function (id){ api.findById(id, function (item){ ManagerView.set("artwork", item); }); } };
At first glance, it looks good, but there are pitfalls. Data acquisition occurs asynchronously, and with fast movement between routes you can get quite different from what was expected. For example, consider the following situation: the user clicked on the link of the second page of the gallery, but in the process of loading he became interested in the work from the first page and decided to see it in detail. As a result, it took two requests. They can work out in random order, and the user instead of the picture will get the second page of the gallery.
This problem can be solved in different ways; everyone chooses his own way. For example, you can call `abort` for the previous request, or transfer the logic to` ManagerView.set`.
What does `ManagerView` do? The `set (name, data)` method takes two parameters: the name of the “screen” and “data” for its construction. In our case, the task is greatly simplified, and the `set` method displays the desired element by id. It uses the view name as a postfix `" app - "+ name`, and the data - to build html. Also `ManagerView` must remember the name of the previous screen and determine when the route began / changed / ended in order to correctly manipulate the appearance.
So we created a one-page application, with our `Router` and` ManagerView`, but it will take time, and we will need to add new functionality. For example, the section "Articles", where there will be descriptions of "works" and links to them. When you go to view "work" you need to build a link "Back to the article" or "Back to the gallery", depending on where the user came from. But how to do that? Neither `ManagerView`, nor` Router` have similar data.
There is also one more important point - these are links. Page navigation, links to sections, etc., how to "build" them? "Sew" right into the code? Create a function that will return the URL by mnemonics and parameters? The first option is completely bad, the second is better, but not perfect. From my point of view, the best option is the ability to set the `id` route and method that allows you to get the URL by ID and parameters. This is good because the route and the rule for the formation of the URL are the same, and this option does not lead to duplication of the logic for obtaining the URL.
As you can see, such a router does not solve the tasks, so, in order not to invent a bicycle, I went to a search engine, creating a list of requirements for an ideal (in my understanding) router:
- the most flexible syntax of the route description (for example, as in Express)
- work with the query, and not just with individual parameters (as in the example)
- “start”, “change” and “end” events of the route (/ gallery / cubism / -> / gallery / cubism / 12 / page / 2 -> / artwork / 123 /)
- possibility of assigning multiple handlers to one route
- the possibility of assigning ID routes and navigating them
- another way of interaction `data ← → view` (if possible)
As you may have guessed, I did not find what I wanted, although there were some very decent solutions, such as:
- Crossroads.js - very powerful work with routes
- Path.js - is the implementation of the events of the beginning and end of the route, 1KB (Closure compiler + gzipped)
- Router.js - simple and functional, only 443 bytes (Closure compiler + gzipped)
Pilot
And now it's time to do the same thing, but using Pilot. It consists of three parts:
- Pilot - the router itself
- Pilot.Route - route controller
- Pilot.View - advanced route controller, inherits Pilot.Route
We define the controllers responsible for the routes. The HTML structure of the application remains the same as in the example in the first part of the article.
Switching between routes involves changing screens, so in the example I use Pilot.View. In addition to working with DOM elements, an instance of its class is initially subscribed to routestart and routeend events. With these events, Pilot.View controls the display of the DOM element associated with it, exposing it `display: none` or removing it. The node itself is assigned via the `el` property.
There are three types of events: routestart, routechange, and routeend. They are caused by the router to the controller (s). Schematically it looks like this:
There are three routes and their controllers:
"/" -- pages.index "/gallery/:page?" -- pages.gallery "/artwork/:id/" -- pages.artwork
Each route can have multiple URLs. If the new URL matches the current route, then the router generates a routechage event. If the route has changed, then its controller receives the routeend event, and the new controller receives the routestart event.
"/" -- pages.index.routestart "/gallery/" -- pages.index.routeend, pages.gallery.routestart "/gallery/2/" -- pages.gallery.routechange "/gallery/3/" -- pages.gallery.routechange "/artwork/123/" -- pages.artwork.routestart, pages.gallery.routeend
In addition to changing the container's visibility (`this.el`), as a rule, you need to update its contents. To do this, Pilot.View has the following methods that need to be redefined depending on the task:
template (data) is a template method within which HTML is generated. The example uses the data obtained in loadData.
loadData (req) is perhaps the most important controller method. Called every time the URL changes, receives the request object as a parameter. It has a feature: if you return $ .Deferred, the router will not go to this URL until the data has been collected.
req - request { url: "http://domain.ru/gallery/cubism/20/page/3", path: "/gallery/cubism/20/page/123", search: "", query: {}, params: { tag: "cubism", perPage: 20, page: 123 }, referrer: "http://domain.ru/gallery/cubism/20/page/2" }
onRoute (evt, req) - auxiliary event. Called after routestart or routechange. The example is used to update the contents of the container by calling the render method.
render () is a method for updating the HTML container (`this.el`). Calls this.template (this.getData ()).
It now remains to build the application. For this we need a router:
var GuideRouter = Pilot.extend({ init: function (){
First of all, we create a router and define routes in the `init` method. The route is set by the `route` method. It takes three arguments: the route id, pattern, and controller.
The syntax of the route, I will not dissemble, borrowed from Express. He came up on all counts, and those who have already worked with
Express , will be easier. The only thing is added groups; they allow you to more flexibly customize the pattern of the route and help you navigate by id.
Consider the route responsible for the gallery:
It turned out very convenient: the route and the URL is the same. This avoids explicit URLs in the code and the need to create additional methods for the generated URLs. Guide.go (id, params) is used to navigate to the desired route.
The last action creates a GuideRouter instance with options to intercept links and use the History API. By default, Pilot works with location.hash, but it is possible to use history.pushState. To do this, set Pilot.pushState = true. But, if the browser does not support location.hash or history.pushState, then to fully support the History API, you need to use a polyfill, or any other suitable library. When implementing, you will have to override two methods - Pilot.getLocation () and Pilot.setLocation (req).
Here in general, that's all. The remaining features can be found in the documentation.
Waiting for your questions, issue and any other returns:]
useful links
-
Example (
app.js )
-
Documentation-
Sources-
jquery.leaks.js (
jQuery.cache monitoring
utility )