Roda源码分析(二)请求响应

前言

大家好,我是MARK24 。可以叫我 MARK。这是我研究Roda的笔记。

这是我研究Roda,做的记录。水平有限,欢迎指正。

主要分为两篇以及有价值的引用,相关系列目录:

研究的Roda源码版本 2022年2月9日 Roda v3.52.0 dc600b763ebf4f15e11c0428e26b812b9d140911

阅读本文章,你可能获得

  1. Rack 应用概念的了解
  2. Roda 树形路由的原理
  3. Ruby Class中 extend、include 的技巧
  4. Ruby 中 catch、throw 的使用
  5. 阅读源码的方法

以及欣赏 Roda 的设计等。

我个人水平和精力有限,研究Ruby时间不长,自认还是个小学生。如有错误欢迎指正,欢迎交流~

一、预备知识:Rack工作原理

熟悉的可以跳过 Rack 部分。

Roda本质上还是有一个 Rack 的应用。用户请求发送至我们的服务器,首先经过 Rack,所以先要明白 Rack的工作原理。

rack_flow

这篇文章简单的介绍了 Rack的工作流程

想要深入了解 Rack 可以查看官网源码

有一部国人编写的笔记也可以提供很大的帮助,可以下载:【RackProgram.pdf

二、研究方法总结

Roda只是一个研究对象,研究方法是通用的。

1.把握宏观,要分清楚几个大类,每个方法在哪个大类下工作,决定了理解他们的角色

大概只需要关注,大类、和 initialize 方法。这里决定了他们初始化的值。

可以理解未来都是围绕着 initialize 里面的数据。

2.从入口开始往上找调用方法看看调用关系

阅读代码不应该从代码层面的 从上往下读。这是错误的。

根据语言的运行特点,以Ruby为例。应该以 解释器或者编译的运行顺序来观察源码。

1)代码初始化,进行了什么

class类声明、以及类中可执行语句在运行。可以关注下 class 中 initialize 中声明的变量,以及动态生成的代码。(Ruby和其他语言不一样,class内部也有代码在运行,你应该知道的。)

2) 入口开始,调用关系

建立完内存中初始化的 class 等对象。就可以站在程序运行的角度,从入口调用的顺序 依次查看源码。

3.分析工具

主要使用他们的一些功能帮助阅读。有人可能觉得 DEBUG 工具就够了,我的体会不是这样,DBEUG 的切入点太细碎,而且不够准(我总是这个感觉)。跳转的地方往往涉及面太广,出错了要再次重来。当然可能我不会用啦。不过这些不重要。

真正重要的部分其实是明白Ruby工作的机制,以及按照Ruby运行的规律,去解读源码的主要部分,识别作者的意图。我觉得这个才比较重要。

1)VScode 编辑器

  • 全局搜索变量、函数定义
  • 查看单个代码文件的 大纲(Outline)即 类、方法 定义一览

2)RubyMine(或者一款IDE)

  • DEBUG 功能,可以查看过程(不过这不是万能的,因为 DEBUG 对窥探全局并不好用。实际结果还是要以阅读为主)
  • 跳转定义或者使用
  • 修改我们使用的 lib库 即 Roda 源码,验证自己的猜想(动态语言的优势,一切都是透明的。使用完可以重新安装)

3) 草稿纸

记录重要的引用关系,记录分析和思考。

三、Roda中使用的Ruby的知识

1.class的上下文中,attr语句会执行、@语句会执行、def语句会注册但是不会执行(实例化被实例调用执行),class上下文的 define_method 会执行

2.def 首次运行只注册不允许,具体的 def 的方法调用顺序,根据 启动app的顺序依次调用

3.Ruby的对象模型,以及 extend、include

这部分主要看插件系统好了。作者在 Ruby 的模型基础上巧妙使用他们构建了自己的插件系统。

4.cath和throw

比如源码的这部分

master/lib/roda.rb#L494

其实相当核心, 我单独介绍了一下 cath 和 throw 的使用

Ruby的catch与throw

四、Roda 的运行流程

4.1 大致的宏观描述

Roda主要做四部分工作也对应着他的三个核心类、和一个插件系统

  • class Roda
  • class RodaRequest
  • class RodaResponse
  • Plugins

1.class Roda & Plugins

其中 class Roda 的代码非常少,主要起到一个协调和名字空间的作用。

