sinatra 0.2.0 源碼學(xué)習(xí)

聲明

本文系 sinatra 源碼系列第 4 篇。系列的目的是通過 sinatra 學(xué)習(xí) ruby 編程技巧。文章按程序運行的先后順序挑重點分析,前一篇文章分析過的略去不說。水平很有限,所寫盡量給出可靠官方/討論鏈接,不坑路人。

重要提醒

一定要先安裝 1.8 版本的 ruby ,因為 1.9+ 的 ruby ,String 的實例是不響應(yīng) each 方法的,這會直接導(dǎo)致 rack 報錯。可以使用 rvm 安裝 1.8.7 版本的 ruby ,如果使用 rvm ,請先升級到最新版本,否則安裝 1.8.7 的 ruby 時也會報錯。

使用命令 git log -1 --format=%ai 0.2.0 ,查看 0.2.0 版本 sinatra 的“出廠日期”,得到 2008-04-11 16:29:36 -0700 ;而 1.8.7 版本的 ruby 是 2008 年 5 月發(fā)布的,兩者兼容性應(yīng)該比較好。

列一下本人運行 sinatra 0.2.0 用到的 ruby 和關(guān)鍵 gem 的版本:

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

change log

  • 大重構(gòu),把功能模塊都壓縮在一個文件中
  • 增加大量測試用例

跑通所有測試用例

首先修改一處代碼錯誤,在 sinatra.rb 文件的 1022 行,將 Rack::File::MIME_TYPES[ext.to_s] = type 改為 Rack::Mime::MIME_TYPES[ext.to_s] = type

然后安裝一些缺少的 gem :

gem install builder -v '2.1.2'
gem install sass -v '3.1.0'
gem install haml -v '1.8.0'

跑測試用例,發(fā)現(xiàn)只有 sym_params_test.rb 文件中的一處跑不通過。

此處的測試是驗證可以用 String 和 Symbol 訪問參數(shù)。實現(xiàn)的關(guān)鍵方法是:

# sinatra.rb 663 行
h = Hash.new { |h, k| h[k.to_s] if Symbol === k }

調(diào)用 Hash.new 時傳進一個 block ,可以設(shè)置當(dāng)訪問某個不存在于 Hash 的 Key 時的一些默認行為,比如上面的代碼就是說,當(dāng) key 不存在且是 Symbol 時,把 key 轉(zhuǎn)換為字符串再找找(再搶救一下...)

Hash.new 還可以用來初始化值為數(shù)組的鍵值對,在記錄事件回調(diào)時很方便:

@events = Hash.new { |hash, key| hash[key] = [] }

# 出自這個版本的 sinatra.rb 的 738 行
# 再也不用先判斷 key 是否存在,也不用手動初始化一個空數(shù)組了

回過頭來修改代碼以跑通測試用例,作者這里粗心寫錯了請求的方法,應(yīng)該用 post_it ,而不是 get_it ,還要相應(yīng)地修改路由:

specify "should be accessable as Strings or Symbols" do
  post '/' do
    params[:foo] + params['foo']
  end
  
  post_it '/', :foo => "X"
  assert_equal('XX', body)
end

要在這個版本的 sinatra 的 get 方法中傳遞參數(shù),需要把參數(shù)寫在 uri 中,下面的寫法也能通過測試:

specify "should be accessable as Strings or Symbols" do
  get '/' do
    params[:foo] + params['foo']
  end
  
  get_it '/?foo=X'
  assert_equal('XX', body)
end

從 at_exit 說起

還是從 at_exit 開始讀代碼。

$! 記錄異常信息,當(dāng)調(diào)用 raise 的時候會設(shè)置這個變量,詳見此處

調(diào)用 load_options! 解釋完啟動參數(shù)后, sinatra 在所有環(huán)境設(shè)置遇到異常和 404 時的回調(diào)方法,在開發(fā)環(huán)境遇到異常和 404 的回調(diào)方法比其他環(huán)境暴露更多的信息。

OpenStruct

值得細看的是在非開發(fā)環(huán)境遇到異常時的回調(diào)方法:

error do
  raise request.env['sinatra.error'] if Sinatra.options.raise_errors
  '<h1>Internal Server Error</h1>'
end

Sinatra.options 實際上是 OpenStruct 的實例。 OpenStructHash 相似,但它通過元編程提供了不少快捷訪問、設(shè)置值的方法。 OpenStruct 用法舉例:

