Rails的Routes的实现定义在actionpack-3.2.13/lib/action_dispatch/routing
目录下。这里以
TestProject::Application.routes.draw do
resources :users
end
为例,解释Routes的初始化过程和查询的过程。
首先,在Rails中,管理Route的类是ActionDispatch::Routing::RouteSet
。在Rails中,每个Engine有且仅有一个RouteSet
对象,用来管理该Engine下所有Routes。
这里在调用TestProject::Application.routes
方法的时候,会先创建TestProject::Application
的对象,然后对这个对象调用routes
方法,代码定义在railties-3.2.13/lib/rails/engine.rb
内:
def routes
@routes ||= ActionDispatch::Routing::RouteSet.new
@routes.append(&Proc.new) if block_given?
@routes
end
这里ActionDispatch::Routing::RouteSet
的构造方法是:
def initialize(request_class = ActionDispatch::Request)
self.named_routes = NamedRouteCollection.new
self.resources_path_names = self.class.default_resources_path_names.dup
self.default_url_options = {}
self.request_class = request_class
@valid_conditions = {}
request_class.public_instance_methods.each { |m|
@valid_conditions[m.to_sym] = true
}
@valid_conditions[:controller] = true
@valid_conditions[:action] = true
self.valid_conditions.delete(:id)
@append = []
@prepend = []
@disable_clear_and_finalize = false
@finalized = false
@set = Journey::Routes.new
@router = Journey::Router.new(@set, {
:parameters_key => PARAMETERS_KEY,
:request_class => request_class})
@formatter = Journey::Formatter.new @set
end
这个构造函数初始化了管理Routes最重要的几个对象:
- NamedRouteCollection,负责生成所有被命名的URL的Helper方法,并将这些方法集中在一个完全自己维护的module中。
- Journey::Routes是Journey::Route的集合,负责维护和生成Route对象。
- Journey::Route则负责记录一个Route所有元信息。
- Journey::Router负责根据URL地址在Routes中找到Route指定的对象。
读取并解析这个routes的Block的方法是ActionDispatch::Routing::RouteSet
实例下的draw
方法,这个方法定义在actionpack-3.2.13/lib/action_dispatch/routing/route_set.rb
,它的代码是:
def draw(&block)
clear! unless @disable_clear_and_finalize
eval_block(block)
finalize! unless @disable_clear_and_finalize
end
其中clear!
和finalize!
前者负责清理已经设置的routes,后者负责执行append的路由。代码分别是:
def clear!
@finalized = false
named_routes.clear
set.clear
formatter.clear
@prepend.each { |blk| eval_block(blk) }
end
def finalize!
return if @finalized
@append.each { |blk| eval_block(blk) }
@finalized = true
end
由于@disable_clear_and_finalize
并不是默认关闭的,因此这里不再详细解释。
核心方法是eval_block
方法,这个方法的源代码是:
def eval_block(block)
if block.arity == 1
raise "You are using the old router DSL which has been removed in Rails 3.1. " <<
"Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/"
end
mapper = Mapper.new(self)
if default_scope
mapper.with_default_scope(default_scope, &block)
else
mapper.instance_exec(&block)
end
end
从代码当中可以看到,主要内容是创建ActionDispatch::Routing::Mapper
对象并且对它调用方法执行传入的block。
这里由于default_scope
尚未定义,因此需要执行instance_exec
来解析block的代码。
然后就执行resources
方法,这些方法都定义在ActionDispatch::Routing::Mapper
类下,位置在actionpack-3.2.13/lib/action_dispatch/routing/mapper.rb
:
# In Rails, a resourceful route provides a mapping between HTTP verbs
# and URLs and controller actions. By convention, each action also maps
# to particular CRUD operations in a database. A single entry in the
# routing file, such as
#
# resources :photos
#
# creates seven different routes in your application, all mapping to
# the +Photos+ controller:
#
# GET /photos
# GET /photos/new
# POST /photos
# GET /photos/:id
# GET /photos/:id/edit
# PUT /photos/:id
# DELETE /photos/:id
#
# Resources can also be nested infinitely by using this block syntax:
#
# resources :photos do
# resources :comments
# end
#
# This generates the following comments routes:
#
# GET /photos/:photo_id/comments
# GET /photos/:photo_id/comments/new
# POST /photos/:photo_id/comments
# GET /photos/:photo_id/comments/:id
# GET /photos/:photo_id/comments/:id/edit
# PUT /photos/:photo_id/comments/:id
# DELETE /photos/:photo_id/comments/:id
#
# === Options
# Takes same options as <tt>Base#match</tt> as well as:
#
# [:path_names]
# Allows you to change the segment component of the +edit+ and +new+ actions.
# Actions not specified are not changed.
#
# resources :posts, :path_names => { :new => "brand_new" }
#
# The above example will now change /posts/new to /posts/brand_new
#
# [:path]
# Allows you to change the path prefix for the resource.
#
# resources :posts, :path => 'postings'
#
# The resource and all segments will now route to /postings instead of /posts
#
# [:only]
# Only generate routes for the given actions.
#
# resources :cows, :only => :show
# resources :cows, :only => [:show, :index]
#
# [:except]
# Generate all routes except for the given actions.
#
# resources :cows, :except => :show
# resources :cows, :except => [:show, :index]
#
# [:shallow]
# Generates shallow routes for nested resource(s). When placed on a parent resource,
# generates shallow routes for all nested resources.
#
# resources :posts, :shallow => true do
# resources :comments
# end
#
# Is the same as:
#
# resources :posts do
# resources :comments, :except => [:show, :edit, :update, :destroy]
# end
# resources :comments, :only => [:show, :edit, :update, :destroy]
#
# This allows URLs for resources that otherwise would be deeply nested such
# as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
# to be shortened to just <tt>/comments/1234</tt>.
#
# [:shallow_path]
# Prefixes nested shallow routes with the specified path.
#
# scope :shallow_path => "sekret" do
# resources :posts do
# resources :comments, :shallow => true
# end
# end
#
# The +comments+ resource here will have the following routes generated for it:
#
# post_comments GET /posts/:post_id/comments(.:format)
# post_comments POST /posts/:post_id/comments(.:format)
# new_post_comment GET /posts/:post_id/comments/new(.:format)
# edit_comment GET /sekret/comments/:id/edit(.:format)
# comment GET /sekret/comments/:id(.:format)
# comment PUT /sekret/comments/:id(.:format)
# comment DELETE /sekret/comments/:id(.:format)
#
# === Examples
#
# # routes call <tt>Admin::PostsController</tt>
# resources :posts, :module => "admin"
#
# # resource actions are at /admin/posts.
# resources :posts, :path => "admin/posts"
def resources(*resources, &block)
options = resources.extract_options!.dup
if apply_common_behavior_for(:resources, resources, options, &block)
return self
end
resource_scope(:resources, Resource.new(resources.pop, options)) do
yield if block_given?
collection do
get :index if parent_resource.actions.include?(:index)
post :create if parent_resource.actions.include?(:create)
end
new do
get :new
end if parent_resource.actions.include?(:new)
member do
get :edit if parent_resource.actions.include?(:edit)
get :show if parent_resource.actions.include?(:show)
put :update if parent_resource.actions.include?(:update)
delete :destroy if parent_resource.actions.include?(:destroy)
end
end
self
end
首先执行apply_common_behavior_for
方法,这个方法通过可以处理部分选项,然后调用自身来实现其功能:
def apply_common_behavior_for(method, resources, options, &block)
if resources.length > 1
resources.each { |r| send(method, r, options, &block) }
return true
end
if resource_scope?
nested { send(method, resources.pop, options, &block) }
return true
end
options.keys.each do |k|
(options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
end
scope_options = options.slice!(*RESOURCE_OPTIONS)
unless scope_options.empty?
scope(scope_options) do
send(method, resources.pop, options, &block)
end
return true
end
unless action_options?(options)
options.merge!(scope_action_options) if scope_action_options?
end
false
end
def resource_scope?
[:resource, :resources].include? @scope[:scope_level]
end
def action_options?(options)
options[:only] || options[:except]
end
def scope_action_options
@scope[:options].slice(:only, :except)
end
从代码中可以看到,由于apply_common_behavior_for
处理所有resources
数量大于1的情况,因此在执行到这句语句之后所有语句都不再考虑resources包含多个元素的可能性。
然后将创建一个Resource
对象以封装resource的功能。将Resource
对象传入resource_scope
方法中,这个方法将为block中的代码设置一个scope:
def resource_scope(kind, resource)
with_scope_level(kind, resource) do
scope(parent_resource.resource_scope) do
yield
end
end
end
可以看到这个方法由两部分构成:
def with_scope_level(kind, resource = parent_resource)
old, @scope[:scope_level] = @scope[:scope_level], kind
old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
yield
ensure
@scope[:scope_level] = old
@scope[:scope_level_resource] = old_resource
end
with_scope_level
将scope设置在@scope
对象中的:scope_level
和:scope_level_resource
两项中。
def scope(*args)
options = args.extract_options!
options = options.dup
options[:path] = args.first if args.first.is_a?(String)
recover = {}
options[:constraints] ||= {}
unless options[:constraints].is_a?(Hash)
block, options[:constraints] = options[:constraints], {}
end
scope_options.each do |option|
if value = options.delete(option)
recover[option] = @scope[option]
@scope[option] = send("merge_#{option}_scope", @scope[option], value)
end
end
recover[:block] = @scope[:blocks]
@scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
recover[:options] = @scope[:options]
@scope[:options] = merge_options_scope(@scope[:options], options)
yield
self
ensure
scope_options.each do |option|
@scope[option] = recover[option] if recover.has_key?(option)
end
@scope[:options] = recover[:options]
@scope[:blocks] = recover[:block]
end
scope
方法主要是将options merge到@scope
中,根据参数的不同,将会调用merge_#{option}_scope
方法来实现不同的merge方法。凡是被覆盖的@scope
的参数也会保存在一个叫recover
的hash中,这样在退出这个scope
的时候会恢复原来所有@scope
参数。
然后分析传入resources
的block的代码:
collection do
get :index if parent_resource.actions.include?(:index)
post :create if parent_resource.actions.include?(:create)
end
new do
get :new
end if parent_resource.actions.include?(:new)
member do
get :edit if parent_resource.actions.include?(:edit)
get :show if parent_resource.actions.include?(:show)
put :update if parent_resource.actions.include?(:update)
delete :destroy if parent_resource.actions.include?(:destroy)
end
其中collection
,new
,member
这三个方法的构成几乎一致:
# To add a route to the collection:
#
# resources :photos do
# collection do
# get 'search'
# end
# end
#
# This will enable Rails to recognize paths such as <tt>/photos/search</tt>
# with GET, and route to the search action of +PhotosController+. It will also
# create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
# route helpers.
def collection
unless resource_scope?
raise ArgumentError, "can't use collection outside resource(s) scope"
end
with_scope_level(:collection) do
scope(parent_resource.collection_scope) do
yield
end
end
end
# To add a member route, add a member block into the resource block:
#
# resources :photos do
# member do
# get 'preview'
# end
# end
#
# This will recognize <tt>/photos/1/preview</tt> with GET, and route to the
# preview action of +PhotosController+. It will also create the
# <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
def member
unless resource_scope?
raise ArgumentError, "can't use member outside resource(s) scope"
end
with_scope_level(:member) do
scope(parent_resource.member_scope) do
yield
end
end
end
def new
unless resource_scope?
raise ArgumentError, "can't use new outside resource(s) scope"
end
with_scope_level(:new) do
scope(parent_resource.new_scope(action_path(:new))) do
yield
end
end
end
这三个方法的差异主要除了with_scope_level
设置的@scope
不同以外,主要是传入scope
的参数区别很大,collection
是生成类似于/#{controller_name}
的路径,member
是生成类似与/#{controller_name}/:id
的路径,new
生成类似于/#{controller_name}/new
的路径。
在执行设置每个路径的方法前,都会执行parent_resource.actions.include?
方法判断是否设置这个路径,这个方法的功能是,将定义scope时设置的:only
或者:except
的action起效:
def actions
if only = @options[:only]
Array(only).map(&:to_sym)
elsif except = @options[:except]
default_actions - Array(except).map(&:to_sym)
else
default_actions
end
end
def default_actions
[:index, :create, :new, :show, :update, :destroy, :edit]
end
接着就是在代码中各种设置Restful Route的方法了,这些方法实际实现都调用了相同的方法:
# Define a route that only recognizes HTTP GET.
# For supported arguments, see <tt>Base#match</tt>.
#
# Example:
#
# get 'bacon', :to => 'food#bacon'
def get(*args, &block)
map_method(:get, *args, &block)
end
# Define a route that only recognizes HTTP POST.
# For supported arguments, see <tt>Base#match</tt>.
#
# Example:
#
# post 'bacon', :to => 'food#bacon'
def post(*args, &block)
map_method(:post, *args, &block)
end
# Define a route that only recognizes HTTP PUT.
# For supported arguments, see <tt>Base#match</tt>.
#
# Example:
#
# put 'bacon', :to => 'food#bacon'
def put(*args, &block)
map_method(:put, *args, &block)
end
# Define a route that only recognizes HTTP PUT.
# For supported arguments, see <tt>Base#match</tt>.
#
# Example:
#
# delete 'broccoli', :to => 'food#broccoli'
def delete(*args, &block)
map_method(:delete, *args, &block)
end
private
def map_method(method, *args, &block)
options = args.extract_options!
options[:via] = method
args.push(options)
match(*args, &block)
self
end
可以看到它们调用的共同方法就是map_method
,而调用map_method
实际上就是调用带:via
参数的match
函数:
def match(path, *rest)
if rest.empty? && Hash === path
options = path
path, to = options.find { |name, value| name.is_a?(String) }
options[:to] = to
options.delete(path)
paths = [path]
else
options = rest.pop || {}
paths = [path] + rest
end
path_without_format = path.to_s.sub(/\(\.:format\)$/, '')
if using_match_shorthand?(path_without_format, options)
options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1')
end
options[:anchor] = true unless options.key?(:anchor)
if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
end
paths.each { |_path| decomposed_match(_path, options.dup) }
self
end
match
方法初始部分都是关于参数的处理,如果传入的options是个Hash的话,则找到一个key是字符串的key value对,比如'/users' => 'users#index'
,将后一部分变成key为:to
的options,前一部分则作为路径记录进数组。如果不是的话,仅最后一个参数是选项,其余都作为路径记录进数组。
using_match_shorthand?
判断路径是否是一个URL的相对路径,如果是的话,且又没有生成过to或是action选项的话,把地址中的第一个字符/
去除,之后的第一个/
转换成#
,以此把controller和action分开,放入key为:to
的选项:
def using_match_shorthand?(path, options)
path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
end
最后调用decomposed_match
方法添加routes:
def decomposed_match(path, options) # :nodoc:
if on = options.delete(:on)
send(on) { decomposed_match(path, options) }
else
case @scope[:scope_level]
when :resources
nested { decomposed_match(path, options) }
when :resource
member { decomposed_match(path, options) }
else
add_route(path, options)
end
end
end
decomposed_match
负责对带有嵌套性质的参数的实现,如果是:on
参数,相当于直接调用:on
参数对应值的方法,然后将自身放在一个block里传入。如果有:scope_level
参数(意味上级scope)的值为resources或是resource的话,前者调用nested
,后者调用member
去处理,如果都没有,才调用核心方法add_route
:
def add_route(action, options) # :nodoc:
path = path_for_action(action, options.delete(:path))
action = action.to_s.dup
if action =~ /^[\w\/]+$/
options[:action] ||= action unless action.include?("/")
else
action = nil
end
if !options.fetch(:as, true)
options.delete(:as)
else
options[:as] = name_for_action(options[:as], action)
end
mapping = Mapping.new(@set, @scope, path, options)
app, conditions, requirements, defaults, as, anchor = mapping.to_route
@set.add_route(app, conditions, requirements, defaults, as, anchor)
end
首先,先要为action生成路径,生成的方法是path_for_action
:
def path_for_action(action, path)
prefix = shallow_scoping? ?
"#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path]
path = if canonical_action?(action, path.blank?)
prefix.to_s
else
"#{prefix}/#{action_path(action, path)}"
end
end
首先确定是不是shallow scope,二者生成的前缀并不相同,差异在例子中已经提到:
# [:shallow_path]
# Prefixes nested shallow routes with the specified path.
#
# scope :shallow_path => "sekret" do
# resources :posts do
# resources :comments, :shallow => true
# end
# end
#
# The +comments+ resource here will have the following routes generated for it:
#
# post_comments GET /posts/:post_id/comments(.:format)
# post_comments POST /posts/:post_id/comments(.:format)
# new_post_comment GET /posts/:post_id/comments/new(.:format)
# edit_comment GET /sekret/comments/:id/edit(.:format)
# comment GET /sekret/comments/:id(.:format)
# comment PUT /sekret/comments/:id(.:format)
# comment DELETE /sekret/comments/:id(.:format)
随后判断是不是canonical_action?
,判断条件是:
def canonical_action?(action, flag)
flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
end
def resource_method_scope?
[:collection, :member, :new].include? @scope[:scope_level]
end
由于resource中除edit以外其余操作均属于canonical_action,因此生成的URL都不会包含action的名字,仅仅只有前缀部分,也就是controller部分。
接着add_route
继续处理:action
参数和:as
参数。其中处理:as
参数用到了name_for_action
方法来生成url helper:
def name_for_action(as, action)
prefix = prefix_name_for_action(as, action)
prefix = Mapper.normalize_name(prefix) if prefix
name_prefix = @scope[:as]
if parent_resource
return nil unless as || action
collection_name = parent_resource.collection_name
member_name = parent_resource.member_name
end
name = case @scope[:scope_level]
when :nested
[name_prefix, prefix]
when :collection
[prefix, name_prefix, collection_name]
when :new
[prefix, :new, name_prefix, member_name]
when :member
[prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name]
when :root
[name_prefix, collection_name, prefix]
else
[name_prefix, member_name, prefix]
end
if candidate = name.select(&:present?).join("_").presence
# If a name was not explicitly given, we check if it is valid
# and return nil in case it isn't. Otherwise, we pass the invalid name
# forward so the underlying router engine treats it and raises an exception.
if as.nil?
candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i
else
candidate
end
end
end
首先通过prefix_name_for_action
方法获取到action的前缀:
def prefix_name_for_action(as, action)
if as
as.to_s
elsif !canonical_action?(action, @scope[:scope_level])
action.to_s
end
end
可以看到前缀主要是:as
参数和调用canonical_action?
方法。
如果有前缀的话,还需要调用Mapper.normalize_name
来正规化:
def self.normalize_name(name)
normalize_path(name)[1..-1].gsub("/", "_")
end
# Invokes Rack::Mount::Utils.normalize path and ensure that
# (:locale) becomes (/:locale) instead of /(:locale). Except
# for root cases, where the latter is the correct one.
def self.normalize_path(path)
path = Journey::Router::Utils.normalize_path(path)
path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
path
end
可以看到这个操作主要实现的功能是,除去多余的/
,将/
转换成_
。接着,导出collection或者是member的名字,然后根据:scope_level
的不同,返回不同的数组,去除数组中nil的部分,用_
连接在一起,得到候选名字,最后再在已经存在的routes中进行搜索,如果已经存在这个名字,或是名字本身有非法字符,将返回nil,否则即为这个route的正式名字。
然后生成一个Mapping
对象,调用其to_route
方法获取app, conditions, requirements, defaults, as, anchor
这样六个参数,传入@set.add_route
这一核心方法即可完成添加route的过程。
Mapping
的构造函数如下:
def initialize(set, scope, path, options)
@set, @scope = set, scope
@options = (@scope[:options] || {}).merge(options)
@path = normalize_path(path)
normalize_options!
end
可以看到主要是一个normalize_path
的过程和一个normalize_options
的过程。
normalize_path
的代码如下:
def normalize_path(path)
raise ArgumentError, "path is required" if path.blank?
path = Mapper.normalize_path(path)
if path.match(':controller')
raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module]
# Add a default constraint for :controller path segments that matches namespaced
# controllers with default routes like :controller/:action/:id(.:format), e.g:
# GET /admin/products/show/1
# => { :controller => 'admin/products', :action => 'show', :id => '1' }
@options[:controller] ||= /.+?/
end
# Add a constraint for wildcard route to make it non-greedy and match the
# optional format part of the route by default
if path.match(WILDCARD_PATH) && @options[:format] != false
@options[$1.to_sym] ||= /.+?/
end
if @options[:format] == false
@options.delete(:format)
path
elsif path.include?(":format") || path.end_with?('/')
path
elsif @options[:format] == true
"#{path}.:format"
else
"#{path}(.:format)"
end
end
首先依然是Mapper.normalize_path
方法,之前已经解释过。然后如果路径中包含了:controller
,将设置:controller
在@options
中,值为正则表达式/.+?/
。对于存在*
通配符的路径,并且没有关闭format功能,在@options
中设置同名选项,值也是这个正则表达式。最后根据是否开启或关闭了format功能决定是否在尾部添加.:format
或是(.:format)
路径。
随后是normalize_options!
操作:
def normalize_options!
path_without_format = @path.sub(/\(\.:format\)$/, '')
@options.merge!(default_controller_and_action)
requirements.each do |name, requirement|
# segment_keys.include?(k.to_s) || k == :controller
next unless Regexp === requirement && !constraints[name]
if requirement.source =~ ANCHOR_CHARACTERS_REGEX
raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
end
if requirement.multiline?
raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
end
end
end
首先调用default_controller_and_action
设置默认的controller和action:
def default_controller_and_action
if to.respond_to?(:call)
{ }
else
if to.is_a?(String)
controller, action = to.split('#')
elsif to.is_a?(Symbol)
action = to.to_s
end
controller ||= default_controller
action ||= default_action
unless controller.is_a?(Regexp)
controller = [@scope[:module], controller].compact.join("/").presence
end
if controller.is_a?(String) && controller =~ %r{\A/}
raise ArgumentError, "controller name should not start with a slash"
end
controller = controller.to_s unless controller.is_a?(Regexp)
action = action.to_s unless action.is_a?(Regexp)
if controller.blank? && segment_keys.exclude?("controller")
raise ArgumentError, "missing :controller"
end
if action.blank? && segment_keys.exclude?("action")
raise ArgumentError, "missing :action"
end
hash = {}
hash[:controller] = controller unless controller.blank?
hash[:action] = action unless action.blank?
hash
end
end
可以看到,首先从@options[:to]
参数中取出值并且针对#
进行分割。或者当:to
参数是Symbol
类型时转换成String
类型赋值给action。如果这个操作并没有使controller或是action得到赋值,调用default_controller
和default_action
方法:
def default_controller
@options[:controller] || @scope[:controller]
end
def default_action
@options[:action] || @scope[:action]
end
然后再将@scope[:module]
应用进去,如果这样还没有获取到controller或是action,并且path中也未包含:controller
或是:action
,则抛出异常,否则将获得到的controller和action返回。
随后对requirements
进行检查,requirements
是@options[:constraints]
,@scope[:constraints]
还有@options
中所有值为正则表达式的项结合的结果:
def requirements
@requirements ||= (@options[:constraints].is_a?(Hash) ? @options[:constraints] : {}).tap do |requirements|
requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints]
@options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) }
end
end
随后,当requirements中存在正则表达式,并且在表达式中具有\A,^,\z,\Z,$这样具备表示句首或句末的功能,或是允许多行的特性,都将抛掷异常。
to_route
调用方法生成各个返回值予以返回:
def to_route
[ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
end
app
用以返回一个Constraints
对象:
def app
Constraints.new(
to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
blocks,
@set.request_class
)
end
Constraints
封装了判断一个URL是否符合路径constraint的操作,另外包含一个Middleware供调用。其中用到了生成@constraints
对象的方法blocks
:
def blocks
constraints = @options[:constraints]
if constraints.present? && !constraints.is_a?(Hash)
[constraints]
else
@scope[:blocks] || []
end
end
conditions
也增加了一些限制选项:
def conditions
{ :path_info => @path }.merge(constraints).merge(request_method_condition)
end
期间request_method_condition
设置了与request method相关的限制。
defaults
代表一些其他默认选项:
def defaults
@defaults ||= (@options[:defaults] || {}).tap do |defaults|
defaults.reverse_merge!(@scope[:defaults]) if @scope[:defaults]
@options.each { |k, v| defaults[k] = v unless v.is_a?(Regexp) || IGNORE_OPTIONS.include?(k.to_sym) }
end
end
defaults
也由@options[:defaults]
和@scope[:defaults]
merge而成,同时@options中不是标准选项的部分也会放入@defaults
中。
@set.add_route
是更加核心的部分,这里重新退回到ActionDispatch::Routing::RouteSet
的代码中:
def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor)
conditions = build_conditions(conditions, valid_conditions, path.names.map { |x| x.to_sym })
route = @set.add_route(app, path, conditions, defaults, name)
named_routes[name] = route if name
route
end
可以看到,这个add_route
方法主要是调用build_path
创建path对象,build_conditions
创建conditions对象。然后调用@set.add_route
将route放入,再将route名字放入named_routes
对象即可。这里将逐一分析每个操作:
def build_path(path, requirements, separators, anchor)
strexp = Journey::Router::Strexp.new(
path,
requirements,
SEPARATORS,
anchor)
pattern = Journey::Path::Pattern.new(strexp)
builder = Journey::GTG::Builder.new pattern.spec
# Get all the symbol nodes followed by literals that are not the
# dummy node.
symbols = pattern.spec.grep(Journey::Nodes::Symbol).find_all { |n|
builder.followpos(n).first.literal?
}
# Get all the symbol nodes preceded by literals.
symbols.concat pattern.spec.find_all(&:literal?).map { |n|
builder.followpos(n).first
}.find_all(&:symbol?)
symbols.each { |x|
x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/
}
pattern
end
这里将先创建Journey::Router::Strexp
对象,然后根据这个对象创建Journey::Path::Pattern
对象,然后根据结果调用spec
方法返回一个Journey::Nodes::Cat
对象创建Journey::GTG::Builder
对象,Builder
对象将path按照语法树分离。随后对pattern中符合要求的symbol进行处理,随后返回了pattern对象。
def build_conditions(current_conditions, req_predicates, path_values)
conditions = current_conditions.dup
verbs = conditions[:request_method] || []
# Rack-Mount requires that :request_method be a regular expression.
# :request_method represents the HTTP verb that matches this route.
#
# Here we munge values before they get sent on to rack-mount.
unless verbs.empty?
conditions[:request_method] = %r[^#{verbs.join('|')}$]
end
conditions.delete_if { |k,v| !(req_predicates.include?(k) || path_values.include?(k)) }
conditions
end
build_conditions
首先clone了当前的conditions,然后将:request_method
转换成正则表达式,随后去除conditions中key与req_predicates
和path_values
内容相重的部分,其中前者即是RouteSet
的valid_conditions
变量,内容是Request类所有实例方法以及:controller
和:action
,生成代码之前已经展示过:
@valid_conditions = {}
request_class.public_instance_methods.each { |m|
@valid_conditions[m.to_sym] = true
}
@valid_conditions[:controller] = true
@valid_conditions[:action] = true
self.valid_conditions.delete(:id)
path_values
则是pattern中所有Symbol的集合。
这里也有一个@set
变量,但是它是Journey::Routes
类的实例,调用它的add_route
方法将route设置进去。至于Journey内部实现这里不予分析。
named_routes
是ActionDispatch::Routing::RouteSet::NamedRouteCollection
类的实例,这个类的主要作用是维护路由的名字并且定义路由的helper方法。这里调用[]
方法实际是add
方法的alias:
def add(name, route)
routes[name.to_sym] = route
define_named_route_methods(name, route)
end
可以看到除了将route设置到内部的routes
变量中去之外,将通过define_named_route_methods
定义方法:
def define_named_route_methods(name, route)
{:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
hash = route.defaults.merge(:use_route => name).merge(opts)
define_hash_access route, name, kind, hash
define_url_helper route, name, kind, hash
end
end
可以看到这里有两次循环,第一次是定义url,:only_path
为false,第二次定义path,:only_path
为true。对每次循环调用define_hash_access
和define_url_helper
方法。
define_hash_access
定义如下:
def define_hash_access(route, name, kind, options)
selector = hash_access_name(name, kind)
# We use module_eval to avoid leaks
@module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
remove_possible_method :#{selector}
def #{selector}(*args)
options = args.extract_options!
result = #{options.inspect}
if args.size > 0
result[:_positional_args] = args
result[:_positional_keys] = #{route.segment_keys.inspect}
end
result.merge(options)
end
protected :#{selector}
END_EVAL
helpers << selector
end
这里先为定义的方法设置一个名字,这个名字由hash_access_name
生成。定义非常简单:
def hash_access_name(name, kind = :url)
:"hash_for_#{name}_#{kind}"
end
随后这个方法仅仅是将传入这个方法的options参数增加:_positional_keys
和:_positional_args
两项后返回回去:
# Create a url helper allowing ordered parameters to be associated
# with corresponding dynamic segments, so you can do:
#
# foo_url(bar, baz, bang)
#
# Instead of:
#
# foo_url(:bar => bar, :baz => baz, :bang => bang)
#
# Also allow options hash, so you can do:
#
# foo_url(bar, baz, bang, :sort_by => 'baz')
#
def define_url_helper(route, name, kind, options)
selector = url_helper_name(name, kind)
hash_access_method = hash_access_name(name, kind)
@module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
remove_possible_method :#{selector}
def #{selector}(*args)
url_for(#{hash_access_method}(*args))
end
END_EVAL
helpers << selector
end
url_helper_name
返回即将创建的helper的方法名:
def url_helper_name(name, kind = :url)
:"#{name}_#{kind}"
end
这里调用了之前创建的hash_for_*
方法,将它们传入url_for既可得到结果。至于url_for
的实现并不复杂,这里不予解释。
至此添加route的过程已经完成。
在Journey的帮助下,查询是相当简单的,只需要在Rails middleware stack的endpoint进行判断即可,代码就是ActionDispatch::Routing::RouteSet
的call
方法:
def call(env)
finalize!
@router.call(env)
end
这里的@router
对象是Journey::Router
对象,调用它的call
方法传入rails env即可实现查询功能。当查询到了正确的结果,将会调用route结果的app对象,即当初传入@set.add_route
的第一个参数app的call
方法,具体的处理将会在下一章详细解释。而如果查询失败,则返回404信息,然后交还给middleware进行处理。