終於到最後一天了,那就不囉嗦直接進入正題吧!
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_path
、layout_path
、controller_name
這些 method 就是檔案位置的取得和字串轉換,前面應該都有類似的實作就不多說明
剛剛有提到將 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 是用 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 不用寫呀,那現在該怎麼加回去呢?其實實作方式不難
# 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