Rails Reading Rails - inside into routes

soulspirit1229 · August 09, 2015 · Last by soulspirit1229 replied at August 10, 2015 · 2513 hits

Reading Rails - inside into routes

前言

今天刚去看了破风,两句话一直在我耳边,与诸君共勉:

  1. 风在前,无惧
  2. 人生不应该为了赢而搞乱自己。

这两句话都是人生态度,一刚一柔,一挑战一接受。面对生活,我们要努力向前。而一旦生活给了我们失败,我们也应该坦然接受,不要怨天尤人。

Tips

先介绍个小技巧,当我们在判断一个路径是否能 match 到我们自己编写的 controller 的时候,我们可以使用下面的方法: 在 Rails console 中使用 recoginzie_path 方法。

[7] pry(main)> app._routes.recognize_path("http://localhost:3000/ninja/users/11")
=> {:action=>"show", :controller=>"admins/users", :id=>"11"}

源代码解读

Route 的代码并不好读,第一个原因是相比其他的模块,route 的注释较少。第二个原因是:约定先于配置使得代码需要处理的情况比较多,同时路径的匹配算法也不容易理解。但我们今天还是通过尝试解读源码来理解

  1. routes.rb 是怎么起作用的
  2. request 请求又是如何与 controller 相匹配的

Routes 的产生

首先来看个例子,用代码来实现这样的输入输出

# hash = config do
#   namespace :users do
#     config :session_timeout, 30
#     config :minimum_password_length, 20
#     namespace :nickname do
#             config :default, "Nick"
#             config :max_length, 50
#     end
#   end
# end
# 
# { “users.session_timeout”: 30, “users.minimum_password_length”: 20, “users.nickname.max_length”: 50, “users.nickname.default”: “Nick”}

大家应该发现了,这个 config 的实现结构跟路由的结构很像,所以我尝试用路由的实现方式来实现了一下,其结果如下:

def config(&block)
  eval_block(&block)
end

def eval_block(&block)
  mapper = Mapper.new
  mapper.instance_exec(&block)
  p mapper.config_hash
end

class Scope

  def initialize(name,parent,scope_level)
    @name = name.to_s
    @parent = parent
    @scope_level = scope_level
  end

  def name
    if @parent && @parent.name != ""
      "#{@parent.name}.#{@name}"
    else
      @name
    end
  end
end

class Mapper

  attr_accessor :config_hash

  def initialize
    @scope = Scope.new("",nil,"")
    @config_hash = {}
  end

  def namespace(name, &block)
    old, new_scope = @scope, Scope.new(name, @scope, "namespace")
    @scope = new_scope
    # apply_behaiver_for(name, new_scope, &block)

    yield if block_given?
    self
  ensure
    @scope = old
  end

  def config(*options)
    merge_config_name(options[0], options[1])
  end

  def merge_config_name(key,value)
    config_hash[@scope.name + "." + key.to_s] = value
  end
end

要想理解 Rails 中 Routes 的生成,这个看懂就理解了一半,我们就可以比较好深入的了解 routes 的实现原理了。这其中 Scope 类非常重要,在最终生成 config 的时候,就是取出 scope 链中的实例变量来拼装。Rails 中也是如此,路由的最终拼装也是根据 scope 的变量来进行拼装的。 以最简单的 routes.rb 为例来开始分析:

Rails.application.routes.draw do
  root to: 'home#index'
  ...

这里的 Rails.application.routes 调用的是 Engine 中的 routes 方法

def routes
  @routes ||= ActionDispatch::Routing::RouteSet.new
  @routes.append(&Proc.new) if block_given?
  @routes
end

Rails.application.routes 就是 Routing::RouteSet, 一个 Application 包含一个 RouteSet,一个 RouteSet 包含 N 个 Route,一个 Route 就代表一个路由信息。接下来调用 RouteSet 中的 draw 方法。

def draw(&block)
  clear! unless @disable_clear_and_finalize
  eval_block(block)
  finalize! unless @disable_clear_and_finalize
  nil
end

def eval_block(block)
  ...
  mapper = Mapper.new(self)
  if default_scope
    mapper.with_default_scope(default_scope, &block)
  else
    mapper.instance_exec(&block)
  end
end

draw 方法中比较重要的是 eval_block 方法。而例子中 root to: 'home#index' 作为 block 方法传入到这个方法中。eval_block(block) 的这句代码带入了 routing 中最重要的 Mapper 类。eval_block 方法执行的是 mapper.instance_exec(&block),定义在 routes.rb 中的方法是由 Mapper 这个类来负责解析。我们来看看这个重要的 Mapper 类。

Mapper

  1. 解析 routes.rb 中的 block,生成代表路由的 Route
  2. 最重要的方法:add_route,定义在 routes.rb 中的 resource,match,get,post 等方法最终执行的都是这个方法。
module ActionDispatch
  module Routing
    class Mapper
      ...
      include Base
      include HttpHelpers
      include Redirection
      include Scoping
      include Concerns
      include Resources

我们先查看 Mapping 的祖先链