# 1
person = OpenStruct.new
person.name    = "John Smith"
p person.name    #=> "John Smith"

# 2
person = OpenStruct.new(:name => "John Smith")
p person.name    #=> "John Smith"

一個簡單版本的 OpenStruct 實現(xiàn):

class OpenStruct
  attr_accessor :h
  def initialize(hash = {})
    @h = hash

    h.each do |key, value|
      self.class.send(:define_method, key) do
        h[key]
      end
      self.class.send(:define_method, "#{key}=") do |value|
        h[key] = value
      end
    end
  end
  
  def method_missing(m, *args)
    if args.size == 1
      # m is  :name=
      # change m to :name
      h[m.to_s.chop.to_sym] = args[0]
    elsif args.size == 0
      h[m]
    end
  end

  def respond_to?(m)
    h.respond_to?(m) || super
  end

end

require 'test/unit'

class TestOS < Test::Unit::TestCase
  def setup
    @person_1 = OpenStruct.new
    @person_2 = OpenStruct.new(:name => 'zhu')
  end

  def test_case_1
    assert_equal true, @person_1.respond_to?(:name)
    assert_equal nil, @person_1.name
    @person_1.name = 'zhu'
    assert_equal 'zhu', @person_1.name
  end

  def test_case_2
    assert_equal true, @person_2.respond_to?(:name)
    assert_equal 'zhu', @person_2.name
    @person_2.name = 'jude'
    assert_equal 'jude', @person_2.name
  end
end

以上只是我心血來潮寫的, OpenStruct 的實現(xiàn)遠遠不是上面寫的那么簡單,有興趣可以看看源碼。

Sinatra.options.raise_errors 的值只能在代碼里設(shè)置,當(dāng)其值不為 nil 或 false 時,默認在非開發(fā)環(huán)境下直接拋出異常。要想在命令行啟動時設(shè)置值,只需要在 load_options! 方法中添加一行:

op.on('-r') { |env| default_options[:raise_errors] = true }

在訂制開發(fā)環(huán)境下的異常和 404 頁面時,使用到 %Q(...) 。 ruby 會特殊處理以百分號 '%' 開頭的字符串,幫你省去不少轉(zhuǎn)義引號的麻煩:

The string expressions begin with % are the special form to avoid putting too many backslashes into quoted strings. 出處

更多相似的用法見Ruby 里的 %Q, %q, %W, %w, %x, %r, %s, %i

在顯示異常信息時,用 escap_html 來轉(zhuǎn)義 &,<,>,/,'," ,把這些 ascii 字符編碼成實體編碼,防止 XSS 攻擊,不過源碼有注釋說有 bug :

On 1.8, there is a kcode = 'u' bug that allows for XSS otherwhise

源碼中用正則表達式替換轉(zhuǎn)義字符的實現(xiàn)值得參考。

更多關(guān)于 XSS 的知識,可以看看本人之前寫的這篇

lookup

接下來看 Application 的 call 方法。

首先由 lookup 方法實現(xiàn)根據(jù)請求找到正確的路由。

def lookup(request)
  method = request.request_method.downcase.to_sym
  events[method].eject(&[:invoke, request]) ||
    (events[:get].eject(&[:invoke, request]) if method == :head) ||
    errors[NotFound].invoke(request)
end

sinatra 在 Enumerable 上擴展了 eject 方法,因為 Array 加載了 Enumberable 模塊,所以 Array 實例能用 eject 方法。

def eject(&block)
  find { |e| result = block[e] and break result }
end

eject 方法內(nèi)部,使用 find 方法找到第一個產(chǎn)生非 false 結(jié)果的 block ,并返回這個結(jié)果。find 方法本來會返回第一個符合條件的元素,通過 break 可以訂制自己的返回值。

這里 e 是 Event 的實例。 block 是由 Array 實例轉(zhuǎn)化而來的 Proc 。

系列第一篇文章提到過, 如果跟在 & 后面對象的不是 Proc ,首先會調(diào)用這個對象的 to_proc 方法得到一個 Proc 實例,最后會調(diào)用這個 Proc 的 call 方法。

sinatra 擴展了 Array 的 to_proc 方法:

def to_proc
  Proc.new { |*args| args.shift.__send__(self[0], *(args + self[1..-1])) }
end

