Controlling routes loading order from Engines
So in Rails3 Engines come with their own models/controllers/views and of course routes. Now the question is: How do you ensure that Engine routes will be loaded before (or after) application routes and all other Engines that are present?
Here's an example of my Rails app routes:
match '*(path)', :to => 'foo_controller#bar_action'开发者_如何学编程
And my Engine:
match '/news', :to => 'bar_controller#foo_action'
So by default Engines routes will be loaded after the application ones. This means that Engine routes are inaccessible due to that catch-all route in my app. How can force Engine routes to be loaded first (or last)?
What you're trying to do is kinda difficult. As has been mentioned engine routes are loaded after the app routes and overriding this behaviour may be problematic. I can think of several things that you can try.
Use An Initializer After Routing Paths Initializer
There is an initializer in engine.rb
inside the rails source, one way to accomplish what you're after is to try to hook into the functionality that it deals with. The initializer looks like this by default:
initializer :add_routing_paths do |app|
paths.config.routes.to_a.each do |route|
app.routes_reloader.paths.unshift(route) if File.exists?(route)
end
end
Essentially, this should take the paths to all the routes files that Rails knows about and try and add them to the routes reloader (the thing that reloades your routes file automagically for you if it is changed). You can define another initializer to be executed right after this one, you will then inspect the paths stored in the routes reloader, pull out the path that belongs to your engine, remove it from the paths array and insert it back, but at the end of the paths array. So, in your config/application.rb
:
class Application < Rails::Application
initializer :munge_routing_paths, :after => :add_routing_paths do |app|
engine_routes_path = app.routes_reloader.paths.select{|path| path =~ /<regex that matches path to my engine>/}.first
app.routes_reloader.paths.delete(engine_routes_path)
app.routes_reloader.paths << engine_routes_path
end
end
This may or may not work, either way I don't really recommend it, it's not particularly elegant (i.e. ugly hack playing with the guts of rails).
Use Rails 3.1
This may not be an option, but if it is, I'd probably go with this one. In Rails 3.1 you can have 2 different types of Engines, full and mountable (here is an SO question talking about some of the differences). But in essence you would alter your Engine to be a mountable engine, the routes in a mountable engine are namespaced and you can explicitly include them in the routes file of your main app e.g.:
Rails.application.routes.draw do
mount MyEngine::Engine => "/news"
end
You can also scope your mounted engine routes and do all sorts of other fancy routy things (more info here). Long story short, if you can go to 3.1 then this is the approach to use.
Dynamically Insert The Routes From Your Engine Into Your Main App
One of the most well known Rails engines around at the moment is Devise. Now, devise is an engine that will potentially add quite a number of routes to your app, but if you'll look at the devise source you'll see that it doesn't actually have a config/routes.rb
file at all! This is because devise dynamically adds its routing goodness to your main app's routes.rb
file.
When you run the model generator that comes with devise, one of the things the generator will do is add a line such as devise_for :model
at the top of your routes.rb file, right after the Rails.application.routes.draw do
line. So your route.rb looks similar to this after you execute a generator to create a User model:
Rails.application.routes.draw do
devise_for :users
...
end
Now, devise_for is a magical method that comes as part of devise (in lib/devise/rails/routes.rb
), but in essence it will create a bunch of regular routes that we all know based on the model that you generated.
The thing we need to know, is how devise insert this line in the apps routes.rb
file, then we can write a generator in our engine that will insert any of our routes at the top of the main apps routes.rb
file. For this we look at lib/generators/devise/devise_generator.rb
. In the add_devise_routes
method the last line is route devise_route
. Route
is a Thor action which inserts the string passed to it into the main app's routes.rb
file. So we can write a generator of our own and do something similar e.g.:
class MyCrazyGenerator < Rails::Generators::NamedBase
...
def add_my_crazy_routes
my_route = "match '/news', :to => 'bar_controller#foo_action'"
route my_route
end
end
Of course you would need to make sure all the generator infrastructure is in place but that's the essence of it. Devise is written by some very smart rails dudes and used by quite a lot of people, emulating what they do is likely a pretty good way to go. Out of the 3 things that I suggested this one would be the way I would handle your issue (given that moving to rails 3.1 is probably not an option).
Having the same issue right now. One other not particularly elegant but safe solution I came up with is adding another engine gem at the end of your gem file, just containing the catch all route, nothing else.
Edit: Actually, counterintuitively, the routing gem needs to be listed before all other engine gems for its routes to be loaded last.
You can write a method in your engine that can be called to configure your engine's routes:
module MyEngine
class Engine < Rails::Engine
def self.configure_routes(mapper)
mapper.match '/news', :to => 'news_controller#index'
end
end
end
This allows you to insert your engine's routes before a catch-all in my_application/config/routes.rb
:
Rails.application.routes.draw do
# routes
MyEngine::Engine.configure_routes(self)
# more routes
# catch-all route
end
Then in the engine's my_engine/config/routes.rb
file:
unless Rails.application.routes.named_routes.key?(:news)
Rails.application.routes.draw do
MyEngine::Engine.configure_routes self
end
end
If you're mounting the engine and don't include the explicit route configuration method, the engine routes will be added as usual to the end of the application routes.
I'm not an expert, but I was playing with sferik/rails_admin yesterday when I came across their routing solution. With their rails_admin:install
rake task, they directly modify your config/routes.rb
file by adding mount RailsAdmin::Engine => '/admin', :as => 'rails_admin'
at the beginning of the file so they are sure their routes always have the highest priority. That mount
method refers to their own routes.rb :
RailsAdmin::Engine.routes.draw do
# Prefix route urls with "admin" and route names with "rails_admin_"
scope "history", :as => "history" do
controller "history" do
[...]
end
end
end
Could this be your solution?
精彩评论