iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 26
0
Modern Web

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

[DAY 26] 復刻 Rails - 關於 Routing

還記得我們最一開始是怎麼處理 routing 嗎?

程式碼大概是像這樣

class App
  def call(env)
    if env['REQUEST_METHOD'] == 'GET' && env['PATH_INFO'] == '/hello'
      [200, { 'Content-Type' => 'text/plain' }, ['hello']]
    else
      [404, { 'Content-Type' => 'text/plain' }, ['Not found']]
    end
  end
end

run App.new

利用簡單的 if...else 來判斷該回傳什麼樣的 Response,但我們都知道對於現實生活中的需求,routing 可沒那麼簡單,如果要針對每一個路徑都寫一次 if...else 的判斷,想想那是多麼可怕的一件事情

在實作 routing 之前,不知道大家有沒有用過另一個 Ruby 比較輕型的網頁框架 - Sinatra,他的 routing 寫法像是這樣

# 官方範例

# myapp.rb
require 'sinatra'

get '/' do
  'Hello world!'
end

用 DSL 的方式來撰寫 routing,我們今天就先來仿照實作一個 mini 版的 Sinatra routing

關於 Sinatra 的 Routing

一樣建立一個新的資料夾,裡面放 config.ru,程式碼如下

# demo/config.ru

class MiniSinatra
  class Route
    attr_accessor :method, :path, :block

    def initialize(method, path, block)
      @method = method
      @path = path
      @block = block
    end

    def match?(env)
      env['REQUEST_METHOD'] == method && env['PATH_INFO'] == path
    end
  end

  def initialize
    @routes = []
  end

  def add_route(method, path, &block)
    @routes << Route.new(method, path, block)
  end

  def call(env)
    route = @routes.detect { |route| route.match?(env) }
    if route
      body = route.block.call
      [200, { 'Content-Type' => 'text/plain' }, [body]]
    else
      [404, { 'Content-Type' => 'text/plain' }, ['Not found']]
    end
  end
end

app = MiniSinatra.new
app.add_route('GET', '/') { 'hello world !' }
app.add_route('GET', '/lobster') { 'hello lobster !' }
run app

我們建立了一個叫 MiniSinatra 的 Rack Application,接著在裡面又建立了一個 Route 的 class

  class Route
    attr_accessor :method, :path, :block

    def initialize(method, path, block)
      @method = method
      @path = path
      @block = block
    end

    def match?(env)
      env['REQUEST_METHOD'] == method && env['PATH_INFO'] == path
    end
  end

Route 裡面我們接收了三個參數,methodpath 會用來比對這個 request(也就是 env 裡面所包含的資訊) 與這個 route 是不是符合

def match?(env)
  env['REQUEST_METHOD'] == method && env['PATH_INFO'] == path
end

另外我們在 MiniSinatra 這個 application 加了一個 add_route,將 route 規則存放在 @routes 裡面,加規則的寫法就像是這樣

app.add_route('GET', '/') { 'hello world !' }
app.add_route('GET', '/lobster') { 'hello lobster !' }

接著打開瀏覽器就可以測試結果,瀏覽對應的 URL,應該會有不同的文字印在畫面上

例如瀏覽 http://127.0.0.1:3001/ 會出現 hello world!,瀏覽 http://127.0.0.1:3001/lobster,就會出現 hello lobster !

這樣的寫法雖然也可行,但並沒有發揮到 Ruby 方便實作 DSL 的優勢,我們需要再修改一下

關於用 DSL 來寫 Routing

為了要做到用 DSL 來寫 Routing,我們需要用一個變數來紀錄 Application,並且在 MiniSinatra 加上一個 class method

# demo/config.ru

def self.application
  @application ||= MiniSinatra.new
end

接著在寫一個 get method 在 MiniSinatra 外面,像是這樣

# demo/config.ru

class MiniSinatra
  class Route
  # .
  # .
  # (略)
  end
end

def get(path, &block)
  MiniSinatra.application.add_route('GET', path, &block)
end

透過建立一個 get 的 helper method,並且傳遞 &block 方式將 block 傳到方法裡面做處理,我們就做出了一個簡單的 DSL

然後在底下我們就可以用 Sinatra 的寫法來加上 Routing 規則

# demo/config.ru

get '/' do
  'hello world !'
end
get '/lobster' do
  'hello lobster !'
end
run MiniSinatra.application

是不是感覺蠻容易的?

最後整段程式碼就會長這樣

class MiniSinatra
  class Route
    attr_accessor :method, :path, :block

    def initialize(method, path, block)
      @method = method
      @path = path
      @block = block
    end

    def match?(env)
      env['REQUEST_METHOD'] == method && env['PATH_INFO'] == path
    end
  end

  def initialize
    @routes = []
  end

  def add_route(method, path, &block)
    @routes << Route.new(method, path, block)
  end

  def call(env)
    route = @routes.detect { |route| route.match?(env) }
    if route
      body = route.block.call
      [200, { 'Content-Type' => 'text/plain' }, [body]]
    else
      [404, { 'Content-Type' => 'text/plain' }, ['Not found']]
    end
  end

  def self.application
    @application ||= MiniSinatra.new
  end
end

def get(path, &block)
  MiniSinatra.application.add_route('GET', path, &block)
end

get '/' do
  'hello world !'
end
get '/lobster' do
  'hello lobster !'
end
run MiniSinatra.application

利用這個觀念,明天我們就來改善 Mavericks 的 Routing 的寫法


上一篇
[DAY 25] 復刻 Rails - 千層蛋糕 Rack Middleware
下一篇
[DAY 27] 復刻 Rails - Routing 威力加強版 - 1
系列文
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List30

尚未有邦友留言

立即登入留言