Skip to content

Latest commit

 

History

History
178 lines (157 loc) · 6.04 KB

07.helpers.md

File metadata and controls

178 lines (157 loc) · 6.04 KB

An Http Request Through Rails

07. Helpers

Rails的Helper主要是为View提供方法,使得与前端相关的复杂的逻辑无需出现在Template的代码当中。它的实现其实非常简单,主要代码集中在两个文件中,首先是AbstractController::Helpers,定义在actionpack-3.2.13/lib/abstract_controller/helpers.rb中,初始化模块的代码在included语句中:

included do
  class_attribute :_helpers
  self._helpers = Module.new

  class_attribute :_helper_methods
  self._helper_methods = Array.new
end

当一个模块include了AbstractController::Helpers时,它会定义两个属性,_helpers是包含了所有helper方法的模块,而_helper_methods是记录了所有helper方法名字的数组。在Rails中,include这个模块的正是ActionController::Helpers,定义在actionpack-3.2.13/lib/action_controller/metal/helpers.rb,而这个模块就是被所有Controller包含的模块。

当任何一个类继承了这个模块,例如ApplicationController及其子类,inherited方法被执行:

# 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.class_eval { default_helper_module! unless anonymous? }
  super
end

从代码中可见,每一个子类继承父类的时候,会将父类的_helper包含进一个新的Module,然后作为自己的_helper,相当于完成了继承的工作。然后调用了default_helper_module!方法:

def default_helper_module!
  module_name = name.sub(/Controller$/, '')
  module_path = module_name.underscore
  helper module_path
rescue MissingSourceFile => e
  raise e unless e.is_missing? "helpers/#{module_path}_helper"
rescue NameError => e
  raise e unless e.missing_name? "#{module_name}Helper"
end

这个方法想将类名中的'Controller'一词去除,然后调用helper方法:

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

  _helpers.module_eval(&block) if block_given?
end

helper本身的功能很强,既可以传入Helper类的名字也可以传入block也可以二者都传,这得意于ActionSupport::Dependencies的强大,这里看modules_for_helpers的实现:

# Overwrite modules_for_helpers to accept :all as argument, which loads
# all helpers in helpers_path.
#
# ==== Parameters
# * <tt>args</tt> - A list of helpers
#
# ==== Returns
# * <tt>array</tt> - A normalized list of modules for the list of helpers provided.
def modules_for_helpers(args)
  args += all_application_helpers if args.delete(:all)
  super(args)
end

这个方法定义在ActionController::Helpers,仅仅处理了:all参数,主要定义在AbstractController::Helpers内:

def modules_for_helpers(args)
  args.flatten.map! do |arg|
    case arg
    when String, Symbol
      file_name = "#{arg.to_s.underscore}_helper"
      require_dependency(file_name, "Missing helper file helpers/%s.rb")
      file_name.camelize.constantize
    when Module
      arg
    else
      raise ArgumentError, "helper must be a String, Symbol, or Module"
    end
  end
end

可以看到这里通过Rails的Convention生成了Helper模块所在的文件,然有调用require_dependency在autoload path中查询并且require(或者load)了这个文件然后调用camelize将名字变成驼峰命名,再查询出对应的Helper模块。

接着就是调用add_template_helper完成了添加Helper方法的过程:

# Makes all the (instance) methods in the helper module available to templates
# rendered through this controller.
#
# ==== Parameters
# * <tt>module</tt> - The module to include into the current helper module
#   for the class
def add_template_helper(mod)
  _helpers.module_eval { include mod }
end

可以看到,这里将查询出来的Helper模块include在当前类的_helpers中。

至于通过block来定义helper方法更加简单:

_helpers.module_eval(&block) if block_given?

最后,按上一章节所说的,在view_context_class中取出_helpers

def view_context_class
  @view_context_class ||= begin
    routes  = _routes  if respond_to?(:_routes)
    helpers = _helpers if respond_to?(:_helpers)
    ActionView::Base.prepare(routes, helpers)
  end
end

然后在新创建的ActionView::Base的子类里将helpers模块包含进去,这样在View中就能自由使用Helper方法了。

# This method receives routes and helpers from the controller
# and return a subclass ready to be used as view context.
def prepare(routes, helpers)
  Class.new(self) do
    if routes
      include routes.url_helpers
      include routes.mounted_helpers
    end

    if helpers
      include helpers
      self.helpers = helpers
    end
  end
end

对于定义在Controller中的方法,我们也可以用helper_method方法将其也同时声明为helper:

# Declare a controller method as a helper. For example, the following
# makes the +current_user+ controller method available to the view:
#   class ApplicationController < ActionController::Base
#     helper_method :current_user, :logged_in?
#
#     def current_user
#       @current_user ||= User.find_by_id(session[:user])
#     end
#
#      def logged_in?
#        current_user != nil
#      end
#   end
#
# In a view:
#  <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%>
#
# ==== Parameters
# * <tt>method[, method]</tt> - A name or names of a method on the controller
#   to be made available on the view.
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)
        controller.send(%(#{meth}), *args, &blk)
      end
    ruby_eval
  end
end

这里仅仅只是采用了controller.send的方法来实现这个调用,因此这样做也可以支持调用private的方法。