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的方法。