聲明
本文系 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
的實例。 OpenStruct
與 Hash
相似,但它通過元編程提供了不少快捷訪問、設(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 方法
三,使用 UnboundMethod 和 define_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
,而 @response
是 Rack::Response
的實例,可以讀寫 body
。
本小節(jié)參考文章:
- Using splats to build up and tear apart arrays in Ruby
- Splat Operator in Ruby
- The Strange Ruby Splat
- Where is it legal to use ruby splat operator?
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é)參考文章:
- When to Use Protected Methods in Ruby
- Protected Methods and Ruby 2.0
- Private and Protected: They might not mean what you think they mean
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)容。
全文完。