在controller可调用helper和helper_method,给view增加方法。它们源码如下

# actionpack-5.2.1/lib/abstract_controller/helpers.rb
def helper_method(*meths)
  meths.flatten!
  self._helper_methods += meths

  meths.each do |meth|
    _helpers.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
      def #{meth}(*args, &blk)                               # def current_user(*args, &blk)
        controller.send(%(#{meth}), *args, &blk)             #   controller.send(:current_user, *args, &blk)
      end                                                    # end
    ruby_eval
  end
end

def helper(*args, &block)
  modules_for_helpers(args).each do |mod|
    add_template_helper(mod)
  end

  _helpers.module_eval(&block) if block_given?
end

private

def add_template_helper(mod)
  _helpers.module_eval { include mod }
end

可见,这两个方法都是在给_heleprs增加方法。而_helpers是一个module

module AbstractController
  module Helpers
    extend ActiveSupport::Concern

    included do
      class_attribute :_helpers, default: Module.new
      class_attribute :_helper_methods, default: Array.new
    end

那_helpers是如何mixin到view上的呢?搜寻actionview的代码,可发现

module ActionView
  module Rendering
    extend ActiveSupport::Concern
    include ActionView::ViewPaths

    module ClassMethods
      def view_context_class
        @view_context_class ||= begin
          supports_path = supports_path?
          routes  = respond_to?(:_routes)  && _routes
          helpers = respond_to?(:_helpers) && _helpers

          Class.new(ActionView::Base) do
            if routes
              include routes.url_helpers(supports_path)
              include routes.mounted_helpers
            end

            if helpers
              include helpers
            end
          end
        end
      end
    end

而view_context_class方法正是每个view对象的类,即是每个view类都mixin了对应的helpers。那么这里是如何访问到_helpers方法的呢?给helper module加上断点如下

require "active_support/dependencies"
      
module AbstractController
  module Helpers
    extend ActiveSupport::Concern
      
    included do
      #class_attribute :_helpers, default: Module.new
      m = Module.new do
        def self.included(base)
          binding.pry
          super
        end
      end
      class_attribute :_helpers, default: m
      class_attribute :_helper_methods, default: Array.new
    end

然后访问一下humen#index,检查调用栈

From: /home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/abstract_controller/helpers.rb @ line 13 #.included:

   12: def self.included(base)
=> 13:   binding.pry
   14:   super
   15: end

[1] pry(#)> caller
=> [# ...
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/binding_of_callers-0.1.8/lib/binding_of_callers/pry.rb:12:in `pry'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/abstract_controller/helpers.rb:13:in `included'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/abstract_controller/helpers.rb:41:in `include'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/abstract_controller/helpers.rb:41:in `block in inherited'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/abstract_controller/helpers.rb:41:in `initialize'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/abstract_controller/helpers.rb:41:in `new'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/abstract_controller/helpers.rb:41:in `inherited'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.1/lib/action_view/layouts.rb:219:in `inherited'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/action_controller/metal/rendering.rb:23:in `inherited'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/action_controller/metal/parameter_encoding.rb:10:in `inherited'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/action_controller/metal/params_wrapper.rb:237:in `inherited'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/abstract_controller/railties/routes_helpers.rb:9:in `block (2 levels) in with'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/action_controller/railties/helpers.rb:7:in `inherited'",
"/home/z/test_rails/rays/app/controllers/application_controller.rb:1:in `
'", # ... "/home/z/test_rails/rays/app/controllers/humen_controller.rb:1:in `
'", # ... ]

可见当前humen_controller有父类ActionView::Layouts,它的inherited会调用更上一级父类AbstractController::Helpers的inherited。但断点并没出现在ActionView::Rendering的Class.new(ActionView::Base)里,再检查一下,发现AbstractController::Helpers的inherited是给子类HumenController的_helpers赋了一个新的Module.new { include helpers }

module AbstractController
  module Helpers
    extend ActiveSupport::Concern

    module ClassMethods
      def inherited(klass)
        helpers = _helpers
        klass._helpers = Module.new { include helpers }
        klass.class_eval { default_helper_module! } unless klass.anonymous?
        super
      end

于是将断点加在此处

module AbstractController
  module Helpers
    extend ActiveSupport::Concern

    module ClassMethods
      # When a class is inherited, wrap its helper module in a new module.
      # This ensures that the parent class's module can be changed
      # independently of the child class's.
      def inherited(klass)
        helpers = _helpers
        #klass._helpers = Module.new { include helpers }
        klass._helpers = Module.new do
          include helpers
          def self.included(base)
            binding.pry
            super
          end
        end
        klass.class_eval { default_helper_module! } unless klass.anonymous?
        super
      end

这就显示出ActionView::Rendering的Class.new(ActionView::Base)了

From: /home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/abstract_controller/helpers.rb @ line 38 #.included:

   37: def self.included(base)
=> 38:   binding.pry
   39:   super
   40: end

[1] pry(#)> caller
=> [# ...
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/binding_of_callers-0.1.8/lib/binding_of_callers/pry.rb:12:in `pry'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/abstract_controller/helpers.rb:38:in `included'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.1/lib/action_view/rendering.rb:51:in `include'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.1/lib/action_view/rendering.rb:51:in `block in view_context_class'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.1/lib/action_view/rendering.rb:44:in `initialize'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.1/lib/action_view/rendering.rb:44:in `new'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.1/lib/action_view/rendering.rb:44:in `view_context_class'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.1/lib/action_view/rendering.rb:61:in `view_context_class'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.1/lib/action_view/rendering.rb:74:in `view_context'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.1/lib/action_view/rendering.rb:97:in `_render_template'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/action_controller/metal/streaming.rb:219:in `_render_template'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.1/lib/action_view/rendering.rb:84:in `render_to_body'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/action_controller/metal/rendering.rb:52:in `render_to_body'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/action_controller/metal/renderers.rb:142:in `render_to_body'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/abstract_controller/rendering.rb:25:in `render'",
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionpack-5.2.1/lib/action_controller/metal/rendering.rb:36:in `render'"
# ...
]

至于为什么继承链中会出现Layouts,是因为ActionController有mixin了Layouts

module ActionController
  class Base < Metal

    MODULES = [
      # ..
      Helpers,
      # ...
      ActionView::Layouts,
       # ...
    ]

    MODULES.each do |mod|
      include mod
    end

而Layouts又mixin了ActionView::Rendering

module Layouts
  extend ActiveSupport::Concern

  include ActionView::Rendering

作用就是controller处理完业务逻辑后,调用layout -> rendering的render来生成view

顺提,helper module中所定义的方法是不能在controller中被直接调用的,从上面源码也可见,我们只是把_helpers给mixin到view上,而没有到controller上,真要在controller上调用的话,得用view_context