5 Single Page Application (SPA) Best Practices
Customers have high expectations for client application experiences. Much has been written in the debate between native vs HTML5 applications in recent years. One thing that is certain native mobile applications have raised the expectations bar. Other factors that also contribute to increased expectations include ubiquitous high-speed broadband at home, the office and now on our mobile devices with LTE availability. Users want applications that load and respond quickly. Once loaded they expect snappy responses, smooth transitions and engaging interactions.
Many believe HTML5 does not posses the ability to meet these requirements. Using traditional web site architecture it could never achieve these levels. HTML5 adds many features, and is still growing, developers can leverage to build modern rich web apps that rival and in some cases exceed native app capabilities.
One of the hottest web architecture trends today is Single Page Apps or SPA. This architecture eliminates the traditional request response model the classic web was built upon. Instead it requires only a single page be retrieved from the server. Subsequent views (analogous to traditional pages) are brought in and out of view as needed. The SPA architecture relies heavily upon AJAX requests to pass JSON objects to and from the server to the client to merge with views that present data.
The benefits of a SPA include much less network activity, faster responses to user interactions, smaller server loads. It also makes it possible to use fancy view transitions, which give the appearance of a native application.
SPAs are a very new technique and often leave developers confused and looking for best practices. Traditional techniques and architectures no longer apply to web development and need to be replaced with a new way of thinking as well as learning how to use JavaScript and HTML5 features effectively.
Client-Side Routing
Around 6 or 7 years ago the Model View Controller (MVC) architecture went more mainstream and with it came the concept of routing. Not that routing in and of itself has anything to do with MVC, but about the same time search engine friendly URLs gained more traction. Combing to two trends the concept of server-side routing developed. To me, as ASP.NET developer, this meant I could define routes to trigger controllers that would ultimately generate the desired markup and send it on its way to the client.
A SPA is client-heavy and this responsibility must be move to the browser. There are many frameworks available around the web to help with this task and I have built one myself. There are a couple of features a SPA route manager must have:
- Be Simple to Implement
- Handle Route Parameters
I have experimented with defining routes in various ways, hard coding a JavaScript object in my application, extending objects at startup to represent all views and a few other common techniques. I found all of them to ultimately be flawed whether it was the long term maintenance or performance concerns. Ultimately I settled on parsing some data- attributes defined in the view's main element. Let's look at the article view for this Blog, the actual view you are using right now:
12345678 |
|
I want to call your attention to the last data- attribute, data-path. Its value defines the route to display this view and give you the article you wanted. First is what I call the main hook, 'article'. That is followed by a parameter, ':slug'. Notice how each part is split by a \. This is a delimiter my engine uses to parse the route to see if the requested route actually exists. If it does not my engine routes you to a 404 view, otherwise you proceed and display the view and initiate the callback method defined in the data-callback attribute.
Once the route engine identifies the route it parses it for any parameters. In this example the slug is a parameter that acts as the key to query the database for the article's content. This part is ultimately passed to my application's loadArticleView method, which retrieves the JSON either from the server or localStorage and ultimately merges it with the article template. We will learn more about those techniques later.
I did sort of skip a step in the process. When my site if first loaded my SPA architecture does several things. It looks for any new views, parses the data- attributes to create localStorage entries for each one and builds the routing table. This table is used as different views are requested by the routing engine. It's just a piece to the process that makes the Single Page Application architecture work.
Markup Management
Traditionally web developers have relied on the server to generate any dynamic markup a page request needs. This generally does not apply for a single page application, except for the initial page request. Instead a SPA relies more on creating views in the client as they are needed.
A user's session with a web app has to start somewhere, usually the home page. Depending on your application you may need to retrieve data and send it to the browser from the server, but generally this is not done in a SPA. Instead you would return a page's layout sans content. This scares many developers, but they need to realize the content will be added shortly on the client.
One notable exception to this rule is when search engine spiders may visit. In this scenario you do want to provide the initial content so they can consume and parse it accordingly. Unfortunately this is one of the primary areas of debate around SPAs because search engines want the exact same content served to their spiders as a visitor would see. Typically search engine spiders do not execute a page's JavaScript (but my understanding this is slowly changing), which means it would not see a page's content. This could be devastating for search engine placement. So make sure you provide a page's content in this situation.
Google has defined guidelines to help webmasters still have their sites spidered and properly indexed, https://support.google.com/webmasters/answer/174992?hl=en.
If search engines are not a concern then return the initial page without content. The initial page's payload also contains the markup for the application's remaining views. This includes markup layouts for each view and potential script templates used to build a pages content on demand.
For smaller applications it is fine to include the entire application's markup. For larger applications like those found in the enterprise you may consider loading additional views and templates asynchronously once the initial page has been loaded. Now the application's markup has been loaded it can reference each view's markup.
Be sure to hide non-visible views. This can be done with CSS rules. It is a good idea to apply a common CSS rule to each view's main element and an additional rule to indicate the current view. These view indicate if the view is displayed or hidden. Of course you could define other layout specific rules at your discretion, but as far as the SPA architecture is concerned a view is either hidden or visible.
123456 |
|
You also want to make sure there is only one view visible at a time. A SPA will swap views in JavaScript, here you can remove and add the above CSS rules as needed. The following is the simplest view transition as it just adds and removes the CSS rules.
12 |
|
Swapping the classes is not good enough. You will also want to make sure the new view is placed after the existing displayed view in the DOM. The following simple method to perform a view swap, using the above CSS class name technique as well as making sure the new view is placed after the existing view.
12345678 |
|
These are some simple examples of how to manage a SPA's markup. For many production quality applications you will want to improve upon these examples. You can extend these concepts to integrate deferred content loading, offloading markup and templates to localStorage to be retrieved on demand as well as more sophisticated JavaScript.
Data Retrieval And Storage
After the web application's markup has been loaded by the browser the real fun begins with data retrieval. As I mentioned earlier the initial page may or may not have been supplied with data by the server. Either way you will need a way for the application to display the data and corresponding markup on demand. This means leveraging AJAX, JavaScript templates and in many cases web storage.
Data should be retrieved using an AJAX call. Generally you will want to consume data in a JSON (JavaScript Object Notation, http://www.json.org/) format. JSON is a very simple data format that is really a nice compromise between overly verbose XML and far too simple comma separate formats. It has the structure so you can create nested data structures, but does not include the descriptive overhead that makes parsing XML slow and complicated.
There are many JavaScript libraries available to make AJAX calls to the server. jQuery is probably the most popular. The following is an example of a jQuery $.ajax() call:
123456789 |
|
The dataType is set to 'json' which tells the server end point why data format you are expecting. If you are using ASP.NET Web API this will automatically be handled for you. If you are using some other server technology you may need to check with its documentation on how to return JSON formatted data.
If you are calling an endpoint on a 3rd party domain then you will probably need to set the dataType to 'jsonp'. This is a common hack to make cross-domain GET calls. As modern browsers become common you can start relying on CORS, which is generally a more secure way to make cross-domain calls. Joel Cochoran details how to work with Web API and CORS, http://dotnet.dzone.com/articles/cors-ajax-and-post-data-web.
Once data has been retrieved you need a method to merge it with markup and inject it into the DOM. This can be done with a JavaScript template library like Handlebars, JSRender, the jQuery templates, etc. Personally I am using Handlebars, http://handlebarsjs.com, these days.
Template libraries make is possible to merge a JSON object with a template to produce markup combined with content. The following is a simple Handlebars template, it merges an array of movie object to produce a list of movies.
12345678 |
|
The merged markup can now be added inside it's parent element using the parent's innerHTML property. You could also use any of the jQuery DOM manipulation functions like appendTo, before and after. The important point is you have a viable way to create the necessary DOM based on the actual data and render it to the user in the correct place.
Using WebStorage To Persist Data
Modern browsers all support web storage such as localStorage and sessionStorage. Internet Explorer, Firefox and Chrome support IndexDB, a light document database in the browser. Safari supports the deprecated WebSQL standard. localStorage and sessionStorage typically offer 5MB of client-side storage in a fast hash table mechanism. IndexDB offers up to 50MB of storage with more advanced querying capabilities. A study recently performed by the Financial Times developer team discovered Internet Explorer 10 gives you up to 500MB of IndexDB and 10MB of localStorage, ample for any modern application to sprawl as will, http://labs.ft.com/2012/06/text-re-encoding-for-optimising-storage-capacity-in-the-browser/.
Using these storage technologies as a persistent storage for AJAX calls reduces the amount of server-round trips a web application makes over its lifetime. Studies have shown around 95% of the data retrieved via AJAX or to build a page has already been retrieved by the client since the data was last updated. This means there are a lot of wasted trips back and forth to the server. This makes the client application slower and adds unnecessary burden on the server infrastructure.
An easy way to persist data to localStorage using jQuery AJAX is the localStorage AjaxPrefilter written by Paul Irish, http://bit.ly/117p7E9. The preFilter checks to see if the requested data is already locally stored before making the call to the server. If it is stored it returns the cached data and cancels the server call. If the data does not exist or is stale it will call the server and cache the response for future requests.
Caching data is a great way to speed up an application, but you can also cache markup in localStorage. This is a technique originally made popular by Bing and Google, http://bit.ly/117puyn. This technique involves persisting the markup and templates needed to build application views in web storage. The view markup and templates are retrieved as needed to build each view on demand. This reduces the size of the application's DOM, which means you will avoid common performance issues created for larger SPA applications with many views and templates. It also reduces the amount of memory needed to run your application in the browser, which on mobile devices is very crucial.
It is a good idea to combine the markup caching with Application cache or offline mode, but don't rely on it as your only caching source. The reason Google and Bing started using this technique was to get around some of the issues with Application cache, http://bit.ly/117q27A. While application cache is also useful to make SPA applications perform better, it can be intrusive to the end user. By persisting markup in storage you can get around some of these limitations. This is also a good technique to use with native applications because it gets around the need to go through a native app store approval process to push updates to clients.
Deep Linking and History Management
A Single Page application can work just fine on a single URL, but this is not a good idea practice even for the simplest web applications. Since hyper linking was first there has been a hash or url fragment available. Typically it is designated with the #. Back in the Mosaic days the url fragment was used to designate what part of the page to display. Typically an anchor tag would be in the markup with a name property, like this:
I used this technique with several research papers I published when I was in graduation school. I would place a table of contents in the top portion of my paper with links to the corresponding section. This saved the user from scrolling down the page to find the section they cared about. You can still see this technique in use with the W3C standards, for example http://dev.w3.org/html5/webstorage/#the-localstorage-attribute.
The History management API, http://www.whatwg.org/specs/web-apps/current-work/multipage/history.html#the-history-interface, is also supported by modern browsers. However I have not really found this useful for Single Page Apps. When using the web storage concepts described earlier it just adds unnecessary complexity to the application architecture. I have found it better to write a custom history management layer, if needed for my SPA applications, but the techniques I have already described meet the majority of cases I have found so far. You can still use the history members to navigate backwards and forwards, but the state management pieces add no value. If you do not want to use the localStorage technique then the history state management methods can be very helpful.
You do want to take advantage of url fragments to trigger view changes and provide deep linking to views. When the hash fragment changes the window triggers the 'hashchange' event. Binding to this event gives you the hook you need to trigger the appropriate methods to swap the view and retrieve and merge data to produce the complete target view. It also provides a unique url the user can bookmark and share with other users.
When it comes to consumer sites that rely on search engine rankings, there is not a single technique to manage these deep links. Google has published guidelines, http://bit.ly/117sTgL, but other search engines like Bing have not jumped on board with this technique at this time. The reason why the url fragment needs some help for searching engines is the url sent to the server when the initial page is requested does not pass the url fragment to the server. If it did then the whole single page app concept could not work because each page request would be sent to the server. The only time there is a problem using url fragments is with search engine spiders since they do not execute the JavaScript necessary to dynamically build a view. When a human visits the site the page can easily be dynamically built. Google's compromise is a good start toward a unified answer to the search engine spider issue.
Conclusion
Single page applications are an exciting new web application technique. When properly built web developers can build native like client application experiences for users. In many cases this eliminates the need to build platform specific client applications resulting in a fragmented, expensive and hard to maintain code base. Single Page architecture is new and therefor does not have many great technical references available. But many, like myself have begun pioneering this technique in many production applications and are rapidly building a solid set of best practices based on our real-world experiences. This article highlighted several SPA best practices that should help you get started on the the path to rich client application success.