Skip to content

Latest commit

 

History

History
1095 lines (933 loc) · 35.7 KB

04.routes.md

File metadata and controls

1095 lines (933 loc) · 35.7 KB

An Http Request Through Rails

04. Routes

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

其中collectionnewmember这三个方法的构成几乎一致:

# 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_controllerdefault_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_predicatespath_values内容相重的部分,其中前者即是RouteSetvalid_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_routesActionDispatch::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_accessdefine_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::RouteSetcall方法:

def call(env)
  finalize!
  @router.call(env)
end

这里的@router对象是Journey::Router对象,调用它的call方法传入rails env即可实现查询功能。当查询到了正确的结果,将会调用route结果的app对象,即当初传入@set.add_route的第一个参数app的call方法,具体的处理将会在下一章详细解释。而如果查询失败,则返回404信息,然后交还给middleware进行处理。