提出问题:为什么要研究这个?
在日常开发中controller中的render用的很多,或者说大部分用法都知道这么用,但是我好奇这个render到底做了什么,要不然用起来总感觉缺了点什么,下面就来尝试研究下源码。
先前准备:
welcome_controller.rb
class WelcomeController < ApplicationController
def index
end
end
index.html.erb
<h1>hello world</h1>
routes.rb
get 'welcome/index', to: 'welcome#index'
上面的代码很简单,启动下应该就能在页面显示了,在一般的开发中,如果在index这个动作下不写render 什么的,默认我们知道会render这个动作下的页面,具体为什么会调用默认的, 这里先不说了,下面我们稍微修改下代码:
class WelcomeController < ApplicationController
def index
render action: :index
end
end
好到这里准备工作完毕,下面开始正式研究render实现。
首先我们思考下这里的render是什么?
经过思考后我们应该是知道的, render其实就是个方法,然后传入option= {} 这样的参数,既然是方法, 那就应该有对象吧~。没错这里的对象就是WelcomeController的实例对象,然后我们发现这个类里面并没有render方法,既然如此我们应该清楚应该向ancestors中去寻找。
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
通过上面代码我们知道要想找到render只用去源码了。
打开actionpack这个gem:
找到ActionController::Base对应的文件位置,打开文件,可惜我们发现并没有render方法
MODULES = [
AbstractController::Rendering,
Helpers,
UrlFor,
Redirecting,
ActionView::Layouts,
Rendering,
Renderers::All,
ConditionalGet,
EtagWithTemplateDigest]
MODULES.each do |mod|
include mod
end
看这个MODULES(贴出部分细节看源码),经过我们观察Rendering很可能有这个方法,关键长得像对吧,打开这个文件:
def render(*args, &block)
options = _normalize_render(*args, &block)
rendered_body = render_to_body(options)
if options[:html]
_set_html_content_type
else
_set_rendered_content_type rendered_format
end
self.response_body = rendered_body
end
功夫不负有心人找到了,这个render就是我们需要的。
这里的render总共干了三件事:
第一: 格式化render
第二:处理第一步结果,返回response
第三:设置response的content_type
下面看看这些步骤到底做了什么
def _normalize_render(*args, &block)
options = _normalize_args(*args, &block)
if defined?(request) && !request.nil? && request.variant.present?
options[:variant] = request.variant
end
_normalize_options(options)
options
end
这个方法是为了格式化render的,这个方法做了三件事:
第一:格式化我们传入的render参数
第二:判断request,设置variant(这个步骤我们具体不说了,走不到这一步)
第三:对第一步的结果在进行格式化
下面继续看:
def _normalize_args(action=nil, options={})
if action.respond_to?(:permitted?)
if action.permitted?
action
else
raise ArgumentError, "render parameters are not permitted"
end
elsif action.is_a?(Hash)
action
else
options
end
end
这个方法是格式化参数的,那这里的传进来的action是什么呢?
前面我们知道我们传的是*args,其实就是我们render后面的参数
那我们就明白了
action = { action: :index }
action.respond_to?(:permitted?)判断了action有没有方法permitted?, 我们在console下申明个hash调用下,最终返回的是false, 当然action.is_a?(Hash)肯定是hash呢。返回了这个hash
返回格式化render中的:
options = _normalize_args(*args, &block)
经过上面的分析options = { action: :index }
第二步跳过,进行第三步:
_normalize_options(options) 这个到底做了什么?
def _normalize_options(options) #:nodoc:
_normalize_text(options)
if options[:text]
ActiveSupport::Deprecation.warn <<-WARNING.squish
`render :text` is deprecated because it does not actually render a
`text/plain` response. Switch to `render plain: 'plain text'` to
render as `text/plain`, `render html: '<strong>HTML</strong>'` to
render as `text/html`, or `render body: 'raw'` to match the deprecated
behavior and render with the default Content-Type, which is
`text/html`.
WARNING
end
if options[:html]
options[:html] = ERB::Util.html_escape(options[:html])
end
if options.delete(:nothing)
ActiveSupport::Deprecation.warn("`:nothing` option is deprecated and will be removed in Rails 5.1. Use `head` method to respond with empty response body.")
options[:body] = nil
end
if options[:status]
options[:status] = Rack::Utils.status_code(options[:status])
end
super
end
这个方法很长,我们分离下,看看做了什么
第一步:格式化文本
第二步:对option参数进一步处理进行判断
第三步:调用super(这个重点)
我们先来看看格式化文本做了什么?
RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html]
def _normalize_text(options)
RENDER_FORMATS_IN_PRIORITY.each do |format|
if options.key?(format) && options[format].respond_to?(:to_text)
options[format] = options[format].to_text
end
end
end
上面做了简单的逻辑判断,我们这里返回false具体没用到,一般也跳不到这块,这里的to_text,具体请看ruby api介绍吧,好继续往下走
返回上述方法,在这第二步做了简单判断
通过前面的介绍我们知道:
render text: ‘ok’ 到这里转化的options = { text: ‘ok’ }
但是我们看到options[:text] 如果是有值的话有有个警告,意识就是说这么写不好,如果修改的话改为: render plain: “ok”
同理
render html: ‘ok’ => options = { html: ‘ok’ }
render nothing: true => options = { nothing: true }
render status: 200 => options = { status: 200 }
看这三个值的判断条件:
ERB::Util.html_escape(options[:html])
这个是转义,不懂去看rails api html_escape方法有详细介绍
options.delete(:nothing)
options = { nothing: true } => {}
去掉了key,此时的option = {},相应的option[:body] = nil
option = {body: nil}, 这里有个警告看看应该能懂。继续向下看
options[:status]
Rack::Utils.status_code(options[:status])
status_code方法对传入的值进行了处理,我这里传的是200返回的还是200,如果传入的是”200” 也会转为200,最终会转化为整形。
好,回到我们之前的例子,之前我们的option = {action: :index}
那么经过上面的判断,options还是原来这个值
经过上面的分析格式化render就差最后一步了,这个最后的super到底干了什么?
其实通过寻找你发现找到的_normalize_options都不是我们想要的,
因为这个方法并不在actionpack这个gem里面,那当然不会有了
打开actionview gem
action_view/rendering.rb
def _normalize_options(options) # :nodoc:
super
if _include_layout?(options)
layout = options.delete(:layout) { :default }
options[:layout] = _layout_for_option(layout)
end
end
绕了这么半天调用到这个gem了,这个方法其实是对options的进一步封装,最终的结果肯定是我们想要的了
这里的super又跳到另一个方法了
def _normalize_options(options)
options = super(options)
if options[:partial] == true
options[:partial] = action_name
end
if (options.keys & [:partial, :file, :template]).empty?
options[:prefixes] ||= _prefixes
end
options[:template] ||= (options[:action] || action_name).to_s
options
end
跳到这个方法我们在方法中发现还有个super(option),我只想说真会跳,哎,继续看吧,看它跳到哪了,其实在我们终端打断点的时候是有提示的,可以看出跳到哪里,一看发现又跳到actionpack gem呢~
AbstractController::Rendering#_normalize_options
def _normalize_options(options)
options
end
这个方法当然啥都没干,那么上面的super(options)调用后返回的还是options,继续向下看:
回到之前的方法,我这里为了分析先设置个render
render partical: true => options = { partical: true }
这里我们应该是清楚的,上面分析过
经过上面的变换:
options[:partial] = action_name = self.action_name = index
那么:
options = { partical: :index }
那么:
(options.keys & [:partial, :file, :template]).empty? ==false
那么:
options[:template] ||= (options[:action] || action_name).to_s=“index”
那么:
options = {:partial=>”index”, :template=>”index”}
render action: :index => options = { action: :index }
经过上面的变换:
options[:partial] =nil
那么:
options = { action: :index }
那么:
(options.keys & [:partial, :file, :template]).empty? == true
options = { action: :index, prefixes: [“welcome”, “application”] }
那么:
options[:template] ||= (options[:action] || action_name).to_s=“index”
那么:
options = {action: :index,
prefixes: [“welcome”, “application”],
template: “index”}
方法最后返回options,返回原来的调用方法
那么:
options = {:partial=>”index”, :template=>”index”}
_include_layout?(options) = false
那么最终:
options = {:partial=>”index”, :template=>”index”}
看看下面一种比较复杂了
options = {action: :index,
prefixes: [“welcome”, “application”],
template: “index”}
_include_layout?(options) = true
layout = options.delete(:layout) { :default } = :default
options[:layout] = _layout_for_option(layout)
=>
{:action=>:index,
:prefixes=>["welcome", "application"],
:template=>"index",
:layout=>
#<Proc:0x007f97fbf4a4f8@/Users/baodong/.rvm/gems/ruby-2.3.0/gems/actionview-5.0.4/lib/action_view/layouts.rb:387>}
其实这个逻辑就多生成了layout
def _layout_for_option(name)
case name
when String then _normalize_layout(name)
when Proc then name
when true then Proc.new { |formats| _default_layout(formats, true) }
when :default then Proc.new { |formats| _default_layout(formats, false) }
when false, nil then nil
else
raise ArgumentError,
"String, Proc, :default, true, or false, expected for `layout'; you passed #{name.inspect}"
end
end
when :default then Proc.new { |formats| _default_layout(formats, false) }
最终会跳到这里
def _default_layout(formats, require_layout = false)
begin
value = _layout(formats) if action_has_layout?
rescue NameError => e
raise e, "Could not render layout: #{e.message}"
end
if require_layout && action_has_layout? && !value
raise ArgumentError,
"There was no default layout for #{self.class} in #{view_paths.inspect}"
end
_normalize_layout(value)
end
value = _layout(formats) if action_has_layout?
value = app/views/layouts/application.html.erb
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def _layout(formats)
if _conditional_layout?
#{layout_definition}
else
#{name_clause}
end
end
private :_layout
RUBY
end
def _normalize_layout(value)
value.is_a?(String) && value !~ /\blayouts/ ? "layouts/#{value}" : value
end
看到上面的value结果我们应该知道layout_definition获取到了路径
_normalize_layout(value)这个方法也就做了下判断最终返回的还是路径,那么到这里格式化render就结束了,那么下面就来看下根据options生成response.
render_to_body(options) 会触发下面方法
ActionController::Renderers#render_to_body
def render_to_body(options)
_render_to_body_with_renderer(options) || super
end
def _render_to_body_with_renderer(options)
_renderers.each do |name|
if options.key?(name)
_process_options(options)
method_name = Renderers._render_with_renderer_method_name(name)
return send(method_name, options.delete(name), options)
end
end
nil
end
这个方法只是做了下判断,看判断结果,如果为真会进行后面步骤,否则就直接返回nil呢~
下面我们看看什么时候判断是真,什么时候判断为假
在这里主要看_renderers这个值了,那这个值到底是多少了?
打断点我们知道
_renderers = #
RENDERERS = Set.new
included do
class_attribute :_renderers
self._renderers = Set.new.freeze
end
included do
binding.pry
self._renderers = RENDERERS
end
上面我贴出一部分代码,通过上面我们知道,ActionController::Base在include这些module的时候,在ActionController::Base上定义了_renderers属性,所以我们知道
ActionController::Base._renderers = RENDERERS = Set.new
是一个空的集合,还没有我们需要的值,只能继续往下看了
def self.add(key, &block)
define_method(_render_with_renderer_method_name(key), &block)
RENDERERS << key.to_sym
end
RENDERERS << key.to_sym
这个方法的这句让我好奇了,是不是这里做的呢?继续看
add :json do |json, options|
json = json.to_json(options) unless json.kind_of?(String)
if options[:callback].present?
if content_type.nil? || content_type == Mime[:json]
self.content_type = Mime[:js]
end
"/**/#{options[:callback]}(#{json})"
else
self.content_type ||= Mime[:json]
json
end
end
add :js do |js, options|
self.content_type ||= Mime[:js]
js.respond_to?(:to_js) ? js.to_js(options) : js
end
add :xml do |xml, options|
binding.pry
self.content_type ||= Mime[:xml]
xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
end
看到上面的方法我们已经明白了,调用add方法,把key传进去了,那么当然有
_renderers = {:json, :js, :xml}的set集合
回到这个方法的位置,这也就是个集合,如果想为真的话?我们应该render什么?
通过上面分析我们知道
@province = Province.all
render xml: @province
那么:
options = {:xml=> ???,
:prefixes=>[“welcome”, “application”],
:template=>”index”,
:layout=>
#Proc:0x007fa5ca8e77c8@/Users/baodong/.rvm/gems/ruby-2.3.0/gems/actionview-5.0.4/lib/action_view/layouts.rb:389}
大概是上面的格式,那肯定是有xml的,就为真了。好继续向下走
回到之前的方法
_process_options(options)
method_name = Renderers._render_with_renderer_method_name(name)
return send(method_name, options.delete(name), options)
_process_options(options)这个不说了返回自身的options,下面两句其实调用的是_render_with_renderer_xml_name方法,传入了两个参数,继续看吧
add :xml do |xml, options|
binding.pry
self.content_type ||= Mime[:xml]
xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
end
其实调用的就是这段,就是把options.delete(name)返回的值转为xml格式的,按照我们上面的就是把@province转为xml呢~
当然这里的:
option = {:prefixes=>[“welcome”, “application”],
:template=>”index”,
:layout=>
#Proc:0x007fa5ca8e77c8@/Users/baodong/.rvm/gems/ruby-2.3.0/gems/actionview-5.0.4/lib/action_view/layouts.rb:389}
好为真的情况分析完毕,那么为假了?
看到后面的super没有,有点想哭,继续看吧
def render_to_body(options = {})
super || _render_in_priorities(options) || ' '
end
不知道为什么看到super就不高兴,继续跳,
def render_to_body(options = {})
_process_options(options)
_render_template(options)
end
_render_template(options)是主要的,主要看它的返回内容,本来想分析的,但是后面的调用太多了,有兴趣的直接看源码吧,这里不多说了,其实这个方法就是根据options返回模板的内容。
到这里其实已经结束了,response都有了下面也直接根据response返回页面了,后面的设置content-type不说了,不是很重要,前面是核心。
总结:从上面的分析我们知道,render其实是个方法,这个方法会根据我们传的参数格式化为options,在根据options生成response,如果是render
xml, json, js其实就直接返回了,如果不是,还会根据options的参数信息生成response,具体细节还需要看源码去思考。