Sprockets Dissected: Asset Tags

sprocketsmag

Rails is a favored Ruby web framework and a popular choice for building web applications. It has numerous awesome parts, many of which can be used independently of Rails. ActiveSupport, for example, is usable in any Ruby project to get some of the great features that Rails core provides. ActiveRecord is a standalone ORM, providing a sound way of dealing with different databases with a single codebase.

There is another component of Rails that is widely being used outside of Rails: the Asset Pipeline or, technically speaking, Sprockets. Sprockets is a Ruby gem developed by Sam Stephenson that provides support for asset compilation, asset minification, asset serving, and much more. In this article, we will discuss how Rails serves assets using Sprockets.

I would recommend reading How Asset Precompile Works, Part I and How Asset Precompile Works, Part II before proceeding. There are many concepts used here that are discussed in great detail in those articles.

This article is based on Rails 3.2.17 and Sprockets 2.2.2. Please install Rails 3.2.17 first if you haven’t already.

Let’s answer the following questions related to Rails assets:

  • How asset tags are inserted?
  • What is /assets and it’s purpose?
  • What is ActionDispatch::Static middleware used for?

How Asset Tags Are Inserted?

The first step to serving an asset is to have proper asset tags (JavaScript tags, Stylesheet tags, image tags etc.) in the HTML response sent to the client. If there are no asset tags in the HTML, then we aren’t using/linking any external libraries, which is pretty taboo nowadays. We need at least one Stylesheet tag, one JavaScript tag, and some image tags in our site. Let’s see how a JavaScript tag gets inserted into the HTML response generated from a typical Rails application.

Source code paths have been mentioned both local and online, as appropriate. Two folders are of interest here, which can be discovered with the following commands in a Rails app:

bundle show actionpack

bundle show sprockets

Use your editor to open each of these libraries so that you can following along in the source.

The default way of inserting a JavaScript asset tag in Rails is:

<%= javascript_include_tag "application.js" %>

As of Rails 3.1, the default application.js might look like:

//= require jquery
//= require jquery_ujs
//= require_tree .

Rails developers know that they can define dependencies of any JavaScript file by using Sprockets special syntax and additional files that get inserted are actually dependencies of application.js.

javascript_include_tag is a Rails helper defined here. Default Rails helpers are defined in actionpack/lib/action_view/helpers directory and javascript_include_tag is no exception. But this method can only add script tags to the HTML response and cannot intelligently parse files to resolve dependencies. Resolving asset dependencies is handled by Sprockets.

Sprockets is configured according to the Rails environment in actionpack/lib/sprockets/railtie.rb. Railtie is the core of the Rails framework and provides several hooks to extend Rails and/or modify the initialization process. If we want to modify Rails behavior at any point during or after the initialization process, we can use Railtie.

Sprockets extends Rails behavior by including the Sprockets::Helpers::RailsHelper module after action_view has been loaded. This means that the newly added module will override methods with same name that are already defined in modules under action_view.

Now it’s time for a little experiment. Create a new Rails application with a controller and set it’s route.

# creates a new rails application
rails new rails_asset_serve

# navigate to newly created rails directory
cd rails_asset_serve

# create a new controller named test with one method index
rails generate controller test index

Go ahead and edit app/views/test/index.html.erb and add some dummy text there. Now edit config/routes.rb and add following line so that test controller can be accessed from browser.

root :to => 'test#index'

Run the Rails server, navigate to localhost:3000, and you will see your desired output. However, our point of interest is the source of this page. View the source and you’ll see the following JavaScript tags included in your HTML response.

<script src="/assets/jquery.js?body=1" type="text/javascript"></script>
<script src="/assets/jquery_ujs.js?body=1" type="text/javascript"></script>
<script src="/assets/test.js?body=1" type="text/javascript"></script>
<script src="/assets/application.js?body=1" type="text/javascript"></script>

These JavaScript files are dependencies of application.js, as mentioned above. Now the fun part. Comment out actionpack/lib/sprockets/railtie.rb:46 and restart your server.