主要的代码 在 roda/lib/roda.rb#L29 把主要的路基都放在了 module Base 中。

为什么这样做? 因为 如果在 Roda 的 class 上下文中定义自己的类方法、实例方法,会成为最高优先级,会覆盖自己的祖先类。

于是 Roda 使用了一个小技巧,把自己的 行为 封装在 Base 模块中,也当成插件 expand 进来

这样插件也可以被 expand, 这样就可以实现 插件对 Roda 的方法进行覆盖、或者添加方法,从而实现了插件机制。

2.class RodaRequest

RodaRequest 继承自 Rack::Request 主要添加了 Roda 特色的 路由方法 route.on/is/get/post 等,还有匹配路由的适配器。

3.class RodaResponse

RodaResponse 继承自 Rack::Response

4.2 响应过程

4.2.1 生成响应的RackApp

我们知道要想成为 Rack 的应用,必须实现一个 call 方法。这部分可以参考 Rack 的工作原理。

Roda 作为 Rack 应用要遵循这套。

Roda 在 Base 的 ClassMethods 实现了 call 方法

# https://github.com/Mark24Code/roda/blob/master/lib/roda.rb#L52

  def call(env)
    app.call(env)
  end

call方法的调用方式是 返回 app.call(env)

我们寻找 app 是一个方法:

# https://github.com/Mark24Code/roda/blob/master/lib/roda.rb#L33
  def app
    @app || build_rack_app
  end

他返回了一个 @app || build_rack_app

这是一个技巧就是 如果 @app 有值即返回,无值 则 构建一个 rack_app,我们再看看 build_rack_app 方法做了什么

# https://github.com/Mark24Code/roda/blob/master/lib/roda.rb#L388

# Build the rack app to use
def build_rack_app
  app = base_rack_app_callable(use_new_dispatch_api?)

  @middleware.reverse_each do |args, bl|
    mid, *args = args
    app = mid.new(app, *args, &bl)
    app.freeze if opts[:freeze_middleware]
  end

  @app = app
end

做的事情就是 去构建一个 Rack 应用。把中间件套成 app 并且返回。

其中又用到了一个方法 base_rack_app_callable

# https://github.com/Mark24Code/roda/blob/master/lib/roda.rb#L378

  # The base rack app to use, before middleware is added.
  def base_rack_app_callable(new_api=true)
    if new_api
      lambda{|env| new(env)._roda_handle_main_route}
    else
      block = @rack_app_route_block
      lambda{|env| new(env).call(&block)}
    end
  end

这里似乎运行到核心了,在这里我们要 返回一个基础的 Rack App 而这个基础的 App是:

# https://github.com/Mark24Code/roda/blob/master/lib/roda.rb#L380

lambda{|env| new(env)._roda_handle_main_route}

new 方法等于隐形的调用 self 即 Roada自己。这里等价于

Roda.new(env)._roda_handle_main_route

这样我们就明白了,原来绕来绕去要生成的基础 Rack App 竟是我自己。

Roda 的初始化函数,来自于前面提到的 Base module

他是被 include 到 Roda 中才会发挥作用的,没有直接定义。

# https://github.com/Mark24Code/roda/blob/master/lib/roda.rb#L485

def initialize(env)
  klass = self.class
  @_request = klass::RodaRequest.new(self, env)
  @_response = klass::RodaResponse.new
end

哈哈这里面做的事情其实也挺简单,记录了自己的 class,生成了一个 @_request、@_response

这里就是另外两个重要的类罢了。

额外提一嘴,之所以用下划线开头,这是一个技巧就是暗示这是内部变量,减少名字冲突的。 Python里面更常见,还喜欢用双下划线。

如果认真了解了 Rack 工作原理,到这里大概就明白了,或者能猜到 Roda 后面要做什么了。

他要做的就是 拿到 RodaRequest 这里携带了 Rack传递过来的环境变量 env。

每一次的用户请求,都会创建一个 Rack App 实例,进而创建一个 RodaRequest 实例,携带请求的 env,这里有一些 UA、HTTP_PATH 之类的消息。

我们要做一些工作,比如路由处理,一些中间件处理。

然后访问数据库,获得数据,然后把结果 写入 RodaResponse 返回给 Rack。

这就完成了 RackApp 的一次工作循环。

4.2.2 处理路由

