[DAY 30] 复刻 Rails - View 威力加强版 - 2

终於到最後一天了,那就不罗嗦直接进入正题吧!

关於 rendering.rb

之前我们的做法是把 render 写在 Controller 里面,毕竟 render 是 Controller 的 mehtod 这样的写法也没错,可是如果要做到单一职责的话,还是需要让 ActionView 来做这件事情,所以我们先修改一下 metal.rb

# mavericks/lib/action_controller/metal.rb

module ActionController
  class Metal
    attr_accessor :request, :response

    def process(action)
      send action
    end

    def params
      request.params
    end
  end
end

还有 route_set.rb

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

def dispatch(request)
  controller = controller_class.new
  controller.request = request
  controller.response = Rack::Response.new
  controller.process(action)
  controller.response.finish
end

将原先 render 相关的程序码都做移除,接着在 ActionController 我们做 include render 的动作

# mavericks/lib/action_controller/base.rb

module ActionController
  class Base < Metal
    include Callbacks
    include ActionView::Rendering
  end
end

这里还记得 ActionController 做些什麽事吗?,忘记了可以翻前面的文章复习一下

include 完後我们就要开始实作 rendering.rb 里面的程序码,这里我们分一个个 method 来看

# mavericks/lib/action_view/rendering.rb

module ActionView
  module Rendering
    def render(action)
      context = Base.new(view_assigns)
      path = template_path(action)

      content = Template.find(path).render(context)
      body = Template.find(layout_path).render(context) do
        content
      end

      response.body = [body]
    end

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

    def template_path(action)
      "#{Mavericks.root}/app/views/#{controller_name}/#{action}.html.erb"
    end

    def layout_path
      "#{Mavericks.root}/app/views/layouts/application.html.erb"
    end

    def controller_name
      self.class.name.chomp("Controller").to_underscore
    end
  end
end

首先是 render,会传一个 Action 进来,告诉我们要 render 那个 view,接着我们要 new 一个 Base 的物件,透过 view_assigns 来取得 Controller 里面的 实体变数,接着带着这些 实体变数 new 一个物件出来我们叫 context

这里做的就是我们昨天提到的第一步骤,另外我们实作另一个 Template 来寻找并且解析 .erb 档案的内容,最後将处理完後的 .erb 得出的页面内容写入到 response.body 做回应

不知道有没有注意到 layout 那段程序码?我们透过在 View 那边使用 yield 来将内容当成 block 的技巧,来处理 layout 的问题

def render(action)
  context = Base.new(view_assigns)
  path = template_path(action)

  content = Template.find(path).render(context)
  body = Template.find(layout_path).render(context) do
    content
  end

  response.body = [body]
end

view_assigns 就是将 Controller 里面的实体变数,一个一个的取出并且转成 Hash

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

最後 template_pathlayout_pathcontroller_name 这些 method 就是档案位置的取得和字串转换,前面应该都有类似的实作就不多说明

关於 Base.rb

刚刚有提到将 Controller 里面的 实体变数 取出,Base 基本上就是在处理将 实体变数 的值「带到」View 里面,搭配昨天提到的将 .erb 的 Template 程序码转成 实体方法,而这些 实体方法 摆放的位子就是 CompiledTemplates

# mavericks/lib/action_view/base.rb

module ActionView
  class Base
    include CompiledTemplates

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

关於 template.rb

还记得我们之前处理 template 是用 erubi 这个套件吗?但其实 Ruby 也有内建 erb 可以来处理 template,这里我们也分一个个 method 来讲解

require 'erb'

module ActionView
  class Template
    CACHE = Hash.new do |cache, file|
      cache[file] = Template.new(File.read(file), file)
    end

    def initialize(source, name)
      @source = source
      @name = name
    end

    def self.find(file)
      CACHE[file]
    end

    def render(context, &block)
      compile
      context.send(method_name, &block)
    end

    def method_name
      @name.gsub(/[^\w]/, '_')
    end

    def compile
      return if @compiled
      code = ERB.new(@source).src

      CompiledTemplates.module_eval <<-CODE
        def #{method_name}
          #{code}
        end
      CODE

      @compiled = true
    end
  end
end

