sinatra 0.0.1 源码学习

重要提醒

一定要先安装 1.8 版本的 ruby ,因为 1.9+ 的 ruby ,String 的实例是不响应 each 方法的,这会直接导致 rack 报错。可以使用 rvm 安装 1.8.7 版本的 ruby ,如果使用 rvm ,请先升级到最新版本,否则安装 1.8.7 的 ruby 时也会报错。

列一下本人运行 sinatra 0.0.1 用到的 ruby 和关键 gem 的版本:

  • ruby-1.8.7-p374
  • rack 1.4.1
  • mongrel 1.1.5

前期准备

sinatra克隆下来,用 git tag 可以看到最开始的版本是 0.0.1 。

checkout 到 0.0.1 分支可以看到目录十分简单

├── examples
│   └── hello
│       ├── test.rb
│       └── views
│           └── index.erb
├── files
│   ├── default_index.erb
│   └── not_found.erb
├── lib
│   ├── sinatra
│   │   ├── core_ext
│   │   │   ├── class.rb
│   │   │   └── hash.rb
│   │   ├── dispatcher.rb
│   │   ├── dsl.rb
│   │   ├── event.rb
│   │   ├── logger.rb
│   │   └── server.rb
│   └── sinatra.rb
├── test
│   ├── helper.rb
│   └── sinatra
│       ├── dispatcher_test.rb
│       └── event_test.rb
└── vendor
    └── erb
        ├── init.rb
        └── lib
            └── erb.rb

12 directories, 17 files

test.rb

跳到 examples/hello 目录下运行 ruby test.rb ,用浏览器打开 localhost:4567 便看到一个提交表单的页面。把文件里的路由都玩一遍,就能感觉到 sinatra 麻雀虽小,但五脏俱全。

test.rb 采用 $LOAD_PATH.unshift '../../lib/' 把 lib 目录放进加载路径中。

这种写法会导致如果在别的目录下,比如根目录,执行 ruby examples/hello/test.rb 报错,可以改为:

$LOAD_PATH.unshift File.expand_path('../../lib',File.dirname(__FILE__))

rubygems 和 rack

根据 lib/sinatra/sinatra.rb 里加载文件的顺序,逐一看看这个版本的 sinatra 做了些什么。

sinatra.rb 第1到7行加载 两个 gem

%w(rubygems rack).each do |library|
  begin
    require library
  rescue LoadError
    raise "== Sinatra cannot run without #{library} installed"
  end
end
  • rubygems 对 ruby 1.9+ 来说已经是多余的,见链接

  • rack 是 sinatra 以及 rails 的基础,简介以及基本用法见官网,用一句话描述 rack :为 web 服务器向 ruby 应用提供简洁的接口。再详细点描述怎样应用 rack :

    • 运行 rack 时需要提供一个能响应 call 方法的对象
    • 这个对象的 call 方法必须返回一个包含三个元素的数组
    • 这个返回数组第一个元素是 http 状态码,第二个是 http 响应头,第三个是 http body 对象
    • body 对象要求能响应 each 方法

核心扩展 core_ext

接下来 sinatra 加载两个核心扩展 core_ext/class.rb 以及 core_ext/hash.rb

** class.rb **

ruby 内置了 attr_reader/attr_writer/attr_accessor 方法,可以方便地在类实例中生成 getter/setter 方法,core_ext/class.rb 重新打开了 Class 类,对应地在类中生成 getter/setter 方法。

class_eval 在这当中起到重要作用。

在 class.rb 中,因为没有显式调用 class_eval ,其 receiver 会指向 self ,而当前的 self 就是 Class 类的实例,即普通类,假设为 class 。 class_eval 把 self 指向 class ,也会重新打开 class ,当你需要动态地为类添加方法时很有用。如下例:

#!/usr/bin/env ruby
class TestClassEval
end

TestClassEval.class_eval do 
  def instance_method
    P 'instance method'
  end
  def self.class_method
    p 'class method'
  end
end

TestClassEval.class_method # 'class method'
TestClassEval.new.instance_mehtod # 'instance method'
TestClassEval.instance_mehtod # NoMethodError

顺便说说 instance_evalinstance_eval 把 self 指向 实例 ,也会重新打开实例的 singleton class ,为其添加单例方法,继续用上面的 TestClassEval 作例,下面的写法在其上定义了另一个类方法。

#!/usr/bin/env ruby
TestClassEval.instance_eval do
  def another_class_method
    p 'another class method'
  end
end

TestClassEval.another_class_method # 'another class method'  

** hash.rb **

hash.rb 里使用了 inject 方法 symbolize hash 的 key

def symbolize_keys
  self.inject({}) { |h, (k, v)| h[k.to_sym] = v;h}
end

事件处理 event.rb

** EventManager **

EventManager 模块里有一个 extend self 方法调用,值得学习。

extend 方法是把 module 的实例方法加入到 class 的类单例方法中,其常规用法如下:

module M
  def p_method
    p 'method'
  end
end

class C
  extend M
end

C.p_method # 'method'

如果不使用 extend ,也有其他途径的实现,如下:

module M
  def p_method
    p 'method'
  end
end

class D
  class << self
    include M
  end
end

D.p_method # 'method'

在 EventManager 中,self 指向 module 自身, 因此 extend self 是用来高效地生成单例方法,可见链接

** EventContext **

为 block 提供执行上下文, block 借此可以访问 request/params ,可以设置和访问 status/headers/body

这里用到 alias, alias :header :headers , ruby 中类似的用法还有 alias_method ,如果使用 alias_method ,要这样写 alias_method :header, :headers ,多一个逗号。