我们讲完生成 Rack Base App 的事情,别忘了,他还调用了一个 方法 _roda_handle_main_route

那么 _roda_handle_main_route 从名字上看就是处理主路由。来看看它是怎么执行路由的。

Roda 特别的就在于他的 树状结构的路由。下面让我们看下:

# https://github.com/Mark24Code/roda/blob/master/lib/roda.rb#L493


# Handle dispatching to the main route, catching :halt and handling
# the result of the block.
def _roda_handle_main_route
  catch(:halt) do
    r = @_request
    r.block_result(_roda_run_main_route(r))
    @_response.finish
  end
end

其实这句很关键,几乎可以说,这句直接闭环了整个Rack流程。

block_result是一个方法,他的作用就是判断返回是字符串就按照规定写入 @_response.body,不符合的返回报错。空返回则 返回 nil 这个会触发 404 中间件工作。

这里就是 获得 request对象,传入路由,进行求值,然后把返回传递给 response 对象,完成了整个 Rack的流程。

这里面使用了 catch、 throw。我大概描述下,这部分有趣的可能需要后面展开才会知道。但是不妨碍我在这里提一嘴为什么这里非要用 catch&throw 包裹程序。

一个简单的 Roda程序看起来像这样:

require "roda"

class App < Roda

  route do |r|
    r.on "a" do
      r.on "b" do
        r.on "c" do
          # /a/b/c
          puts "hello world"
        end
      end
    end
  end
end

简单说,Roda 处理路由看起来就像是一个 一环扣一环的调用。

它先匹配到 “a” 如果 “a” 匹配路径,就会执行 它的块,即 “b” 部分,然后再匹配 “b” 以此类推。只要匹配中,就对他的块求值。

我们知道,块 是 Ruby特有的概念,但是实际上在底层,他的工作方式看起来就像一个 匿名函数 lambda。

这里就像一个一个函数彼此链式的调用。一直到触达最底层,比如 C。

在 Ruby 的视角这是个深层嵌套的字面解构了。我们想从 “c” 处直接返回,这是用就使用到了 catch、throw技巧。 别忘了,我们可是在最外侧,早已注册了 catch(:halt)

# https://github.com/Mark24Code/roda/blob/master/lib/roda.rb#L493


# Handle dispatching to the main route, catching :halt and handling
# the result of the block.
def _roda_handle_main_route
  catch(:halt) do
    r = @_request
    r.block_result(_roda_run_main_route(r))
    @_response.finish
  end
end

我们只需要在 内部,不论多么深的嵌套里面调用 throw :halt, response_value 程序就会跳出来,并且携带者我们的 response_value 作为返回值。这个效果有点像其他语言的 goto

好了,我们已经大致描述了其实最关键的流程了。下面来详细看下 Roda 如何做到这些的。继续分析 _roda_run_main_route 方法。这是 被 _roda_handle_main_route 调用到的方法。

# https://github.com/Mark24Code/roda/blob/master/lib/roda.rb#L517


# Run the main route block with the request.  Designed for
# extension by plugins
def _roda_run_main_route(r)
  _roda_main_route(r)
end

源码的注释顺便告诉我们这是一个预留的接口,可以给插件做拓展。不影响我们继续看 _roda_main_route

# https://github.com/Mark24Code/roda/blob/master/lib/roda.rb#L512

# Default implementation of the main route, usually overridden
# by Roda.route.
def _roda_main_route(_)
end

这个函数竟然是空的,什么都不执行。一切竟然就此打住了。 WTF ???

不过注释说了,这也是类似一个接口。他是预留给 Roda.route 覆盖的。他的实现在另一处,在 Roda初始化的时候,就会覆盖这个方法,从而依然可以完成被调用的实名。我们去看看 Roda.route:

# https://github.com/Mark24Code/roda/blob/master/lib/roda.rb#L316

# Setup routing tree for the current Roda application, and build the
# underlying rack application using the stored middleware. Requires
# a block, which is yielded the request.  By convention, the block
# argument should be named +r+.  Example:
#
#   Roda.route do |r|
#     r.root do
#       "Root"
#     end
#   end
#
# This should only be called once per class, and if called multiple
# times will overwrite the previous routing.
def route(&block)
  unless block
    RodaPlugins.warn "no block passed to Roda.route"
    return
  end

  @raw_route_block = block
  @route_block = block = convert_route_block(block)
  @rack_app_route_block = block = rack_app_route_block(block)
  public define_roda_method(:_roda_main_route, 1, &block)
  @app = nil
