注:本帖不是使用手册,仅仅分享自己探索源码的一些理解,以便有共同爱好的朋友一起共勉,本人水平有限,也是初次发帖,恳请大家拍砖轻点
源码版本:4.2.0.beta
Action Pack 是 Rails 的核心框架之一,负责从 Request 到 Response 的整个处理过程,Action Pack 提供了 Request、Routing、Controller、Action、Views、Response 的完整实现,在 MVC 模型中,Action Pack 实现了 Controller 和 View(注:View 的具体实现由 Action View 模块,但 Action Pack 负责 View 的逻辑调用关系)两部分,Action Pack 有两大核心模块:
Action Dispatch:主要负责分析 Web Request 的信息参数,查找用户自定义的路由信息,分发到目标处理程序,一个 Controller’s Action 或者一个 Rack Base Application,又或者一个 Rails Engine,又或者另外一个路由器。目标处理程序处理完成后,返回 Rack Base 的 Response [ status, headers, [body] ],最终返回给应用服务器,通过应用服务器返回客户浏览器。
Action Controller:提供了一个 Base Controller 的实现,Base Controller 提供了 Filters 和 Actions,隐藏了 Controller 与 View、Template 之间的数据传递、调用关系及多种 Helper 模块的自动 Mixin,以便开发者集中精力开发本身的业务逻辑。
Rails 路由是 Action Pack 的核心子系统,属于 Action Dispatch 模块,系统框架图:
类图:
路由的实质是什么,路由就是一种简单的映射关系,路径 Path 到目标 App 的映射,非常简单,无需过多解释,但现实世界往往一句简单的描述,演变成一个复杂的过程和系统,因为在一句简单的描述前面和后面都会加上非常多的限定词,也就是人们的需求往往远大于简单的描述,如 Rails 的 Routing 会加上简单、智能、快速,以及 Rails 的核心价值观“约定优于配置”,这些限定词就让 Rails 的路由系统不那么简单。Rails 路由系统分为三个部分:
Generator
;注:以下所有未注明 module 的类和模块,都默认是 ActionDispatch::Routing
路由映射涉及到的类和模块:Mapper、Mapping、Scope、Resource、SingletonResource
,模块:Base、HttpHelpers、Scoping、Resources、Concerns
,类关系图如下:
Mapper 是路由映射器,所有路由映射都是通过 Mapper 对象来映射,但其核心功能都是通过 include 其他模块来实现,按如下顺序 include 到 Mapper 类中:
include Base
include HttpHelpers
include Redirection
include Scoping
include Concerns
include Resources
注:因为所有 module 都会 include 到 Mapper 类,未注明 module 的方法,都默认是 Mapper 对象的实例方法
其中需要注意的是 Base 模块必须在最前面,因为 Base 模块定义了 root、match、mount 三个模块实例方法,其中 root、match 都会被其 Resources 模块重载。
Rails 中的路由配置通常都是Rails.application.routes.draw do … end
,与其他非 Ruby 框架不一样,其他框架大都是通过配置文件(通常都是 XML 文件)来配置路由,一般配置文件如 XML 文件都是要先加载配置文件,然后通过 XmlParser 分析配置文件,最后通过分析结果进行处理相应的配置。Rails 的路由文件 routes.rb 并不是传统意义上的配置文件,其本身就是一段可以执行的脚本文件,他与其他 Ruby 源文件没有任何区别,这也是 Ruby 语言的强大之处,非常适合开发领域语言 DSL,而 Rails 路由映射的实质就是某种强大的 DSL。Rails 的路由映射具有简单、灵活、强大、智能等特点,对于 Rails 的新手来说,在配置路由时即兴奋又忐忑,简单一条配置语句,如:resources users,就会发生一系列化学变化,产生一系列你需要的结果,八条路由映射,四个路由 helper,可以说是全智能化完成配置和产生 helper 方法,为什么忐忑呢,因为太不可思议了,作为软件开发人员来说,没有做任何代码编写和配置就得到自己需要的结果,内心是有一种逆反的声音,那需要我做啥,我如果在添加一些选项又会产生什么变化呢。软件开发人员本身就喜欢掌控一切,随心所欲的修改和升级,突然觉得自己失去了掌控,无所适从,需要了解真相。
Ruby 编写 DSL 语言非常方便和强大,首先不需要你额外去解析某种文件和特殊语法规则,一切尽在 Ruby 语言中,也就是说 DSL 语言本身也是某种 Ruby 代码,你所看到的不过是某个类或对象的方法而已,我个人的理解就是具有某种参数规则的一系列方法的集合,就是一种 DSL。回到 Rails 的路由映射,其实就是由 root、mount、match、scope、namespace、resource、resources、controller、constraints、defaults、concern、concerns 以及 http 方法如:GET、POST 等一系列配置方法的集合,当然还远不止这些,还有一些 resources 下的二级配置方法如:new、member、collection、nested、shallow、namespace 等。除了 resources 下的二级方法必须在 resources 作用域下,还有就是 concern 及 concerns 有限制外,几乎所有的配置都可以出现在任何地方,甚至哪些带有 block 的方法只要你愿意可以一直嵌套下去。感谢 Ruby 语言的 block,是他使 Ruby 灵活而强大。
在分析之前我们来看看有哪些标准选项:
:path, :as, :to, :on, :via, :shallow_path, :shallow_prefix, :module, :controller, :action, :path_names, :constraints, :shallow, :blocks, :defaults, :options, :only, :except, :param, :anchor, :format, :concerns.
这么多的方法,这么多的选项,要管理好真不是一间容易的事,如果每个方法都是独立的,那么很简单每个方法独立实现就可以,但是 Rails 的路由映射允许有多层嵌套,有上下文关系,同一个方法放在不同的上下文,最终结果都不一样的。所以要理解路由映射,就必须理解上下文(scope 虽然是范围的意思,开始我认为是作用域,但其实质与作用域相去甚远,所以最终用上下文来定义), 上下文是什么,可以把它理解成一个环境对象,具有非常多的属性和方法,属性就是前文列出的标准选项,每一个不同的上下文就是具有不同属性的环境对象。Rails 路由映射上下文的核心就是类 Scope 和 scope 方法,scope 方法可以建立一个新的上下文,每个上下文都是一个 Scope 对象。
研究一个类首先要研究其构造函数和初始化函数,因为这两个函数往往能够看到类最为核心的属性,也就能够大致理解类的实现方式以及与他关系最紧密的类。
def initialize (hash, parent = {}, scope_level = nil)
@hash = hash
@parent = parent
@scope_level = scope_level
end
很简单三个参数,简单三行语句,@hash
存放 Scope 对象的 options 的哈希表,@parent
表明 Scope 上下文实际上是一个链表,就是在路由映射中有非常多的 Scope 对象,通过一个链表连接在一起,同时又具有堆栈的特性,就是当前上下文永远指向最顶端的 Scope 对象,要返回到父级上下文,必须弹出自己,但是请记住在 当前上下文中没有某个 key 值时,当前上下文可以逐级访问父级上下文对应的 key 值,所以我把它当作链表和堆栈的结合体。scope_level 其实很难理解,它与前面堆栈没有任何关系,并不代表这个堆栈的层级,而是表示当前上下文的特性,某种程度上代表是属于什么样的上下文,其取值范围:resource, resources, nested, collection, member, root, new, shallow 等。
OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, :controller, :action, :path_names, :constraints, :shallow, :block, :defaults, :options]
以上就是 Scope 类定义的标准 options,也就是 Scope 只会处理已定义的选项,其他会自动忽略,你也可以把他不支持的选项全部放在:options 键值对中,他仅仅在 Mapping.build 函数中合并选项,options = scope[:options].merge(options) if scope[:options]
def [](key)
@hash.fetch(key) { @parent[key] }
end
该函数实现取当前上下文中的某个选项值,但一定要记住,如果当前上下文为空,逐级取父级上下文的相应 key 的选项值,直到取到为止,这一点非常重要,否则后面非常难以理解上下文嵌套的关系。
def new(hash)
self.class.new hash, self, scope_level
end
顾名思义,新创建一个上下文,把当前上下文作为 parent,以新的选项哈希表覆盖父级上下文选项表,也就是继承与覆盖了当前上下文,继承了当前上下文的选项及 scople_level,但同时用新的 hash 覆盖部分父级上下文选项表,这就是通过函数[](key)
实现的。
def new_level(level)
self.class.new self, self, level
end
顾名思义,新创建一个上下文,把当前上下文作为 parent,完全继承当前上下文的所有选项,仅仅是改变了 scope_level,换言之就是更改为某种特殊的上下文。
def scope(*args)
options = args.extract_options!.dup
scope = {}
options[:path] = args.flatten.join(‘/’) if args.any?
options[:constraints] ||= {}
unless nested_scope?
options[:shallow_path] ||= options[:path] if options.key?(:path)
options[:shallow_prefix] ||= options[:as] if options.key?(:as)
end
if options[:constraints].is_a?(Hash)
defaults = options[:constraints].select do
|k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum))
end
(options[:defaults] ||= {}).reverse_merge!(defaults)
else
block, options[:constraints] = options[:constraints], {}
end
@scope.options.each do |option|
if option == :blocks
value = block
elsif option == :options
value = options
else
value = options.delete(option)
end
if value
scope[option] = send("merge_#{option}_scope", @scope[option], value)
end
@scope = @scope.new scope
yield
self
ensure
@scope = @scope.parent
end
*args 万能参数,它可以传任意个参数,他把任一个参数转换成数组,其中 block 是自动传入,数组中最后一个参数是一个哈希表,如果有哈希表的话,首先分离万能参数的哈希表为 options。
scope ‘admin/’, ‘posts’, path: ‘users’ do … end
options[path] 是多少,是 admin/还是 posts 还是 users,以上都不是,options[path] 为 admin/posts,到处都是陷阱,也就是说传入的路径优于 path:设置的路径。
最为关键的部分是@scope.options.each do …end
,这部分处理通过 scope 方法创建上下文时选项继承方式,既不是简单继承也不是简单覆盖,而是根据传进来不同的选项 key,采取不同的方式,一定要记住显式传进来的选项会根据不同的 key 做不同的拼接处理,通过
scope[option] = send("merge_#{option}_scope", @scope[option], value)
实现,隐式的参数通过 Scope 类的[](key)
方法以 lazy 的方式继承父级上下问的选项参数,就是后面有显式用到的地方才会继承父级上下文。
显式参数的处理 (注以下都忽略 parent 的逻辑判断,具体参照不同的函数实现):
as: #{parent}_#{child} shallow_path: #{parent}/#{child}
path: #{parent}/#{child} shallow_prefix: #{parent}/#{child}
module: #{parent}/#{child} controller: child
action: child shallow: child ? true : false
path_names: merge 相同的话 child 覆盖 parent,不做拼接处理
defaults: merge 相同的话 child 覆盖 parent,不做拼接处理
options: merge 相同的话 child 覆盖 parent,不做拼接处理
blocks: merge 相同的话 child 覆盖 parent,不做拼接处理
constraints: merge 相同的话 child 覆盖 parent,不做拼接处理
only: child 覆盖 parent,不做拼接处理, child 未设置,保留 parent
except: child 覆盖 parent,不做拼接处理, child 未设置,保留 parent
最后一段代码就是上下文堆栈的实现,配合 block,实现上下文的嵌套调用。
root 上下文在 Mapper 的初始化函数中创建:
@scope = Scope.new({ :path_names => @set.resources_path_names })
,@set 为 RouteSet 对象,@set.resources_path_names
为 RouteSet.default_resources_path_names
,其值为: {new: 'new', edit: 'edit' }
,也就是说 root 上下文,是一个非常干净的上下文,仅有 path_names 选项参数。path_names 只有在 resource 和 resources 上下文中才会有作用,而在这两个环境上下文中通过 default_actions 进一步扩展为[:index, :create, :new, :show, :update, :destroy, :edit]
默认设置,所以 root 上下文是一个非常干净的上下文环境。
范例:
match '/posts' => 'posts#index'
其结果为path: /posts
, 目标to: PostsController#index
。
scope path: '/admin', as: 'admin' do #新建上下文,path:/admin, as: admin
scope path: '/user', as: 'user' do #新建上下文, path:/admin/user, as: admin_user
match '/posts' => 'posts#index' #在当前上下文环境中执行。
end
end
path: /admin/user/posts, to: PostsController#index, name: admin_user_posts
。
def namespace(path, options = {})
path = path.to_s
defaults = {
module: path,
path: options.fetch(:path, path),
as: options.fetch(:as, path),
shallow_path: options.fetch(:path, path),
shallow_prefix: options.fetch(:as, path)
}
scope(defaults.merge!(options)) { yield }
end
namespace 就是指定一系列默认值为 path 的 scope 实现,同时支持显式指定选项参数,显式指定参数优先于默认值。 范例:
namespace :admin do
resources posts
end
产生八条路由,路径前缀为/admin
, 名字前缀为admin_
, 目标为Admin::PostsController
控制器。
资源是一种抽象定义,在 RESTful 中定义资源为任何网络实体,每一个资源都有唯一的 URI,在 Rails 中通常对应一种数据模型,Resources 正是 RESTful 的 Rails 实现。核心包括类 Resource,SingletonResource 和模块 Resources。Resources 实现了 resource, resources, member, collection, nested, new
等一系列方法。
VALID_ON_OPTIONS = [:new, :collection, :member]
RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
CANONICAL_ACTIONS = %w(index create new show update destroy)
三个常量定义,具体意义后续说明,按常规先看初始化函数:
def initialize(entities, options = {})
@name = entities.to_s
@path = (options[:path] || @name).to_s
@controller = (options[:controller] || @name).to_s
@as = options[:as]
@param = (options[:param] || :id).to_sym
@options = options
@shallow = false
end
太多有用信息了,一系列默认值设置,这也是约定优于配置的具体表现,如果不显式设置选项参数,那么所有的都是实体的名称,除了 param:参数设为:id 之外。
def plural
@plural ||= name.to_s
end
def singular
@singular ||= name.to_s.singularize
End
def resource_scope
{ :controller => controller }
end
alias :member_name :singular
def collection_name
singular == plural ? "#{plural}_index" : plural
end
def member_scope
"#{path}/:#{param}"
end
alias :shallow_scope :member_scope
def new_scope(new_path)
"#{path}/#{new_path}"
end
def nested_param
:"#{singular}_#{param}"
end
def nested_scope
"#{path}/:#{nested_param}"
End
以上一系列方法,主要分为两类,xxx_scope,xxx_name
,xxx_scope
主要是用于 resource 上下文的二级方法,或者说某种特定上下文环境如 collection, member 等:path 一部分或全部,xxx_name
则是:as 的一部分或全部,请记住与 scope 一样,单个来看 Resource 是没什么意义的,一定要在上下文堆栈中才能得到最终结果。
SingletonResource
是 Resource
的子类,不是一个单例类,而是一个单利资源,就是该资源只有单数形式访问和存在,最典型的例子就是 Profile
,不同之处:
alias :member_name :singular
alias :collection_name :singular
alias :member_scope :path
alias :nested_scope :path
无论是 member 上下文还是 collection 上下文,name 都是单数形式,path 都是指向 options[:as] 或者实体名称,不会做拼接处理,从某种意义上讲都是单数形式,没有复数形式。
如前文定义,为什么 Resource 会单独定义 options 呢,前面讲过 Scope 也定义过一系列标准选项,为什么要重复定义呢,我们先来看一段代码,当我们配置 resources xxx 时,首先会执行 resources 方法,再调用函数 apply_common_behavior_for
,该函数有一段代码:
scope_options = options.slice!(*RESOURCE_OPTIONS)
unless scope_options.empty?
scope(scope_options) do
send(method, resources.pop, options, &block)
end
return true
发现其中秘密没,先挑选出不在 RESOURCE_OPTIONS 之中的选项参数,调用 scope 建立上下文,把挑选出的选项参数传入 scope 方法,并以此建立上下文,在建立的上下文中再次调用 resources 方法。其实质就是,RESOURCE_OPTIONS 之中的选项参数只会影响该 Resource,而不会传入上下文环境中,因为一旦传入上下文,那么就会在后续的上下文中产生某种全局影响,这样就会让 resource 相对独立,聪明的读者很快就会看出问题,等等这样的话,resources 怎么实现嵌套,呵呵,resources 有自己简单的机制来嵌套,后续会详细介绍。
resources 方法恐怕是利用率最高的方法,如前文所述,不用人工干预就会产生八条路由和 4 个 helper,简单实用。
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?
concerns(options[:concerns]) if options[:concerns]
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)
set_member_mappings_for_resource
end
self
end
apply_common_behavior_for
方法就不要详细介绍,该方法非常重要,起到承上启下的作用,但由于篇幅原因,代码就不列出,他的主要功能就是进行递归调用,和一些参数的预处理,如果你传人多个实体资源,那么他会进行递归调用,如果设置 shallow: true,那么会先调用 shallow 方法建立 shallow 上下文;如果是 resources 嵌套调用,那么会建立 nested 上下文;如前文所讲,如果传入非 RESOURCE_OPTIONS
参数,就会建立包含这些选项的上下文, 无论建立哪种上下文,都会在该上下文中再次递归调用 resources。
def with_scope_level(kind)
@scope = @scope.new_level(kind)
yield
ensure
@scope = @scope.parent
end
def resource_scope(kind, resource)
resource.shallow = @scope[:shallow]
@scope = @scope.new(:scope_level_resource => resource)
@nesting.push(resource)
with_scope_level(kind) do
scope(parent_resource.resource_scope) { yield }
end
ensure
@nesting.pop
@scope = @scope.parent
end
with_scope_level
函数,通过前面 Scope 的介绍,应该非常清楚,就是建立一个某种特殊的上下文,如 resources,nested,collection 等,但同时要注意它实现了堆栈操作,这就可以简单实现嵌套调用,嵌套调用本身就是先建立新环境,运行该环境下的代码,代码执行完成后恢复现场,也就是弹出堆栈,此种设计在路由映射中无处不在。resource_scope 函数代码简单,但是逻辑却不简单,算是一个核心函数。注意前文的 resources 方法在调用 resource_scope 时候,传了两个参数,一个是符号,代表的是某种 scope_level,另一个是 Resource 对象,各位看官,要注意了, 传给 resources 方法的 options(经过筛选后的)是传给了 Resource 的构造函数,而不是传给了 scope,也就是说这些 options 只会影响 Resource 对象。
source_scope
函数首先判断 shallow 选项,在 new 一个上下文,仅仅设置 :scope_level_resource 的值,该值非常重要,通过他来访问当前上下文中的 parent_resource,来判断是不是处于 resources 中的嵌套调用,注意不是父级 scope,而是 Resource 对象,在 resources 嵌套调用中起到承上启下的作用,然后压入@nesting
,该值主要用来判断是否是在 resources 嵌套中。通过 with_scope_level 建立新的:resources 上下文,在:resources 上下文中,执行scope(parent_resource.resource_scope)
,parent_resource 就是刚刚传进来的 Resource 对象,该对象的 resource_scope 为{:controller => controller}
,controller 函数返回的是 opintons[:controller] 或者实体资源的名字,这就建立了一个明确指定 :controller 的上下文,而在该 resource 上下文中执行诸如 collection, member 等上下文时,如果不明确指定 :controller,那么都是默认的实体资源对应的 controller,同样具有压栈出栈设计。
Source_scope
函数返回到 resources 方法中,通过 resource_scope
函数建立好 resources 上下文,并在该上下文中执行 block,通过 block 来实现嵌套调用,完成所有嵌套调用后再执行后面的代码。从此可以分析出,任何层级的嵌套调用都可以,并且执行顺序都是最内层的先执行,然后逐级恢复上下文执行该上下文的配置定义,最后在执行顶层(root 上下文的子级上下文)的配置,这样就有很好的隔离作用,就是只有父级影响子孙级,而子级不能影响父级。
紧接着调用 concerns 方法,concern 和 concerns 其实非常简单,concern 字面意思,好像非常高大上,其实就是定义一个 callable,并注册到全局@concerns
中,在 Ruby 语言中,callable 可以是一个 block(实质是一个匿名 Proc 对象)、Proc、Lamda、Method、一个有 call 实例方法的对象或有 call 类方法的类;concerns 就是一个调用,根据名字在@concerns
查找,然后 call 调用 callable,具体产生什么结果,就是根据 callable 的定义和执行 concerns 的上下文来决定的。
接下来调用 collection 方法,先忽略 shallow,shallow 后续详细介绍,建立一个 resource 上下文的子级 collection 上下文,然后执行配置定义,这里执行两个方法:get :index, post :create。
先简单介绍一下 HttpHelpers 模块,该模块定义 5 个方法:get, post, patch, put, delete。五个方法都是通过map_method
实现:
def map_method(method, args, &block)
options = args.extract_options!
options[:via] = method
match(*args, options, &block)
self
end
每个函数传递一个 HTTP method,通过 :via 选项,传递给 match 函数,来实现一条路由映射,match 函数后续会详细介绍。
new, member 与 collection 一样都是在当前 resource 上下文中建立各自的上下文,再执行相应的配置。
new, member 与 collection 一样都是在当前 resource 上下文中建立各自的上下文,再执行相应的配置。现在让我们看看八条路由怎么产生,collection 上下文中,也就是集合路由生成两条 index, create,new 上下文中,生成一条路由 new,member 上下文中,生成五条成员路由(通过函数set_member_mappings_for_resource
):edit, show, update(PUT), update(PATCH) , destroy,其中 update 有两条路由映射,路径和名称都相同,仅仅是 HTTP Method 不一样,分别是 PUT 和 PATCH。至此,八条路由映射全部生成。
match 方法是路由映射核心函数之一,任何路由映射都由 match 方法来完成,那有朋友就会问,既然如此,何不直接介绍 match 方法,我一再强调,Rails 的路由映射中,任何一个方法都不能单独去解析,否则你会得到错误的结论,因为他们都是在不同的上下文中执行,所以都必须在某个上下文中去分析才会得到正确的结论,简单方法如 root、get 等,他们在不同的上下文中都会得到不同的 path 和 name。
match 方法的核心功能就是路由映射,既然是路由映射,那么就需要分析出:path 和:to,如果是命名路由,还需分析出 :name。我们来看看 match 定义:
def match(path, *rest)
根据定义,参数有两种情况,一种是传进来的 path 是一个 Hash,那么他会分析该哈希表,把第一个 key 是字符串类型的键值对当作:path 和:to(这里算不算是一个陷阱呢),同时对于:to 又有四种情况,:to 是一个符号,options[:action] = :to; :to 是一个字符串,并且符合’xxx#xxx’,options[:to] = :to; to 是一个字符串不符合’xxx#xxx’,options[:controller] = :to; :to 既不是符号也不是字符串,options[:to] = :to。另外一种是传进来一个或多个 path,加上一个哈希表,最后把分析好的 path 放到一个数组中。Path 数组分析好后,对每一个 path 根据 options 选项和上下文来分析最终路径和名称,还需注意的是,path 是符号还是字符串,也是有些许不同的,如果是符号且是 Resource 标准 action,并且没有设置 options[:path] 的情况下,最终路径为 @scope[:path].to_s
;如果是一个字符串,会覆盖 options[:path] 的值,最终路径为#{@scope[:path]}/#{action_path(action, path)}
。从这里也可以看出,不同的 path 可以映射到相同的目标。
match 方法最终会为每个 path 调用 add_route
函数,add_route
会调用path_for_action
和name_for_action
两个函数来生成最终的路径和名称,这两个函数非常关键,但是只要真正理解了上下文环境,那么他们就非常简单,只是简单的拼装和连接。有了路径和名称后就调用 Mapping.build 函数生成一路由映射,有了映射,就需要把该映射加入 (通过 RouteSet 对象的 add_route 方法) 到 RouteSet 对象中,至此,一个路由映射就全部完成。
要开启 shallow,有两种方法,一是显式调用 shallow 方法,二是显式设置选项属性:shallow:true
。两个方法实质都是一样,因为 shallow 仅仅生成一个 shallow:true
的上下文,shallow 是有继承性质的,只要子上下文不显式关掉,那么就一直有效的。需注意的是,虽然每个配置方法都可以设置 shallow 属性,但是真正有效只有在 resources 和 resource 这两个上下文中,并不是说只能在 resource 和 resources 方法中设置,而是只有这两个会被 shallow 属性影响。在分析实现之前,来看看shallow_scope
函数定义:
def shallow_scope(path, options = {})
scope = { :as => @scope[:shallow_prefix],
:path => @scope[:shallow_path] }
@scope = @scope.new scope
scope(path, options) { yield }
ensure
@scope = @scope.parent
end
是不是眼熟阿,前面介绍了 resource_scope
,原理都一样,resource_scope
建立一个 resource 上下文,shallow_scope
建立一个 shallow 上下文,非常简单,设置了 :as 和 :path 选项属性,shallow 的秘密就在此,想象一下,如果没有 shallow 的话,:path 会在 scope 时做#{parent}/#{child}
拼接,:as 会执行#{parent}_#{child}
拼接,这样就使在嵌套环境中,一层一层传递下去。Shallow 的目的是什么,是缩短路径和名称,shallow_scope 就是截断了::as 和:path 的传递,指向了固定的值,就是把当前上下文@scope[:as]
和@scope[:path]
设为固定值,就无法向下传递了。
如果能一下就看出下面两个例子的区别并知道为什么,那么就不需要往下看了,因为你已经是专家了。
scope shallow: true, path:'store', as: 'sekret' do
resources :books do
resources :dirs do
resources :pages
end
end
end
scope shallow: true, shallow_path: 'store', shallow_frefix: 'sekret' do
resources :books do
resources :dirs do
resources :pages
end
end
end
两者都会生成总共 24 条路由和 12 个路由 helper,都有 shallow 属性,会生成短路径和名称,我们来看看结果,这里只分析路径和名称:
Prefix Verb URI Pattern Controller#Action
sekret_dir_pages GET /store/dirs/:dir_id/pages(.:format) pages#index
POST /store/dirs/:dir_id/pages(.:format) pages#create
new_sekret_dir_page GET /store/dirs/:dir_id/pages/new(.:format) pages#new
edit_sekret_page GET /store/pages/:id/edit(.:format) pages#edit
sekret_page GET /store/pages/:id(.:format) pages#show
PATCH /store/pages/:id(.:format) pages#update
PUT /store/pages/:id(.:format) pages#update
DELETE /store/pages/:id(.:format) pages#destroy
sekret_book_dirs GET /store/books/:book_id/dirs(.:format) dirs#index
POST /store/books/:book_id/dirs(.:format) dirs#create
new_sekret_book_dir GET /store/books/:book_id/dirs/new(.:format) dirs#new
edit_sekret_dir GET /store/dirs/:id/edit(.:format) dirs#edit
sekret_dir GET /store/dirs/:id(.:format) dirs#show
PATCH /store/dirs/:id(.:format) dirs#update
PUT /store/dirs/:id(.:format) dirs#update
DELETE /store/dirs/:id(.:format) dirs#destroy
sekret_books GET /store/books(.:format) books#index
POST /store/books(.:format) books#create
new_sekret_book GET /store/books/new(.:format) books#new
edit_sekret_book GET /store/books/:id/edit(.:format) books#edit
sekret_book GET /store/books/:id(.:format) books#show
PATCH /store/books/:id(.:format) books#update
PUT /store/books/:id(.:format) books#update
DELETE /store/books/:id(.:format) books#destroy
Prefix Verb URI Pattern Controller#Action
sekret_dir_pages GET /store/dirs/:dir_id/pages(.:format) pages#index
POST /store/dirs/:dir_id/pages(.:format) pages#create
new_sekret_dir_page GET /store/dirs/:dir_id/pages/new(.:format) pages#new
edit_sekret_page GET /store/pages/:id/edit(.:format) pages#edit
sekret_page GET /store/pages/:id(.:format) pages#show
PATCH /store/pages/:id(.:format) pages#update
PUT /store/pages/:id(.:format) pages#update
DELETE /store/pages/:id(.:format) pages#destroy
sekret_book_dirs GET /store/books/:book_id/dirs(.:format) dirs#index
POST /store/books/:book_id/dirs(.:format) dirs#create
new_sekret_book_dir GET /store/books/:book_id/dirs/new(.:format) dirs#new
edit_sekret_dir GET /store/dirs/:id/edit(.:format) dirs#edit
sekret_dir GET /store/dirs/:id(.:format) dirs#show
PATCH /store/dirs/:id(.:format) dirs#update
PUT /store/dirs/:id(.:format) dirs#update
DELETE /store/dirs/:id(.:format) dirs#destroy
books GET /books(.:format) books#index
POST /books(.:format) books#create
new_book GET /books/new(.:format) books#new
edit_sekret_book GET /store/books/:id/edit(.:format) books#edit
sekret_book GET /store/books/:id(.:format) books#show
PATCH /store/books/:id(.:format) books#update
PUT /store/books/:id(.:format) books#update
DELETE /store/books/:id(.:format) books#destroy
发现差别没有,与我们想象的不太一样,第一个例子 scope 传了 path 和 as 参数,按照一般理解,在shallow_scope
时,会执行以下两条语句,覆盖 path 和 as:
scope = { :as => @scope[:shallow_prefix],
:path => @scope[:shallow_path] }
为什么没有被覆盖呢,我们返回去看看 scope 方法,发现一段代码:
unless nested_scope?
options[:shallow_path] ||= options[:path] if options.key?(:path)
options[:shallow_prefix] ||= options[:as] if options.key?(:as)
end
原来如此,在这里对shallow_path
和shallow_prefix
赋值,为什么要这么做,前面有讲过,shallow 只会影响 resources 和 resource 上下文,我个人理解是为了在 shallow 上下文中保留 resources 之前的上下文环境。
第二个例子就相对比较简单,基本上是按照我们的理解来实现的,但是要注意一点,就是顶层的 resources,在 collection 上下文处理集合路由时,是不需要在 shallow 上下文中进行,因为如果在嵌套中集合成员必须要父级的member_id
才能正确识别路由,所以对于顶层 resources,集合成员路由与在非 shallow 环境中一样。
在理解了上下文关系后,再来分析就非常简单,程序通过parent_resource
自动判断出是否是处于嵌套中,如果处于嵌套中就会建立嵌套上下文环境。nested 方法就是用来建立 nested 上下文的,方法简单,再次证明只有理解了上下文的含义,一切都很简单。在 nested 方法中,通过如下一条语句来实现:
scope(parent_resource.nested_scope, nested_options) { yield }
path 是parent_resource
的nested_scope
,如上文所述,nested_scope: #{path}/:#{nested_param}
,这就解决了路径的嵌套,名字嵌套怎么解决呢,还有一个 nested_options
,该函数通过语句
options = { :as => parent_resource.member_name }
设置了 member_name
,实现了名称的嵌套。
前面有介绍,每一条路由都对应有一个 Mapping 对象,都是通过 Mapping.build
建立的 Mapping 对象。在 match 方法中,主要负责分析最终路径和名称,还有一个目标的问题没有解决,Rails 中把目标简称为 app,Mapping 主要功能就是把目标包装成一个 app,这个 app 并不是 Rack app,严格来说是一个 Dispatcher,就是具有 serve(req) 实例方法的类。
Mapping 有几个核心功能,根据传进来的@scope
上下文和 options,分析出 app, conditions, requirements, defaults, blocks,这些参数最终会传给 Journey::Router,来完成路由映射。
首先来分析目标,目标有两种表现形式,一是目标本身是一个 Rack base app 或更广泛的定义,能够响应 call 函数的对象,如果是这种 app,直接赋值给:to; 二是分别设置:controller 和:action。Mapping.build 传进来的也是:to, :controller, :action,三个参数,如果传入参数是:to,那么会分为两种情况:一种是 Rack base app,另一种是’xxx#xxx’,前一种直接传给:to,后一种需要转变成 :controller 和 :action。处理好这些后,分别对:controller 和:action 进行模块设定和有效性检查。
现在所有 options 都已准备就绪,需要进一步分离,分离的大致步骤是:首先对@scope[:constraints]
进行分析,如果 key 是:controller 或在 path_params 中,并且是正则表达式,就放入 requirements 中,否则放入 conditions, 接着对 options 进行筛选把所有正则表达式筛选出来做进一步分析,其它没有包含 path_params 中的全部放入 conditions[:required_defaults]
中;再对刚刚筛选出来进一步分析,如果 key 是:controller 或在 path_params 中,并且是正则表达式,就放入 requirements 中,其它则放入 conditions;最后对 defaults 进行处理,先对 URL_OPTIONS 中的 key 进行初始化赋值,再对 options 进行扫描,非正则表达式全部放入 defaults 中,至此就把所有 options 全部分离为 requirements, conditions, defaults。
blocks 仅仅对 options[:constraints] 进行分析,如果不是哈希表,并且是一个 callable 或 matches 对象,那么把该对象赋值给 blocks,否则把@scope[:blocks]
赋值给 blocks。
blocks 虽然分析出来了,可能有些朋友还不是很清楚,blocks 到底是什么,简单说每一个 block 都是一个限制条件。Rails 路由限制条件有三种形式:一是正则表达式,如id: /[A-Z]\d{5}/
;二是任何responds to matches?
为 true 的对象,如:String, Array, Range, CustomMatches 等;三是 callable 对象,如 Proc, Lamda, Method, respond_to call Object。
根据分析,:constraints 和:blocks 都是某种条件限定,就是说只有满足所有限定条件,才会执行目标程序,按源代码分析,限定条件可以在两个地方执行,一是 Journey::Router 识别的时候,另一个是在目标 app 执行,目标 app 优先执行:constraints 的 callable 或 matches 对象的限定条件,其它都放在识别的时候。
Mapping 利用 app(blocks) 函数来封装一个 app,如果:to 是 callable 对象,用:to 和 blocks 封装成一个 Constraints 对象,该对象是一个有限定条件的 Endpoint;如果:to 不是 callable,并且 blocks 不为空,用 blocks 封装成一个 Constraints, 该对象封装了一个有限定条件的 Dispatcher;如果:to 不是 callable,并且 blocks 为空,那么仅仅简单用缺省条件封装成一个 Dispatcher。
至此,app, conditions, requirements, defaults, as 都有了,就可以把它添加到 RouteSet 了。
路由映射部分已基本完成,其它未完待续。