經(jīng)過 to_proc 轉(zhuǎn)換, Proc#call 把參數(shù)轉(zhuǎn)換為一個數(shù)組,把這個數(shù)組第一個元素作為 receiver ,把調(diào)用 to_proc 方法的數(shù)組的第一個元素作為方法,把兩個數(shù)組余下的元素作為方法的參數(shù),拿前面的代碼作例子:

# 在 lookup 方法里下面的這行代碼

&[:invoke, request]

# 會得到這樣一個 Proc

#=> Proc.new { |*args| args.shift.__send__(:invoke, *(args + [request])) }

# 在 eject 方法定義中

find { |e| result = block[e] and break result }

# block[e] 就是把 e 當(dāng)作參數(shù)調(diào)用  Proc#call ,做的事情是: 以 `request` 作為參數(shù),調(diào)用 `e` 的 `invoke` 方法。

block[e] 不能寫成 block(e) ,否則 ruby 會把 block 當(dāng)作是 main 的一個方法來調(diào)用。有三種方法可以調(diào)用 Proc#call

# 1
 a_proc.call()
# 2
 a_proc.()
# 3
 a_proc[]

invoke

Event#invoke 方法實現(xiàn)路由匹配和參數(shù)匹配。除了可以匹配路徑,這個版本的 sinatra 還可以匹配 user_agent 和 host :

if agent = options[:agent] 
  return unless request.user_agent =~ agent
  params[:agent] = $~[1..-1]
end
if host = options[:host] 
  return unless host === request.host
end

用法和測試舉例如下:

require 'sinatra'

get '/path', :agent => /Windows/
  request.env['HTTP_USER_AGENT']
end
# get_it '/', :env => { :agent => 'Windows' }
# should.be.ok
# body.should.equal 'Windows'

# get_it '/', :agent => 'Mac'
# should.not.be.ok



get '/path', {}, HTTP_HOST => 'foo.test.com'
  'in foo'
end

get '/path', {}, HTTP_HOST => 'bar.test.com'
  'in bar'
end

# get_it '/foo', {}, 'HTTP_HOST' => 'foo.test.com'
# assert ok?
# assert_equal 'in foo', body

# get_it '/foo', {}, 'HTTP_HOST' => 'bar.test.com'
# assert ok?
# assert_equal 'in bar', body

# get_it '/foo'
# assert not_found?

request.user_agent 最終調(diào)用 env['HTTP_USER_AGENT'] ,在 /lib/sinatra/test/methods.rb 中, sinatra 重寫了 Rack::MockRequest#env_for 方法:

class Rack::MockRequest
  class << self
    alias :env_for_without_env :env_for
    def env_for(uri = "", opts = {})
      env = { 'HTTP_USER_AGENT' => opts.delete(:agent) }
      env_for_without_env(uri, opts).merge(env)
    end
  end
end

這樣在測試時就可以傳遞 :agent => 'Windows' 作為 user_agent 的參數(shù),否則要這樣寫: 'HTTP_USER_AGENT' => 'Windows'

call the overridden method from the new

在 ruby 中重寫一個方法,新方法中還要調(diào)用未被重寫前的舊方法,有幾個技巧。

一,繼承。需要修改每一處用到新方法的 reciever 。

class Foo
  def say
    'Hello'
  end
end

class Bar < Foo
  def say
    super + ' World!'
  end
end

Foo.new.say #=> 'Hello'
Bar.new.say #=> 'Hello World!'
# 把 reciever 從 Foo 改為 Bar

二,修改祖先鏈。這與繼承類似,但修改的方向不一樣。

moudle Bar
  def say
    super + ' World!'
  end
end

class Foo
  prepend Bar
  def say
    'Hello'
  end
end

Foo.new.say #=> 'Hello World!'

# 使用了 prepend 把 Bar 放在 Foo 祖先鏈的下游,當(dāng)尋找 say 方法時,首先找到 Bar 定義的 say 方法

三,使用 UnboundMethoddefine_method

class Foo
  def say
    'Hello'
  end
end

# 在某處重新打開 Foo

class Foo
  old_say = instance_method(:say)
  define_method(:say) do
    old_say.bind(self)[] + ' World!'
    # 調(diào)用 instance_method 得到一個 UnboundMethod ,你需要在調(diào)用它之前 bind 一個 Foo 的實例
    # 前面說過調(diào)用 Proc#call 的三種方法,調(diào)用 Method#call 也是一樣。這里采用了 [] ,你也可以用 .()
  end
