Browser History support in Liferay 6.1

Liferay 6.1 will have one more significant improvement because we just added History support to trunk. It will allow JavaScript developers to restore the normal browser behavior and the user will be able to navigate through pages as usual - by pressing Back/Forward buttons.

The issues

For those, who are not aware why is that important, let me explain it with a few words. Normally, the Web is driven by request/response. The browser sends a request to the server and it responds with an answer. The user may request a few pages and she can always return to the previous one by pressing Back button of her browser. Correspondingly, she can press Forward button and navigate to the last page again. This is the normal browser's behavior and people feel comfortable with it.

When Ajax became popular in 2005, more and more applications started to use JavaScript and they no longer reloaded the full page, but only parts of it. This was an awesome improvement, but one of its downsides was that it broke the built in History support in the browser. In fact, people were no longer able to navigate through the history states as they normally would. For that reason the developers tried to restore the normal behavior by adding state entries to the history by using hashes. Let's suppose the following request:
http://liferay.com/manage?p_p_id=123&param_one=1#param_two=2&param_three=3

Now, if JavaScript is enabled, everything on client side will be fine - the application can read these parameters after the # and restore the state accordingly. The problem is that server is unaware of these parameters, because fragment identifiers (the parts after the #) are not sent to the server with http requests. For that reason, the server is not able to provide the required content and that led to more issues. Developers usually managed that situation by sending an Ajax request as soon as the page was loaded in order to retrieve the required content. As you may have guessed, that leads to a bad user experience - the page initially loads with some content (or without any content at all) and almost immediately it is being replaced because the server just returned the response to the Ajax request.

Browsers tried to help developers and as result a new event has been introduced - 'hashchange' (now defined in HTML5 specification). But what about browsers that don't support this event? We can achieve that by using an iframe. In order to simplify the whole picture, I will skip the details, but you can just remember that this is not a trivial task. And the worst part is that this approach works only if JavaScript is enabled in the browser.

How HTML5 resolves these issues

With HTML5 everything gets much better. It added some significant improvements, one of the most important being that when adding history state, the developer may change the URL in the address bar too.
The HTML5 specification defined the following interface:

 

interface History {

    readonly attribute long length;

    readonly attribute any state;

    void go(in optional long delta);

    void back();

    void forward();

    void pushState(in any data, in DOMString title, in optional DOMString url);

    void replaceState(in any data, in DOMString title, in optional DOMString url);

};

 

In the browser environment (you know that the browser is not the only environment where JavaScript may run, don't you?) we can retrieve the implementation of this interface from window.history. Then we can invoke 'back()' or 'forward()' functions and navigate the user through the history. With pushState we can add history entries. Currently, the second argument (title) is ignored by some browsers - for example in Firefox 4/5 and Opera 11.50. If the developer provides the third parameter (please note that it is optional), the browser will fully replace the URL with the provided one. That is huge, because we can easily turn this request:

http://liferay.com/manage?p_p_id=123&param_one=1#param_two=2&param_three=3

into:

http://liferay.com/manage?p_p_id=123&param_one=1&param_two=2&param_three=3

See the difference? Now we have 'normal' parameters and server may provide the required content on page load without any further requests! And that will always work, regardless of JavaScript enabled/disabled status in the browser.

Imagine this scenario: let's say that your users are navigating your site, and taking advantage of the faster response of Ajax, but that they want to share a link with their friends over Twitter. Then let's say that Google sees that link and decides to crawl it. Because that URL is a full link to the content of your site, Google can index your content without problem. This also means that people without JavaScript enabled will have the same access to content as people with JavaScript enabled.

Now compare that with the old approach – by using hashes. If you have link like this:

http://liferay.com/manage?p_p_id=123&param_one=1#param_two=2&param_three=3

you are in trouble. Why? Because, as you know, Google crawlers don't understand JavaScript and fragment identifiers (the parts after the #) are not sent to the server with http requests. Google cannot just follow this link. That won't work properly.

In order to resolve this, Google suggested a specification - “an agreement between web servers and search engine crawlers that allows for dynamically created content to be visible to crawlers”. Basically, if you replace “#” with “#!” and make your links to look like this:
http://liferay.com/manage?p_p_id=123&param_one=1#!param_two=2&param_three=3

then Google will send the following request to your server:

http://liferay.com/manage?p_p_id=123&param_one=1&_escaped_fragment_=param_two=2%26param_three=3

Now, you must return HTML snapshot of the corresponding #! URL and it must contain the same content as the dynamically created page. In some cases that might be not so easy task and you may even duplicate your work.

Liferay History module

Enough theory, back to the real world. How have we implemented this in Liferay and how can portlet developers reap the benefits?

First of all, as you know, we are using Alloy, great UI meta-framework, built on top on YUI3. In YUI3 we already have the History module, which does the most of the hard work for us. Let me explain what we have added on top on it.

There were some situations we had to resolve. For example - what should we do if the user loads URL, which contains hashes, in a browser that supports the new history interface? Should we leave these parameters as they are? Or we should convert them? Also, what should we do when the next browser version introduces HTML5 History support? Opera did that in version 11.50, released recently. Well, if you paste a URL that contains parameters after the #, Liferay's History module will convert them automatically to 'normal' parameters, so you can update your bookmarks after that. Also, it will add them to its initial state, so later, when a developer requests a value, it will be provided successfully!

In general, the Liferay History module will try its best to provide you with the required values. This means that if developer tries to get a value from History and it is not found, the module will check for it in the 'normal' parameters too. For example, let's suppose you have AlloyUI Paginator on your page and on load you would like to set its 'page' and 'rowsPerPage' configuration properties. In this case you can ask History module for their current values and it will transparently provide them from History - either from the parameters after the # or from the 'normal' parameters!

How Liferay History module deals with old browsers

Now the second question - what should we do with old browsers? What will happen if a user pastes a URL, generated in HTML5 browser in an HTML4 one and starts working? In this case we will add these parameters after the # and once you get back to HTML5 browser, History module will convert them back to 'normal' parameters, taking in consideration the importance. For example, let's suppose that the user pastes this URL in IE7:

http://liferay.com/manage?p_p_id=123&param_one=1

She starts working and over time she gets the following URL:

http://liferay.com/manage?p_p_id=123&param_one=1#param_one=2

In this case the parameter has been overwritten. Now, if she were to switch back to an HTML5 browser and loads the same URL there, the Liferay History module will convert it to:

http://liferay.com/manage?p_p_id=123&param_one=2

Usage and further improvements

We just added this module to trunk, so only two portlets are currently using it - the TagsAdmin portlet and the new Document Library. Before our 6.1 release, we are working to optimize it as much as possible.

Feedback

This module is experimental, so we would be extremely happy to get your feedback!

Credits

Nate Cavanaugh, who helped me during the implementation of the module and edited this article too.

Blogues