Rich Page Applications

Single Page Applications (or ‘SPAs’) are, simplified, client-side JavaScript applications where we don’t do anything view-layer related on the server and instead just supply raw data to the browser, letting it handle all the routing, rendering, and behaviour.

It’s a pretty appealing way to work in a lot of ways. Your back-end becomes more like a straight-forward API and the front-end handles… the front-end. This article is about an alternative to SPAs (which I like to call Rich Page Applications, or ‘RPAs’) which I think gets you some of the benefits but with a less extreme change in workflow from traditional web apps.

The Role of the Server

The server still handles routing and as much of the view-layer as it can, but intelligently skips highly interactive parts of pages.

So for example, here is a CourseCraft editor page:

The server renders the page header, the sidebar, the top bit with the save button and the title/description fields. It also drops JSON seed data – everything needed to bootstrap the rendering of the rest of the page by (in this case) a Backbone ‘micro app’.

The Role of the Client

The client hooks into the rendered skeleton of the page. It injects behavior into the yellow shaded area and uses seed data to show the CourseCraft editor interface.

Each of the blocks on the right and all of their various extra interface bits and behavior (you can insert new blocks in between two blocks, re-order blocks, upload files, change options, etc), the previewing, and auto-saving all happens on the client.

Why Do This?

The stuff we really want to handle on the client is 100% client-coded (supported by some API endpoints on the backend), but basic stuff like login forms, forgotten passwords, feature/marketing pages, blog posts, profile pages, etc are all rendered by the server. This feels like the best of both worlds to me.

In a way it’s the same concept we used to use back in the jQuery-spaghetti days, but you still get to organize your code. Each page of your application is optionally backed by a JavaScript application that handles all the appropriate-for-the-client stuff.

Most importantly for me though, the complex JavaScript (actually, it’s all CoffeeScript) behavior/interfaces I write ends up less concerned with stuff the server already has figured out (like routing), handling page transitions, cleaning up garbage to prevent leaking memory and crashing the browser, etc.

An Implementation

I do most of my web stuff in Rails. So here’s a quick breakdown for how I do this kind of thing in Rails 4.x.

First off, figure out how you’re going to do client-side dependencies, what kind of stack you’ll use on the client, etc.

I add bower-rails to my Gemfile, so I can manage client-side dependencies the same way I already do with bundler, but you can just as easily drop copies of Backbone (or whatever) into vendor/assets/javascripts. For client side templates I like to use handlebars, so I do gem 'handlebars_assets' to get nice precompiling of javascripts/templates.

We’ll want a namespace on the page. Add the following method to ApplicationHelper:

module ApplicationHelper
  #...

  def js_namespace
    h = {
      # here we put our Backbone.View classes
      # including micro app 'entry points'
      Views: {},

      # global config values
      config: {
        # such as this autosave interval example
        autosaveInterval: 30.seconds
      },

      # a place to put seed data later
      seed: {
        # but you could also populate it here with things
        # you know you'll need
        uid: current_user.try(:id)
      },

      # which micro app(s) to run
      boot: []
    }

    javascript_tag "window.App = #{h.to_json.html_safe};"
  end

  # ...
end

And include it in your layout:

%head
  - # ...

  - # define namespace _before_ adding application code
  = js_namespace

  - # alternatively put this at the end of 'body'
  = javascript_include_tag 'application'
%body
  - # ...

So now we have App defined in our scripts on every page. When we are ready to add some RPA goodness – we just write one of these micro apps, and set it up to only run on the appropriate page. So let’s say we’ve got a page that lists Bitbucket repos for a username:

- # we do as much as we can to skeleton out the page

.controls
  %label{ for: 'username' } Bitbucket Username
  %input#username{ type: 'text', value: 'rfunduk' }
  %button#find Find

%ul#repos

:javascript
  App.boot.push('repos');

To wire this up we use the idea of a boot list – which micro apps we want to run. We want this to happen magically, so we write a common configuration handler that runs on every page and delegates to an entry point meant for this page.

$(document).ready ->
  for app in (App.boot || [])
    viewModel = App.Views[_.classify(app)] # uses underscore.string
    continue unless viewModel?
    view = new viewModel()
    view.render()

