Rigel Group

They shoot Yaks, don't they?

Batman's Secret Cache

The real Batman has caches of weapons stashed across Gotham City, but the Batman.js javascript framework could use a cache of it’s own, a Views cache. I love the fact that Batman views are all stored in separate files (a la Rails) when doing development, it makes organizing a large project very easy. But when it comes time to deploy to a production system, stock Batmanjs will happily request each view as it is first requested via AJAX, but all those individual views should really be bundled up and served in one file, for obvious performance reasons.

To accomplish this, Ryan Funduk and I recently came up with this approach. First we will leverage the Rails asset pipeline to create a json file that contains all of our app’s views.

1
 /app/assets/javascripts/all_views.json.erb
1
2
3
4
5
6
7
8
9
10
11
<%=
  prefix = "#{Rails.root}/app/assets/javascripts/views"
  paths = Dir.glob("#{prefix}/**/*").select{|f| File.file?(f) && (f =~ /\.(html|erb)$/i) }
  paths.inject({}) do |all_views, f|
    viewname = f.sub( /^#{prefix}/, '' ).sub( /\..*$/i, '' )
    view = File.read(f)
    view = ERB.new(view).result if f =~ /\.erb$/i
    all_views[viewname] = view
    all_views
  end.to_json
%>

Now, if you go to http://localhost:3000/assets/all_views.json you should see all your views in one large json object. Note that your Batman views will be processed through ERB, so if you need the server to do some processing, go right ahead. (Just remember that the ERB code will be run only once, when the views are compiled, not each time they are requested.)

Next, tell the Rails asset pipeline to precompile this file, like so:

1
 /config/environments/production.rb
1
 config.assets.precompile += %w(all_views.json)

Next, we write a Batman helper that will request the all_views.json file, and create Batman views for each of the individual views. That looks like this:

1
 /app/assets/javascripts/helpers/views_preloader.js.coffee.erb
1
2
3
4
5
6
7
8
MyApp.preloadViews = () ->
  new Batman.Request
    url: '<%= asset_path("all_views.json") %>'
    type: 'json'
    error: (response)  -> throw new Error("Could not load views")
    success: (all_views) =>
      for view of all_views
        Batman.View.store.set(view, all_views[view])

Note that we run this file through ERB first, so that we get the proper path (including the digest, if applicable) to the all_views file. The last line pre-populates the view store with each of the views. (You will need to be on the master Batmanjs branch, as the View store is a new addition since the 0.8 release).

The last thing we need to do is kick off the preloader, and a good place to do that is like so:

/app/assets/javascripts/application.js.coffee
1
2
3
4
window.MyApp = class MyApp extends Batman.App

  @on 'run', ->
    MyApp.preloadViews()

So, putting it all together, when your Rails app is deployed and the assets are precompiled, the asset pipeline will write out an all_views-1234.json file to the public/assets directory. On the client side of things, when your Batman app starts, it calls the preloader, which loads the all_views-1234.json file and creates Batman views for them all in one fell swoop. The really cool thing about this setup is that the Rails asset pipeline takes care of caching the views for you. Winning!

UPDATE: Andrew Bennett came up with an even nicer way to grab the view source using the asset pipeline, which will let you use HAML or ERB and also any Rails view helpers:

1
2
3
4
5
6
7
8
9
10
11
12
<%=
  prefix = File.expand_path('../views', __FILE__)
  paths = Dir.glob("#{prefix}/**/*").select{|f| File.file?(f) && (f =~ /\.(html|haml|erb)$/i) }
  paths.inject({}) do |all_views, f|
    viewname = f.sub( /^#{prefix}/, '' ).sub( /\..*$/i, '' )
    all_views[viewname] = environment["views#{viewname}.html"].to_s
    ## the following appear to be identical ##
    # all_views[viewname] = asset_environment["views#{viewname}.html"].to_s
    # all_views[viewname] = Rails.application.assets["views#{viewname}.html"].to_s
    all_views
  end.to_json
%>