ActionController::Base#render源码解析
来源:互联网 发布:川大生活服务 网络 编辑:程序博客网 时间:2024/05/20 08:41
提出问题:为什么要研究这个?
在日常开发中controller中的render用的很多,或者说大部分用法都知道这么用,但是我好奇这个render到底做了什么,要不然用起来总感觉缺了点什么,下面就来尝试研究下源码。
先前准备:
welcome_controller.rb
class WelcomeController < ApplicationController def index endend
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 endend
好到这里准备工作完毕,下面开始正式研究render实现。
首先我们思考下这里的render是什么?
经过思考后我们应该是知道的, render其实就是个方法,然后传入option= {} 这样的参数,既然是方法, 那就应该有对象吧~。没错这里的对象就是WelcomeController的实例对象,然后我们发现这个类里面并没有render方法,既然如此我们应该清楚应该向ancestors中去寻找。
class ApplicationController < ActionController::Base protect_from_forgery with: :exceptionend
通过上面代码我们知道要想找到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,具体细节还需要看源码去思考。
- ActionController::Base#render源码解析
- Rails源代码分析(40):ActionController Base的render方法
- Metal - 精简的 ActionController::Base
- base-adapter-helper源码解析
- 关于js继承---Base类的源码解析
- sklearn 源码解析 基本线性模型 base.py
- Android Render(四)supportVersion 27.0.0源码RecyclerView绘制流程解析
- Angular源码解读之Render
- 【React】ReactDOM.render源码分析
- base库 解析
- |Image$$RO$$Base|解析
- Render;
- render
- vtkRenderer中Render()函数解析
- ActionController::InvalidAuthenticityToken
- Delta3D 源码 之 dtCore::Base
- 云客Drupal8源码分析之渲染数组(render array)
- 浅谈Three.js源码-render之WebGLAttributes.js
- bzoj 3504: [Cqoi2014]危桥(最大流)
- 【转】ng-class的三种用法
- 2017面试题个人汇总
- 用DQN玩flappy bird(TensorFlow学习框架)
- Matlab的算术运算符
- ActionController::Base#render源码解析
- Unix intro
- * 24种设计模式——备忘录模式
- Python实现设计模式--03.抽象工厂模式(Abstract Factory Pattern)
- [CodeForces510B]
- 为什么需要消息队列,及使用消息队列的好处?
- thinkphp框架中后台传数组到前端js的方法
- hdu2107 Founding of HDU(C语言)
- Android实现不重复启动APP的方法