免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12下一页
最近访问板块 发新帖
查看: 8430 | 回复: 12
打印 上一主题 下一主题

sinatra分析 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2011-02-23 10:07 |只看该作者 |倒序浏览
本帖最后由 中关村村草 于 2011-02-23 10:09 编辑

sinatra分析



备注: 本文是基于sinatra1.0版本,在线文档: sinatra 分析



1.sinatra简介
Sinatra is a DSL for quickly creating web applications in Ruby with minimal。
Fewer classes, less inheritance
controller object mapping & routes vs. URLs---Dont's fear the URLs
Exposed Simplicity instead of hidden complexity
Small things, loosely joined, written fast

2.sinatra分析
2.1.Rack机制
sinatra作为一个web框架,是基于rack规范的。rack规范和Java的servlet规范有点类似,Rack中间件和filter机制有些类似,都是能够拦截 request/response做一些事情。所谓的rack兼容的中间件无非是一个可以执行 call(env) 的对象,详细关于rack的内容可以参考rack官网,还有这个rack 入门文档也很好。
在源码中可以看到,sinatra的Request和Response都是基于rack扩展的,并对 Rack::Request和Rack::Response分别做了一些调整。

sinatra是通过Application.run!来启动服务器的

Ruby代码

  1.      def run!(options={})
  2.         set options
  3.         handler      = detect_rack_handler
  4.         handler_name = handler.name.gsub(/.*::/, '')
  5.         puts "== Sinatra/#{Sinatra::VERSION} has taken the stage " +
  6.           "on #{port} for #{environment} with backup from #{handler_name}" unless handler_name =~/cgi/i
  7.         handler.run self, :Host => bind, :Port => port do |server|
  8.           trap(:INT) do
  9.             ## Use thins' hard #stop! if available, otherwise just #stop
  10.             server.respond_to?(:stop!) ? server.stop! : server.stop
  11.             puts "\n== Sinatra has ended his set (crowd applauds)" unless handler_name =~/cgi/i
  12.           end
  13.           set :running, true
  14.         end
  15.       rescue Errno::EADDRINUSE => e
  16.         puts "== Someone is already performing on port #{port}!"
  17.       end  
复制代码
其中detect_rack_handler是通过 Rack::Handler.get来检测rack处理器的,默认的server有thin/mongrel/webrick,绑定的地址是 0.0.0.0,端口是4567

Ruby代码
  1. module Sinatra
  2.     class Base
  3.         set :server, %w[thin mongrel webrick]
  4.         set :bind, '0.0.0.0'
  5.         set :port, 4567
  6.     end
  7. end
复制代码
注意到handler.run self, : Host => bind, : Port => port do |server|,这个self指的是Sinatra :: Base,根据rack规范,最终的请求的入口就是 Sinatra :: Base.call(env)方法

Ruby代码
  1.   def prototype
  2.         @prototype ||= new
  3.       end

  4.       # Create a new instance of the class fronted by its middleware
  5.       # pipeline. The object is guaranteed to respond to #call but may not be
  6.       # an instance of the class new was called on.
  7.       def new(*args, &bk)
  8.         builder = Rack::Builder.new
  9.         builder.use Rack::Session::Cookie if sessions?
  10.         builder.use Rack::CommonLogger    if logging?
  11.         builder.use Rack::MethodOverride  if method_override?
  12.         builder.use ShowExceptions        if show_exceptions?
  13.         middleware.each { |c,a,b| builder.use(c, *a, &b) }

  14.         builder.run super
  15.         builder.to_app
  16.       end

  17.       def call(env)
  18.         synchronize { prototype.call(env) }
  19.       end  
复制代码
从 call方法可以看到,是通过生成一个Sinatra::Base实例对象来运行的,最终会调用的是call(env) -> call!(env)

,接下去的工作就是等客户端发送请求过来就可以了。在生成这个实例对象@prototype的时候,直接引入 rack中间件机制,同样,sinatra允许你使用use方法来增加新的中间件(use只是把中间件加入@middleware变量中去而已)。这样 sinatra就已经启动起来了。

2.2.路由机制
sinatra的路由机制和 rails不大一样,sinatra是在controller里边用get/post path这样来指定的。而rails是把controller和map分开处理,通过map来找到对应的controller和action。rails 当初这么搞主要是为了兼容controller和路由不匹配的情况,个人觉得sinatra的写法是非常直观的,也非常的灵活。

Ruby代码
  1. delegate :get, :put, :post, :delete, :head, :template, :layout,
  2.              :before, :after, :error, :not_found, : configure, :set, :mime_type,
  3.              :enable, :disable, :use, :development?, :test?, :production?,
  4.              :helpers, :settings
复制代码
看main.rb可以看到include Sinatra:: Delegator,可以把get/post等众多方法代理给Sinatra::Application去执行,在后面使用get '/' do xxx end的时候其实会调用Sinatra::Application(即Sinatra::Base )的get方法。

Ruby代码
  1. require 'rubygems'
  2.   require 'sinatra'
  3.   get '/' do
  4.     'Hello world!'
  5.   end  
