Sinatra 阅读 Sinatra 源码的感悟 (水平不高,纯属纪录)

wpzero · January 06, 2015 · Last by shiweifu replied at December 14, 2015 · 13243 hits

rails sinatra 等一些 ruby 的 web 框架,都是属于 rack app,建立在 rack 基础上的。 rack 是什么?官网上的解释是 Rack provides a minimal interface between webservers supporting Ruby and Ruby frameworks.我的理解就是链接服务器和 ruby 应用框架的,它解析 http,rack 里有两个比较重要的 class, Rack::Request, Rack::Response,Rack::Request 负责解析 request, Rack::Response 负责 http 的 response,想了解具体 rack 的应用查看http://www.rubydoc.info/github/rack/rack/Rack/Mime。 rack 可以 use 多个 middleware 和 run 一个 app, middleware 一般如下格式:

class MyMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    puts 'mymiddleware'
    puts env.inspect
    if env['PATH_INFO'] == '/'
      @app.call(env)
    else
      [404, {'Content-Type' => 'text/plain'}, ['not ok']]
    end
  end
end

run 的那个 app 要响应 call,可以如下:

my_app = proc do |env|
  [200, {'Content-Type' => 'text/plain'}, ["ok"]]
end

那么就可以用 rack 在 config.ru 中这样写服务,如下:

use MyOther
run my_app

以上只是展示了 rack 的感念,以及 rack 写服务的基础。因为 sinatra 是建立在 rack 基础之上的,所有这些应该算是读 sinatra 源码的前提准备。 1.sinatra 的整体结构 那么,我们来看一下,sinatra 是怎样来应对 rack 的要求的,sinatra 大体结构是这样的,代码如下:

module Sinatra
  class Base

    # 用于作为middleware用
    def initialize(app=nil)
      super
      @app = app
      yield self if block_given?
    end

    def call(env)
      dup.call!(env)
    end

    def call!(env)
    end

    class << self

      # 用于作为app用
      def call(env)
        prototype.call(env)
      end

      def prototype
        @prototype ||= new
      end

    end
  end
end

以上的代码结构就是 sinatra 应对 rack 的要求而组织的,使得 sinatra 既可以作为 middleware 又可以作为 app,当然,sinatra 要多一些 magic,后面再说,如果只是这样的结构,sinatra 程序就只能写成 modular-style structure,不能写成 classic style 的 structure,对于 modular-style structure 我写了一 seed https://github.com/wpzero/sinatra_seed.gitclone下来看一下。,可以 下面我们来看,sinatra 路径 magic get/post/put/delete 2.sinatra 路径 class macro sinatra 应用的代码如下:

require "sinatra/base"
class UserController < Sinatra::Base
  get '/hello' do
    'hello'
  end
end

这个 get 方法就是 sinatra 的一些 magic,用 class macro 实现的。 class macro,涉及到一些 meta programming 的概念,我这里稍微介绍一下 ruby 为什么有 singleton method? 例如:

class C < D
end

c1 = C.new
c2 = C.new

def c1.hello
  puts 'hello'
end

c1.hello
c2.hello

#=> 'hello' #=> undefined method `hello' for # 这是为什么?c1 和 c2 不是属于一同一个 class 吗?c1 look up method 一定也是向右一步找到它的 class(C),然后一路向上找 class C 的 ancestors, 找是否有 hello 这个 instance_method 没,有就调用,可是 c2 为什么不能找到 hello 这个 method,原因是 ruby,在 instance 和它的 class 之间还有一层 class(也就是大家所说的 eigenclass),每一个 instance 和它的 eigenclass 是唯一对应的。这就是 hello 这个方法的悉身之所,而 instance c2 的 eigenclass 与 c1 的 eigenclass 不同,所以 c2 不可找到 hello。 有了 eigenclass 这个概念,我们就可以利用往 eigenclass 中加入 instance method,那么相应的 instance 就可以调用了。而 class 也是一个 instance 只不过它的 class 是 Class 而已。 具体关系如图: 那么我们就来看一个最简单的 class macro。