On page load, look through App.boot and try to instantiate a view class for each matching name, e.g. App.boot.push('repos'); will try to find and instantiate App.Views.Repos. I’ll generally put the boot handling code in common/boot.coffee, and then use directories for each micro app. In this case, say, repos/index.coffee which will then use the Rails asset pipeline to require other files it needs (whether they are external dependencies or, if things are getting complex, more files in subdirectories of repos).

Something you might consider if you use this technique a lot is to put the app to load as a data attribute on body, and populate that with the controller and action.

%body{ data: { boot: "#{controller_name}_#{action_name}" } }
  - # ...

Now we could just read this instead of (or in addition to) using App.boot. But I like to see what will be booting in the template that needs the app to be running, and the controller/action combo isn’t always going to map 100% of the time. For instance say you have a comment section on multiple pages, you want to boot the same micro app on all of them.

Anyway, let’s just define something basic for the repo list page:

class App.Views.Repos extends Backbone.View
  el: 'body'
  events:
    'click #find': 'find'

  initialize: ->
    @list = @$('#repos')
    @repos = []
    @reload()

  reload: ->
    baseUrl = "https://bitbucket.org/api/2.0/repositories"
    url = "#{baseUrl}/#{@$('#username').val()}"
    $.getJSON url, ( r ) =>
      @repos = r.values
      @render()

  render: ->
    @list.empty()
    _.each @repos, ( repo ) =>
      # you'll probably render a template from somewhere like
      # window.HandlebarsTemplates instead of inline markup :)
      @list.append """
        <li>
          <h2>
            <a href='#{repo.links.html.href}' target='_blank'>
              #{repo.full_name}
            </a>
          </h2>
          <p>#{repo.description}</p>
        </li>
      """

Easy as that. Code is organized, but no client-side hoop jumping where it isn’t needed.

Client-side Hoop Jumping?

Even though this is straight-forward stuff, it’s obvious we have no need for a few key things:

  • No Backbone.Router instance. routes.rb is the only place any routing happens.

  • We don’t have to do a lot of meticulous view cleanup. Because a lot of views are going to live for the lifetime of the page, and users navigate between pages which are running their own micro apps from scratch, there’s a lot more headroom for accidentally leaking an event handler. Of course you still try to clean up after yourself, but one mistake in an SPA and all sorts of bad things happen.

  • No handling of client-side URLs on the server-side. If <username>/<repo>/commits/<sha> is handled by your client-side app, you still need your backend to respond to that URL so that your client side app can bootstrap itself to the right place. Sometimes this is as simple as:

    SinglePageRailsApp::Application.routes.draw do
      # ...
      match '*anything' => 'spa#index'
    end

    But inevitably you’ll go to optimizing things and want to load appropriate data and seed the page instead of doing the pageload and then an ajax call right away (Backbone specifically recommends against this).

    With an RPA you are already going to have real controller actions that render individual pages and thus a natural place to fetch data.

Also, of course, let’s not discount the fact that doing this means you get to write more Ruby/Rails/HAML than with an SPA approach.

But I Like Ember.js!

Oh, me too! Ember is on my list of things to start experimenting with (also React, and others), and I have used the SPA style on some projects (like Mr. Password), so this is very much a use the best tool for the job situation.

And this isn’t anything new. Basecamp Next is famously not an SPA, but uses a Backbone micro app to handle the calendar. There’s a great talk about this stuff from back in 2012 where DHH describes something a lot like this. He begins around 34 minutes and starts talking about the Backbone-ified calendar part around 45 minutes). He calls it a ‘hybrid model’ where 90-95% of Basecamp is stock Rails, and the client-side stuff is only used where needed.

CourseCraft is a bit more than 5-10% client-side. Some stuff I handle with Backbone micro apps:

  • Activity feed rendering & updating
  • List/add/remove/edit of promo codes
  • Comment sections
  • Profile editing (setting up payment gateway and white label settings)
  • The primary content editor (as mentioned above)
  • Course stats page

Enough code is needed to make these things go that I think they deserve to be organized into little apps. But there’s enough other stuff in the app that doesn’t need this, so going all-in on an SPA approach doesn’t feel right.

Ultimately it’s a judgement call, and depends a lot on what you enjoy writing the most, that will help you decide which approach to take for any given project.