Rails Rails 路由系统源码探索

gerry · 2014年11月17日 · 最后由 torvaldsdb 回复于 2017年02月20日 · 12477 次阅读
本帖已被管理员设置为精华贴

注:本帖不是使用手册,仅仅分享自己探索源码的一些理解,以便有共同爱好的朋友一起共勉,本人水平有限,也是初次发帖,恳请大家拍砖轻点

源码版本:4.2.0.beta

Route 概述


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 有两大核心模块:

  1. Action Dispatch:主要负责分析 Web Request 的信息参数,查找用户自定义的路由信息,分发到目标处理程序,一个 Controller’s Action 或者一个 Rack Base Application,又或者一个 Rails Engine,又或者另外一个路由器。目标处理程序处理完成后,返回 Rack Base 的 Response [ status, headers, [body] ],最终返回给应用服务器,通过应用服务器返回客户浏览器。

  2. Action Controller:提供了一个 Base Controller 的实现,Base Controller 提供了 Filters 和 Actions,隐藏了 Controller 与 View、Template 之间的数据传递、调用关系及多种 Helper 模块的自动 Mixin,以便开发者集中精力开发本身的业务逻辑。

Rails 路由是 Action Pack 的核心子系统,属于 Action Dispatch 模块,系统框架图:

System Diagram

类图:

ClassDiagram

路由的实质是什么,路由就是一种简单的映射关系,路径 Path 到目标 App 的映射,非常简单,无需过多解释,但现实世界往往一句简单的描述,演变成一个复杂的过程和系统,因为在一句简单的描述前面和后面都会加上非常多的限定词,也就是人们的需求往往远大于简单的描述,如 Rails 的 Routing 会加上简单、智能、快速,以及 Rails 的核心价值观“约定优于配置”,这些限定词就让 Rails 的路由系统不那么简单。Rails 路由系统分为三个部分:

  1. 路由映射;
  2. URL 自动生成 Generator
  3. 路由识别与分发

注:以下所有未注明 module 的类和模块,都默认是 ActionDispatch::Routing

路由映射


路由映射涉及到的类和模块:Mapper、Mapping、Scope、Resource、SingletonResource,模块:Base、HttpHelpers、Scoping、Resources、Concerns,类关系图如下:

Route Class Diagram

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 对象。

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 的定义:

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,换言之就是更改为某种特殊的上下文。

scope 方法:

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 上下文:

root 上下文在 Mapper 的初始化函数中创建: @scope = Scope.new({ :path_names => @set.resources_path_names })@set 为 RouteSet 对象,@set.resources_path_namesRouteSet.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

namespace 与 scope 的区别:

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控制器。

Resources:

资源是一种抽象定义,在 RESTful 中定义资源为任何网络实体,每一个资源都有唯一的 URI,在 Rails 中通常对应一种数据模型,Resources 正是 RESTful 的 Rails 实现。核心包括类 Resource,SingletonResource 和模块 Resources。Resources 实现了 resource, resources, member, collection, nested, new等一系列方法。

Resource 类:

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_namexxx_scope主要是用于 resource 上下文的二级方法,或者说某种特定上下文环境如 collection, member 等:path 一部分或全部,xxx_name则是:as 的一部分或全部,请记住与 scope 一样,单个来看 Resource 是没什么意义的,一定要在上下文堆栈中才能得到最终结果。

SingletonResourceResource 的子类,不是一个单例类,而是一个单利资源,就是该资源只有单数形式访问和存在,最典型的例子就是 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:

如前文定义,为什么 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 方法:

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 方法来完成,那有朋友就会问,既然如此,何不直接介绍 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_actionname_for_action两个函数来生成最终的路径和名称,这两个函数非常关键,但是只要真正理解了上下文环境,那么他们就非常简单,只是简单的拼装和连接。有了路径和名称后就调用 Mapping.build 函数生成一路由映射,有了映射,就需要把该映射加入 (通过 RouteSet 对象的 add_route 方法) 到 RouteSet 对象中,至此,一个路由映射就全部完成。

shallow 方法:

要开启 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_pathshallow_prefix赋值,为什么要这么做,前面有讲过,shallow 只会影响 resources 和 resource 上下文,我个人理解是为了在 shallow 上下文中保留 resources 之前的上下文环境。

第二个例子就相对比较简单,基本上是按照我们的理解来实现的,但是要注意一点,就是顶层的 resources,在 collection 上下文处理集合路由时,是不需要在 shallow 上下文中进行,因为如果在嵌套中集合成员必须要父级的member_id才能正确识别路由,所以对于顶层 resources,集合成员路由与在非 shallow 环境中一样。

resources 嵌套实现:

在理解了上下文关系后,再来分析就非常简单,程序通过parent_resource自动判断出是否是处于嵌套中,如果处于嵌套中就会建立嵌套上下文环境。nested 方法就是用来建立 nested 上下文的,方法简单,再次证明只有理解了上下文的含义,一切都很简单。在 nested 方法中,通过如下一条语句来实现:

scope(parent_resource.nested_scope, nested_options) { yield }

path 是parent_resourcenested_scope,如上文所述,nested_scope: #{path}/:#{nested_param},这就解决了路径的嵌套,名字嵌套怎么解决呢,还有一个 nested_options,该函数通过语句 options = { :as => parent_resource.member_name } 设置了 member_name,实现了名称的嵌套。

Mapping:

前面有介绍,每一条路由都对应有一个 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 了。

路由映射部分已基本完成,其它未完待续。

先来点个赞

先来点个赞

迟来点个赞

好文,建议楼主上传个头像,如此文章加上个性头像,方便快速积累社区声望。

匿名 #8 2014年11月19日

:plus1:

点个收藏吧

:thumbsup:

多谢!!

这个也太厉害了吧,基本上没看懂,但是一定要顶

牛人就是这么炼成的吧!

谢谢各位捧场,没想到处女贴,还能加精,有点小小的激动

:plus1: 可以开始写书啦

赞 mark

必须赞一个

先 mark,然后赞一个

mark 慢慢看 ~~赞

赞一个~~

scope shallow: true, shallow_path: 'store', shallow_frefix: 'sekret' do
shallow_prefix

马克下,~~赞

34 楼 已删除
realwol Rails 对请求的处理过程 提及了此话题。 07月26日 09:44

整体还是不错的.但是有点纠结与细节中了. 还有一个点就是 Rails 的路由 path 语法并不是传统一样上用 ruby 实现的 DSL,而是 自定义的一种 DSL

我屮艸芔茻,博大精深,望洋兴叹。

zhuoerri Rails 路由 Journey 与 有限状态自动机 提及了此话题。 03月04日 13:01
需要 登录 后方可回复, 如果你还没有账号请 注册新账号