class C
  class << self
    def macro
      puts 'macro'
    end
  end

  macro
end

#=> macro

在 class C 的 eigenclass 中定义一个方法,那么 class C 就可以调用。 那么 sinatra 是怎么实现 get/put 这些 routes 的方法的? 大体结构如下:

class Base
  class << self

    def get
      #具体内容
    end

    def post
      #具体内容
    end

  end
end

这样我们就可以调用 get 等方法在 Base 的 subclass 中。 下面我们具体分析它的实现的代码。 3.sinatra get 的具体实现(unboundmethod) 先介绍一下 unboudmethod。 一般 instance method 都是属于某个 class,而调用它的是该 class 的 instance,那么该在 instance method 中就可以 access 该 instance 的 instance variable。那么我们可不可以把一个 instance method 指定给一个 instance?ruby 是支持这个功能的,那就是 bind。 先看一个例子:

class Square
  def area
    @side * @side
  end
  def initialize(side)
    @side = side
  end
end

area_un = Square.instance_method(:area)

s = Square.new(12)
area = area_un.bind(s)
area.call   #=> 144

instance_method Returns an UnboundMethod representing the given instance method in mod. 而这有什么用呢?我们就可以把一些 method 存起来,在某一时刻 bind 给一个 instance 来执行。 联想一下 sinatra 的 magic get 方法,其实,它就是实现了在 url 的 path_info 符合某个路径时,执行传给 get 的 block,而且这个 block 是 bind 到一个 Application instance 上的(也就是前边的举的例子的 UserController 的 instance)。 那么来看一下 sinatra 的源码,如下:

class Base
   class << self

      def get(path, opts={}, &block)
        route('GET', path, opts, &block)
      end

      def route(verb, path, options={}, &block)
        signature = compile!(verb, path, block, options)
        (@routes[verb] ||= []) << signature
        signature
      end

      def compile!(verb, path, block, options = {})
        method_name             = "#{verb} #{path}"
        unbound_method          = generate_method(method_name, &block)
        # string to regular expression, and get the route keys
        pattern, keys           = compile path
        conditions, @conditions = @conditions, []

        [ pattern, keys, conditions, block.arity != 0 ?
            proc { |a,p| unbound_method.bind(a).call(*p) } :
            proc { |a,p| unbound_method.bind(a).call } ]
      end

      # path to regular expression and keys
      def compile(path)
        keys = []
        if path.respond_to? :to_str
          pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c) }
          pattern.gsub!(/((:\w+)|\*)/) do |match|
            if match == "*"
              keys << 'splat'
              "(.*?)"
            else
              keys << $2[1..-1]
              "([^/?#]+)"
            end
          end
          [/^#{pattern}$/, keys]
        elsif path.respond_to?(:keys) && path.respond_to?(:match)
          [path, path.keys]
        elsif path.respond_to?(:names) && path.respond_to?(:match)
          [path, path.names]
        elsif path.respond_to? :match
          [path, keys]
        else
          raise TypeError, path
        end
      end

      # 产生Base 的unboundmethod
      def generate_method(method_name, &block)
        # the current class is Base, self is Base
        define_method(method_name, &block)
        method = instance_method method_name
        remove_method method_name
        method
      end

   end
end

这里 generate_method 用于把一个 block 变为 Base 的 unboundmethod 用的就是 define_method 加上 instance_method。 -------------今天周五,哎!!带工资写一些吧。

依次类推,其他的 sinatra routes DSL,也都是相同的原理。 看下 sinatra 的源码,如下:

class Base
  class << self
    attr_reader :routes, :filters
    # routes 是个hash, :get => [], :post => [] ...
    # filters 是 hash, :before => [], :after => [].....

    def get(path, opts={}, &block)
      # get
      conditions = @conditions.dup
      route('GET', path, opts, &block)
      # head
      @conditions = conditions
      route('head', path, opts, &block)
    end

    def put(path, opts={}, &bk)     route 'PUT',     path, opts, &bk end
    def post(path, opts={}, &bk)    route 'POST',    path, opts, &bk end
    def delete(path, opts={}, &bk)  route 'DELETE',  path, opts, &bk end
    def head(path, opts={}, &bk)    route 'HEAD',    path, opts, &bk end
    def options(path, opts={}, &bk) route 'OPTIONS', path, opts, &bk end
    def patch(path, opts={}, &bk)   route 'PATCH',   path, opts, &bk end
  end
end

现在,实现了 get /post/put/delete/ 等 sinatra 的 DSL。 但是 sinatra 程序执行的时候是怎么用到着 routes, filters 的呢?其实想一想也简单,就是 app instance 的 class 或者 superclass ...的 instance 属性 routes,filters 中存在呢,下面我们来研究一下 sinatra 是怎么实现的。 4.sinatra 程序如何实现 routes dispatch(根据 path_info,来执行相应的程序逻辑) 其实这主要的程序执行过程就是在 Base 的 instance method call! 中,回顾一下,前边我说的 sinatra 的主要框架。 来看一下 sinatra 代码的实现,如下:

class Base
  # 这是rack app的写法
  attr_accessor :request, :response, :params, :env

  def initialize(app=nil)
    super()
    @app = app
    yield self if block_given?
  end

  def call(env)
    dup.call!(env)
  end

  def call!(env)
    @env = env

    # app instance 的instance variable, 用于分析request, 
    # 这里我用了Rack::Request, sinatra其实是自己写了一个Request继承Rack::Request, 其实就是加了一些instance method helper。
    # 为了简单,我直接用Rack::Request
    @request = Rack::Request.new(env)

    # app instance 的instance variable, 用于完成reponse
    # 这里同样,sinatra自己写了一个subclass继承了Rack::Response
    # 这里为了简单明了,我用Rack::Response
    @response = Rack::Response.new

    # request.params 是Rack帮我们解析的一个http请求的参数,这里包括url data(url中的参数,如: \users?available=1中的:available => 1) 和
    # form data(请求body中的信息,一般是put, post中的)
    @params = indifferent_params(@request.params)

    @response['Content-Type'] = nil

    # 这个是程序的主要执行过程(dispatch)。这里用sinatra这个框架很特别的用法
    invoke { dispatch! }


    # 后面专门分析sinatra的错误处理
    invoke { error_block!(response.status) }
    # 这里可以先忽略content_type,status, body这些method,其实是很简单的,就是设置response,后面我们再介绍。
    unless @response['Content-Type']
      if Array === body and body[0].respond_to? :content_type
        content_type body[0].content_type
      else
        content_type :html
      end
    end
    # 返回response
    @response.finish
  end
end

这里前面不过是对 env 的解析,用了 Rack::Request,至于 reponse 用了 Rack::Response。 最重要就是 invoke { dispatch! }这行代码,我们要好好分析学习一下。 下面我们看一下:invoke 和 dispatch! 这两个 method 的代码:

class Base


  def invoke
    # catch和throw sinatra完美诠释和应用了,应该学习。这里sinatra用catch throw来实现 1.随时跳出routes的处理(跳回到catch),2.同时返回结果。有些像c的goto。但是也是和goto一样是一把双刃剑
    res = catch(:halt) { yield }


    # 根据catch的结果设置status, body, headers
    res = [res] if Fixnum === res or String === res
    if Array === res and Fixnum === res.first
      status(res.shift)
      body(res.pop)
      headers(*res)
    elsif res.respond_to? :each
      body res
    end
    nil # avoid double setting the same response tuple twice
  end

  #
  def dispatch!
    invoke do
      # 如果是静态文件,文件服务器
      static! if settings.static? && (request.get? || request.head?)
      # before filter执行
      filter! :before
      # route 执行
      route!
    end
  rescue ::Exception => boom
    # 错误处理后面在来分析
    invoke { handle_exception!(boom) }
  ensure
    # after filter 执行
    filter! :after unless env['sinatra.static_file']
  end

end

我读 invoke 这里的代码,其实是很惊讶的,本来我只是知道有 catch 和 throw 这东西,因为没有机会用,我一直认为它们和 begin rescue ensure 相似,没想到,他们竟然是 ruby 的 goto,用于跳出一个程序 logic , 而且是可以带返回值,这里返回值付给 res,然后,更新 response。 这里发一个小小的 throw catch 的例子:


res = catch(:halt) {tt}

def tt
     throw :halt, [1,2]
end

这里 res 的值为 [1,2],希望帮助大家理解 sinatra 的 invoke 的原理。 下面我来分析 dispatch! 方法。 1.先是判断是否是静态文件,用 static! 方法,static! 用了 send_file 这个 method,send_file 这个 method 用到了 Rack::File,这个有兴趣的,可以读一下。 2.执行 filter! before,来执行符合条件的 before。代码如下:

class Base

    def self.settings
      self
    end

    # 访问 the class Base 得到 这些属性
    # Access settings defined with Base.set.
    def settings
      self.class.settings
    end

  def filter!(type, base = settings)
    # one right step to the class and along the ancestors to find the methods which respond_to? :filters
    # 其实就是Base
    # filters 是 Application class的filters数据
    filter! type, base.superclass if base.superclass.respond_to?(:filters)
    base.filters[type].each { |args| process_route(*args) }
  end
end

这里,我们可以看到,settings 其实就是 Application(继承了 Base 的 controller)。filter! 默认的 settings 参数就是 Application。filter! type, base.superclass if base.superclass.respond_to?(:filters) 这就 Application 的 superclass 一层一层的爬,都判断一遍,是否有符合的。base.filters 就是我们前边说的,Base instance variable @filters, 然后 each 执行 process_route,这里我先不讨论这个方法,我们和 route! 一起来看。 3.执行 route!, 代码如下:

def route!(base = settings, pass_block=nil)
  if routes = base.routes[@request.request_method]
    routes.each do |pattern, keys, conditions, block|
      pass_block = process_route(pattern, keys, conditions) do |*args|
        route_eval { block[*args] }
      end
    end
  end

  # 如果 当前application中,即controller中没有匹配的routes, Run routes defined in superclass.
  if base.superclass.respond_to?(:routes)
    return route!(base.superclass, pass_block)
  end

  route_eval(&pass_block) if pass_block
  route_missing
end

# 一个request 只对应一个路径
def route_eval
  throw :halt, yield
end

大体逻辑和 filter! 相似,但是注意它的 process_route 在 route_eval 中调用,那么 route_eval 作用是什么? route_eval 确保 routes 中只要有一个路径符合要求就执行返回结果,不再继续查找。 4.用 ensure 确保 flter! after 执行 就算是 throw:halt ensure 的代码还是执行的,小例子:

def tt
     puts 'execute'
     throw :halt
     ensure
          puts 'ensure'
end

catch(:halt){ tt }
# => execure
# => 'ensure'

最后 filter! after 一定是执行的。 其实,我感觉 dispathc! 中的 invoke 完全没有必要,反而多执行一边 对 reponse 的设置,这个我有做测试,没有必要多执行一边 invoke 因为去掉,dispathc! 中的 invoke,ensure 还是执行的,这是 ensure 的属性,无非就是为了如果是静态文件时,不执行 filter! after,这个我感觉完全可以弄一个 instance_varibale 记录一下,没必要多执行,一遍 invoke!。也许是为了,代码的一致性。 我有把 sinatra 代码中的 dispathc! 中 invoke 去掉和没去掉之前做过对比,没有影响结果的正确定。反而少之行一遍 invoke。 没去掉之前 ----------------invoke----------------------- ----------------invoke----------------------- -------------second------------------- -------------ensure------------------- -------------first------------------- ----------------invoke----------------------- -------------third-------------------

去掉之后 ----------------invoke----------------------- -------------ensure------------------- -------------first------------------- ----------------invoke----------------------- -------------third------------------- 我感觉 dispatch! 这里的 invoke 完全可以去掉,还能提高性能。 5.process_route method 分析 代码如下:

def process_route(pattern, keys, conditions, block = nil, values = [])
  route = @request.path_info
  route = '/' if route.empty?
  # 判断是否匹配
  return unless match = pattern.match(route)
  values += match.captures.to_a.map { |v| force_encoding URI.decode_www_form_component(v) if v }

  if values.any?
    original, @params = params, params.merge('splat' => [], 'captures' => values)
    keys.zip(values) { |k,v| Array === @params[k] ? @params[k] << v : @params[k] = v if v }
  end

  catch(:pass) do
    #  执行conditions,先忽略掉
    conditions.each { |c| throw :pass if c.bind(self).call == false }
    #  执行block
    block ? block[self, values] : yield(self, values)
  end
ensure
  @params = original if original
end

这个方法也不复杂 1.match = pattern.match(route) 判断 request.path_info 是否符合条件,不符合条件 return

  1. values += match.captures.to_a.map { |v| force_encoding URI.decode_www_form_component(v) if v }取出 route params 3.如果有 route params,和 keys 转化为 hash, merge 到 params 中。if values.any? original, @params = params, params.merge('splat' => [], 'captures' => values) keys.zip(values) { |k,v| Array === @params[k] ? @params[k] << v : @params[k] = v if v } end 4.执行程序逻辑 block ? block[self, values] : yield(self, values)。回顾 compile! 返回的,存在 filters 和 routes 中的 element, [ pattern, keys, conditions, block.arity != 0 ? proc { |a,p| unbound_method.bind(a).call(*p) } : proc { |a,p| unbound_method.bind(a).call } ] 5.params 复原,这就代表这。params 来自 route 的参数,是和 get/post...before/after 的路径参数有关,例如:假设 URL 为/users/1 before do puts params.inspect end 中 params 是没有 id => 1 这东西的。

曾经看过一些,sinatra 有些代码还是蛮有意思的,比如 catch throw 的用法。

感觉 rake 在 Http Server 上又包了一层。

比如 cookie 有了加密、解密。然后为 session 提供了基于 cookie 的存储。

有一种工具叫做 Blog。

rack 实现 HTTP 协议。

其实 rack 就是一种类似最初 web 程序开发中使用 cgi 方式的实现

#1 楼 @zgm 对对的,它的 throw catch 用的很巧妙,算是 sinatra 的一个亮点写法。以前不知道 catch 和 throw 可以这么用,throw 还可以 with data to catch。

#6 楼 @wpzero 支持楼主继续写下去

#6 楼 @wpzero 支持楼主继续写下去

rack 可以 run 多个 app 哦!lz 可以看看 cascade 的代码,另外还有 URLMap 就知道了。

求继续。。。。

做过类似的事情,按照 sinatra 源码的组织方式,一个 commit 一个 commit 的重新搭建 sinatra https://github.com/serco-chen/rebuilding_sinatra

看过后终于理解 eigenclass 和 unbound method 了。。。

学习了。。

#3 楼 @Rei 是的,新人不了解,谢谢指点。有空儿一定弄一个 blog

#9 楼 @lmorenbit 对的,我说的不严谨,多谢提醒

#11 楼 @serco 看了您的 repo, 发现我们走了相同的一段路。。。sinatra 代码还是很精简的巧妙的。

学习收藏

Good job!

谢谢学习了
You need to Sign in before reply, if you don't have an account, please Sign up first.