end

route 做的事情确实是除了缓存 block 之外,就是动态的定义了 _roda_main_route 方法。

并且 route 定义在 ClassMethods 部分,他会在 Roda 第一次进行初始化的时候就运行,也就是 define_roda_method(:_roda_main_route, 1, &block) 这句定义的方法会在实例产生之前就绪可用。

但是这里 不是用 define_method 而是 define_roda_method

# https://github.com/Mark24Code/roda/blob/master/lib/roda.rb#L85

def define_roda_method(meth, expected_arity, &block)
  if meth.is_a?(String)
    meth = roda_method_name(meth)
  end
  call_meth = meth

  # ..... 省略 .....
  # 此处做了参数检查
  # ...... 省略 ....

  call_meth
end

define_roda_method 稍微复杂一点,我简化了一下,简单的说就是如果传入的是Symbol就会直接使用这个方法,如果传入的是字符串会生成唯一的方法名,再使用。还做了一些参数校验的工作。

我们这里 传入的是 :_roda_main_route 相当于直接使用了这个方法。而这个方法的内容就是 route 声明的 block 罢了。

所以我们根据上文的链路可以简单总结下:

像下面的这样的程序,当第一次开始运行的时候,ruby 会先 require 整个 roda 进行 class Roda 的初始化工作。 App 作为 Roda 的子类,在 Roda 中会做一些额外的工作,把 共享变量 复制一份给子类。这个可以在源码中看到。

require "roda"

class App < Roda

  route do |r|
    r.on "a" do
      r.on "b" do
        r.on "c" do
          # /a/b/c
          puts "hello world"
        end
      end
    end
  end
end

在初始化 Roda 的过程中,Roda::Base 会作为插件 先在 Roda 的上下文中运行,提供了 实例方法、类方法,分别通过 include、extend 去插入。这部分可以参考 Roda源码分析(一)插件系统

route 部分相当于运行在 Roda class 上下文中,route 的块,被当做 :_roda_main_route方法进行了注册。

当用户的请求打过来,经过 HTTP Server(可能是 Puma、Thin、WEBrick…) 调用 Rack,Rack再调用 Roda, Road.call 方法实际上返回的是 Roda.new(env)._roda_handle_main_route_roda_handle_main_route 最终转发到了 _roda_main_route 其实就是 route 中声明的块。等于直接运行了 route中定义的块。

好家伙,这跟传球一样,踢来踢去的,终于要到了处理路由的部分了。

那么 树状路由 到底如何处理的呢? 咱们接着往下看吧:

  route do |r|
    r.on "a" do
      r.on "b" do
        r.on "c" do
          # /a/b/c
          puts "hello world"
        end
      end
    end
  end