This time, when you visit the page, you won’t see any difference in output, unless you view the source. Here, observe that there is only one JavaScript tag pointing towards application.js. This is because the default Rails helper is not intelligent, and hence, has skipped dependencies. Sprockets is intelligent enough to resolve dependencies, leading to the links to all our JS source files.

Let’s see how Sprockets performs this magic.

Don’t forget to uncomment actionpack/lib/sprockets/railtie.rb:46

The Journey Begins

Let’s take application.js as an example. In actionpack/lib/sprockets/helpers/rails_helper.rb at line:7 you can see that the Sprockets::Helpers::RailsHelper module has included the ActionView::Helpers::AssetTagHelper module. ActionView::Helpers::AssetTagHelper is the main module that holds the default Rails helpers. By including them within it’s own module, Sprockets is ensuring that it still has access to all the default Rails helpers.

On line:20 you can see javascript_include_tag is defined. This method gets called when we want to include a JavaScript asset. On line:26 there is a collect enumerator called which returns an array of script tags separated. In our case sources will be pointing to application.js.

On line:27 you can see an if statement. If we are in the development environment then debug evaluates to true. The other part (asset = asset_paths.asset_for(source, 'js')) is a bit interesting. If you have read those two recommended articles that I mentioned above, then you will easily understand what is happening.

assets_paths is a method defined at line:9 which returns an instance of class AssetPaths defined at line:117. There are a bunch of assignments in that method, but the one we are interested in is on line:12.

asset_environment is a method whose result is assigned to paths.asset_environment. asset_environment is defined at line:113. This method is returning Rails.application.assets which is an instance of Sprockets::Environment, the top-level class responsible for everything to do with assets under Sprockets.

asset_for, at line:122, is an instance method which is called to get the desired asset. asset_environment is responsible for returning an instance of Sprockets::BundledAsset that points towards the desired asset (application.js in our example). [] is a shorthand for the find_asset method and is defined here.

asset_environment returns an instance of Sprockets::BundledAsset containing all the dependencies of our asset. Iterating over these dependencies is done with the to_a method, just as Sprockets is doing online:28.

At line:29, there is a call to super, which is the default Rails helper. Yeah, that’s right. Sprockets has done some clever things. First, it has included its module when action_view gets loaded resulting in overriding those methods. Then, it included the default Rails helpers in its own modules. By calling super in javascript_include_tag, it is calling javascript_include_tag from the default Rails helpers. The default javascript_include_tag helper is capable of adding script tags for sources, so at line:29 we are calling it and passing the necessary information and voila! The script tag for each dependency of application.js is inserted.

Summarizing this process, Sprockets first overrides the default Rails helpers, intelligently resolves asset dependencies, and then calls the overrided methods again to complete its job.

You might be thinking that the resolving of assets is carried out on each call to our Rails application, but this is not true. When we start/restart the Rails server, all asset resolution happens on the first call for those assets. The assets are cached and reused on subsequent calls until the assets are modified.

In the production environment, this process is straightforward. Let’s head back to line:20, where the javascript_include_tag is defined.

When we are in production environment, the if statement on line:27 evaluates to false because debug is false. The else case (line:31 will be executed and call super by passing the appropriate values. In production, only one asset tag is inserted because all of it’s dependencies are merged into a single file by Sprockets (when we run rake assets:precompile).

Process for inserting Stylesheet asset tags is same as of JavaScript one. Inclusion of other asset tags such as image, font, audio etc. is pretty easy. In default Rails helpers paths to those assets were prepended with appropriate directory (images for image, audios for audio, fonts for font). But in Sprockets this thing is replaced with one method and that is prepend all assets paths with Rails.application.config.assets.prefix (which is /assets by-default) which is defined here. We all know that every asset in Rails application using Sprockets have this format /assets/__asset_name__. At line:94 there is a method asset_prefix which is returning Rails.application.config.assets.prefix.

Wrap Up

We have seen how Sprockets performs its magic to resolve dependencies and insert the appropriate asset tags. In a forthcoming article, we will answer the remaining two points mentioned at the beginning of this article. Until then, go tell all your friends that you have mastered Sprockets and asset tags!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments

Comments on this post are closed.