为什么这么说呢?从没有见过运用如此广泛,几乎是所有Ruby Web Framework基础的开源项目中,这个文件里竟然有如此之多令人费解的部分(完全可以认为是Bug)

简单的举个例子吧,首先,对于Rack来说,.ru文件和.rb文件的区别在哪里?

def self.parse_file(config, opts = Server::Options.new)
  options = {}
  if config =~ /\.ru$/
    cfgfile = ::File.read(config)
    if cfgfile[/^#\\(.*)/] && opts
      options = opts.parse! $1.split(/\s+/)
    end
    cfgfile.sub!(/^__END__\n.*\Z/m, '')
    app = new_from_string cfgfile, config
  else
    require config
    app = Object.const_get(::File.basename(config, '.rb').capitalize)
  end
  return app, options
end

代码中告诉了我们答案,如果是.ru文件,Rack::Builder不仅仅会将其读出,而且还会从中解析出以#\开头的部分,作为Rack::Server启动的选项,举个例子:

#\--port 3000
run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "Hello Rack!"]}

比如这个config.ru文件,rackup在执行这个文件是将启动出3000端口的服务器而非9292。

这本来是个不错的设计啦,但是仔细看代码就会发现,只支持一个选项,第二个选项用方括号运算符是匹配不到的,额。。。你妹。。

不得不承认Rack::Builder支持map包含map,也就是形成树结构,这点不错。更重要的是Rack仅仅执行那些不得不执行的map的代码块,如果路由没匹配到就不予执行这些都很好。但是当你发现Rack::Builder没接收到一个请求就会创建一个新的URLMap对象然后把路由计算成正则表达式,并且计算出的正则表达式不予缓存,每次都重新计算,就会感觉之前争取到的性能在这里又浪费了的惋惜啊。。好吧,这一定是考虑到开发模式下的需求。

接着就是最坑的一个Bug了

require 'rack'

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, 'root']}
builder = Rack::Builder.new do
  map '/' do
    run infinity
  end

  use Rack::CommonLogger

  map '/version' do
    map '/' do
      run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity 0.1"] }
    end

    map '/last' do
      run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity beta 0.0"] }
    end
  end
end
Rack::Handler::Thin.run builder, :Port => 9292
curl 0.0.0.0:9292/version/last

虽然看上去结果应该是infinity beta 0.0,但实际上结果是root,额。。

这个Bug的关键在于use Rack::CommonLogger这句语句的位置,可以看到,它写在两个map的中间。而use方法是这么实现的:

def use(middleware, *args, &block)
  if @map
    mapping, @map = @map, nil
    @use << proc { |app| generate_map app, mapping }
  end
  @use << proc { |app| middleware.new(app, *args, &block) }
end

为了让middleware处于middleware stack的外面,代码先判断当前有没有执行过map方法,如果执行过的话,先将它放进middleware stack,外面放middleware对象。这样执行的时候就能先执行到middleware了(大概作者是这么想的,但是这么做完全是错的,实际情况是先放进middleware stack的先被执行到)。如果你没有执行过map方法,那么虽然现在middleware stack上看上去只有middleware对象,但是等会执行的时候会把map包进来,这样就解决问题了。但是它显然没有想到,如果我把use写两个map中间的情况。此时,middleware stack对象会先把第一个map写进去,然后跟着middleware。然后在下一个map执行的时候,重新赋值map对象,并在最后被middleware stack的两个元素包住。这样请求一进来的时候就会发现,竟然是第一个map直接接受请求,连middleware都不是(之前讲过,实际情况是先写进middleware stack的先被执行到)。

算起来好像只有把use语句写在所有map之前的时候才是能真正正常工作的。大概确实很少人不这么做吧,否则这么明显的问题为何不被解决呢。

虽然我们可以看到,Rack::Builder实现了自己的路由和middleware调用方法,但是无论是Rails还是Sinatra,都无一基于Rack::Builder,不仅仅是因为Rack::Builder没有更多的功能,已有功能也不容易扩展,而且,里面坑太多了,还不如自己从头写呢。