iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 24
0
Modern Web

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

[DAY 24] 復刻 Rails - 關於 before_action

昨天我們示範了如何在 module 裡面用 super 來擴充方法,今天就來將昨天學到的觀念應用在我們的框架裡面

關於Metal

Rails 在 ActionController 有一個有趣的機制,就是分成ActionController::MetalActionController::Base,會這樣分的原因是,有時候你不想要用到每一個 Controller 的功能,就可以使用 ActionController::Metal 做客製化,當然如果想要完整的 Controller, 就使用 ActionController::Metal,所以在 Rails 的原始碼你會看到

module ActionController
  class Base < Metal
  # .
  # .
  # (略)
  end
end

Base 來繼承 Metal,如果要擴充呢?就像昨天舉例的 ChildParent 的做法一樣,可以在 Child 裡面做 include,另外昨天也了解到 The Include-and-Extend Trick 的技巧,用一行 include 來做到許多事情

所以我們希望 Mavericks 也可以做到一樣的事情,就可以這樣規劃

# mavericks/lib/action_controller/metal.rb

module ActionController
  class Metal
    # 從 Base.rb 複製程式碼過來
    # .
    # .
    # (略)
  end
end

接著在 Base.rb 繼承 Metal

module ActionController
  class Base < Metal
  end
end

別忘了在 action_controller.rb autoload 所有相關的檔案

# mavericks/lib/action_controller/base.rb
module ActionController
  autoload :Base, "action_controller/base"
  autoload :Metal, "action_controller/Metal"
end

關於 before_action

現在我們知道 Metal 裡面放的是 Controller 最基本的功能,Base 裡面放的是擴充的功能,所以如果要做 before_action,應該要放在 Base 裡面,並且用 include module 的方式來擴充想要的功能

現在我們直接來實作 before_action 的部分,建立一個 module,程式碼總共分兩部份

# mavericks/lib/action_controller/callbacks.rb

module ActionController
  module Callbacks

    # part1
    class Callback
      def initialize(method, options)
        @method = method
        @options = options
      end

      def match?(action)
        if @options[:only]
          @options[:only].include? action.to_sym
        else
          true
        end
      end

      def call(controller)
        controller.send @method
      end
    end

    # part2
    def self.included(base)
      base.extend ClassMethods
    end

    module ClassMethods
      def before_action(method, options = {})
        before_actions << Callback.new(method, options)
      end

      def before_actions
        @before_action ||= []
      end
    end

    def process(action)
      self.class.before_actions.each do |callback|
        if callback.match?(action)
          callback.call(self)
        end
      end
      super
    end
  end
end

part2

我們先從 part2 開始說起,self.included 昨天有提過他是 Ruby 的 Hook Method,可以在 include 被觸發,裡面做得就是 extend 的事情,利用這樣的技巧,可以同時達到 includeextend 的效果

def self.included(base)
  base.extend ClassMethods
end

ClassMethods 裡面包含了兩個 class method,其中 before_action 就是我們之後在 just_do 的 Controller 會呼叫的方法,我們將每一個 before_action 又另外包成一個 Callback 的物件,並且放到 @before_action 保存起來

module ClassMethods
  def before_action(method, options = {})
    before_actions << Callback.new(method, options)
  end

  def before_actions
    @before_action ||= []
  end
end

part2 的最後,我們擴充了 process,這個用法就是昨天提到如何在 module 運用 supersuper 裡面其實就是 Action 原本要做的事情,如果 before_action 有 method 需要執行,我們就會在 super 前做事

def process(action)
  self.class.before_actions.each do |callback|
    if callback.match?(action)
      callback.call(self)
    end
  end
  super
end

還記得我們剛剛把 before_aciton 包成一個一個的 Callback 的物件,利用 callback 的 match 來檢查這個 Action 是不是符合條件(例如 only 條件),如果符合的話,用 call 來執行這個 method,接下來就到 part1 看看怎麼實作 Callback

part1

我們有提到要把 before_action 包成一個一個 Callback 的物件,現在就來實作這部分,這裡主要有兩個 method,一個是 match?,另一個是 call

class Callback
  def initialize(method, options)
    @method = method
    @options = options
  end

  def match?(action)
    if @options[:only]
      @options[:only].include? action.to_sym
    else
      true
    end
  end

  def call(controller)
    controller.send @method
  end
end

match? 裡面,我們做了 only 的比對,如果有符合的話,這個 callback 會執行 call,裡面就會用 send 來執行要做的方法

基本的 before_action 做差不多了,最後一樣記得做 autoload,還有在 base.rb 做 include

# mavericks/lib/action_controller/base.rb

module ActionController
  class Base < Metal
    include Callbacks
  end
end
# mavericks/lib/action_controller.rb

module ActionController
  autoload :Base, "action_controller/base"
  autoload :Callbacks, 'action_controller/callbacks'
  autoload :Metal, "action_controller/Metal"
end

最後回到 just_do 來看看實作後的成果吧

# just_do/app/controllers/tasks_controller.rb

class TasksController < ActionController::Base
  before_action :find_task, only:[:show]

  def index
    @tasks = Task.all
  end

  def show; end

  private

  def find_task
    @task = Task.find(params['id'])
  end
end

開啟伺服器用瀏覽器執行 Action show,應該會先執行 find_task,這樣代表我們完成了!


上一篇
[DAY 23] 復刻 Rails - 用 Rails 的方式整理程式碼 ActionController
下一篇
[DAY 25] 復刻 Rails - 千層蛋糕 Rack Middleware
系列文
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List30

尚未有邦友留言

立即登入留言