如何構建Sinatra?

今天是偉大的爵士樂大師法蘭克.辛納區(Frank Sinatra)誕辰一百週年。天時地利人和,正是翻譯這篇文章的好日子,錯過再等一百年。git

杭州此刻在下雨,陰冷潮溼。耳邊是搖曳的爵士樂,我把貓關進了陽臺,打開電腦,開始胡說八道。github

(能夠跳過正文直接看最後的完整代碼)ruby

原文連接:https://robots.thoughtbot.com/lets-build-a-sinatraapp


構建一個Sinatra

Sinatra是一個基於Ruby的快速開發Web應用程序基於特定域(domain-specific)語言。在一些小項目中使用事後,我決定一探究竟。dom

Sinatra是由什麼組成的?

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。測試

OK,開搞。

就叫它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 /helloGET /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\> } }

這個返回結果,咱們的路由表工做的很好。

引入了 Rack 的 Nancy

如今咱們給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狀態碼和錯誤信息。

從 HTTP 請求中獲得更多信息

nancy.get如今只能獲得路徑,但要想正常工做,它須要獲得更多的信息,好比請求的參數等。有關請求的環境變量被封裝在Rack::Requestparams中。

咱們給Nancy::Base增長一個新的方法params

module Nancy
  class Base
    #
    # ...other methods....
    #

    def params
      @request.params
    end
  end
end

須要這些請求信息的回調處理中,能夠訪問這個params方法來獲得。

訪問 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,有關請求的信息,都會被打印出來。

支持任意的 HTTP 方法

到目前爲止,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

一般在POSTPUT請求中,咱們會想訪問請求的內容(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

現代化進程

咱們來作如下優化:

  1. 使用params實例方法來替代直接調用request.params

def params
  request.params
end
  1. 容許回調方法返回一個字符串

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

使用代理模式繼續優化 Nancy::Application

在使用Sinatra的時候,咱們使用getpost來進行請求處理優雅強大又直觀。它是怎麼作到的呢?先考慮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::DelegateNancy模塊:

include Nancy::Delegator

Nancy::Delegator提供代理如getpatchpost,等一系列方法。當在Nancy::Application中調用這些方法的時候,它會按圖索驥找到代理器的這些方法。咱們實現了和Sinatra同樣的效果。

如今能夠刪掉那些建立Nancy::Base::newnancy_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.getget

  • 支持子類化Nancy::Base來實現更豐富的自定義。

課外閱讀

Sinatra的代碼幾乎所有都在base.rb。代碼密度有點大,閱讀完本文再去看,更容易理解一些了。從call!開始是個不錯的選擇。而後是Response類,它是Rack::Response的子類,請求返回的信息封裝在這裏。還有Sinatra是基於類的,Nancy是基於對象,有些在Nancy中的示例方法,在Sinatra中是做爲類方法實現的,這也是須要注意的一點。

相關文章
相關標籤/搜索