iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 25
0
Modern Web

向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List系列 第 25

[DAY 25] 復刻 Rails - 千層蛋糕 Rack Middleware

鐵人賽已經接近尾聲,今天要來聊聊 Rails 很重要的一個部分,也是初學者比較少了解到的東西,就是Rack Middleware,你可以在手邊的 Rails 專案裡面下這個指令

rake middleware

會發現類似像這樣的東西

use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper

不知道大家有沒有做過千層蛋糕?要先用平底鍋做出一片一片的餅皮,然後在一層一層的堆疊上去,Middleware 其實就有點像是千層蛋糕,透過一層層的服務所加疊起來的 Rack Application,換句話說利用這個概念,可以選擇去增加或減少某些服務,為了讓大家更了解 Rack Middleware,接下來會做一些小範例

首先我們開啟一個新的資料夾,並且新增一個 config.ru,加上以下的程式碼

# demo/config.ru

run proc {
[200, {'Content-Type' => 'text/html'},
 ["Hello, world!"]]
}

我們建立一個簡單的 Rack Application,並且回應 200,這裡相信大家都已經不陌生

或是你也可以這樣寫

# demo/config.ru

obj = Object.new
def obj.call(*args)
[200, {'Content-Type' => 'text/html'},
 ["Hello, world!"]]
end
run obj

只要 run 一個有 call method 的物件,一樣可以運作

現在我們來加上另一個服務看看

# demo/config.ru

SERVICE_B = proc {
 "I am Service B!"
}

SERVICE_A = proc {
 service_b_string = SERVICE_B.call
 [200, {'Content-Type' => 'text/html'},
 ["I am Service A and " + service_b_string]]
}
run SERVICE_A

# 執行結果 I am Service A and I am Service B!

這裡我們建立了有兩個服務的 Rack Middleware,在最底下我們用 run 先執行 SERVICE_A,並且在 SERVICE_A 裡面去呼叫 SERVICE_B

以上範例只是基本的概念,但實際應用上並不會在 SERVICE_A 裡面直接呼叫 SERVICE_B,這樣如果那天使用者把 B 拿掉,那A 就會跟著壞掉,我們希望這些服務只專注在自己的事情上面,服務與服務之間並不知道對方的存在,唯一聯繫的方式就是透過參數傳遞訊息,請看下面例子

# demo/config.ru

use Rack::Auth::Basic, "apa" do |_, pass|
 'it_30' == pass
end

run proc {
 [200, {'Content-Type' => 'text/html'},
 ["Hello, world!"]]
}

這裡我們使用了 Rack::Auth::Basic 來做簡單的驗證,開啟伺服器打開瀏覽器觀看以後,會跳出輸入帳號密碼的提示框,輸入 「apa」 和 「it_30」 之後才會做 200 回應,會發現我們這裡用了 use 這個關鍵字來執行另一個服務,而兩個服務之間並不會知道對方的存在,這就是實際上 Rack Middleware 在做的事情,再來看看更複雜的例子

# demo/config.ru

class ServiceA
 def initialize(app, arg = "")
   @app = app
   @arg = arg
 end

  def call(env)
    status, headers, content = @app.call(env)
    content[0] += ' I am ' + @arg
    [ status, headers, content ]
  end
end

use ServiceA, 'apa'

run proc {
  [200, {'Content-Type' => 'text/html'},
  ["Hello, world !"]]
}

# 在瀏覽器上印出 Hello, world ! I am apa

這裡建立了一個 ServiceA 的 class,裡面一樣有一個 call method,當最底下呼叫的 run 呼叫 proc 之後,因為我們有用 use來「登記」ServiceA 這個服務,所以會傳入整包 「app」 給 ServiceA 做處理,最後再由 ServiceA 做回應

那如果有多個 use 做登記呢?,例如像下面例子

use ServiceA
use ServiceB

run App.new

上面那段程式碼,實際上就等於下面這段程式碼

run ServiceA.new(
  ServiceB.new(
    App.new
  )
)

所以現在你看到 Rails 的 config.ru 這樣寫,再搭配本篇文章一開頭的 rake middleware 指令

require_relative 'config/environment'

run Rails.application

應該就會比較清楚知道,Rails 背後的服務是怎麼運作的

做一個自己的 Middleware 來處理 logger

為了更加深讀者的印象,我們用剛剛所學的觀念,來建立一個簡單的 logger,將 User 使用瀏覽器的瀏覽紀錄印在終端機上

# demo/config.ru

app = lambda do |env|
  [200, { 'Content-Type' => 'text/plain' }, ['hello from lambda!']]
end

class Logger
  def initialize(app)
    @app = app
  end

  def call(env)
    method = env['REQUEST_METHOD']
    path = env['PATH_INFO']
    puts "#{method} #{path}"
    @app.call(env)
  end
end

use Logger
run app

這裡我們做了一個 MiddlewareLogger,將接受到的資訊,也就是 env,取出 REQUEST_METHODPATH_INFO 並且印出,這樣我們就知道使用者看了那些頁面

實際上我們做得 Logger,也可以用 Rack::CommonLogger 來取代

關於 Rack 的 map

當然 Rack 其實還能做到更多事情,另外一個有趣的服務是 Rack::URLMap,能做到類似 routing 的效果,讓我們來看看下面例子

# demo/config.ru

require "rack/lobster"

use Rack::ContentType

map '/lobster' do
  use Rack::ShowExceptions
  run Rack::Lobster.new
end

map '/lobster/is_disappear' do
  run proc {
    [200, {}, ['where is lobster?']]
  }
end

run proc {
  [200, {}, ['I want to eat lobster']]
}

我們在最上面登記了一個 Rack::ContentType,用預設的方式來回傳 HTML content type,這樣每個 block 就算沒有回傳 content type 也不受到影響,接著用瀏覽器瀏覽 http://127.0.0.1:3001/,沒有意外上面會秀出 I want to eat lobster 的字樣,那如果瀏覽 http://127.0.0.1:3001/lobster 呢?

會發現,看到一隻龍蝦!

不要問我為什麼有龍蝦

利用 map 我們可以定義出,哪個 URL 要做哪些事情,這跟我們在 application.rb 裡面做得事情很類似,另外我們也在 /lobster 裡面登記了另一個服務 Rack::ShowExceptions,當你按下龍蝦那頁的 crash! 時,就會秀出 Exceptions 的頁面

今天介紹了 Rack Middleware 龍蝦 的用法,明天開始就要來實作比較複雜的 routing

話說,又一個連假要繼續寫鐵人賽...


上一篇
[DAY 24] 復刻 Rails - 關於 before_action
下一篇
[DAY 26] 復刻 Rails - 關於 Routing
系列文
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List30

尚未有邦友留言

立即登入留言