alias 与 alias_method 还有其他区别,前者是关键字,后者是定义在 Module 的方法,这意味着 alias_method 可以被重写;调用 alias 时,其 self 是在定义时就已经决定下来,而 alias_method 的 self 是在运行时才决定的,见下面的例子(出自):

# alias_method
class User

  def full_name
    puts "Johnnie Walker"
  end

  def self.add_rename
    alias_method :name, :full_name
  end
end

class Developer < User
  def full_name
    puts "Geeky geek"
  end
  add_rename
end

Developer.new.name #=> 'Gekky geek'

# alias
class User

  def full_name
    puts "Johnnie Walker"
  end

  def self.add_rename
    alias :name :full_name
  end
end

class Developer < User
  def full_name
    puts "Geeky geek"
  end
  add_rename
end

Developer.new.name #=> 'Johnnie Walker'

基于上面的介绍,社区推荐使用 alias_method 。

** Event **

Event 在定义 initialize 方法时最后一个参数&block ,这表明在调用 Event.new 时如果接收到一个 block ,用 & 操作符把 block 转化为 proc 之后,在 initialize 方法内部可以调用 proc 的 call 方法。

如果没有在方法内部调用 call 方法的需求,或者没有把 block 当成变量传给别的方法的需求,就没必要在定义方法时写上 &block ,直接用 yield 关键字调用就行。

可以用 & 把 proc 转变为 block 。这个技巧用在 attend 方法里面,因为 instance_eval 只有两种调用方式:要么接收一个字符串,要么接收一个 block ,所以要把 proc 转变为 block 当作参数传进去。

如果跟在 & 操作符后面的对象不是 proc ,& 会调用跟在它后面的对象的 :to_proc 方法。 symbol 对象也有自己的 :to_proc 方法,你经常能看到类似下面的代码:

def tag_names
  @tag_names || tags.map(&:name).join(' ')
end

事实上这跟下面的代码是等效的:

def tag_names
  @tag_names || tags.map({ |tag| tag.name }).join(' ')
end

symbol 对象的 :to_proc 方法最早是 Rails 引进的,后面合并到 ruby 1.8.7 版本:

class Symbol
  def to_proc
    Proc.new do |obj, *args|
      obj.send self, *args
    end
  end
end

上面整个例子都出自 stackoverflow

关于 & ,更多资料可以看看这篇文章

路由请求,封装响应 dispatcher.rb

dispatcher 负责将请求转发到对应的 event ,经 event 处理后,返回符合 rack 要求的响应。

rack 会直接调用 dispatcher 的 call 方法,传入 env ,利用 rack 提供的方法可以得到 request 对象。

用 array 的 detect 方法实现转发功能, detect 接受一个 ifnone 参数,如果找不到合适的 event ,就调用 ifnone 的 call 方法,将 call 方法的返回值作为 detect 的返回值,借此实现大家都熟悉的 404 。

这里的 ifnone 是一个 lambda

lambda { not_found }

调用 lambda 的 call 会执行 not_found 方法,而 not_found 返回一个 Event 实例。

server.rb

rack 2.0 开始不支持 Mongrel 的 handler 。

trap("INT") do ... end 这段代码会捕获用户输入的 ctrl + c ,从而调用 Mongrel::HttpServer 的 stop 方法。

dsl.rb

重新打开 Kernel ,增加 4 个 http 方法。

所有 ruby 类(除了 BasicObject)都继承模块 Kernel ,这意味着一旦 require 这个文件,就连字符串都带这4个方法。

可以打开 irb 验证一下,查看类的祖先链的方法是 ancestors ,如 String.ancestors

erb

** init.rb ** 里有这样一行:

Sinatra::EventContext.send(:include, Sinatra::Erb::InstanceMethods)

ruby 里的方法调用其实就是向被调用方发送(send)一个方法,所以被调用方又叫 receiver 。 EventContext include 了 Erb::InstanceMethods ,就是把 Erb::InstanceMethods 插入到 EventContext 继承链的父亲节点,这样 EventContext 的实例就都可以调用 Erb::InstanceMethods 里面的方法。你也可以直接打开 EventContext 达到同样的目的:

class Sinatra::EventContext
  include Sinatra::Erb::InstanceMethods
end

** erb.rb **

erb.rb 实现找到正确的模板,然后把渲染后的值赋给 EventContext 的实例变量 @body。

在找模板时使用到一个全局变量 $0 ,官方文档的描述

Contains the name of the script being executed. May be assignable.

可以用它来实现类似 Python 判断当前文件是否被直接运行的功能(用来写测试用例):

if __FILE__ == $0
  # code goes here
end

sinatra 用它来实现寻找模板的默认路径。

ERB 的 result 方法接收一个 binding 参数,默认值是 TOPLEVEL_BINDING 。 binding 是 Kernel 的方法,它会返回 当前作用域(local scope)。因为 binding 是在 EventContext 的某个实例里面运行,它返回的是那个实例的作用域,所以可以访问实例变量及实例方法,如 params 。举例如下:

class MyClass
  def initialize(x)
    @x = x
  end
  def get_binding
    binding
  end
  def print
    p 'print out something'
  end
end

my_instance = MyClass.new(1)
my_binding = my_instance.get_binding

eval '@x', b # 1
eval 'print', b # 'print out something'

最后, at_exit

at_exit 也是 Kernel 里的方法,它接受一个 block ,并将其转换成 Proc ,当程序退出时会调用 Proc 。如果调用了多次 at_exit 。那么这些 Proc 按倒序执行,如:

at_exit do
  puts '再见!'
end

at_exit do
  puts '已经写完,'
end

at_exit do
  print 'sinatra 0.0.1 版本源码学习'
end
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容