复制代码
例如这样一个简单的web应用就可以响应'/'的请求路径,那么Sinatra::Base是怎么识别到这个路由的呢?我们继续来看看上面的get方法做了什么事情,可以看到最终是调用route方法的(同时,从代码可以看到sinatra支持get/post/put/post/delete/head几种method的请求)。按照我们的大概思路,在看到某个请求方法的时候,sinatra会把{请求类型_路径 => 代码块}放到一个专门放路由的地方上去,然后在每一次请求调用call(env)的时候,根据“请求类型_路径”来获得需要执行的代码块。好,继续看看 route的代码是怎么实现的?

Ruby代码
  1.    def route(verb, path, options={}, &block)
  2.         # Because of self.options.host
  3.         host_name(options.delete(:bind)) if options.key?(:host)
  4.         options.each {|option, args| send(option, *args)}

  5.         pattern, keys = compile(path)
  6.         conditions, @conditions = @conditions, []

  7.        define_method "#{verb} #{path}", &block
  8.         unbound_method = instance_method("#{verb} #{path}")
  9.         block =
  10.           if block.arity != 0
  11.             proc { unbound_method.bind(self).call(*@block_params) }
  12.           else
  13.             proc { unbound_method.bind(self).call }
  14.           end

  15.         invoke_hook(:route_added, verb, path, block)

  16.         (@routes[verb] ||= []).
  17.           push([pattern, keys, conditions, block]).last
  18.       end  
复制代码
这个代码处理的事情比较多,我们来仔细分析分析,前面两句代码是用来记录能够处理的请求的约束(例如特定的host_name,user_agent),然后compile(path)的工作是把path换成一个正则表达式(这样通过match就可以获得匹配的组),还有提取keys(例如*的就变成splat,:name就变成name)。重要的是把get '/' do xxx end动态生成一个"#{verb} #{path}"的方法并最终封装成一个带有上下文状态的proc对象,最终是把[pattern, keys, conditions, block]加入@routes[verb]里边去。而call(env)能够处理请求就得靠这个@routes来实现。
先来看看 call(env) -> call!(env),最重要的部分是invoke { dispatch! },可以看到dispatch!的整个流程是
判断并处理static文件 -> before_filter! -> route! -> after_filter!,主要的处理过程是route!方法

Ruby代码
  1.    def route!(base=self.class, pass_block=nil)
  2.       if routes = base.routes[@request.request_method]
  3.         original_params = @params
  4.         path            = unescape(@request.path_info)

  5.         routes.each do |pattern, keys, conditions, block|
  6.           if match = pattern.match(path)
  7.             values = match.captures.to_a
  8.             params =
  9.               if keys.any?
  10.                 keys.zip(values).inject({}) do |hash,(k,v)|
  11.                   if k == 'splat'
  12.                     (hash[k] ||= []) << v
  13.                   else
  14.                     hash[k] = v
  15.                   end
  16.                   hash
  17.                 end
  18.               elsif values.any?
  19.                 {'captures' => values}
  20.               else
  21.                 {}
  22.               end
  23.             @params = original_params.merge(params)
  24.             @block_params = values

  25.             pass_block = catch(:pass) do
  26.               conditions.each { |cond|
  27.                 throw :pass if instance_eval(&cond) == false }
  28.               route_eval(&block)
  29.             end
  30.           end
  31.         end

  32.         @params = original_params
  33.       end
复制代码
首先sinatra先从@routes里边取得符合请求类型的[pattern, keys, conditions, block]列表,然后逐个扫描,通过pattern来match路径,如果符合的话,取得通配符,命名参数的值并封装到params去(得益于 compile(path)的工作)。接下去判断conditions是否符合,如果都符合,则执行业务,即block。整个流程处理完之后,把 params恢复为原本的状态。

2.3.拦截器
在上面已经提到,sinatra的拦截器是通过before_filter!和after_filter!来执行的,如下所示:

Ruby代码
  1. def before_filter!(base=self.class)
  2.       before_filter!(base.superclass) if base.superclass.respond_to?(:before_filters)
  3.       base.before_filters.each { |block| instance_eval(&block) }
  4.     end
复制代码
配置过滤器也非常简单,定义一个前置过滤器,例如

Ruby代码
  1.   before do
  2.     @note = 'Hi!'
  3.     request.path_info = '/foo/bar/baz'
  4.   end  
复制代码
sinatra 通过Sinatra::Base的before把block加入到@before_filters中去,这个应该很容易明白的。不过,这个拦截器功能比起 rails那个显得简陋了,毕竟不能直接针对某些路径进行拦截处理。

2.4.模板渲染
sinatra通过Tilt实现多模板的渲染机制,生成页面的过程是在业务代码块那里注明的,例如

Ruby代码
  1. require 'erb'
  2.   get '/' do
  3.     erb :index
  4.   end
复制代码
sinatra的模板方法是在 Sinatra::Templates模块里边定义的,能够支持erb,erubis,haml,sass,less,builder,具体的实现如下:

