iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 29
0
Modern Web

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

[DAY 29] 復刻 Rails - View 威力加強版 - 1

其實我們的 Mavericks 已經做得差不多了,但就是那個 View 總覺得還可以再更好,如果仔細看 Rails 的原始碼,會發現有一個叫 ActionView 的部分,是用來處理有關 Template render,既然這樣的話...,那最後兩天就來做這個部分吧!

在開始之前,我們要先建立一些觀念

關於 Controller 裡面的 Instance variables

經過將近一個月的實作,我們已經了解到 Rails 在實作 Controller 時會 new 一個 Controller 的物件出來,而每一個 Action 其實就是 Controller 的 mehtod,所以你會看到類似這樣的程式碼

# mavericks/lib/action_controller/metal.rb

module ActionController
  class Metal
    def process(action)
      send action
    end
    # .
    #.
    # (略)
  end
end

那 View 和 Controller 的關係又是什麼呢?我們都知道在 Ruby 裡面,一個物件裡面包含了 實體變數,然後可以用物件的 實體方法 來操作 實體變數,這樣的觀念相信大家都不陌生

就像下面的例子

class A
  def initialize(name)
    @name = name
  end

  def call
    @name
  end
end

puts A.new('apa').call
# apa

所以你可以這樣想像

  1. 當一個 Controller 的物件有包含 實體變數 時,我們可以將這個 實體變數 先取出來然後放到 View 的 實體變數 裡面
  2. 接著將 erb 的 template 檔案內容轉成一個 實體方法
  3. 當 View 執行那個 template 的 實體方法 時,自然就會代入 實體變數

有點複雜?沒關係,所以這裡這裡才要花上兩天實作...XD,那我們先來搞清楚怎麼將 Controller 的 實體變數 取出來,放到 View 的 實體變數 裡面,這裡用的是 Ruby 的 instance_variables,透過這個 method 我們可以將這個物件的 實體變數 給列出來

像是這樣

# demo.rb

class Controller
  def initialize(name)
    @name = name
  end

  def index
    @name
  end
end

puts Controller.new('apa').instance_variables
# @name

取出來後要怎麼放到 View 的物件裡面呢?我們可以先搭配 instance_variable_get 來取得值,並且轉換成 Hash

像是這樣

# demo.rb

class TasksController
  def initialize(name)
    @name = name
  end

  def index
    @name
  end

  def render
    assigns = {}
    instance_variables.each do |name|
      assigns[name[1..-1]] = instance_variable_get(name)
    end
    assigns
  end
end


puts TasksController.new('apa').render
# {"name"=>"apa"}

再用另外一個很像的方法,叫 instance_variable_set,來存放到 View 的物件裡面,

# demo.rb

class ActionView
  def initialize(assigns = {})
    assigns.each_pair do |name, value|
      instance_variable_set "@#{name}", value
    end
  end
end

class TasksController
  def initialize(name)
    @name = name
  end

  def index
    @name
  end

  def render
    assigns = {}
    instance_variables.each do |name|
      assigns[name[1..-1]] = instance_variable_get(name)
    end
    assigns
  end
end


controller_instance_variable = TasksController.new('apa').render
action_view = ActionView.new(controller_instance_variable)

puts action_view.instance_variables
# @name

puts action_view.instance_variable_get(:@name)
# apa

這樣我們就有步驟 1 的概念了

關於 module_eval

我們在步驟 2 有提到要將 .erb 檔案裡面的程式碼,轉成 View 的 實體方法 來執行,該怎麼做?我們可以用 include 搭配 module_eval 來實作到這點

一樣直接看範例

# demo.rb

module CompiledTemplates
end

class Base
  def compiled(method_name, code)
    CompiledTemplates.module_eval <<-CODE
      def #{method_name}
        #{code}
      end
    CODE
  end
end

class Template
  include CompiledTemplates
end


Base.new.compiled('index_template', "'<h1>I am apa</h1>'")
puts Template.new.index_template
# <h1>I am apa</h1>

我們建立一個空的 Module,接著實作一個 Base,裡面有一個方法用來將 .erb Template 裡面的程式碼轉成另一個 Class 的 實體方法,用法就是裡面 module_eval 來「增加」CompiledTemplates 裡面的 mehtod,接著 Template include 以後,就可以做到動態增加 instance method 的效果

有沒有發現 module_eval 和前面所提到的 class_evalinstance_eval 很像?如果不清楚的話,建議透過多看幾次官方的範例,來了解三者之間的差異

接著我們將前面兩個步驟合在一起

# demo.rb

module CompiledTemplates
end

class Base def compiled(method_name, code)
    CompiledTemplates.module_eval <<-CODE
      def #{method_name}
        #{code}
      end
    CODE
  end
end

class ActionView
  include CompiledTemplates

  def initialize(assigns = {})
    assigns.each_pair do |name, value|
      instance_variable_set "@#{name}", value
    end
  end
end

class TasksController
  def initialize(name)
    @name = name
  end

  def index
    @name
  end

  def render
    assigns = {}
    instance_variables.each do |name|
      assigns[name[1..-1]] = instance_variable_get(name)
    end
    assigns
  end
end


Base.new.compiled('index_template', "\"<h1>I am \#{@name}</h1>\"")
controller_instance_variable = TasksController.new('apa').render
action_view = ActionView.new(controller_instance_variable)

puts action_view.index_template
# <h1>I am apa</h1>

就完成 Rails 的 Template render 了!帶著這個觀念,明天就來實作在 Mavericks 上吧!


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

尚未有邦友留言

立即登入留言