RodaPlugin之hash_route说明

前言

这里主要讲解下 hash_route 如何使用,官网介绍的不是很清楚。至少我看的一脸懵逼。

这里主要是通过源码进行分析得出的结论。

1.为什么要用hash_route 而不是 multi_route

不论是文档还是 MasteringRoda 一书中都会提到要用hash_route去替换multi_route,为什么?

简单可以看下源码,可以理解为,multi_route存取查询route的时候是通过正则表达式。这就必然很慢。

而hash_route是通过一个约定把路由名字生成固定的命名法的key,和对应的handler对应起来。这样子路由进入解析,直接处理,把path当成key调用方法就可以了。

这个方法最快复杂度只有 O(1)。 所以推荐 hash_route 来拆解复杂路由。

2.只管看下 hash_route 到底做了什么

我们让Roda自己告诉我们

require "roda"

class App < Roda
  plugin :hash_routes
  plugin :json

  hash_branch('test') do |r|
    # 定义 ....
  end

  route do |r|
    r.root do
      # 通过源码我们知道 hash_route 在 Roda class上挂载了opts,而这里会保存着路径和方法名的映射。可以访问首页打印出注册路由名称的关系。
      self.class.opts
    end
    r.hash_routes
  end
end

用以下代码举例子:

require "roda"

class App < Roda
  plugin :hash_routes
  plugin :json

  hash_branch("home") do |r|
    r.is "welcome" do
      "home - welcome"
    end
  end

  hash_path("/login") do |r|
    "login"
  end

  hash_path("/logout") do |r|
    "logout"
  end

  route do |r|
    r.root do
      self.class.opts
    end
    r.hash_routes
  end
end

打开浏览器访问 http://0.0.0.0:9292 我们可以得到

{
    "hash_branches":
    {
        "":
        {
            "/home": "_roda_hash_branch__/home_1"
        }
    },
    "hash_paths":
    {
        "":
        {
            "/login": "_roda_hash_path__/login_2",
            "/logout": "_roda_hash_path__/logout_3"
        }
    },
    "hash_routes_methods":
    {},
    "json_result_classes": ["Array", "Hash"],
    "json_result_serializer": "#<Proc:0x00000001010bb718(&:to_json) (lambda)>",
    "json_result_content_type": "application/json"
}

3.namespace 和 自定义

源码中 namespace、path 是两个不同的概念,不要混淆。

比如:

  hash_branch("home") do |r|
    r.is "welcome" do
      "home - welcome"
    end
  end

当他是一个参数的时候,其实默认有一个未知参数是 namespace 他的值默认是 ""。所以你在观察前面 JSON 的时候可以看到很多方法挂载空字符串下。

这是它默认的工作方式。

我们可以通过覆盖第一个参数,来提供名字空间。

  hash_branch("roda_blog", "home") do |r|
    r.is "welcome" do
      "home - welcome"
    end
  end

这时候输出变成

{
    "hash_branches":
    {
        "roda_blog":
        {
            "/home": "_roda_hash_branch_roda_blog_/home_1"
        }
    },
    "hash_paths":
    {
        "":
        {
            "/login": "_roda_hash_path__/login_2",
            "/logout": "_roda_hash_path__/logout_3"
        }
    },
    "hash_routes_methods":
    {},
    "json_result_classes": ["Array", "Hash"],
    "json_result_serializer": "#<Proc:0x000000010259f738(&:to_json) (lambda)>",
    "json_result_content_type": "application/json"
}

我们可以看到 roda_blog 有了自己名字空间。

4.使用namespace

如果我们知道了namespace,来看下Roda是如何来构建复杂的应用程序。

namespace 其实就是一个名字,我们使用 hash_* 类方法的时候,可以通过第一参数命名这个路由。

而我们也可以在 r.hash_* RodaRequest实例方法 里面通过 namespace 显示的指定在这儿处理的路由。

require "roda"

