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,具体细节还需要看源码去思考。

原创粉丝点击