This article introduces and documents some of the key concepts that need to be understood for working with Liferay Single-Page Application (SPA). Please see Senna.js documentation for more information.
Single-Page Application
Even modest improvements in latency can have measurable impact on usage. The most obvious metric is load time. This is influenced by many factors: DNS lookup, network speed, how many resources need to be loaded before the page is visible (i.e., stylesheets, javascript, images, fonts), etc.
Sites will often optimize and minify stylesheets, combine images into a single sprite, defer loading of javascript, and serve static files from a CDN. All in order to speed up load time. This is great, however, even with cached resources the browser still has to re-parse and execute the CSS and JS on every page load, and it still has to lay out the HTML and redraw the UI. This slows down the actual navigation, but can also add perceived slowness and often introduces a white flash.
When Tim Berners Lee invented web he was looking for a system to publish scientific documents remotely, hyperlinks and static pages only. Creating a webapp can get very slow with all the static web born rules. In order to improve actual and perceived latency many sites are moving to SPA model. That is, once the initial page is loaded all subsequent navigations are handled without full page reload. Additional content is loaded using XMLHttpRequest
via History API, which is able to update the URL without refreshing the page, therefore your dynamic site can be shared and bookmarked. Having a URL for each state of your page allows the content to be fully crawled by search engines.
Features
The Liferay platform's GUI is built on top of the AlloyUI toolkit. Liferay needed to do something to bring the single-page application experience to the Liferay portal, then the AlloyUI Surface module was born: a blazing-fast single page application engine that provides several low-level APIs that allows you to build modern web-based applications.
The module was found to be so useful and game-changing that it was isolated from AlloyUI to create a spin-off project, Senna.
In order to create a single page application with good perceived latency and good user experience, the SPA engine must handle the native browser behavior in many aspects, for instance:
- SEO & Bookmarkability: Sharing or bookmarking a link should always display the same content. Sending a link to a friend should get them where we were. More than that, search engines are able to index that same content.
- Hybrid Rendering: Ajax + server-side rendering allows disable pushState at any time, allowing progressive enhancement. The way you render the server side doesn't matter, it can be simple HTML fragments or even template views.
- State Retention: Scrolling, reloading or navigating through the history of the page should get back to where it was.
- UI Feedback: When some content is requested, it indicates to the user that something is happening.
- Pending Navigations: Block UI rendering until data is loaded, then displays the content at once. It's important to give some UX feedback during loading.
- Timeout Detection: Timeout if the request takes too long to load or when navigate to a different link while another request is pending.
- History Navigation: By using History API you can manipulate the browser history, so you can use browser's back & forward buttons.
- Cacheable Screens: Once you load a certain surface this content is cached in memory and is retrieved later on without any additional request. This can speed up a lot your application.
- Page Resources Management: Evaluate scripts and stylesheets from dynamic loaded resources. Additional content loaded using XMLHttpRequest can be appended to the DOM, for security reasons some browsers won't evaluate <script> tags from the new fragment. Therefore, the SPA engine should handle extracting scripts from the content and parsing them, respecting the browser contract for loading scripts.
Not only is the list of features needed for a good SPA huge, but at the moment, History API presents many cross-browser inconsistencies that can get pretty complicated to be handled without an SPA engine.
Desktops
To facilitate your SPA needs, Liferay created the concept of desktops.
When the desktop navigation is enabled all Nested Portlets that aren't inside any other portlet creates a desktop, which shares some aspects with page, like grid / template.
Routes, Screens and Surfaces
- Route: A tuple of path and a route handler function.
- Screen: A class that handles routing for given routes, extends A.Screen.
- Surface: A section of a page that is updated on navigation.
Liferay SPA comes prepared with some screens, like the RenderURLScreen, that abstracts / handles some Liferay portlet logic. They're usually built on top of the HTMLScreen, which is a screen that resolves routes to server-side requests using Ajax (or from cache, if available).
Examples
1.) Adding a Route
Surface.app.addScreenRoutes({ path: /\?section=/, screen: Liferay.Surface.RenderURLScreen });
A screen route can be added anywhere given that it's loaded after Liferay.Surface.app
is created. If, for some reason, your screen route is only to be used in a specific theme it may be a good idea to add ii as a theme's YUI-loaded module. Otherwise, adding it as an adidas-web hook YUI-loaded module is more appropriate.
2.) HTMLScreen without Cache
Sometimes the easiest way to create a screen is by extending an existing one.
Surface.CachelessHTMLScreen = A.Component.create({ ATTRS: { cacheable: { value: false } }, EXTENDS: Surface.HTMLScreen, NAME: 'cachelessHTMLScreen' });
A screen that resolves a route 100% client-side:
Surface.QueryStringScreen = Y.Base.create('queryStringScreen', A.Screen, [], { getSurfaceContent: function(surfaceId) { switch (surfaceId) { case 'header': return 'querystring1'; case 'body': return 'querystring2'; } } });
Tip: To see more examples and understand the inner works of Surface, use its tests as a guide.
3.) Surface
Surfaces are areas that get updated whenever a screen resolves a route. For the example above we're updating the header and body screens.
The contract is id-based like in
<div id="header"> <div id="header-defaultScreen"> Something else. </div> </div> <p>This won't change.</p> <div id="body"> <div id="body-defaultScreen"> Something else. </div> </div>
When the navigation happens the content will be replaced with querystring1
for the header and querystring2
for the body divs.
The original content should be wrapped inside an inner div with the id <parent>-defaultScreen for Liferay SPA to update it. For AlloyUI, it is originally only <parent>-default.
When you enable SPA for a portlet a surface is already created for it automatically so you don't need to add it yourself.
Uploading Files With the SPA Engine
For uploading files we need Multipart form data. We've added LiferaySurfaceFormScreen
in webs/adidas-web/docroot/custom_jsps/html/js/liferay/surface_form.js
. This is a special surface screen that works out of the box with Liferay forms.
Remember to use cache invalidation afterwards if appropriate.
Note: You may decide to always invalidate cache after a surface form submission, at the cost of degraded user experience. If decided to do so, call the cache invalidation method just before navigating on the _defaultSubmitFn
method of Liferay.Form (see hook in webs/adidas-web/docroot/custom_jsps/html/js/liferay/form.js
).
Cache Invalidation
In some circumstances it is important to invalidate the cache to guarantee that your users are going to load fresh content. Call Liferay.Surface.clearCache()
before navigating after such changes. Most create / update / delete actions make this necessary.
Compatibility with Other SPA Engines
Liferay SPA was designed so that it would not interfere with other SPA engines being used at the same time. If a route is not routed by the engine, it won't do anything. However, for easier development and greater consistency we recommend the consistent use of Liferay SPA.
Templates
A template is a grid system that can be used inside pages, nested portlets and desktops. Some templates are available by default as part of the Liferay portal. Others can be installed as a plugin.
To create a new one:
- cd to the
layouttpl
directory on the plugins repository ./create.sh hello-world "Hello World"
- open the new generated
hello-world-layouttpl/docroot
dir - create a 120x120 icon called
hello_world.png
with a thumbnail for the template - edit the
docroot/hello-world.tpl
file - copy changes to
docroot/hellow-rold.wap.tp
l - deploy the template with
ant deploy
on its root directory
Tip: Use an existing template as a base.
Enabling Desktops and SPA
adidas-web
To enable the desktops and SPA, install the adidas-web web plugin to hook the portal.
cd liferay-plugins-ee/webs/adidas-web
ant deploy
The keys desktop.navigation.enabled
and javascript.single.page.application.enabled
control the SPA engine and the desktop active states. You can find them at webs/adidas-web/docroot/WEB-INF/src/portal.properties
.
Themes
Creating a new theme with desktop support
cd liferay-plugins-ee/themes
`./create.sh hello-world "Hello World"
- Add
javascript.single.page.desktop.navigation.enabled=true
to your<theme>/docroot/WEB-INF/classes/portal.properties
and disable the freeform templatewithnested.portlets.layout.template.unsupported=freeform
- Enable desktop navigation on your
<theme>/docroot/WEB-INF/liferay-look-and-feel.xml
like below:
<?xml version="1.0"?> <!DOCTYPE look-and-feel PUBLIC "-//Liferay//DTD Look and Feel 6.2.0//EN" "http://www.liferay.com/dtd/liferay-look-and-feel_6_2_0.dtd"> <look-and-feel> <compatibility> <version>6.2.0+</version> </compatibility> <theme id="adidas" name="Adidas"> <settings> <setting key="desktop.navigation.enabled" value="true" /> </settings> <color-scheme id="01" name="Basic"> <default-cs>true</default-cs> <css-class>adidas-basic</css-class> </color-scheme> <color-scheme id="02" name="NavigationMenu"> <css-class>adidas-navigation</css-class> </color-scheme> </theme> </look-and-feel>
Portlets
You need to add an init-param
to your portlet's <portlet>/docroot/WEB-INF/portlet.xml
to make it compatible with SPA. Don't forget to add any necessary routing and screens.
<name>single-page-application-enabled</name> <value>true</value> </init-param>
Additional Information
- Liferay Migration
- Senna (the demos are very useful for understanding how SPA works)
- Understanding the Java Portlet Specification 2.0 (JSR 286)