I’ve been spending a lot of time lately working on getting the asset pipeline for my application working. A lot of things weren’t very clear from the documentation at the time of this writing, so I am going to narrate the process I went through in case anybody else has the same issue.
For a background on the asset pipeline, please read through the rails guide and these two excellent railscast by Ryan Bates. These got me about 90% of the way there.
One of the main goals of the asset pipeline is to bundle together all of your application’s javascript and stylesheets, condense them, and serve them to the end user with a sophisticated caching approach. This works very well for many of the applications on the web. In the case of my application, we had several javascript files that were specific to individual pages, and didn’t play well (for one reason or another) with other areas of the site. It has long been a requirement that these javascripts are only loaded for these specific pages. We were accomplishing this by creating a yield section in our application.html.erb like so:
<%= yield :head_includes %>
Then, in the pages were we needed to load a javascript file, we would put the javascript_include_tag inside a content_for block, like so:
<% content_for :head_includes do %>
<%= javascript_include_tag "search.js" %>
<% end %>
This worked quite well for us in rails 3.0. However, in Rails 3.1 with the asset pipeline enabled, this would throw an error:
search.js isn't precompiled
My first attempt at getting this to work was to run
bundle exec rake assets:precompile
This compiled all of my coffeescript assets, as well as application.js, however the default rails configuration doesn’t precompile files with .js or .css extensions (although .js.coffee or .css.scss would be precompiled). I could have renamed all of my files to end with .js.coffee, but I wanted to move away from declaring individual files on various views, and I wanted javascript files that are run on similar pages to be bundled together. The rails guide had this bit of advice:
You should put any JavaScript or CSS unique to a controller inside their respective asset files, as these files can then be loaded just for these controllers with lines such as <%= javascript_include_tag params[:controller] %> or <%= stylesheet_link_tag params[:controller] %>.
This was decent advice, however the assets still weren’t being compiled. That would take a few more steps. First, I created a manifest file for any controller that had controller specific javascripts. For example, if a controller named EventsController had specific javascript files, I would create events.js (if it didn’t already exist). To that file, I added the following statements:
//= require_tree "./events"
I would also include the following if the file already existed with existing javascript:
//= require_self
I would then dump any controller specific .js files into the folder specified by the require_tree directive. I also had to add the manifest files to the list of files to precompile by adding the following to config/application.rb (with an entry for each manifest file, comma separated):
config.assets.precompile += ['events.js', 'pages.js']
For the sake of organization, I also moved a lot of javascript files to vendor/assets/javascripts and lib/assets/javascripts, as appropriate. I then updated application.rb to list all of the files that needed to be included on every page, using require and require_tree directives. I then was able to replace my various javascript_include_tag lines in my application.html.erb file with the following:
<%= stylesheet_link_tag 'application'%>
<%= stylesheet_link_tag controller.controller_name if individual_css_file_exists?(controller.controller_name) %>
<%= javascript_include_tag 'application' %>
<%= javascript_include_tag controller.controller_name if individual_js_file_exists?(controller.controller_name) %>
Note that controller.controller_name is similar to params[:controller], except where you have namespaced controllers. For example, Admin::EventsController would return “events” for controller.controller_name, and “admin/events” if you use params[:controller]. I chose the former for simplicity’s sake. I also created the following helpers in application_helper.rb
def individual_css_file_exists?(name)
!GoVoluntr::Application.assets.find_asset("#{name}.css").nil?
end
def individual_js_file_exists?(name)
!GoVoluntr::Application.assets.find_asset("#{name}.js").nil?
end
This helped to avoid the dreaded “controller.js is not precompiled” error when a manifest file didn’t exist for that specific controller.
This may seem like an awful lot of work for the application to essentially work the same as it did before. The benefit, however, is that I can remove a lot of redundant code from each of my view files, and I get a reasonable tradeoff between combining multiple javascript files and only loading certain code on specific pages.
Leave a Reply