route 代码部分是真实直接被调用的函数。为什么这样说,我们要看看 route 的 block 一旦开始被调用,那么代码就会运行到 第一句 r.on "a", 我们知道 r 其实就是 RodaRequest 实例。这部分应该去关注 RodaRequest 里面 关于 on 方法的定义。 其实 Roda 里面主要是 onis 两个方法, on 方法代表着 类似 /path/* 粗匹配。 而 is 方法代表类似 /path 精确匹配。由于我是看过的,所以可以直接横向比较两个。


# https://github.com/Mark24Code/roda/blob/master/lib/roda/request.rb#L207
def on(*args, &block)
  if args.empty?
    always(&block)
  else
    if_match(args, &block)
  end
end

# .... 省略 他们之间的代码 ...

# https://github.com/Mark24Code/roda/blob/master/lib/roda/request.rb#L162
def is(*args, &block)
  if args.empty?
    if empty_path?
      always(&block)
    end
  else
    args << TERM
    if_match(args, &block)
  end
end

is 其实比 on 多不了多少。其实看 on 好了比较好理解。可以先看他做了什么,on 的第一个参数 其实就是 Roda 语法里面,填写匹配的一个字段。

r.on "a" do
  r.on "b" do
    # 匹配 /a/b
  end
end

args 就是 “a”,”b” 之类的。如果他是 空字符串,也就是 args.empty?true ,那意味着到头了,匹配到了。直接对 block进行求值。 always 里面做的就是求值返回。不信我们看下:

# .... 省略 他们之间的代码 ...

# https://github.com/Mark24Code/roda/blob/master/lib/roda/request.rb#L495
# Yield to the match block and return rack response after the block returns.
def always
  block_result(yield)
  throw :halt, response.finish
end

always 里面使用了 Ruby的catch与throw 技巧。如果写的很夸张,路由这部分是个复杂的树结构,throw 可以带着值直接返回到 外部定义的 catch(:halt) 的地方,这个地方我们上面提到过。不信你可以搜一下。这其实就像是 goto 语句。

额外提一嘴,学习编程很多年,我水平不行但是还是看了很多书的,书本上都会提到尽量不要用 goto, 这里用 goto 就非常合适,简直比较巧妙。对于标准、规范这件事不要迷信要辩证看。Linus就喜欢用 goto。当然我们水平比较差的时候,还是老老实实遵守规范。希望我们都可以达到随意使用goto 的水平 :)


PS: 我不重要的观点:

我最近觉得写程序这件事,要有点 灵动感 在里面。要灵动,只有灵动能表达出我内心的意思。不要把自己变成机器一样死板。

随着职业生涯变长,看到过越来越多不是就事论事,实事求是,解决问题的程序员。他们要么畏首畏尾,要么迷信最佳实践。喜欢把书上的印刷字氛围圭臬。喜欢追求符号式的宗教感。喜欢茴字有几种写法,但是缺乏对东西的热爱和洞见。无法区分丑陋与美。

我们要做的是把机器变成人,而不是要把人变成机器。人变成机器那就悲剧了。


is 相比于 on 额外做了一个是否全匹配的检查,以及全匹配插入一个 TERM 的自定义类对象。其实是为了方便后面的 if_match 方法。下面就要讲到。

其实路由匹配,就发生在 is_match 这里面了,终于到这里了。不光你看的要睡着了,我手都要写麻了。


# .... 省略 他们之间的代码 ...

# https://github.com/Mark24Code/roda/blob/master/lib/roda/request.rb#L555
def if_match(args)
  path = @remaining_path
  # For every block, we make sure to reset captures so that
  # nesting matchers won't mess with each other's captures.
  captures = @captures.clear

  if match_all(args)
    block_result(yield(*captures))
    throw :halt, response.finish
  else
    @remaining_path = path
    false
  end
end

is_match 也做了一点事情,要说下 @captures 就是一个数组存储已经匹配的字符串的,主要是注入给当下要工作的块的。知道一下就好。block_result 做的事情就是把 yield 求值块的返回值正确的加入到 RodaResponse 的实例里,按照 Rack 的要求写入 response, 很好理解。

所以整体的意思就是,如果 match_all (因为 args 可以是个数组,甚至比较复杂的。我们前面只说了 “a”) 全匹配,那么就开始向内求解他的块。敏感的同学能不能意识到,随着 route 书写路由的是一个嵌套的结构,这里相当于一种嵌套式的调用。

简单的说,就拿一路顺利的情况来说, 一旦我们访问的是 “/a/b/c”,Roda 工作起来像什么呢?

route do |r|
  r.on "a" do
    r.on "b" do
      r.on "c" do
        # do sth ...
      end
    end
  end

  r.on "e" do
    r.on "f" do
      r.on "g" do
        # do sth ...
      end
    end
  end
end

拿 《Mastering Roda》 作者说的,求解的过程就像一个毛毛虫,他首先爬到了 “a”, 接着 匹配到了,就爬到了 “b”, 如此往复,他沿着正确的路线,一直抵达到目标。 而他的工作方式就是匹配,然后开始调用块。逐步向内调用。

块,是Ruby里面的概念,其实就是 lambda 抑或有个名字——匿名函数。 以JavaScript作为类比就是 ` function(){ }` 这个就是一个 函数调用的行为,一级一级往里链式调用。

我们都知道,这种执行栈式的调用,其实是比较快的。他没有复杂的初始化等更加消耗时间的行为。而且很简单粗暴。

这就是Roda巧妙地地方。

说道这里,我都觉得差不多了。大概的概念已经明白了。那么 match_all 发生了什么呢? 猜猜也知道大概做了一些匹配呗。


# .... 省略 他们之间的代码 ...


# https://github.com/Mark24Code/roda/blob/master/lib/roda/request.rb#L600
def match_all(args)
  args.all?{|arg| match(arg)}
end


# .... 省略 他们之间的代码 ...

# https://github.com/Mark24Code/roda/blob/master/lib/roda/request.rb#L572
def match(matcher)
  case matcher
  when String
    _match_string(matcher)
  when Class
    _match_class(matcher)
  when TERM
    empty_path?
  when Regexp
    _match_regexp(matcher)
  when true
    matcher
  when Array
    _match_array(matcher)
  when Hash
    _match_hash(matcher)
  when Symbol
    _match_symbol(matcher)
  when false, nil
    matcher
  when Proc
    matcher.call
  else
    unsupported_matcher(matcher)
  end
end


# .... 省略 他们之间的代码 ...

# https://github.com/Mark24Code/roda/blob/master/lib/roda/request.rb#L428
def _match_string(str)
  rp = @remaining_path
  length = str.length

  match = case rp.rindex(str, length)
  when nil
    # segment does not match, most common case
    return
  when 1
    # segment matches, check first character is /
    rp.getbyte(0) == 47
  else # must be 0
    # segment matches at first character, only a match if
    # empty string given and first character is /
    length == 0 && rp.getbyte(0) == 47
  end

  if match 
    length += 1
    case rp.getbyte(length)
    when 47
      # next character is /, update remaining path to rest of string
      @remaining_path = rp[length, 100000000]
    when nil
      # end of string, so remaining path is empty
      @remaining_path = ""
    # else
      # Any other value means this was partial segment match,
      # so we return nil in that case without updating the
      # remaining_path.  No need for explicit else clause.
    end
  end
end

match_all 把数组按顺序遍历的匹配,调用的其实是 match ,而 match 方法会根据每个 输入的 matcher 也就是路由里 /a/b/c 里面的比如 “a” 开始做匹配。 我们摘要了最基础的 _match_string 看看具体的匹配策略里面到底做了什么。

看起来好像做了很多事情,其实就是一单匹配,就开始编辑 @remaining_path 这是 RodaRequest 的一个实例变量,用来记录剩余路由的部分。

最后把,所有的 _match_* 都在做编辑 @remaining_path 的工作,并且返回是否匹配的布尔值。


2022.03.13 补充: 再细化一下 r.on、r.is

r.on vs r.is

这里想再单独拿 r.onr.is 讨论一下。

上面的描述可能太过细节。整体看下来还是比较抽象的。然后这里简单的描述下过程。

  route do |r|
    r.on "a" do
      r.on "b" do
        r.on "c" do
          # /a/b/c
          puts "hello world"
        end
      end
    end
  end

以此为例说明下,r.on 的工作就是 匹配 arg 这里就是 “a”,”b” 这种路径参数。

如果是 arg 的情况分为几种:

  • 无参数,也就是 空值(nil),这里就被认为是 r.on的终点了,一旦进行到了这步,就会开始对 block求值,并且返回了。
  • 一个参数,也就是上面 ‘a’ 这种情况,会进行匹配,匹配中了,会进行 对 block 求值。

    这里情况特殊在于,r.on 里面也是一个 block 怎么办?类似于链式调用,再调用子的 block 并等待它的值返回。一直到请求到最内部为止,Roda会以 throw :halt 的方式, 跳出很深的嵌套,直接弹出结果。类似goto。

  • 多个参数 ,类似于 r.on "a",Integer,"b" 这种,r.on 会使用循环的方式对 args 数组进行挨个匹配,如果匹配成功就会消耗掉 @remaining_path ,进行下一个匹配。全部正确就 求 block 的值。如果失败了会还原 @remaining_path 。返回 false

这里不论是哪种情况,匹配失败,都是返回 false

false 不意味着程序被中断,意味着这段代码块被执行完毕。而 false 值也没什么意义,假设这个值在最外层的话,会被人为是 false 返回给 @_response 那么 Roda 会把 false、nil 都解释为 404 NOT found。如果不是最外面一层,仅仅意味着一段 block 的代码被执行完毕。然后就会继续在代码层面,向平级的函数往下继续执行 —— 也就是执行嵌套路由的平行部分。

这个过程有几个显然的结论:

1.想要路由被充分的处理,需要在充分的使用 r.on 进行树状分叉的定义路由。这样才会体现每个节点都调用了 块。

Roda 还可以在公共结点处,共享逻辑。

2.除非 r.on 后面无值,直接进行计算,否则都是 匹配了开始执行块返回结果。所以 r.on 有 /path/to/* 的匹配效果。其实是因为不管有没有匹配完成,匹配到后面,直接运行了块返回的结果。

3.额外 r.is 和 r.on 的区别 —— 终点精确匹配的原理

  def on(*args, &block)
    if args.empty?
      always(&block)
    else
      if_match(args, &block)
    end
  end

  def is(*args, &block)
    if args.empty?
      if empty_path?
        always(&block)
      end
    else
      args << TERM
      if_match(args, &block)
    end
  end

r.is 插入了 TERM

而在 match 方法中, 字符串、正则、数组等,都要匹配并且编辑 @remaining_path ,但是存在几个匹配的终点。

比如 TERM,false,nil,true 最终程序遇到他们就必须返回。这个作为一切匹配的终结。

r.is 其实就是通过 尾部加入 TERM,比如 r.is "test" 等价于 r.on "test",TERM 进行了一个精确地匹配罢了,因为对于 r.on 来说这个 args 必须完全匹配才会继续下去。

4.方法匹配 r.get, r.post 的原理

def post(*args, &block)
  _verb(args, &block) if post?
end

def get(*args, &block)
  _verb(args, &block) if is_get?
end

def is_get?
  @env["REQUEST_METHOD"] == 'GET'
end


def _verb(args, &block)
  if args.empty?
    always(&block)
  else
    args << TERM
    if_match(args, &block)
  end
end

r.getr.post 就是多一步骤判断方法 动词,如果方法不匹配就提前返回了。其他的依然是运行了 r.is 的逻辑,插入了 TERM 作为终点匹配。

5.并列句


r.on "sample" do
  # 公共部分
  post_list.values.join(" | ")

  r.on do
    # A 部分
  end

  r.is Integer do |index|
    post_list[index]

    # B部分
  end

  # 也可以返回
end

别忘了route里面的块 就是一段代码而已。如果可以匹配就会不断的更深入的求内部嵌套的块。

而如果 A部分匹配失败了,就像正常代码一样,运行到 B部分匹配。对于路由就是平行关系。

而 A、B 部分运营完了,依然有机会运行到 下部分。

所以这里没有魔法,执行的过程是纯粹的。

这里最差情况寻找到路由是 O(n), 而最好的情况可以得到 O(ln(n))。

还有这里如果没有明显的 return 语句,返回的是最后一个表达式。如果不注意这个,可能会对结果有点意外。

最终写入返回结果

整个过程可以看成这段代码

  def _roda_handle_main_route
    catch(:halt) do
      r = @_request
      r.block_result(_roda_run_main_route(r))
      @_response.finish
    end
  end

_roda_run_main_route 中 r 就是我们的route,整个返回值就是我们 route 中块的返回值。

这里有一个疑问,最终结果通过 r.block_result 把结果挂在了 @_request 中,到底最后结果如何传递?

其实是 r.block_result 中调用了 response 这是一个方法,用来返回 @_response, write 方法就是写入 body

  def block_result(result)
    res = response
    if res.empty? && (body = block_result_body(result))
      res.write(body)
    end
  end

然后再回到开头 catch(:halt) 部分,最后 @_response.finish 返回结果。

一切如此平淡,没有什么魔法。

总结

看到这里不容易。至此,我觉得 Roda 的思想已经足够清晰。

树状路由,更像是——我们人为的编写了运行路径一般。路由一旦被响应,就会把路由 “/a/b/c” 传入 RodaRequest 也就是 r 对象,然后开始运行真实的 Ruby 代码,也就是我们书写的 route 部分的 DSL。

一棵树的根节点,一旦不匹配,他的块会直接返回 false 的结果,等于这块不会再继续深入。从而像正常 Ruby 代码一样的运行下面的另一棵树。直到一层一层找到正确的路径,一层一层的执行他内部的块。

一直到匹配完毕,结束了,直接通过 throw 跳出嵌套结构,返回结果。

后记

一直在用摸鱼时间研究学习Ruby。目前很喜欢的语言,有我觉得一些问题,但是瑕不掩瑜。

感谢 Jeremy Evans 的作品。他的坚持和 0 issues 的精神很让我感动。

摸鱼了快一周,记录了结果。不能让我的老板知道。:P

意识流笔记,忽略我的烂字:

roda_note_01 roda_note_02

Mark24

Everything can Mix.