end

四, alias 。就是 sinatra 采用的方法。

class Foo
  def say
    'Hello'
  end
end

# 在某處重新打開 Foo

class Foo
  alias :old_say :say
  def say
    old_say + ' World!'
  end
end

Foo.new.say #=> 'Hello World!'
Foo.new.old_say #=> 'Hello'
# 使用這種技巧,仍然可以訪問舊的方法

更多的技巧,可參考這里

繼續(xù)看 Event#invoke 的實現(xiàn),下面代碼這行實現(xiàn)匹配路徑:

return unless pattern =~ request.path_info.squeeze('/')

String#squeeze 方法用單個字符替換連續(xù)出現(xiàn)的字符,用法很靈活,參見文檔

sinatra 實現(xiàn)路徑匹配的參數(shù)匹配的思路是:

  • 將用戶預(yù)先定義的路徑轉(zhuǎn)換為正則表達式
  • 用這些正則表達式去匹配實際請求的路徑
  • 如果匹配成功,則把捕獲的參數(shù)與定義的參數(shù)組成鍵值對保存起來

Event#initialize 實現(xiàn)了路徑轉(zhuǎn)換正則表達式:

URI_CHAR = '[^/?:,&#\.]'.freeze unless defined?(URI_CHAR)
PARAM = /:(#{URI_CHAR}+)/.freeze unless defined?(PARAM)
SPLAT = /(.*?)/
attr_reader :pattern

def initialize(path, options = {}, &b)
  @path = URI.encode(path)
  @param_keys = []
  regex = @path.to_s.gsub(PARAM) do
    @param_keys << $1
    "(#{URI_CHAR}+)"
  end
  
  regex.gsub!('*', SPLAT.to_s)
  
  @pattern = /^#{regex}$/
end

首先把用戶定義的路徑編碼成 URI ,因為 rfc1738 文檔規(guī)定在 URL 中出現(xiàn)的字符只能是 字母和數(shù)字[0-9a-zA-Z]、一些特殊符號"$-_.+!*'() 以及一些保留字符:

only alphanumerics, the special characters "$-_.+!*'(),", and reserved characters sed for their reserved purposes may be used unencoded within a URL.

如果在路徑或查詢參數(shù)中出現(xiàn)其他字符,比如中文,需要先轉(zhuǎn)義。

然后把用戶在定義路徑中的參數(shù)找出來,替換為去掉冒號(:)后的正則表達式字符串。

PARAM 正則表達式———— /:([^/?:,&#\.]+)/———— 匹配以冒號開頭的,接下來的字符不是 / ? : , & # . 當(dāng)中任意一個字符的字符串。

$1 保存了最近一次正則表達式捕獲的第一個匹配結(jié)果。

用戶還可以定義不具名參數(shù): '*' ,這個功能還不完善,現(xiàn)階段只能作占位符用,沒法獲取捕獲的參數(shù)。

接下來的事情就是把捕獲的參數(shù)與定義的參數(shù)組成鍵值對保存在 params 中,之前的系列文章有說過。

保存好參數(shù)后,調(diào)用 Result.new(block, params, 200) 生成 Result ,它是 Struct 的實例。跟 OpenStruct 不同, Struct 只能讀、寫在初始化時設(shè)定的 key ,不能新增 key :

Bar = Struct.new(a,b)
bar = Bar.new(1,2)
bar.a #=> 1
bar.c #=> undefined method `c' for #<struct Bar a=1, b=2>

sinatra 能正確響應(yīng) HEAD 請求方法。根據(jù) rfc 文檔, HEAD 方法跟 GET 方法唯一的區(qū)別就是,響應(yīng) HEAD 方法時,響應(yīng)報文不能帶有 body 。響應(yīng)報文的頭應(yīng)該跟 GET 方法的一致。 HEAD 方法主要用于驗證資源的有效性、可用性以及最近是否修改過。

如上所述,如果是 HEAD 請求, sinatra 會自動去找對應(yīng)的 GET 方法回調(diào):

(events[:get].eject(&[:invoke, request]) if method == :head)

在生成 HEAD 請求的響應(yīng)時,會設(shè)置 body 為空字符:

# line 839
body = '' if request.request_method.upcase == 'HEAD'

to_result

在獲取響應(yīng)的 body 時,不論是正常流程,還是異常流程,都調(diào)用了 to_result 方法。 sinatra 在很多類中都擴展了這個實例方法。

正常流程的代碼如下:

returned = run_safely do
  catch(:halt) do
    filters[:before].each { |f| context.instance_eval(&f) }
    [:complete, context.instance_eval(&result.block)]
  end
end
body = returned.to_result(context)
# 一切正常時, returned 是 [:complete, context.instance_eval(&result.block)]

與此相關(guān)的兩個 to_result 方法是:

class Array
  def to_result(cx, *args)
    self.shift.to_result(cx, *self)
  end
end

class Symbol
  def to_result(cx, *args)
    cx.send(self, *args)
  end
end

returned.to_result(context) 最終是在 context 上調(diào)用 complete 方法,傳入的參數(shù)是 context.instance_eval(&result.block) 的返回值。

異常流程,如在 before filters 中拋出 :halt ,在 README.doc 文檔中詳細說明了多種情況:

Set the body to the result of a helper method

throw :halt, :helper_method

Set the body to the result of a helper method after sending it parameters from the local scope

throw :halt, [:helper_method, foo, bar]

Set the body to a simple string

throw :halt, 'this will be the body'

Set status then the body

throw :halt, [401, 'go away!']

Set the status then call a helper method with params from local scope

throw :halt, [401, [:helper_method, foo, bar]]

Run a proc inside the Sinatra::EventContext instance and set the body to the result

throw :halt, lambda { puts 'In a proc!'; 'I just wrote to $stdout!' }

在眾多應(yīng)對以上情況的 to_proc 中,值得一提的是以下這兩個:

class String
  def to_result(cx, *args)
    args.shift.to_result(cx, *args)
    self
  end
end

class NilClass
  def to_result(cx, *args)
    ''
  end
end

throw :halt, 'this will be the body' 之后,最終會用到 String#to_result 方法,傳入的參數(shù)只有一個 context ,因此 args 是個空數(shù)組, args.shift 得到 nil ,所以得擴展 NilClass#to_result ,但它什么也沒做,徑直返回空字符串。

context.body

在處理返回報文的正文時,有如下代碼:

context.body = body.kind_of?(String) ? [*body] : body

kind_of? 方法跟 is_a? 一樣,回溯祖先鏈,找到祖先返回 true ,否則返回 false 。

[*body] 中的 * (splat operator)有很多用途,之前也說過它可以把函數(shù)的多個參數(shù)變?yōu)橐粋€數(shù)組。此處是另外兩種用法。

其一是強制類型轉(zhuǎn)換,把當(dāng)前類型轉(zhuǎn)換為 Array 類型:

# Range 轉(zhuǎn)換為 Array
a = *(1..3) #=> [1,2,3]

# String 轉(zhuǎn)換為 Array
b = *"one string" #=> ["one string"]

# Array 仍然是 Array
c = *[1,2,3] #=> [1,2,3]

# nil 轉(zhuǎn)換為 Array
d = *nil #=> []

其二是展平數(shù)組:

e = [*[1,2],*[3,4]] #=> [1,2,3,4]

# 這跟下面是一樣的

f = [[1,2],[3,4]].flatten

回頭看 [*body] ,如果只是把字符串強制轉(zhuǎn)換為數(shù)組的話, *body 就夠了。但是這里必須用中括號([])包著,否則會報語法錯誤。用中括號包住,解決了語法問題,得到的還是原來的那個數(shù)組。

* 實際上并不是 operator ,而是 token ,而且很容易就會用錯。大致有以下幾種用法:

# 用于賦值

first, *rest = [1,2,3]
#=> first = 1
#=> rest = [2,3]

*rest, last = [1,2,3]
#=> last = 3
#=> rest = [1,2]

first, *m, last = [1,2,3,4]

# 收集參數(shù),分解參數(shù)

def foo(first, *args); end #=> *args 只能放在最后
foo(1,2,3,4) #=> args = [2,3,4]

def bar(a, b); end
bar(*[1,2]) #=> a = 1, b = 2

# 強制類型轉(zhuǎn)換,很容易出語法錯誤,所以最好用中括號包住

context#body 由在 Class 類中的 dslify_writer 方法實現(xiàn):寫入 body 的值,并返回這個值。

class Class
  def dslify_writer(*syms)
    syms.each do |sym|
      class_eval <<-end_eval
        def #{sym}(v=nil)
          self.send "#{sym}=", v if v
          v
        end
      end_eval
    end
  end
end

class Foo
  dslify_writer :bar
  # 相當(dāng)于這樣寫:
  # def bar(v=nil)
  #   self.send('bar=', v) if v
  #   v
  # end
end

context 并沒有實現(xiàn) body= 方法,但它有實現(xiàn) method_missing 方法,把找不到的 method 轉(zhuǎn)發(fā)給 @response ,而 @responseRack::Response 的實例,可以讀寫 body

本小節(jié)參考文章:

context.finish

context.finish 也是轉(zhuǎn)發(fā)到 response.finish

def finish(&block)
  @block = block

  if [204, 205, 304].include?(status.to_i)
    header.delete "Content-Type"
    header.delete "Content-Length"
    [status.to_i, header, []]
  else
    [status.to_i, header, self]
  end
end

包含以下狀態(tài)碼的響應(yīng)會被刪除響應(yīng)頭的 Content-Type / Content-Length 字段:

  • 204 No Content ,服務(wù)器成功處理了請求,但不需要返回任何實體內(nèi)容,瀏覽器不產(chǎn)生任何文檔視圖上的變化
  • 205 Reset Content ,服務(wù)器成功處理了請求,但不需要返回任何實體內(nèi)容,瀏覽器要重置文檔視圖,比如重置表單
  • 304 Use Proxy ,被請求的資源必須通過指定的代理——在 location 字段中指定——才能被訪問

并且返回數(shù)組中的第三個元素是個空數(shù)組,表明響應(yīng)正文為空。

其他狀態(tài)碼返回數(shù)組中的第三個元素是 self ,能這樣做的前提是 response 實現(xiàn)了 each 方法。

設(shè)置 body

application_test.rb 里有一個測試用例如下:

class TesterWithEach
  def each
    yield 'foo'
    yield 'bar'
    yield 'baz'
  end
end

specify "an objects result from each if it has it" do

  get '/' do
    TesterWithEach.new
  end
  
  get_it '/'
  should.be.ok
  body.should.equal 'foobarbaz'

end

如果沒有在 get block 中設(shè)置 body 值, sinatra 就會用 block 的返回值作為 body ,如果這個返回值不響應(yīng) each 方法, body 就會被設(shè)置為空字符。可以模仿這里的 TesterWithEach#each 實現(xiàn)一個簡單的 each

class Foo
  attr_reader :bar
  
  def initialize(*bar)
    @bar = bar
  end
  
  def each
    return nil unless block_given?
    i = 0
    while i < bar.length
      yield bar[i]
      i += 1
    end
  end
end

# foo = Foo.new(1,2,3,4)
# foo.each { |i| p i  }

目前為止, sinatra 的基本功能都已經(jīng)實現(xiàn),剩下的擴展功能——如重定向、渲染xml/erb/sass/haml、傳輸文件等等——都是通過加載模塊來實現(xiàn)。

Streaming

這一模塊取自 ActionPack ,目的是用更少的內(nèi)存消耗傳輸更大的文件,大體的做法是用流傳輸取代一次性輸出整個文件。

實現(xiàn) Streaming 的關(guān)鍵代碼如下:

class FileStreamer
  
  #...

  def to_result(cx, *args)
    self
  end
  
  def each
    File.open(path, 'rb') do |file|
      while buf = file.read(options[:buffer_size])
        yield buf
      end
    end
  end
  #...

end

#...

def send_file(path, options = {})

  #...

  if options[:stream]
    throw :halt, [options[:status] || 200, FileStreamer.new(path, options)]
  else
    File.open(path, 'rb') { |file| throw :halt, [options[:status] || 200, file.read] }
  end

end

如果 options[:stream] 為 true 則通過自身的 each 方法每讀入 4096 個字節(jié)就對外輸出,否則一次性讀入內(nèi)存再輸出。

protected

Streaming 模塊中有兩個 protected 方法。 ruby 的 protected 跟 java 的很像,一般情況下被設(shè)置為 protected 的實例方法只能從類(或子類)實例方法中訪問。(借助 send 方法可以突破這層限制)

class Person

  def initialize(age)
    @age = age
  end

  def older_than?(other_person)
    if self.class == other_person.class
      age > other_person.age
    end
  end
  
  protected

  attr_reader :age

end

class Monkey

  def initialize(age)
    @age = age
  end

  def older_than?(person)
    age > person.age
  end
  
  protected

  attr_reader :age
end

p1 = Person.new(10)
p2 = Person.new(11)
p1.older_than?(p2) #=> false

# p1.age #=> protected method `age' called for #<Person:0x007f80cc0263c8 @age=10> (NoMethodError)

m1 = Monkey.new(13)

# m1.older_than?(p1) #=> protected method `age' called for #<Person:0x007fd3e4963880 @age=10> (NoMethodError)

ruby 的 protected 方法很少用到,如果要用的話,通常用于同類之間的比較(參見上面的 Person 類)。

本小節(jié)參考文章:

RenderingHelpers

sinatra 渲染的過程大致可以分為兩個步驟:

  • 根據(jù)傳進來的參數(shù) (String/Symbol/Proc) ,找到對應(yīng)的模板
  • 調(diào)用具體的渲染引擎渲染模板

第一個步驟是共用的,抽出來形成 RenderingHelpers 。

RenderingHelpers 的實現(xiàn)體現(xiàn)了兩個軟件設(shè)計原則: 1. 依賴反轉(zhuǎn); 2. 開閉原則(對擴展開放,對修改閉合)。

舉例說明一下本人所理解的依賴反轉(zhuǎn):把高層次的模塊比作電器,把低層次的模塊比作插座。要使兩者配合起來為人所用,高層次的模塊必須實現(xiàn)低層次模塊指定的接口,這個接口就是特定的插頭(或兩腳或三腳)。

RenderingHelpers 對外提供 render 方法,但要使用 render 方法,必須實現(xiàn) render_renderer 方法,這個 render_renderer 就是特定的插頭。

這個版本的 sinatra 增加了多個渲染引擎的支持,這些引擎的實現(xiàn)細節(jié)各有不同(如 sass 不支持 layout),但增加這些引擎支持都不用修改 RenderingHelpers 里面的代碼。你甚至可以加入自己的引擎,無需改動 RenderingHelpers ,只要它提供的 render 方法,并實現(xiàn)自己的 render_renderer 方法。這體現(xiàn)了開閉原則。

use_in_file_templates!

渲染時需要的模板,除了可以放在別的文件中,還可以放在當(dāng)前文件中:

get '/stylesheet.css' do
  header 'Content-Type' => 'text/css; charset=utf-8'
  sass :stylesheet
end

# 這里需要的模板可以放在 "views/stylesheet.sass" 文件中,假設(shè)包含以下內(nèi)容

  #  body
  #    #admin
  #      :background-color #CCC

# 也可以放在當(dāng)前文件中,需要事先調(diào)用 use_in_file_templates! ,如下:

use_in_file_templates!

__END__
## stylesheet
body
  #admin
    :background-color #CCC

use_in_file_templates!實現(xiàn)的細節(jié)是首先找到調(diào)用 use_in_file_templates! 方法的文件。 caller 方法會以數(shù)組形式返回當(dāng)前方法的調(diào)用棧,形式如下:

def a(skip)
  caller(skip)
end
def b(skip)
  a(skip)
end
def c(skip)
  b(skip)
end
c(0)   #=> ["prog:2:in `a'", "prog:5:in `b'", "prog:8:in `c'", "prog:10"]
c(1)   #=> ["prog:5:in `b'", "prog:8:in `c'", "prog:11"]
c(2)   #=> ["prog:8:in `c'", "prog:12"]
c(3)   #=> ["prog:13"]

然后把這個文件轉(zhuǎn)換為字符串,定位到字符串的一個特殊標記。這里作者寫錯了這個特殊標記,應(yīng)該是 __END__ ,而不是 __FILE__ 。雖然寫成 __FILE__ 也能跑過測試用例,但這個標記與 __END__ 是完全不同的。

ruby 有一個特殊的常量 DATA ,它是一個 File 對象,包含了文件中的數(shù)據(jù)。你可以把數(shù)據(jù)和代碼放在同一個文件當(dāng)中, ruby 通過 __END__ 這個標記分開代碼和數(shù)據(jù):

# t.rb
puts DATA.gets
__END__
hello world!

# ruby t.rb 
# => hello world!

定位到數(shù)據(jù)部分后,把這部分字符串轉(zhuǎn)換為 StringIO 對象,以便把字符串當(dāng)作文件逐行解釋。

只要匹配到以 ## 開頭的行,就把捕獲的字符串當(dāng)作新的模板名字,沒匹配行的就當(dāng)作是模板的內(nèi)容。

全文完。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容