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

wpzero · 发布于 2015年01月06日 · 最后由 shiweifu 回复于 2015年12月14日 · 5231 次阅读
15317

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 #<0x007fe5442665c0> 这是为什么?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这东西的。
共收到 20 条回复
115

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

7072

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

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

1

有一种工具叫做 Blog。

273

rack 实现HTTP协议。

515

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

15317

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

115

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

115

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

96

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

9800

求继续。。。。

1667

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

96

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

3807

学习了。。

15317

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

15317

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

15317

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

18464

学习收藏

18364

Good job!

23844
谢谢学习了
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册