Mapping.ancestors:
[ActionDispatch::Routing::Mapper, ActionDispatch::Routing::Mapper::Resources, ActionDispatch::Routing::Mapper::Concerns, ActionDispatch::Routing::Mapper::Scoping, ActionDispatch::Routing::Redirection, ActionDispatch::Routing::Mapper::HttpHelpers, ActionDispatch::Routing::Mapper::Base, Object, PP::ObjectMixin, Delayed::MessageSending, ActiveSupport::Dependencies::Loadable, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]

Mapper include 这些 module,比如 Base 中定义了 3 个方法:root,match,mount.而 HttpHelpers 定义了 get,post 等方法。这些方法就是 routes 中使用的常用方法。所以如果我们通过打开 Mapper 类,定义方法,那么 routes.rb 中就可以使用自己定义的方法。以 devise 为例,devise 定义了 devise_for 方法。因此 routes.rb 中可以使用 devise_for 方法。

module ActionDispatch::Routing
  class RouteSet
    class Mapper
      def devise_for(*resources)
        ...
        devise_scope mapping.name do
          with_devise_exclusive_scope mapping.fullpath, mapping.name, options do
            routes.each { |mod| send("devise_#{mod}", mapping, mapping.controllers) }
          end
        end
      end

同时,Mapping 的 initialize 方法定义了成员变量:scope 以及 concern 和 nesting。这些变量存储了 scope,concern 以及 nesting 的一些配置。

def initialize(set) #:nodoc:
  @set = set
  @scope = { :path_names => @set.resources_path_names }
  @concerns = {}
  @nesting = []
end

其根据你传入的 hash 参数进行一系列组装(默认的值的加入)后,其最终会调用 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.tr('-', '_') 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, URI.parser.escape(path), options)
  app, conditions, requirements, defaults, as, anchor = mapping.to_route
  @set.add_route(app, conditions, requirements, defaults, as, anchor)
end

我们看到 Mapping 的生成,是传入了当下的@scope@scope中的变量就是你最后生成的路由的最重要信息,举个例子 sprocket-rails 生成的 mapping 长这样,其中@scope就是原生的 scope。

=> #<ActionDispatch::Routing::Mapper::Mapping:0x007fa216bd0820
 @conditions={:path_info=>"/assets", :required_defaults=>[]},
 @constraints={},
 @defaults={},
 @options=
  {:to=>
    #<Sprockets::Environment:0x3fd10a2cd948 root="/Users/soulspirit/GitHub/ninja", paths=["/Users/soulspirit/ninja/app/assets/images", "/Users/soulspirit/ninja/app/assets/javascripts", "/Users/soulspirit/ninja/app/assets/stylesheets",...>,
   :anchor=>false,
   :format=>false},
 @path="/assets",
 @requirements={},
 @scope={:path_names=>{:new=>"new", :edit=>"edit"}},
 @segment_keys=[],
 @set=#<ActionDispatch::Routing::RouteSet:0x007fa215cfb638>>

总结:

  1. 一个 Rails 应用中会有一个 RouteSet,可以通过 Rails.application.routes 访问。
  2. RouteSet 中有一个 Routes,我们可以通过 Rails.application.routes.routes 或者 Rails.application.routes.set 来访问,它保存了 Rails 中的 routes。
  3. Routes 中有一个 routes 数组,这其中就是保存的 routes。
  4. RouteSet 和 Routers 中各维护了一个 named_routes。
  5. RouteSet 中包含 router,routes 其中 router 是包含了 routes。 Rails.application.routes.router.routes == Rails.application.routes.routes
  6. 一个 Route 的模样长这样,它包含一个 Dispatcher。
[18] pry(#<ActionDispatch::Journey::Routes>)> route
=> #<ActionDispatch::Journey::Route:0x007fbc1da52810
 @app=
  #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007fbc1b67ba90
   @controller_class_names=#<ThreadSafe::Cache:0x007fbc1b67b9a0 @backend={}, @default_proc=nil>,
   @defaults={:controller=>"home", :action=>"index"},
   @glob_param=nil>,
 @constraints={:required_defaults=>[:controller, :action], :request_method=>/^GET$/},
 @decorated_ast=nil,
 @defaults={:controller=>"home", :action=>"index"},
 @dispatcher=true,
 @name="root",
 @parts=nil,
 @path=
  #<ActionDispatch::Journey::Path::Pattern:0x007fbc1db4a2e0
   @anchored=true,
   @names=[],
   @offsets=nil,
   @optional_names=nil,
   @re=nil,
   @required_names=nil,
   @requirements={},
   @separators="/.?",
   @spec=#<ActionDispatch::Journey::Nodes::Slash:0x007fbc1db491d8 @left="/", @memo=nil>>,
 @precedence=0,
 @required_defaults=nil,
 @required_parts=nil>

接下来会开始分析路由的匹配。有了解 generalized transition graph (GTG) 和 non-deterministic finite automata (NFA),也请多多指教。

往期回顾

Reading Rails - Rack

暂时还没有深入到这个度~

#1 楼 @lonely21475 Rails 的代码还是比较好看懂的,注释很多

You need to Sign in before reply, if you don't have an account, please Sign up first.