Lately we’ve had the desire to move some of our rails services / middleware from our main web project to Rails Engines as gems to help us scale out and reuse that functionality across servers and sites.
Haven’t heard of Rails Engines? Rails engines are essentially miniture rails applications that provide funtionality to its host application – you could think of engines of what used to be Rails plugins in Rails 2 and upgraded in Rails 3.2. RailsGuides is a great place to start, RailsCasts has a nice walk through. I want to focus on showing some tips, tricks, and pitfuls I’ve encountered to help you be more successful.
Why I started using Rails Engines
For our needs, we had to display a mobile version of our application on our main website webserver. All of the code for the mobile application was hosted in another git repository (Note: I love the Git Gem) and had different themes that needed to be applied. The big question was, how do we have this hosted on different web nodes and control our changes from the mobile repository?
There are so many different ways to approach this problem, the method I chose was to try out a Rails Engine. The main reasons this was attractive was:
- A set of controllers to handle the requests for only the mobile application
- Being able to configure the mobile gem based on any environment it may be in
- Being able to precompile our assets with the rails asset pipeline
- Handle updating the mobile app by just bundle updating the Gem
Rails Engines – Full vs Mountable
There are two types of Rails Engines available, a full engine and a mountable engine (see this StackOverflow article or Adam St Johns Blog for more wisdom). The difference lies in how they handle requests. The full engine sets up routes and controllers as if they were part of the hosting app, whereas the mounted engine is mounted in the hosting app and handles its own routing.
Getting started is easy. For a full engine where all requests are handled as part of hosting app, do this:
rails plugin new NameOfEngine --full
For a mountable engine, where we namespace our requests, do this:
rails plugin new NameOfMountableEngine --mountable
Let’s take a look at how a full engine gets requests routed. For a full engine, we’d have something like this in our Hosting application routing file for everything in our mobile engine.
Rails.application.routes.draw do namespace "mobile" do match "/app" => "mobile#app", :via => :get match "/modules" => "mobile#modules", :via => :get match "/start" => "mobile#start", :via => :get match "/tutorials" => "mobile#tutorials", :via => :gets match "/" => "mobile#generic", :via => :get # Any many more /mobile routes.. end # Other hosting application routing goes here, and mixes in with our mobile routes. end
Taken straight from Rails Engine Code Comments –
… sometimes you want to isolate your engine from the application, especially if your engine has its own router. To do that, you simply need to call +isolate_namespace+. This method requires you to pass a module where all your controllers, helpers and models should be nested to.
Great, so now we dont’ have to uglify our routing file AND we get routing and code reusablity across servers for a mounted engine, that means we have our own routing.rb file in our engine:
MobileEngine::Engine.routes.draw do match "/app" => "mobile#app", :via => :get match "/" => "mobile#generic", :via => :get end
We specify the isolate_namespace in our engine file:
module MobileEngine class Engine < ::Rails::Engine isolate_namespace MobileEngine end end
Finally we mount our engine in the hosting app:
Rails.application.routes.draw do mount MobileEngine::Engine => "/mobile" #Other hosted application routing end
The main difference between full and mounted is found in your
module FullEngine class Engine < ::Rails::Engine end end
module MountedEngine class Engine < ::Rails::Engine isolate_namespace MobileEngine end end
Awesome, so now no matter what, any hosted rails application that uses our mounted engine gets all of its routes namespaced by whatever we choose, in this case, /mobile. Awesome.
Engines and routing / matching controllers and views
Quick reminder on Rails routing:
Rails routes are matched in the order they are specified, so if you have a resources :photos above a get ‘photos/poll’ the show action’s route for the resources line will be matched before the get line. To fix this, move the get line above the resources line so that it is matched first.
One thing to note is the way you namespace your engine which is done with using your engine name in your file structure, both in your controllers and engines, such as this structure:
/mobile_engine /app /assets /controllers /mobile_engine application_controller.rb mobile_controller.rb /helpers /mobile_engine /mailers /mobile_engine /models /mobile_engine /views /layouts application.html.erb /mobile_engine /mobile app.html.erb
So for our mobile_controller.rb, we simply have this:
# Note the use of Engine.root for finding its path require File.join(MobileEngine::Engine.root, 'lib', 'mobile_engine', 'file_we_need') module MobileEngine class MobileController < ApplicationController before_filter :adjust_format_for_iphone layout false def app # Code end end end
Let’s look at the full request cycle for mounted engines for “/mobile/app”:
- A request for “/mobile/app” goes through our hosting application, no routes are found for “/mobile/app” for the hosting application routing file, so it looks at the engine.
- The engine has a route, so it then begins looking in the hosting application for the mobile controller
- If the hosting application has no mobile controller, it then begins looking at it’s included engines for a mobile controller
- The engine has a controller, so it uses that controller’s action and goes to render the view.
- The hosting application begins looking for a view in the hosting application for “views/mobile/app”
- If the hosting application did not have a view, it then looks for the view in the engine “views/mobile_engine/mobile/app”
Why is this important? I had an issue where I still had a view in my hosting application that was being used over my engine views – so always note that the hosting application trumps its engines controllers and views. Full overriding capabilities is always the control of the hosting application.
Public folder mashing & configuration
The second point in choosing an engine is leaving all of our mobile assets in our mobile engine gem’s public folder so we don’t have to worry about getting those assets copy/pasted between projects that may want the mobile code. Just plunk your JS/CSS/HTML in your public folders as you normally would and make this modification in your engine.rb file in your engine gem:
Then in our hosting application, we can use those configuration settings via an initializer:
MobileEngine::Engine.config.mobile_app_name = "RaiseMore"
Great, we’ve isolated our namespace to leave all requests for mobile_engine, as well as setting up some configuration settings for what the mobile code may depend on, as well as merged our engine’s public folder with the hosting site application.
One thing to note, when using an Engine, it sets up several helpers for you to use. One of which I use there is root, which is the root path of the engine, not the root path of the hosting application. To get the root path of the application, you can use main_app, like this:
<%= link_to "Home", main_app.root_path %>
Precompiling any assets
My file structure for assets looks like this:
Mobile.js is nothing special, and looks like this:
//= require ./mobile/
Now access them as usual
The big picture
All in all, I’ve really enjoyed learning about Rails Engines and I can already see several uses for them:
- Pulling all of our API calls to an engine so we can place that engine on any web nodes and keep API functionality in one place
- Put together an error handling gem to handle incoming error requests to record and a page to see errors
- Database models / access methods to be used by other projects
I hope I’ve shed some light on Rails Engines and given a few tips/tricks that I’ve found along the way.