ActionController::Base#render源码解析

6 篇文章 0 订阅

提出问题:为什么要研究这个?

在日常开发中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,具体细节还需要看源码去思考。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值