今天刚去看了破风,两句话一直在我耳边,与诸君共勉:
这两句话都是人生态度,一刚一柔,一挑战一接受。面对生活,我们要努力向前。而一旦生活给了我们失败,我们也应该坦然接受,不要怨天尤人。
先介绍个小技巧,当我们在判断一个路径是否能 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 的注释较少。第二个原因是:约定先于配置使得代码需要处理的情况比较多,同时路径的匹配算法也不容易理解。但我们今天还是通过尝试解读源码来理解
首先来看个例子,用代码来实现这样的输入输出
# 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 类。
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>>
总结:
[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),也请多多指教。