class App < Roda
  plugin :hash_routes
  plugin :json

  hash_branch("roda_blog", "roda_home") do |r|
    r.is "welcome" do
      "home - welcome"
    end
  end

  hash_branch("home") do |r|
    r.is "welcome" do
      "home - welcome"
    end

    r.hash_routes("roda_blog")
  end

  route do |r|
    r.root do
      self.class.opts
    end
    r.hash_routes
  end
end

我们访问 http://0.0.0.0:9292/home/roda_home/welcome可以获得期待的返回。

Roda执行路由十分简单,就是在运行Ruby代码。这里我们来分析一下:

Roda 的 route 源码定义是解析 路由的入口,所以从这里开始。正确解析 r.root 的时候,这是主页。

r.hash_routes 操作其实很简单,它内部会 先执行 r.hash_paths 然后再 执行 r.hash_branches, 这两者的区别是 hash_paths 其实就是 Rodar.is 思想走的是精确匹配。不过 hash_paths 的匹配思想非常简单粗暴,直接把 路由里面的 path,作为 key 在 hash_paths 注册的字典里查找,找到就返回。这就是精确查找。

r.hash_branches 其实就是 r.on 的感觉是模糊/* 式 匹配路由。他就是以 '/' 分割第一个元素作为 branch 进行匹配,匹配下去的继续执行。

可以简单理解为,执行路由的感觉,他不过是在执行匹配代码。这里 hash_path 如果是直接匹配中就是 O(1) 复杂度。

我们把注意力收回来,继续分析:

route 中的 r.hash_routes 其实是 r.hash_routes("") 的简写,所以它挂载的是 hash_branch("home") 。而hash_branch("home") 里面写了一句 r.hash_routes("roda_blog") 其实这里就是在通过 namespace 的方式去挂载 "roda_blog"

这里要额外注意,作为 namespace,symbol和 string 作为 key 完全不同效果。要区分开,他们不一样。

解析路由方面,namespace 完全不参加过程。解析过程,完全依赖与书写和挂载的顺序。这里 假设我们解析了 "/home" 就会到 hash_branch("home") 中,如果我们继续有参数 "/home/roda_home" 其实这里就会进入 hash_branch("roda_blog", "roda_home") ,因为第一个参数是 namespace 不参与计算,只是用来互相关联的作用,第二个参数是 path 会参与 @remaining_path 的计算中去。这样匹配中,还可以继续往下匹配,精准匹配到 welcome 结束。

所以这里定义成什么样子,就可以匹配。而内部如果想要继续分开,就继续使用 hash_route 划分为更小的文件。

如果不想分开,就可以继续使用 r.on、r.is。他们无非是 Ruby 的代码而已。

5. DSL hash_routes

5.1 hash_routes DSL 介绍

RodaRequest 提供了一个方法 r.hash_routes 用来自动请求 hash_paths\hash_branches,在定义路由的时候,也设计了一个 hash_routes 这是一个 DSL,用来在上下文中快速的声明 hash_paths\hash_branches

5.2 hash_routes DSL 的 两种写法

DSL部分的 hash_routes 核心也非常简单。

可以用两种使用方法,其实差不多。

如果 block.arity=1 参考 proc#method-i-arity 如果你的块需要1个参数,那么会把 dsl实例作为参数返回在块中。如果没有参数,这个块会在 dsl的instance上下文中使用。

# Invoke the DSL for configuring hash routes, see DSL for methods inside the
# block.  If the block accepts an argument, yield the DSL instance.  If the
# block does not accept an argument, instance_exec the block in the context
# of the DSL instance.
def hash_routes(namespace='', &block)
  dsl = DSL.new(self, namespace)
  if block
    if block.arity == 1
      yield dsl
    else
      dsl.instance_exec(&block)
    end
  end

  dsl
end

所以可以这样, 可以省略 |r| 或者 不省略


require "roda"

class App < Roda
  plugin :hash_routes
  plugin :json

  hash_routes("roda_home") do |r|
    r.is "welcome" do
      "home - welcome"
    end

    r.on "login" do
      "home - login"
    end
  end

  hash_routes("plugin_home") do
    is "welcome" do
      "home - welcome"
    end
    
    on "bye" do
      "home - welcome"
    end
  end

  #....
end

5.3 hash_routes DSL 没有 path 了, 只有名字空间。

简单直接第一个参数是 namespace 所以 hash_routes DSL 永远是以 声明 被挂载路由而存在的

而 hash_routes 中的 is\on 的实际上原理,是 把 hash_routes 的 namespace 和 自己声明的 path 组合在一起

is 就是使用 hash_paths, on 就是使用 hash_branches

只是一个快速声明的语法糖。

def on(segment, &block)
  @roda.hash_branch(@namespace, segment, &block)
end

# Use the segment to setup a path in the current namespace.
# If path is given as a string, it is prefixed with a slash.
# If path is +true+, the empty string is used as the path.
def is(path, &block)
  path = path == true ? "" : "/#{path}"
  @roda.hash_path(@namespace, path, &block)
end

没有 path 了, 只有名字空间。这意味着 在 根 App 上,一定是通过原始的 r.on r.is 来分发到 r.hash_routes, 这里给出一个例子:

r.on 'orders' do
  r.is do
    r.hash_routes('orders/index')
  end
end

另一个文件中

class App
  hash_routes('orders/index') do
    is true do |r|
      #....
      view 'orders/index'
    end
  end
end

路由之间的层次关系,必须通过嵌套的调用关系,并且是 path 部分的嵌套关系决定的。

5.4 坑 dsl instance 和 roda_request 傻傻分不清楚

当我们想在 hash_routes DSL 里面进行嵌套访问的时候,别忘了

hash_routes do |hr| 这里 hr 具有 on、is 方法,但是不能与 RodaReques 的 @_request 混淆,我们已经自源码里看清楚,这个不是 request对象。

要想在 hash_routes 里继续调用 hash_routes 进行嵌套,需要使用 is\on 方法的 block 参数 —— 这才是 @_request ,RodaRequest 实例才有 r.hash_routes 方法。


require "roda"

class App < Roda
  plugin :hash_routes
  plugin :json

  hash_routes "list" do |hr| # 这里 hr 是 hash_routes DSL 实例 dsl
    #/shop/list
    hr.is true do
      'lists'
    end
  end

  hash_routes "shop" do
    # /shop
    is true do
      'shops'
    end

    on "list" do |r|  # !!! on 方法的 block 参数,才是 RodaRequest 实例 @_request 具有 hash_routes 方法
      # 这里的 r 不是 @_request 而是  dsl instance
      # 所以这里的方法是空
      r.hash_routes("list")
    end
  end



  route do |r|
    r.root do
      self.class.opts
    end

    r.on "shop" do
      r.hash_routes("shop")
    end
  end
end

5.5 dsl 是返回实例意味着你可以这样

hash_routes 返回的是一个 dsl 实例 ,意味着你也可以通过命令式去声明。

这一切都在意料之中。


def hash_routes(namespace='', &block)
  dsl = DSL.new(self, namespace)
  if block
    if block.arity == 1
      yield dsl
    else
      dsl.instance_exec(&block)
    end
  end

  dsl
end

# =======
require "roda"

class App < Roda
  plugin :hash_routes
  plugin :json

  bhr = hash_routes(:b)

  bhr.is "test" do
    "b:test"
  end

  route do |r|
    r.root do
      self.class.opts
    end

    r.hash_routes(:b)
  end
end

5.6 自由挂载

这里有一个例子,我可以在 route 里面直接挂载三个路由。

也可以在 hash_routes 里面嵌套 namespace 挂载。比较自由。

require "roda"

class App < Roda
  plugin :hash_routes

  hash_routes "a" do
    on "a-path" do |r|
      r.hash_routes("b")
      "a---path"
    end
  end

  hash_routes "b" do
    on "b-path" do
      "b---path"
    end
  end

  hash_routes "c" do
    on "c-path" do
      "c----path"
    end
  end

  route do |r|
    r.hash_routes
    r.hash_routes("a")
    r.hash_routes("b")
    r.hash_routes("c")
  end
end

6.dispatch_from

dispatch_from 的作用是会在 hash_routes 的DSL中发挥作用,它主要会把当前的hash_routes用 hash_branch 包一层,再这层的下面调用 r.hash_routes 以他自己的namespace作为参数。

其实等于给改变了 hash_routes 的 namespance和branch。

比如:

require "roda"

class App < Roda
  plugin :hash_routes

  hash_routes "a" do
    dispatch_from("abc")

    on "a-path" do |r|
      "a---path"
    end
  end

  route do |r|
    r.hash_routes
  end
end

这里 router.hash_routes 实际上是 r.hash_routes("")

不过 route 这边是运行解析时候关注的。

还得切换下视角,hash_routes 声明语句其实是注册路由。到底注册了什么路由呢?

hash_routes "a" 是在 "a"namespace 里注册路由。

普通情况下需要调用 r.hash_routes("a") 挂载生效。

这里比较特别 dispatch_from("abc") 实际上是 dispatch_from(namespace="", "abc") 等于要 r.hash_branch("abc") 包裹,并且依然是注册在""

所以这里

http://127.0.0.1:9292/abc/a-path 可以访问到


如果我们把名字空间加在 dispatch_from 里,比如 dispatch_from("ns", "abc-prefix")

看看下面的会如何反映

require "roda"

class App < Roda
  plugin :hash_routes

  # 以下代码等价于如下情况
  #
  # hash_routes "a" do
  #   on "a-path" do |r|
  #     "a---path"
  #   end
  # end

  # hash_branch("ns", "abc-prefix") do
  #   r.hash_routes("a")
  # end

  hash_routes "a" do
    dispatch_from("ns", "abc-prefix")

    on "a-path" do |r|
      "a---path"
    end
  end

  route do |r|
    r.hash_routes("ns")
  end
end

在运行的时候,我们挂载 "ns" ,这里实际上 dispatch_from("ns", "abc-prefix") 就相当于定义了 "ns" 了。

总之 dispatch_from 这就是一个包装重写 namespacebranch 的一个方法。

7. ??获取参数

TODO

总结:

1.Roda原生的 r.is r.on 只能把路由定义在一个文件中

因为本质上他们只是一段嵌套的block代码,重要的价值在于他们嵌套产生的闭包,可以帮助声明程序。 而执行的时候,递进调用,也会非常方便。这里无法很好地拆成多个文件。

2.hash_routes解决拆分的方法

实际上这里也没有什么魔法可言,hash_routes 所谓的拆分,就是做了一些事情,他把我们其他文件声明 hash_* 的方法在 App < Roda 中执行一遍。这其实是在执行类方法,而这个函数执行的意义实际上在不断地给 Roda.class.opts 里面的一个 hashmap 里面不断地注册方法名,同时也把这些方法定义在 App 上。

这就像是把 App 作为一个全局变量,然后把所有方法都挂在上面了,保持方法名没有冲突即可。

3.hash_routes 的解析

等到路由进入 Roda 的route开始执行,它就像执行Ruby代码一样一段一段的执行,r.is、r.on 他们会递进块式的执行。如果遇到 r.hash_routes 他们会通过传入的名字空间,去App上找到 存储方法的 hashmap,然后从中取出 相应 namespace 作为key的 route hash_map, 然后再通过 @remaining_path 根据是 path还是branch匹配key来获得方法名,然后调用这个方法名。

@remaining_path 作为每一段执行路由里面,都会访问的一个全局变量。不过这个变量是在顺序执行的代码中被访问的。匹配就编辑它,去除匹配部分继续递进顺着 tree-route 的树杈进行匹配。 如果不匹配就把修改的还原回去,跳过自己,传递给下一段代码。直至无法匹配返回 false,Roda遇到 false、nil 处理为 404。

3.小结

hash_routes 是注册方法

r.hash_routes 是挂载方法,默认挂载 ‘’

挂载在同一级别,就是同一级别的路由。

嵌套的 r.hash_routes 才是下一级

Mark24

Everything can Mix.