permalink

109

Building Single Page Applications With jQuery’s Best Friends

Hey guys. Today we’re going to take a look at how you can build a single-page application (SPA) supporting graceful degradation using some of jQuery’s Best friends; DocumentCloud’s Backbone.js, Underscore.js, LAB.js and jQuery templating to name but a few.

Alex Sexton has been giving a great presentation highlighting these tools in few recent conferences and I thought it would be useful to write up a tutorial to compliment their use.

The SPA application we’ll be building today is a three-level bookmarkable image gallery. Before we get started, please feel free to check out the demo below or download the sources for the tutorial.

 

 

Tools Used For Today’s Tutorial

 

What’s A Single-Page Application (SPA)?

 

SPAs are web applications or websites which persistently run in the same page without requiring a reload for further navigation. All of the code needed for your initial load in these applications is driven by either local data or data retrieved from a web server on demand, such as any additional data required from your app that might be driven by user actions.

The basic idea behind an SPA is that regardless of what interactions your users might have with the application, the page doesn’t get reloaded or have it’s control handled by another page outside of the current one.

 

Why Might SPAs Can Be Better Than Multi-Page Applications?

 

SPAs typically contrast from classical multi-page web applications where page changes are regular and the browser is often asked to fetch new content from the server and then reload to fulfil user requests.

The problem with the classical approach is that it has a way of disrupting the user experience quite badly. There’s an obvious transition from one page to the next which requires you to wait until entire new pages have loaded and this often means requesting the same content again and again (eg. the sidebars, header, navigation etc). With an SPA on the other hand, changes of application ‘state’ are handled using approaches such as XHR calls, making the user experience a lot more fluid.

In a server-side web application (without any client-side JavaScript) one can consider the idea of state sequence to be equivalent to the concept of pages. A partial change in an SPA (such as displaying a contact form) implies a change of state in the same way a change of navigation in a server-side app results in a change of page.

 

How Will Today’s App Approach SEO, Hash Routing and Graceful Degradation?

 

In today’s tutorial, we’ll be leveraging the jQuery templating plugin to handle rendering our Backbone views and using hash-based routing for the URLs.

Normally, without any additional work done, this would simply result in a blank page being visible to both search bots (other than Google) and users with JavaScript switched off.

So, how do we handle SEO when bots don’t correctly follow JavaScript-powered routes? How do we also provide an experience for users with JavaScript completely disabled?. In my opinion the solution to hash-bang/based URLs and graceful degradation lie in the same basket.

