终於到最後一天了,那就不罗嗦直接进入正题吧!
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
>>: mostly:functional 第二十九章:Monad 的法则
终於!可以进入真正的爬虫教学啦~ 我们已经有一定的实力来编写Python和分析网页了 今天的影片内容...
sentinel.conf # 高可用配置搭配Sentinel机制 ---> Redis (R...
接续昨天,来介绍第四种、第五种以及第六种方法! 4.代替Java String replace():...
虽然最近忙爆了...但还是告诉自己一但出发就不能半途而废 加油 点击VMS下的的ADD VM即可新...
只要再撑过这一天,就只要写结语就可以达成30天的目标了。 本来已经快想不到可以写甚麽了,那就来拿Li...