Ruby代码
  1. def render(engine, data, options={}, locals={}, &block)
  2.       # merge app-level options
  3.       options = settings.send(engine).merge(options) if settings.respond_to?(engine)

  4.       # extract generic options
  5.       locals = options.delete(:locals) || locals || {}
  6.       views = options.delete(:views) || settings.views || "./views"
  7.       layout = options.delete(:layout)
  8.       layout = :layout if layout.nil? || layout == true

  9.       # compile and render template
  10.       template = compile_template(engine, data, options, views)
  11.       output = template.render(self, locals, &block)

  12.       # render layout
  13.       if layout
  14.         begin
  15.           options = options.merge(:views => views, :layout => false)
  16.           output = render(engine, layout, options, locals) { output }
  17.         rescue Errno::ENOENT
  18.         end
  19.       end

  20.       output
  21.     end  
复制代码
具体的流程是先找到template engine,通过template的render方法渲染子页面,然后在把子页面的内容作为一个block参数放到渲染layout的render方法上去,这样在父页面里边的yield就会被子页面的内容所取代,从而实现整体页面的渲染。

2.5.错误及状态处理
sinatra在这方面的处理,我觉得非常巧妙,还认识了一些从来没用过的api。几个重要的特性:
halt:

Ruby代码
  1. halt 410
  2.   halt 'this will be the body'
  3.   halt 401, 'go away!'error:
复制代码
Ruby代码
  1. error do
  2.     'Sorry there was a nasty error - ' + env['sinatra.error'].name
  3.   end
  4.   error MyCustomError do
  5.     'So what happened was...' + request.env['sinatra.error'].message
  6.   end
  7.   error 400..510 do
  8.     'Boom'
  9.   end
复制代码
error 的实现很简单,只是把error code和block记录到@errors上去,而not_found其实就是404的error了。halt从代码实现上看,它是throw一个 halt的异常。

这些处理方式在sinatra最终是怎么处理的呢?我们先回到dispatch!这个主方法,从源码中可以看到如果是静态页面,会抛出halt(line 173),到了route!方法的时候,如下

Ruby代码
  1.   
  2.             pass_block = catch(:pass) do
  3.               conditions.each { |cond|
  4.                 throw :pass if instance_eval(&cond) == false }
  5.                 route_eval(&block)
  6.             end  
复制代码
catch(args,&block) 这个方法是会忽视在遇到pass异常的时候忽略异常并跳出block的运行,所以conditions验证不通过的时候,就会转入下一个pattern验证,而在验证通过后到了route_eval(&block) 就会抛出halt从而跳出循环,表示已经匹配成功。抛出异常之后会在dispatch!通过rescue来处理。error_block!(*keys) 就是用来处理error的,@errors根据error code来获取block,这样就可以输出自定义的错误页面了

转自:http://www.javaeye.com/wiki/ruby_off_rails/2277-sinatra-analytics

论坛徽章:
0
2 [报告]
发表于 2011-02-23 10:55 |只看该作者
感觉 Sinatra 比 Rails 简单直观很多,不知道其功能和性能怎么样,能否用于生产环境。

论坛徽章:
0
3 [报告]
发表于 2011-02-23 12:44 |只看该作者
框架太多,实用跟兴趣是有差距的。

论坛徽章:
0
4 [报告]
发表于 2011-02-23 12:44 |只看该作者
Cool ~

论坛徽章:
0
5 [报告]
发表于 2011-08-09 12:38 |只看该作者
现在,想关注下Ruby和Sinatra了。

论坛徽章:
0
6 [报告]
发表于 2011-08-09 13:34 |只看该作者
好多网站 即用Rails,也使用Sinatra

论坛徽章:
0
7 [报告]
发表于 2011-08-10 08:44 |只看该作者
回复 6# bugbugbug3


    你用过?

论坛徽章:
0
8 [报告]
发表于 2011-08-10 09:40 |只看该作者
回复 7# 2gua
我是从这里知道的:
http://www.sinatrarb.com/wild.html

里面列出了使用Sinatra的应用及网站,还有公司。
比如说:GitHub就是Rails和Sinatra结合使用。

论坛徽章:
0
9 [报告]
发表于 2011-08-11 08:48 |只看该作者
回复 8# bugbugbug3


    改天也试用一下吧。

论坛徽章:
46
15-16赛季CBA联赛之四川
日期:2018-03-27 11:59:132015年亚洲杯之沙特阿拉伯
日期:2015-04-11 17:31:45天蝎座
日期:2015-03-25 16:56:49双鱼座
日期:2015-03-25 16:56:30摩羯座
日期:2015-03-25 16:56:09巳蛇
日期:2015-03-25 16:55:30卯兔
日期:2015-03-25 16:54:29子鼠
日期:2015-03-25 16:53:59申猴
日期:2015-03-25 16:53:29寅虎
日期:2015-03-25 16:52:29羊年新春福章
日期:2015-03-25 16:51:212015亚冠之布里斯班狮吼
日期:2015-07-13 10:44:56
10 [报告]
发表于 2011-08-12 18:52 |只看该作者
今天刚知道 qiushibaike.com 这个也是用 rail 写的
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP