iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 28
0
Modern Web

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

[DAY 28] 復刻 Rails - Routing 威力加強版 - 2

承續昨天的實作,今天就來補上 Routing 的最後一個步驟

還記得我們怎麼實作 MiniSinatra 的 DSL 嗎?

# MiniSinatra

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

get '/' do
  'hello world !'
end

是不是跟我們的 just_do 裡面的 routes.rb 很像?

Mavericks.application.routes.draw do
  root to: 'tasks#index'
  resources :tasks
end

透過這樣的判斷,我們知道 rootresources 其實也就是一個 mehtod,但為什麼這些 method 可以被包在一個 block 裡面去執行呢?這就是我們今天會提到的一個很重要的觀念 - instance_eval

instance_eval

先來看看一個範例

class MyClass
  def initialize
    @v = 1
  end
end

obj = MyClass.new
obj.instance_eval do
  puts self
  puts @v
end

這裡我們建立了一個 MyClass,在裡面存放了一個實體變數叫 @v 並且給予 1 的值,接著 new 一個 MyClass 的物件以後,執行 instance_eval,裡面會印出什麼呢?

答案是

#<MyClass:0x00007f85a3033538>
1

從這個例子我們可以知道,在 instance_eval 這個 block 裡面執行的環境會是「這個物件」本身,所以可以透過這樣的方式來取得「這個物件」的內容,當然也可以為「這個物件」定義方法

像是這樣

class MyClass
  def initialize
    @v = 1
  end
end

obj = MyClass.new
obj.instance_eval do
  def v
    @v
  end
end

puts obj.v

可以看到我們一開始並沒有定義 attr_accessor,但之後透過 instance_eval 來替「這個物件」加上,為什麼我一直強調「這個物件」呢,因為用 instance_eval 定義出來的方法,其他物件並不能共用

class MyClass
  def initialize
    @v = 1
  end
end

obj = MyClass.new
obj2 = MyClass.new
obj.instance_eval do
  def v
    @v
  end
end

puts obj.v
# 1
puts obj2.v
# undefined method `v'

所以如果要定義出,不管其他物件也可以使用的方法,就要用另一個很類似的 method,只是作用在 class 上的 class_eval

class MyClass
  def initialize
    @v = 1
  end
end

MyClass.class_eval do
  def v
    @v
  end
end

obj = MyClass.new
obj2 = MyClass.new
puts obj.v
puts obj2.v

至於為什麼 instance method,反而要用 class_eval 來定義,而不是用 instance_eval

那是因為 instance method 實際存放的位置是在 Class 上面,只有定義在 Class 上,才可以讓每一個 new 出來的物件做共享,物件裡面存放的只有實體變數

mapper

剛剛我們已經說明了 instance_eval,現在再來看看 draw

def draw(&block)
  mapper = Mapper.new(self)
  mapper.instance_eval(&block)
end

就可以知道,我們利用 instance_eval,來去執行裡面的 Mapper 物件裡面的 method,而在 Mapper,就是我們的各種定義 DSL 語法的地方

# mavericks/lib/action_dispatch/routing/mapper.rb

module ActionDispatch
  module Routing
    class Mapper
      def initialize(route_set)
        @route_set = route_set
      end

      def get(path, to:, as: nil)
        # to => "controller#index"
        controller, action = to.split("#")
        @route_set.add_route("GET", path, controller, action, as)
      end

      def root(to:)
        get "/", to: to, as: 'root'
      end

      def resources(plural_name)
        get "/#{plural_name}", to: "#{plural_name}#index", as: plural_name.to_s
        get "/#{plural_name}/new", to: "#{plural_name}#new",
                                   as: "new_" + plural_name.to_s.singularize
        get "/#{plural_name}/show", to: "#{plural_name}#show",
                                    as: plural_name.to_s.singularize
      end
    end
  end
end

另外可以看到,我們在初始化時傳了一個 route_set 的 instance,是因為我們需要借助 route_setadd_route method 來將規則加到 @routes,讓之後在比對規則時可以取出

因為篇幅的關係,這裡只有實作 get 的部分,接下來一樣別忘記做 autoload

# mavericks/lib/action_dispatch.rb

module ActionDispatch
  module Routing
    autoload :RouteSet, "action_dispatch/routing/route_set"
    autoload :Mapper, "action_dispatch/routing/mapper"
  end
end

並且在 all.rb 裡面把原有的 routing.rb 拿掉,換成新的 action_dispatch

# mavericks/lib/mavericks/all.rb

require 'erubi'
require 'yaml'
require "mavericks"
require "active_support"
require "active_record"
require "action_controller"
require "action_dispatch"

接著回到 just_do,除了新增 routes.rb 以外,其他程式碼不需要更動

# just_do/config/routes.rb

Mavericks.application.routes.draw do
  root to: 'tasks#index'
  resources :tasks
end

然後打開瀏覽器測試看看,沒問題的話,網站應該還是會正常運作!

Mavericks github: https://github.com/apayu/mavericks


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

尚未有邦友留言

立即登入留言