今天是偉大的爵士樂大師法蘭克.辛納區(Frank Sinatra)誕辰一百週年。天時地利人和,正是翻譯這篇文章的好日子,錯過再等一百年。git
杭州此刻在下雨,陰冷潮溼。耳邊是搖曳的爵士樂,我把貓關進了陽臺,打開電腦,開始胡說八道。github
(能夠跳過正文直接看最後的完整代碼)ruby
原文連接:https://robots.thoughtbot.com/lets-build-a-sinatraapp
Sinatra
是一個基於Ruby的快速開發Web應用程序基於特定域(domain-specific)語言。在一些小項目中使用事後,我決定一探究竟。dom
Sinatra的核心是Rack。我寫過一篇文章關於Rack,若是你對Rack的工做原理有些困惑,那篇文章值得一讀。Sinatra在Rack之上提供了一個給力的DSL。來看個例子:curl
get "/hello" do [200, {}, "Hello from Sinatra!"] end post "/hello" do [200, {}, "Hello from a post-Sinatra world!"] end
當這段代碼執行的時候,咱們發送一個GET
給/hello
,將看到Hello from Sinatra!
;發送一個POST
請求給/hello
將看到Hello from a post-Sinatra world!
。但這個時候,任何其餘請求都將返回404.函數
Sinatra 的源碼,咱們一塊兒提煉出一個相似Sinatra的結構。post
咱們將創造一個基於Sinatra那種可繼承可擴展的類。它保存請求路由表(GET /hello handleHello),當收到GET /hello
請求的時候,能去調用handleHello
函數。事實上,它能很好的處理全部的請求。當收到請求的時候,它會遍歷一遍路由表,若是沒有合適的請求,就返回404。測試
就叫它Nancy
吧,別問我爲何。fetch
第一步要作的事是:建立一個類,它有一個get
方法,能捕獲請求的path
並找到對應的函數。
# nancy.rb require "rack" module Nancy class Base def initialize @routes = {} end attr_reader :routes def get(path, &handler) route("GET", path, &handler) end private def route(verb, path, &handler) @routes[verb] ||= {} @routes[verb][path] = handler end end end
route
函數接收一個動詞(HTTP請求方法名),一個路徑和一回調方法並保存到一個Hash結構中。這樣設計可讓POST /hello
和GET /hello
不會混亂。
而後在下面加一些測試代碼:
nancy = Nancy::Base.new nancy.get "/hello" do [200, {}, ["Nancy says hello"]] end puts nancy.routes
能夠看到,Nancy使用了nancy.get
替代了Sinatra的get
顯得沒那麼簡潔,本文最後會解決這個問題。
若是咱們這時執行程序,會看到:
{ "GET" =\> { "/hello" =\> \#\<Proc:0x007fea4a185a88@nancy.rb:26\> } }
這個返回結果,咱們的路由表工做的很好。
如今咱們給Nancy增長調用Rack的call
方法,讓它成爲一個最小的Rack
程序。這些代碼是個人另外一篇Rack文章中的:
# nancy.rb def call(env) @request = Rack::Request.new(env) verb = @request.request_method requested_path = @request.path_info handler = @routes[verb][requested_path] handler.call end
首先,咱們從Rack的請求的env環境變量參數中的獲得請求方法(HTTP/GET等)和路徑(/the/path),而後根據這些信息去路由表中招對應的回調方法並調用它。回調方法需返回一個固定的結構,這個結構包含狀態碼、HTTP Header和返回的內容,這個結構正是Rack的Call所須要的,它會經由Rack返回給用戶。
咱們增長一個這樣的回調給Nancy::Base
:
nancy = Nancy::Base.new nancy.get "/hello" do [200, {}, ["Nancy says hello"]] end # This line is new! Rack::Handler::WEBrick.run nancy, Port: 9292
如今這個Rack App已經能運行了。咱們使用WEBrick
做爲服務端,它是Ruby內置的。
nancy = Nancy::Base.new nancy.get "/hello" do [200, {}, ["Nancy says hello"]] end # This line is new! Rack::Handler::WEBrick.run nancy, Port: 9292
執行ruby nancy.rb
,訪問http://localhost:9292/hello
,一切工做的很好。須要注意,Nancy不會自動從新加載,你所作的任何改動都必須從新啓動纔會生效。Ctrl+C
能在終端中中止它。
訪問路由表中處理的路徑它能正常的工做,可是訪問路由表中不存在的路徑好比http://localhost:9292/bad
你只能看到Rack返回的默認錯誤信息,一個不友好的Internal Server Error
頁面。咱們看下如何自定義一個錯誤信息。
咱們須要修改call方法
:
def call(env) @request = Rack::Request.new(env) verb = @request.request_method requested_path = @request.path_info - handler = @routes[verb][requested_path] - - handler.call + handler = @routes.fetch(verb, {}).fetch(requested_path, nil) + if handler + handler.call + else + [404, {}, ["Oops! No route for #{verb} #{requested_path}"]] + end end
如今,若是請求一個路由表中沒有定義的路徑回返回一個404狀態碼和錯誤信息。
nancy.get
如今只能獲得路徑,但要想正常工做,它須要獲得更多的信息,好比請求的參數等。有關請求的環境變量被封裝在Rack::Request
的params
中。
咱們給Nancy::Base
增長一個新的方法params
:
module Nancy class Base # # ...other methods.... # def params @request.params end end end
須要這些請求信息的回調處理中,能夠訪問這個params
方法來獲得。
再來看一下剛剛添加的這個params
實例方法。
修改調用回調這部分代碼:
if handler - handler.call + instance_eval(&handler) else [404, {}, ["Oops! Couldn't find #{verb} #{requested_path}"]] end
這裏面有一些小把戲讓人困惑,爲啥要用instance_eval
替代call
呢?
handler
是一個沒有上下文的lambda
若是咱們使用call
去調用這個lambda,它是無法訪問Nancy::Base
的實例方法的。
使用instance_eval
替代call
來調用,Nancy::Base
的實例信息會被注入進去,它能夠訪問Nancy::Base
的實例變量和方法(上下文)了。
因此,如今咱們能訪問params
在handler block中了。試試看:
nancy.get "/" do [200, {}, ["Your params are #{params.inspect}"]] end
訪問http://localhost:9292/?foo=bar&hello=goodbye
,有關請求的信息,都會被打印出來。
到目前爲止,nancy.get
能正常的處理GET請求了。但這還不夠,咱們要支持更多的HTTP
方法。支持它們的代碼和get
很類似:
# nancy.rb def post(path, &handler) route("POST", path, &handler) end def put(path, &handler) route("PUT", path, &handler) end def patch(path, &handler) route("PATCH", path, &handler) end def delete(path, &handler) route("DELETE", path, &handler) end
一般在POST
和PUT
請求中,咱們會想訪問請求的內容(request body)。既然如今在回調中,咱們已經能夠訪問Nancy::Base的實例方法和變量了,讓@request變得可見就好(迷糊的去翻上面的call方法代碼):
attr_reader :request
訪問requrest
實例變量在回調中:
nancy.post "/" do [200, {}, request.body] end
訪問測試:
$ curl --data "body is hello" localhost:9292 body is hello
咱們來作如下優化:
使用params實例方法來替代直接調用request.params
def params request.params end
容許回調方法返回一個字符串
if handler - instance_eval(&handler) + result = instance_eval(&handler) + if result.class == String + [200, {}, [result]] + else + result + end else [404, {}, ["Oops! Couldn't find #{verb} #{requested_path}"]] end
這樣處理回調就簡化不少:
nancy.get "/hello" do "Nancy says hello!" end
在使用Sinatra
的時候,咱們使用get
,post
來進行請求處理優雅強大又直觀。它是怎麼作到的呢?先考慮Nancy的結構。它執行的時候,咱們調用Nancy::Base.new
獲得一個新的實例,而後添加處理path的函數,而後執行。那麼,若是有一個單例,就能夠實現Sinatra
的效果,將文件中處理路徑的方法添加給這個單例並執行便可。(譯者注:這段的譯文和原文不要緊,純屬杜撰。若是迷惑,請參考原文)
是時候考慮將nancy.get
優化爲get
了。
增長Nancy::Base
單例:
module Nancy class Base # methods... end Application = Base.new end
增長回調:
nancy_application = Nancy::Application nancy_application.get "/hello" do "Nancy::Application says hello" end # Use `nancy_application,` not `nancy` Rack::Handler::WEBrick.run nancy_application, Port: 9292
增長代理器(這部分代碼來自Sinatra的源碼):
module Nancy module Delegator def self.delegate(*methods, to:) Array(methods).each do |method_name| define_method(method_name) do |*args, &block| to.send(method_name, *args, &block) end private method_name end end delegate :get, :patch, :put, :post, :delete, :head, to: Application end end
引入Nancy::Delegate
到 Nancy
模塊:
include Nancy::Delegator
Nancy::Delegator
提供代理如get
,patch
,post
,等一系列方法。當在Nancy::Application
中調用這些方法的時候,它會按圖索驥找到代理器的這些方法。咱們實現了和Sinatra同樣的效果。
如今能夠刪掉那些建立Nancy::Base::new
和nancy_application
的代碼啦!Nancy的使用已經無限接近Sinatra了:
t "/bare-get" do "Whoa, it works!" end post "/" do request.body.read end Rack::Handler::WEBrick.run Nancy::Application, Port: 9292
還能使用rackup
來進行調用:
# config.ru require "./nancy" run Nancy::Application
Nancy的完整代碼:
# nancy.rb require "rack" module Nancy class Base def initialize @routes = {} end attr_reader :routes def get(path, &handler) route("GET", path, &handler) end def post(path, &handler) route("POST", path, &handler) end def put(path, &handler) route("PUT", path, &handler) end def patch(path, &handler) route("PATCH", path, &handler) end def delete(path, &handler) route("DELETE", path, &handler) end def head(path, &handler) route("HEAD", path, &handler) end def call(env) @request = Rack::Request.new(env) verb = @request.request_method requested_path = @request.path_info handler = @routes.fetch(verb, {}).fetch(requested_path, nil) if handler result = instance_eval(&handler) if result.class == String [200, {}, [result]] else result end else [404, {}, ["Oops! No route for #{verb} #{requested_path}"]] end end attr_reader :request private def route(verb, path, &handler) @routes[verb] ||= {} @routes[verb][path] = handler end def params @request.params end end Application = Base.new module Delegator def self.delegate(*methods, to:) Array(methods).each do |method_name| define_method(method_name) do |*args, &block| to.send(method_name, *args, &block) end private method_name end end delegate :get, :patch, :put, :post, :delete, :head, to: Application end end include Nancy::Delegator
Nancy的使用代碼:
# app.rb # run with `ruby app.rb` require "./nancy" get "/" do "Hey there!" end Rack::Handler::WEBrick.run Nancy::Application, Port: 9292
咱們來回顧一下都發生了什麼:
起名爲N(T)an(i)c(r)y Sinatra(別問爲何)
實現一個以來Rack的Web App
簡化nancy.get
爲get
支持子類化Nancy::Base
來實現更豐富的自定義。
Sinatra的代碼幾乎所有都在base.rb
。代碼密度有點大,閱讀完本文再去看,更容易理解一些了。從call!
開始是個不錯的選擇。而後是Response
類,它是Rack::Response
的子類,請求返回的信息封裝在這裏。還有Sinatra
是基於類的,Nancy
是基於對象,有些在Nancy
中的示例方法,在Sinatra
中是做爲類方法實現的,這也是須要注意的一點。