[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 上吧!


<<:  Notification

>>:  从零开始-30日练习开发iOS APP-铁人赛心得 Day-30

Angular RxJs 各种解订阅方式

昨天说到了将资料订阅出来渲染在页面上的事,那麽就就来说说 RxJs 解订阅这件事吧。 这也是为了避免...

Leetcode: 1971. Find if Path Exists in Graph

思路 用dps从start点走一遍,然後检查end点有没有finish。 程序码 class Sol...

[C 语言笔记--Day29] 6.S081 Lab syscall: Sysinfo ( III )

接续昨天的题目 原本今天打算写完这题的,但一直卡在一个地方, 就先贴出我写到一半的成果吧 // ke...

Day24 Android - databinding(单向绑定)

databinding可用於将class的data与元件绑定,像是(findViewById、onC...

GPU程序设计(3) -- 矩阵运算

前言 GPU卡原来是针对游戏开发及显示加速的设计的,後来才扩散至挖矿、深度学习...等其他领域,而游...