A degraded experience should still allow users to perform most of the basic content-browsing tasks and the URLs this content exposes through links can be used for your SEO with standard parameter-based queries (ie. JavaScript routing may look like #/books/12345, Degraded URLs can look like /?category=books&bookID=12345).

You can then simply ‘connect’ these two schemas using server-side redirect logic should you need to have a greater level of control over what users are taken to.

In the gallery app, I’ve added in a PHP fallback for bots and NoJS which simply reads in my JSON datastore and provides users with similar album/subalbum/image views (with meta-data). This allows me to be free to use whatever client-side templating solutions I need without worrying about JavaScript being disabled.

 

Backbone.js and Underscore.js

 

In case you haven’t used DocumentCloud’s Backbone.js before, it’s a small open-source library that helps bring a variation of the MVC (Model-View-Controller) architecure pattern to your applications – I say ‘variation’ as Backbone also relies on the idea of Collections in addition to models, views and controllers.

One of the reasons I love Backbone is that it helps give you a structure for your code where you don’t have to tie data so heavily to the DOM. Another useful feature is that when your Backbone models change, the views representing the underlying data can be notified, causing them to re-render.

When writing Backbone apps, extending Backbone.Model allows you to define a model with your own properties and methods whilst the framework makes it easy to get and set attributes, validations and serialize the object. Backbone proxies to the Underscore library for handling it’s collections of models.

So..what’s Underscore? Well, it’s basically a utility-belt library that provides a lot of the functional support that you would expect in something like Ruby, but without extending any of the built-in JavaScript objects. (Some call it the tie that goes along with jQuery’s tux), but really, it just gives you some useful functions you might normally be adding to your own utils.js file anyway like each, map, isEmpty, isElement and more.

Going back to Backbone, views can be created by extending Backbone.view and passing in options, models, collections etc. You can also specify a render function on the view to set it up with templating (in today’s tutorial we’ll be using the jQuery templating plugin for this).

I’ll be going over Backbone in more detail shortly.

 

Client-Side Templating With The jQuery Templating Plugin

 

As mentioned, we’re also going to be using the jQuery templating plugin to render our Backbone views today. It basically allows you to create simple client-side templates that are easily re-usable where templates are written in HTML with template tags whilst jQuery code is used for performing the actual data population. A very simple example of how to use the plugin can be found below:

 var albums = [
 { Title: "My Vacation In Malibu", AlbumYear: "1993" },
 { Title: "A Trip To The Sea-side", AlbumYear: "1992" }
 ]; 

 //define the markup for our template
 var template_markup = "
  • ${Title} (${AlbumYear})
"; //compile our markup above as a template called //'albumTemplate' $.template( "albumTemplate", template_markup ); //render the template using 'albums' as our data //source then insert the HTML thats rendered under //the albumList element $.tmpl( "albumTemplate", albums ) .appendTo( "#albumList" );
 

    The official documentation on the jQuery templating plugin is an excellent place to find more examples if you haven’t used it before.

    When it comes to getting templates working with Backbone, this process is very straight-forward. All we need to do is add our template-handling to a view’s render() function based on a template defined in our app’s markup and then append it to our SPA’s main container as needed. This is a little different to how one might use the templating plugin on it’s own, but as you’ll see in the tutorial we’re still going to using $.tmpl for it’s rendering capabilities.

     

    LAB.js Asynchronous Script Loading

     

    Kyle Simpson’s LABjs (Loading And Blocking JavaScript) is a great general purpose script loader which allows you to load scripts in parallel as the browser allows. It also maintains the execution order required when you express the need for keeping dependencies safe, which can be done by inserting .wait() in your chain. In the below example, we wait for jQuery to execute before loading any additional dependancies or plugins and finally wait for those to execute before loading our main application.

     $LAB
     .script("jquery.js").wait()
     .script("another.dependency.js")
     .script("relies.onabove.dependency.js").wait()
     .script("app.js");
    

    There are a lot of great script loaders out there at the moment (including RequireJS) but because the code for today’s app doesn’t really require modules (and LAB.js has a very small footprint) it made sense to select it for our use-case. You can take a look at some more LAB.js examples over at it’s main documentation page.

     
     

    Today’s Single-Page MVC Gallery Application

    Introduction

    In today’s tutorial we’re going to build a three-level, fully bookmarkable image gallery that supports custom views for albums, subalbums and individual photos.

    Backbone will be used to provide a taste of MVC architecture to the app, Underscore for it’s utility functions, LAB.js for asynchronous script loading and the jQuery templating plugin will be used to render our client-side views.

    Data caching is something that can often be ignored in tutorials about client-side app development, so I’m also going to introduce you to Dustin Diaz (Twitter)’s CacheProvider which allows you to cache data both in memory and using localStorage if it’s available.

    As I also covered, we’ll be including some graceful degradation in today’s application as well so that even if JavaScript is turned off, you should still be able to browse the photos in their normal hierarchy.

     

    Application Structure

    Diving right in, let’s first define the structure of our application. The idea behind using Backbone is that it allows you organize your interface into logical views, backed by models, each of which can be updated independently when the model changes, without having to redraw the page.

    Models defined using Backbone.Model contain the interactive data and logic around the information being used in our application. For the gallery app, we’re going to define a simple model called ‘Photo’ to represent the data around image entries.

    Backbone.Collection allows you to define ordered sets of models which can bind ‘change’ events to be notified when models in a collection are modified. We’re going to create a collection called ‘PhotoCollection’ to represent collections of ‘Photo’s.

    Our application views can be defined through Backbone.View. Views don’t actually have anything to do with HTML, but they’re more a convenience concept that support a number of client-side templating solutions.

    Views save you having to dig through JSON, do DOM lookups and update your HTML manually by binding a view’s render function to a model’s ‘change’ event – this way anywhere a model’s data is shown in your UI, it’s always up to date. In our app we’ll be creating an IndexView, SubalbumView and PhotoView to reprepsent the three possible gallery levels that can be viewed.

    The final piece of the puzzle is something to handle our application routes for client-side URL fragments. We also need something that can correctly connect these to both actions and events. Backbone.Controller helps us achieve this and for our gallery app we’re simply going to define a Gallery controller for our routing.

    Data Structure

    The JSON data-structure behind our application needs to be able to support the concept of albums, sub-albums and photos. In addition, we’ll be adding the requirement that each album needs to have it’s own folder thumbnail and each photo in a sub-album needs to have it’s own thumbnail as well. We’ll also need the flexibility to include as much (or as little) meta-data around our images as we need.

    [
      //this is an album with additional meta-data
      {
        "title": "Album title",
        "artist": "Album artists",
        "image": "images/1.jpg",
        "years": "Album date/year",
        "pid" : 0,
        "url": "additional meta-data",
       //this is a sub-album with it's own data
    	"subalbum": [
                     //this is a photo in a sub-album
    				  {
    					"title": "photo title",
    					"artist": "photo artist",
    					"image": "images/michael_small.jpg",
    					"large_image": "images/michael_large.jpg",
    					"price": 13,
    					"pid": 0,
    					"url": "additional url meta-data"
    				  }
    
    				]
    	}
    
    ]
    
    

    jQuery Templating Structure

     

    Application Index (Album view)

    <script id="indexTmpl" type="text/x-jquery-tmpl">
    
    ${attributes.artist}
    ${attributes.title}
    ${attributes.years}
    </script>

     

    Subalbum View

    <script id="subindexTmpl" type="text/x-jquery-tmpl">
    
    
    ${attributes.artist}
    ${attributes.title}
    $${attributes.price}
    </script>
     

    Photo View

    <script id="itemTmpl" type="text/x-jquery-tmpl">
    
    ${attributes.title}
    ${attributes.artist}
    ${attributes.title}
    $${attributes.price}

    </script>

    Models, Views, Controllers & Collections

     

    Here we create the model ‘Photo’ ; used to define individual image items. ‘subalbum’ returns a reference to the current subalbum being viewed via the gallery for use when accessing a Photo item through a hash URL We also define a new CacheProvider for use in our Controller later.

    var cache = new CacheProvider;
    
    var Photo = Backbone.Model.extend({
       subalbum: function() { return 'c' + gallery._currentsub; }
    });
    
    

    PhotoCollection: A collection of Photo items used in index, subalbum and photo views.

    var PhotoCollection = Backbone.Collection.extend({
        model: Photo,
        comparator: function(item) {
            return item.get('pid');
        }
    });
    

     

    IndexView: The default view seen when opening up the application for the first time. This contains the first level of images in the JSON store (the level-one albums). Prior to rendering our jQuery templates here we remove any messages or elements displayed in the version where JavaScript is disabled.

    var IndexView = Backbone.View.extend({
        el: $('#main'),
        indexTemplate: $("#indexTmpl").template(),
    
        render: function() {
    		//remove any graceful degradation views/notices
            $('.jstest,'.gallery').remove();
            var sg = this;
            this.el.fadeOut('fast', function() {
                sg.el.empty();
                $.tmpl(sg.indexTemplate, sg.model.toArray())
    .appendTo(sg.el);
                sg.el.fadeIn('fast');
            });
            return this;
        }
    
    });
    

    SubalbumView: The view reached when clicking on a level-one album or browsing to a subalbum bookmark. This contains the images found in the ‘subalbum’ section of an album entry. Clicking on any of the images shown in a subalbum takes you to the PhotoView of that specific image.

    var SubalbumView = Backbone.View.extend({
        el: $('#main'),
        indexTemplate: $("#subindexTmpl").template(),
           initialize: function(options){
        },
        render: function() {
            var sg = this;
    
            this.el.fadeOut('fast', function() {
                sg.el.empty();
                $.tmpl(sg.indexTemplate, 
                sg.model.toArray())
    .appendTo(sg.el);
                sg.el.fadeIn('fast');
            });
            return this;
        }
    
    });
    
    

     

    PhotoView: The single-photo view for a single image on the third-level of the application. This is reached either by clicking on an image at the second/subalbum level or browsing to a bookmarked photo in a subalbum.

    var PhotoView = Backbone.View.extend({
        el: $('#main'),
        itemTemplate: $("#itemTmpl").template(),
    
    
        initialize: function(options) {
            this.album = options.album;
           
        },
        
        render: function() {
            var sg = this;
            
            this.el.fadeOut('fast', function() {
                sg.el.empty();
                $.tmpl(sg.itemTemplate, sg.model)
                .appendTo(sg.el);
                sg.el.fadeIn('fast');
            });
            return this;
        }
    });
    

    Gallery: The controller that defines our main application ‘gallery’. Here we handle how routes should be interpreted, the basic initialization of the application with data through an $.ajax call to fetch our JSON store and the creation of collections and views based on the models defined previously.

    var Gallery = Backbone.Controller.extend({
    
        _index: null,
        _photos: null,
        _album :null,
    	_subalbums:null,
    	_subphotos:null,
    	_data:null,
    	_photosview:null,
    	_currentsub:null,
    
        routes: {
            "": "index",
            "subalbum/:id": "subindex",
            "subalbum/:id/" : "directphoto",
            "subalbum/:id/:num" : "hashphoto"
        },
    
        initialize: function(options) {
    
            var ws = this;
    
            if (this._index === null){
    
                $.ajax({
    
                    url: 'data/album1.json',
    
                    dataType: 'json',
                    data: {},
                    success: function(data) {
    				    ws._data = data;
                        ws._photos = 
                        new PhotoCollection(data);
                        
                        ws._index = 
                        new IndexView({model: ws._photos});
                        Backbone.history.loadUrl();
                    }
                });
                return this;
            }
            return this;
        },
    
    	//Handle rendering the initial view for the 
    	//application
        index: function() {
            this._index.render();
    
        },
    
    

     

    Gallery -> hashsub: Handle URL routing for subalbums. As subalbums aren’t traversed in the default initialization of the app, here we create a new PhotoCollection for a particular subalbum based on indices passed through the UI. We then create a new SubalbumView instance, render the subalbums and set the current subphotos array to contain our subalbum Photo items. All of this is cached using the CacheProvider we defined earlier.

    hashsub:function(id){
    
    	   var properindex = id.replace('c','');	
    	   this._currentsub = properindex;
    
    	   this._subphotos = cache.get('pc' + properindex) 
    || cache.set('pc' + properindex, new PhotoCollection(
    this._data[properindex].subalbum));
    
    
    	   this._subalbums = cache.get('sv' + properindex) 
    || cache.set('sv' + properindex, new SubalbumView(
    {model: this._subphotos}));
    
    
    	   this._subalbums.render();
    
    
    	},
    

     

    Gallery -> hashphoto: Handle routing for access to specific images within subalbums. This method checks to see if an existing subphotos object exists (ie. if we’ve already visited the subalbum before). If it doesn’t, we generate a new PhotoCollection and finally create a new PhotoView to display the image that was being queried for. As per hashsub, variable/data caching is employed here too.

    hashphoto: function(num, id){
    
    	    this._currentsub = num;
    	    num = num.replace('c','');
    		if(this._subphotos == undefined){
    
    		   this._subphotos = cache.get('pc' + num) || 
    cache.set('pc' + num, new PhotoCollection(
    this._data[num].subalbum));
    
    		 }	
    
    	    this._subphotos.at(id)._view = 
    new PhotoView({model: this._subphotos.at(id)});
    
    	    this._subphotos.at(id)._view.render();
    	    
    
    	  }
    

    Script Loading & Dependency Management

     

    How you structure your LABS.js chains can vary, but as a rule of thumb I like to order mine in a dependency hierarchy. Below is a loose version of this where we wait until jQuery has loaded before loading any additional scripts. We also wait on the last gallery.js dependency to finish loading before we load and execute our application (as per the LAB.js example earlier).

    		   $LAB
    		   .script("jquery.min.js").wait()
    		   .script("jquery.tmpl.min.js")
    		   .script("underscore-min.js")
    		   .script("backbone-min.js")
    		   .script("cacheprovider.js").wait()
    		   .script("gallery.js");      
    
     

    Graceful Degradation For Users With JavaScript Disabled

     

    When JavaScript is either unavailable or a bot’s attempting to crawl our app, we need to be able to provide a basic user-experience that still provides access to the content. In order to do this, I wrote a short PHP script which uses the same JSON gallery store as it’s data source. A snippet of this script can be seen below:

    //expose convenient access to subalbums
    foreach ($json_a as $p => $k){
        foreach($k["subalbum"] as $sub){ 
    		 $subalbums[$i][$j] = $sub;
             $j++;
    	}
    	$i++;
    } 
    
    //handle 'view' switching
    switch($folderType){
    	case "subalbum":
    		echo "";	    
    	break;
    	
    	default:
    	    $ind = 0;
    	    
    		echo "";
    	break;
    }
    

    JSDoc Documentation

     

    Documentation tools are in no way specific to SPAs, but it’s always a good idea to document your code and provide your end-users with a readable form of documentation. JSDoc (The JSDoc Toolkit) is a useful tool for automatically generating multi-page HTML, XML or JSON documentation from comments in your JavaScript code.

    It’s very straight-forward to use and can be run from the command-line or via a shell script once installed. I’ve commented gallery.js using JSDoc compatible syntax and you can see an example of how to format your comments for it below:

       /**
       @description Get's an item based on it's ID
       @param {Integer} itemId
       @type Array
       @returns {returnType} returnDescription
       @author <a href="mailto:test@gmail.com">Test Author</a>
       */
       function getItem(itemId) {
       }

    If you’re interested in reading more about JSDoc, you can check out the official project homepage or the latest documentation.

    SPA Application Building Tips

     

    If you’re looking to begin building your own SPA with jQuery’s Best Friends, I highly recommend planning out your application structure in advance. For example, knowing the different ‘views’ you wish to offer users make writing both your data-structures, template code and finally your Backbone models and views significantly more straight-forward to write.

    Altering your app at a later stage in the coding process isn’t difficult, but it can be time-consuming redefining data-structures, routing and templates then getting them to work in harmony again. That said, *as long* as you have some idea of your app structure in advance, you should be okay. I’ve always found SPAs quite fun to code and I’m sure if you haven’t tried them yet, you will too.

    Downloads & Source Code

     

    To check out a demo of today’s application, feel free to click on Demo. The sources for the tutorial can be found on GitHub and clicking ‘download’ will get you the latest build.

     

     

    Conclusions

     

    We all know that jQuery makes it easy to write scripts and applications that interact with the DOM, but jQuery’s best friends give you the power to take that further, adding the ability to add structure to your application architecture, provide you with utilities so you don’t have to code them yourself and other benefits like client-side templating and lightweight scriptloading.

    There’s absolutely no requirement that you have to use all of these tools together but I hope this post encourages you to try using them in your apps – they really can help you build better cleaner solutions. For more reading on MVC or building large scale applications with jQuery, feel free to check out my post on the topic.

    And that’s it!. If you’ve found today’s tutorial helpful, please feel free to share it with your friends and colleagues by clicking on the Building Single Page Applications With jQuery’s Best Friends button below.

    Thanks and good luck with your projects!

     

    Update Feb 23 , 2001

    Rafael in the comments noticed that there was an issue where directly linking to the photo view without caching was causing the progressive enhancement fallbacks to render rather than being removed. This issue has been fixed on both github and the live demo so if you grab any of the sources from this page they will be up-to-date.

    References

     

    [1] The Single-Page Application Manifesto http://itsnat.sourceforge.net/php/spim/spi_manifesto_en.php

    [2] Building Apps With Backbone.js http://www.oneofthesedaysblog.com/backbone-js/

    [3] The Backbone.js Store (recommended reading. my initial inspiration for writing about Backbone further) http://www.elfsternberg.com/2010/12/08/backbonejs-introducing-backbone-store/

    [4] Script loading with Lab.js and Require.js http://msdn.microsoft.com/en-us/scriptjunkie/ff943568

     

    109 Comments

    1. Pingback: 开源中最好的Web开发的资源 « 时间的灰烬

    2. Pingback: 开源中最好的Web开发的资源 | blog.moocss.com

    3. Pingback: 开源中最好的 Web 开发的资源(下) | 全球网赚联盟资源

    4. Pingback: 开源中最好的Web开发资源 | 免费资源站

    5. Pingback: 开源中最好的Web开发的资源 | 酷壳 – CoolShell.cn « Adamlu's Blog

    6. Pingback: » 开源中最好的Web开发的资源 巴特农神殿

    7. Pingback: Web开发中目前最好的开源资源 - 原木游戏 - 让游戏改变生活!

    8. Pingback: 开源中最好的Web开发学习的资源 | 萝卜草莓酱

    9. Pingback: 最好的 Web 开发的开源资源_开源新闻_WO就.哎_个人博客

    10. Pingback: 开源中最好的Web开发的资源 | 互联网观察(Intsenz)

    11. Pingback: 开源中最好最全的Web开发的资源 - 得奇乐 - 得奇乐:真正的可悲不是失败,而是连失败的机会也没有。

    12. Pingback: 开源中最好的Web开发的资源 – 木依雨心

    13. Pingback: 开源中最好的Web开发的资源 | 刀侠剑客

    14. Pingback: 开源中最好的Web开发的资源 « 日光之上

    15. Pingback: Best “must know” open sources to build the new Web | Jolt's Home

    16. Pingback: Bazzinga! Design » 开源中最好的Web开发的资源[转载]

    17. Pingback: JavaScript e MVC | Alberto Monteverdi

    18. Pingback: 【转载from coolshell.cn】开源中最好的Web开发的资源 » linstein for ArthurNet Tech

    19. Pingback: 开源中最好的 Web 开发的资源 « 儒雅人生

    20. Pingback: Web开发必备资源汇总(精品) | 杜拉克草

    21. hi, just downloaded and ran index.php – i get this error:
      this._index is null
      [Break On This Error] this._index.render();
      gallery.js (line 173)
      is something missing from the zip?
      tyvm

    22. Pingback: Javascript Resources « Kooljoy.com Blog

    23. Pingback: 开源中最好的Web开发资源汇总 | 大学生活Lab

    24. Pingback: 开源中最好的Web开发的资源 | 前端开发博客

      • If you take a look at some of the recent Backbone + Require.js examples up on my GitHub account or on TodoMVC (also on that account) you’ll find examples that cover loading templates in from external files using script loaders. Here’s the account: http://github.com/addyosmani.

    25. Pingback: 开源中最好的Web开发资源汇总 | 互联第一站

    26. Thanks for the great tutorial. I am a traditionally server side developer learning more about client side technologies. I have recently been looking for Backbone/JQuery examples and I thought I saw where JQuery templates are going to be replaced with JViews? If so, would your example work with something like Underscore.js which is a Backbone dependency anyway?

    27. Pingback: 开源中最好的Web开发的资源 |

    28. Pingback: 最好的Web开发的开源资源 | 午•後

    29. Pingback: 开源中最好的Web开发的资源 | 52 b/s

    30. Pingback: websocket api to replace rest api? | Everyday I'm coding

    31. Pingback: 开源中最好的Web开发的资源(转贴) | DBShop博客

    32. Pingback: JavaScript 移动和触摸框架 | 查问题

    33. Pingback: Hello, World! » 开源中最好的Web开发的资源

    34. It’s a shame you don’t have a donate button!
      I’d definitely donate to this fantastic blog! I suppose for
      now i’ll settle for bookmarking and adding your RSS feed to my Google
      account. I look forward to brand new updates and will talk about this website with my
      Facebook group. Chat soon!

    35. Do you mind if I quote a feww of your articles as long as I provide credit and ssources
      back to your site? My blog is in the very same niche as yours and
      mmy users would certainly benefit from a lot of the information you provide here.
      Please let me know if this okay with you. Thanks a
      lot!

    36. Great post. Backbone.js is new to me, just stepping out from design only to some programming and knowing about all these new platforms/technologies will help me create new great stuff for clients.

    Leave a Reply

    Required fields are marked *.