我们将取出来的 .erb 交给 compile 做处理,并且将里面的程序码转换成 实体方法 让 View 做呼叫,在呼叫的同时就会带入 实体变数,而为了不让同个档案重覆做 compile 我们用简单的变数 @compiled 做纪录,并且实作 CACHE 增加效能

修改完後别忘了在 all.rb 加上新增加的 action_view

# mavericks/lib/mavericks/all.rb

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

接着回到 just_do,因为我们还没做省略 render 的写法,所以要先回到 TasksController.rb 将 render 加回去

# just_do/app/controllers/tasks_controller.rb

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

  def index
    @tasks = Task.all
    render :index
  end

  def show; end

  private

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

然後别忘了我们现在用的是 yield,所以要将 layout 也修改一下

<!-- just_do/app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Index</title>
    <link rel="stylesheet" href="https://unpkg.com/@coreui/coreui/dist/css/coreui.min.css">
  </head>
  <body class="c-app">
    <div class="c-wrapper">
      <header class="c-header c-header-light c-header-fixed">
      </header>
      <div class="c-body">
        <main class="c-main">
          <div class="container-fluid">
            <%= yield %>
          </div>
        </main>
      </div>
    </div><!-- Optional JavaScript -->
    <!-- Popper.js first, then CoreUI JS -->
    <script src="https://unpkg.com/@popperjs/core@2"></script>
    <script src="https://unpkg.com/@coreui/coreui/dist/js/coreui.min.js"></script>
  </body>
</html>

接着就可以测试看看了!

关於 render 可以不用写

一定会有人问,我们之前有实作 render 不用写呀,那现在该怎麽加回去呢?其实实作方式不难

# mavericks/lib/action_controller/base.rb

module ActionController
  class Base < Metal
    include Callbacks
    include ActionView::Rendering
    include ImplicitRender
  end
end

我们在 ActionController::Base include 这个功能进来,里面的实作方式也很简单

# mavericks/lib/action_controller/implicit_render.rb

module ActionController
  module ImplicitRender
    def process(action)
      super
      render action if response.empty?
    end
  end
end

直接检查有没有 response,如果没有,代表没有做 render,那就帮开发者呼叫罗

别忘了每次新增 class 或 module 都要做 autoload

# mavericks/lib/action_controller.rb

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

最後回到 just_do 拿掉 render 试试看

感想

从一开始学习 gem 的建立,慢慢一步一步的构建出,一个小型的 MVC 框架,过程真的是充满了困难和各种撞墙,一来是网路上相关的资源比较零碎,再来是很多观念我需要看3次以上才能理解,整个系列文我参考了 metaprogramming 这本书,里面有很多观念是我一开始不太理解为什麽要这样做,但在复刻 Rails 的过程我才了解到,阿~原来是这样阿,算是蛮特别的一个收获

另外我也参考了 Rebuilding Rails,整本内容老实说并不困难,算是浅显易懂的入门书,但在整个架构上着墨的反而比较少,更多的是告诉你一些基本概念

帮助我最多的大概就是 Owning Rails 这个教学课程,也是我花最多时间在吸收理解的部分,但也因为这样让我更加理解一些平常碰不得的东西

写完这个系列文章对我写 Rails 有很大帮助吗?其实我觉得收获更多的是,训练自己的毅力吧 XD,毕竟 30 天每天写下来也是蛮累的,尤其中间还度过两次连假...

不管如何,还是恭喜自己完赛啦!感谢大家!

最後附上程序码
Mavericks github


<<:  Day29-影像侵蚀

>>:  mostly:functional 第二十九章:Monad 的法则

Day16 requests模组一

终於!可以进入真正的爬虫教学啦~ 我们已经有一定的实力来编写Python和分析网页了 今天的影片内容...

Day24 Redis架构实战-Sentinel丛集架构

sentinel.conf # 高可用配置搭配Sentinel机制 ---> Redis (R...

Day27Java StringⅡ

接续昨天,来介绍第四种、第五种以及第六种方法! 4.代替Java String replace():...

VM功能与参数详解

虽然最近忙爆了...但还是告诉自己一但出发就不能半途而废 加油 点击VMS下的的ADD VM即可新...

伸缩自如的Flask [day 29] Line Messaging API

只要再撑过这一天,就只要写结语就可以达成30天的目标了。 本来已经快想不到可以写甚麽了,那就来拿Li...