If you provide a <script> section as part of an API (i.e. DISQUS, Google Analytics, etc.) and you’re using Rails 3.0 or greater, you can leverage the Asset Pipeline to refresh JavaScript and CSS asset files as often as you want.
If you’re using Rails 3.0 or greater, you’ve already met the wonderful Asset Pipeline. It has many great features, one of which is the ability to “fingerprint” your static files (like images, javascript and css files). Fingerprinting is Rails’ smart way of versioning your files. It creates a checksum (MD5 digest) of the contents of your file and appends it as part of the file’s name so that every time you update your file’s content and run the rake assets:precompile command, Rails will generate static versions of your files with a new unique name.
Here’s what a file looks like when Rails adds the fingerprint:
http://ourdomain.com/assets/filename-b6c18350985e53de1f97f8eee15c180a.js
Pretty cool yet kind of cryptic…so why is it important? By adding the fingerprint to the file’s name, you’ve essentially made a unique file that web browsers can cache, indefinitely, if you set the expiration date way into the future using the far-future headers on the server.
Here’s how to set far-future headers in Apache:
cd /etc/apache2/mods-available sudo cp expires.load ../mods-enabled/expires.load sudo /etc/init.d/apache2 restart
<LocationMatch "^/assets/.*$"> Header unset ETag FileETag None # RFC says only cache for 1 year ExpiresActive On ExpiresDefault "access plus 1 year" </LocationMatch>
Rails also makes it very easy to access your static files from within your Rails project without having to know their cryptic name. All you have to do is use the appropriate Rails method for your task and Rails will figure out which file to substitue once your running in a production environment.
Here are some of Rails’ helper methods:
stylesheet_link_tag "application" javascript_include_tag "application" image_tag "my.png"
This functionality is great for standalone websites where the request for a page is processed directly by Rails and the response includes the fingerprinted name for each of the assets your are loading within the page. There’s no need to know anything about the files since they will render properly when someone visits one of the well known URLs on your site. But what if you want to leverage this functionality from somewhere other than a webpage? Maybe from within a script? Do you need to know the name of each asset?
Some sites offer features that extend a webpage’s functionality using <script> sections that a website’s owner can copy into their HTML code. For example, DISQUS lets me add a script similar to this one to my page:
<script type="text/javascript" src="http://disqus.com/forums/frafdez/embed.js"></script>
With this script, DISQUS can load iframe’s into my page and serve up a great commenting system.
Now lets say we have a similar <script> section pointing to:
http://ourdomain.com/assets/embed.js
How can we ensure that we’ll get the latest code, in a timely manner, when we load this script file?
Lets assume that our API is exposed through the embed.js file but instead of placing the implementation in the file, we will dynamically load another <script> file from our Rails server that can serve as a proxy to the actual supporting files.
Here’s what the file might look like:
var my_domain = "http://ourdomain.com/";
function addJS(file_name)
{
var s = document.getElementsByTagName('body')[0];
var my_script = document.createElement('script');
my_script.type = 'text/javascript';
my_script.async = false;
my_script.src = file_name;
s.appendChild(my_script);
};
setTimeout( function(){ addJS(my_domain + "assets/dynamic.js"); }, 0);
The problem with this example is that we haven’t solved anything. We’ve just introduced another HTTP request for apparently no good reason. Why? Because the URL for the dynamic.js file makes it forever unique and therefore will be cached by the browser indefinitely. We’re trying to avoid this, so let’s make a few changes to our JavaScript in order to break uniqueness and bust the browser’s cache.
Lets try this:
var my_domain = "http://ourdomain.com/";
function addJS(file_name)
{
var s = document.getElementsByTagName('body')[0];
var my_script = document.createElement('script');
my_script.type = 'text/javascript';
my_script.async = false;
my_script.src = file_name;
s.appendChild(my_script);
};
function getExpiration()
{
var currentTime = new Date();
var month = currentTime.getMonth() + 1;
var day = currentTime.getDate();
var year = currentTime.getFullYear();
return day.toString() + month.toString() + year.toString();
};
setTimeout( function(){ addJS(my_domain + "api/"+ getExpiration() + "/dynamic.js"); }, 0);
This time I’ve changed a few things. Notice that I am no longer calling a file in the assets directory. I’m actually calling a controller named api instead. Also, notice that I’ve added a string with the current day’s date. This will definitely ensure that the browser will call our website at least once a day since the URL will only change when the date changes. This essentially busts the cache. I can live with a one day grace period but you could adjust it to fit your needs. So how do we get Rails to serve up a JavaScript file with a special expiration date sandwiched in between?
Let’s assume you already have an api controller defined in your Rails app and lets go directly to the routes.rb file and add a new entry for the controller action: dynamic:
get 'api/:duration/dynamic'
Simple enough. We’ve added a parameter called duration to the URL that will let us pass in the expiration date.
Now let’s add the action to the controller:
class ApiController < ApplicationController def dynamic respond_to do |format| format.js end end end
This action will only return a JavaScript file therefore we’ll only support the .js format. Now that we have an action in our controller that serves up dynamic JavaScript, you can actually leverage the Asset Pipeline and create your own response thats not tied to a static file.
Let’s make some changes:
class ApiController < ApplicationController
def dynamic
@main_file = ActionController::Base.helpers.asset_path('main.js')
@additional_file = ActionController::Base.helpers.asset_path('additional_file.js')
respond_to do |format|
format.js
end
end
end
Earlier on I mentioned that Rails has helper methods that would automatically handle the mapping of regular asset file names to MD5 hashed names. The problem with those helpers is that they are task specific and only generate HTML tags with the assets embedded in them. For our dynamic example, we only need the path to the asset, since we will automatically create a JavaScript file that references them directly. Luckily, Rails exposes the method used within the helper methods to resolve the asset’s path. The method is called ActionController::Base.helpers.asset_path. All you need to do is pass in the real name of the file, and in production, Rails will return the MD5 hashed file’s path. So what can we do with this?
Let’s create the dynamic.js.erb file that describes the JavaScript code we want to return when the action is called:
setTimeout(function() { addJS( my_domain + "<%= @main_file %>"); }, 0);
setTimeout(function() { addJS( my_domain + "<%= @additional_file %>"); }, 0);
Here’s how we use the Asset Pipeline. From within the JavaScript file I can reference the two Ruby variables I defined in the dynamic action. Since my previous JavaScript file already had an addJS function, I’m reusing it and the my_domain variable in order to add the fingerprinted files to the calling site. From now on the JavaScript files will be served up with their fingerprinted file names that are cacheable by the calling browser.
In a nutshell:
There is a price to pay for this functionality (an additional HTTP request and <script> section) but it may be a negligible cost compared to the flexibility gained by this approach.