当journey找到请求对应的route时,将会调用之前传入的app
参数,这个参数其实并不一定是一个action,proc,任何rack程序,包括Sinatra在内都是有可能的。不过这里还是以解析action为主,毕竟再没有哪个Ruby的web框架有Rails这么功能强大的。
下面就是这个app
的入口,这段代码并非Rails代码,而是Rails下一个子项目,journey的代码,定义在journey-1.0.4/lib/journey/router.rb
中:
def call env
env['PATH_INFO'] = Utils.normalize_path env['PATH_INFO']
find_routes(env).each do |match, parameters, route|
script_name, path_info, set_params = env.values_at('SCRIPT_NAME',
'PATH_INFO',
@params_key)
unless route.path.anchored
env['SCRIPT_NAME'] = (script_name.to_s + match.to_s).chomp('/')
env['PATH_INFO'] = Utils.normalize_path(match.post_match)
end
env[@params_key] = (set_params || {}).merge parameters
status, headers, body = route.app.call(env)
if 'pass' == headers['X-Cascade']
env['SCRIPT_NAME'] = script_name
env['PATH_INFO'] = path_info
env[@params_key] = set_params
next
end
return [status, headers, body]
end
return [404, {'X-Cascade' => 'pass'}, ['Not Found']]
end
从代码里可以看见,事实上可以搜索多个匹配的routes进行处理,只要headers中X-Cascade
为pass
即可处理下一个搜索结果。
当app
是一个ActionDispatch::Routing::RouteSet::Dispatcher
实例时,call方法将进入下列代码,该代码定义在actionpack-3.2.13/lib/action_dispatch/routing/route_set.rb
:
def call(env)
params = env[PARAMETERS_KEY]
prepare_params!(params)
# Just raise undefined constant errors if a controller was specified as default.
unless controller = controller(params, @defaults.key?(:controller))
return [404, {'X-Cascade' => 'pass'}, []]
end
dispatch(controller, params[:action], env)
end
可以看到,这里先取出了参数,然后送入了prepare_params!
方法中:
def prepare_params!(params)
normalize_controller!(params)
merge_default_action!(params)
split_glob_param!(params) if @glob_param
end
可以明显看到代码分成了三个部分:
def normalize_controller!(params)
params[:controller] = params[:controller].underscore if params.key?(:controller)
end
def merge_default_action!(params)
params[:action] ||= 'index'
end
def split_glob_param!(params)
params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) }
end
这三个方法定义都非常易懂,normalize_controller!
对:controller
项调用了underscore
方法,merge_default_action!
方法为:action
参数增加了默认值'index'
,split_glob_param!
将@glob_param
中的参数取出,按照/
分割后再unescape即可。
随后将调用controller方法找出匹配的controller:
# If this is a default_controller (i.e. a controller specified by the user)
# we should raise an error in case it's not found, because it usually means
# a user error. However, if the controller was retrieved through a dynamic
# segment, as in :controller(/:action), we should simply return nil and
# delegate the control back to Rack cascade. Besides, if this is not a default
# controller, it means we should respect the @scope[:module] parameter.
def controller(params, default_controller=true)
if params && params.key?(:controller)
controller_param = params[:controller]
controller_reference(controller_param)
end
rescue NameError => e
raise ActionController::RoutingError, e.message, e.backtrace if default_controller
end
首先取出controller参数,然后调用controller_reference
方法实际找出controller。controller_reference
的实现是这样的:
def controller_reference(controller_param)
controller_name = "#{controller_param.camelize}Controller"
unless controller = @controllers[controller_param]
controller = @controllers[controller_param] =
ActiveSupport::Dependencies.reference(controller_name)
end
controller.get(controller_name)
end
首先是利用Rails的约定确定类名,然后调用ActiveSupport::Dependencies.reference
方法获得ActiveSupport::Dependencies::ClassCache
对象,最后调用这个Cache对象的get
方法得到最终结果。
ActiveSupport::Dependencies::ClassCache
是Rails设计的可以将字符串转换成类同时又把转换结果缓存起来的类。对于ActiveSupport::Dependencies.reference
方法的调用,如果传入的参数是类的话将会直接建立缓存后将类放回,否则就得到一个ClassCache
对象,然后对它调用get
方法时,get
会调用Inflector.constantize
方法获取类名对应的实际类对象,如果找不到将调用const_missing
方法去根据Rails约定在autoload_paths
中搜索并加载文件,之后再次试图获取。这里由于都是ActiveSupport
的实现,不再解析代码。
随后,将获得的Controller类对象和:action
参数以及Rails env传入dispatch
方法:
def dispatch(controller, action, env)
controller.action(action).call(env)
end
可以看到,首先在所在的controller类中搜索对应的action,方法就是ActionController::Metal
类下的action
方法,定义在actionpack-3.2.13/lib/action_controller/metal.rb
中:
# Return a rack endpoint for the given action. Memoize the endpoint, so
# multiple calls into MyController.action will return the same object
# for the same action.
#
# ==== Parameters
# * <tt>action</tt> - An action name
#
# ==== Returns
# * <tt>proc</tt> - A rack application
def self.action(name, klass = ActionDispatch::Request)
middleware_stack.build(name.to_s) do |env|
new.dispatch(name, klass.new(env))
end
end
这里一开始再次提到了middleware_stack,是指专门为Controller指定的Middleware,其定义的代码就在上面:
class_attribute :middleware_stack
self.middleware_stack = ActionController::MiddlewareStack.new
def self.inherited(base) #nodoc:
base.middleware_stack = self.middleware_stack.dup
super
end
# Adds given middleware class and its args to bottom of middleware_stack
def self.use(*args, &block)
middleware_stack.use(*args, &block)
end
# Alias for middleware_stack
def self.middleware
middleware_stack
end
这里ActionController::MiddlewareStack
是之前用的ActionDispatch::MiddlewareStack
的子类:
# Extend ActionDispatch middleware stack to make it aware of options
# allowing the following syntax in controllers:
#
# class PostsController < ApplicationController
# use AuthenticationMiddleware, :except => [:index, :show]
# end
#
class MiddlewareStack < ActionDispatch::MiddlewareStack
class Middleware < ActionDispatch::MiddlewareStack::Middleware
def initialize(klass, *args, &block)
options = args.extract_options!
@only = Array(options.delete(:only)).map(&:to_s)
@except = Array(options.delete(:except)).map(&:to_s)
args << options unless options.empty?
super
end
def valid?(action)
if @only.present?
@only.include?(action)
elsif @except.present?
!@except.include?(action)
else
true
end
end
end
def build(action, app=nil, &block)
app ||= block
action = action.to_s
raise "MiddlewareStack#build requires an app" unless app
middlewares.reverse.inject(app) do |a, middleware|
middleware.valid?(action) ?
middleware.build(a) : a
end
end
end
可以看到,主要是增添了:only
和:except
的功能。每次请求时,middlware stack都会被重新build一遍,每次build时会把:only
和:except
考虑进去,也就是说,不同的action都可能build出不同的结果。
至于middleware stack的endpoint,就是传入build
方法的block:
new.dispatch(name, klass.new(env))
这里将创建Controller对象的实例,初始化时会初始化多个实例变量(由于Controller类的module较多,这里不一一解释),然后调用实例的dispatch
方法:
def dispatch(name, request)
@_request = request
@_env = request.env
@_env['action_controller.instance'] = self
process(name)
to_a
end
这里process
将负责搜索并调用指定的action,再将结果用to_a
返回。
其中process
实现在ActionController
的父类AbstractController
中,定义位置在actionpack-3.2.13/lib/abstract_controller/base.rb
,process
的实现是:
# Calls the action going through the entire action dispatch stack.
#
# The actual method that is called is determined by calling
# #method_for_action. If no method can handle the action, then an
# ActionNotFound error is raised.
#
# ==== Returns
# * <tt>self</tt>
def process(action, *args)
@_action_name = action_name = action.to_s
unless action_name = method_for_action(action_name)
raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}"
end
@_response_body = nil
process_action(action_name, *args)
end
这里分为两步,首先是调用method_for_action
找出指定的action
方法:
# Takes an action name and returns the name of the method that will
# handle the action. In normal cases, this method returns the same
# name as it receives. By default, if #method_for_action receives
# a name that is not an action, it will look for an #action_missing
# method and return "_handle_action_missing" if one is found.
#
# Subclasses may override this method to add additional conditions
# that should be considered an action. For instance, an HTTP controller
# with a template matching the action name is considered to exist.
#
# If you override this method to handle additional cases, you may
# also provide a method (like _handle_method_missing) to handle
# the case.
#
# If none of these conditions are true, and method_for_action
# returns nil, an ActionNotFound exception will be raised.
#
# ==== Parameters
# * <tt>action_name</tt> - An action name to find a method name for
#
# ==== Returns
# * <tt>string</tt> - The name of the method that handles the action
# * <tt>nil</tt> - No method name could be found. Raise ActionNotFound.
def method_for_action(action_name)
if action_method?(action_name) then action_name
elsif respond_to?(:action_missing, true) then "_handle_action_missing"
end
end
这里同样也分两步,首先调用action_method?
搜索action对应的方法,如果不存在的话,如果定义了action_missing
方法,则调用_handle_action_missing
方法处理,在该方法中将会调用到action_missing
方法处理错误。
这里主要关心action_method?
的定义:
# Returns true if the name can be considered an action because
# it has a method defined in the controller.
#
# ==== Parameters
# * <tt>name</tt> - The name of an action to be tested
#
# ==== Returns
# * <tt>TrueClass</tt>, <tt>FalseClass</tt>
#
# :api: private
def action_method?(name)
self.class.action_methods.include?(name)
end
这里就是将所有action_methods
列出后确定搜索的action是否在它们之间即可:
# A list of method names that should be considered actions. This
# includes all public instance methods on a controller, less
# any internal methods (see #internal_methods), adding back in
# any methods that are internal, but still exist on the class
# itself. Finally, #hidden_actions are removed.
#
# ==== Returns
# * <tt>array</tt> - A list of all methods that should be considered actions.
def action_methods
@action_methods ||= begin
# All public instance methods of this class, including ancestors
methods = (public_instance_methods(true) -
# Except for public instance methods of Base and its ancestors
internal_methods +
# Be sure to include shadowed public instance methods of this class
public_instance_methods(false)).uniq.map { |x| x.to_s } -
# And always exclude explicitly hidden actions
hidden_actions.to_a
# Clear out AS callback method pollution
methods.reject { |method| method =~ /_one_time_conditions/ }
end
end
这里我们看到了action_method的界定过程,public_instance_methods
(包括ancestors里的方法)- internal_methods
+ public_instance_methods
(不包括ancestors里的方法),去重后再去掉hidden_actions
里的方法即可。这里internal_methods
指的是:
# A list of all internal methods for a controller. This finds the first
# abstract superclass of a controller, and gets a list of all public
# instance methods on that abstract class. Public instance methods of
# a controller would normally be considered action methods, so methods
# declared on abstract classes are being removed.
# (ActionController::Metal and ActionController::Base are defined as abstract)
def internal_methods
controller = self
controller = controller.superclass until controller.abstract?
controller.public_instance_methods(true)
end
即从当前controller出发,不断搜索父类直到找到第一个被标记成abstract的类,取出它的public_instance_methods
。
也就是说,这里其实就是将当前类以及那些不是abstract的父类的public_instance_methods
,再加上这个类自身定义的public_instance_methods
方法(可能覆盖掉其祖先的同名方法),最后再减去hidden_actions
这个专门用于被子类覆盖的方法。
最后还要再抹去名字中包含_one_time_conditions
的方法,这些方法是Callback
类定义出来的内部临时方法。
之后还会有一些子类对action_methods
添加额外的规则,比如AbstractController::UrlFor
会给action_methods
减去所有_routes.named_routes.helper_names
,即由它创建出的_url
,_path
,hash_for_
方法集。
然后,回到process
方法,如果action
确实可以找到的话,将调用process_action
方法:
# Call the action. Override this in a subclass to modify the
# behavior around processing an action. This, and not #process,
# is the intended way to override action dispatching.
#
# Notice that the first argument is the method to be dispatched
# which is *not* necessarily the same as the action name.
def process_action(method_name, *args)
send_action(method_name, *args)
end
# Actually call the method associated with the action. Override
# this method if you wish to change how action methods are called,
# not to add additional behavior around it. For example, you would
# override #send_action if you want to inject arguments into the
# method.
alias send_action send
虽然send_action
会被子类增加各种额外代码,但是核心代码正如这里所示,是send
方法的alias
,因此这里就会直接调用send
方法来调用action
了。
当action执行后,to_a
方法将被调用,其实现是:
def to_a
response ? response.to_a : [status, headers, response_body]
end
在一个完整的Rails应用中,response
总是返回一个ActionDispatch::Response
对象,这个是由ActionController::RackDelegation
模块在覆盖dispatch
方法时赋值的,这个方法定义在actionpack-3.2.13/lib/action_controller/metal/rack_delegation.rb
:
def dispatch(action, request, response = ActionDispatch::Response.new)
@_response ||= response
@_response.request ||= request
super(action, request)
end
这段代码在ActionDispatch::Routing::RouteSet::Dispatcher
的dispatch
方法执行前就已经执行,因此这里的@_response
对象总是会被赋值的。因此这里会调用到@_response
对象的to_a
的方法,这个方法定义在actionpack-3.2.13/lib/action_dispatch/http/response.rb
:
def to_a
assign_default_content_type_and_charset!
handle_conditional_get!
@header[SET_COOKIE] = @header[SET_COOKIE].join("\n") if @header[SET_COOKIE].respond_to?(:join)
if [204, 304].include?(@status)
@header.delete CONTENT_TYPE
[@status, @header, []]
else
[@status, @header, self]
end
end
这里首先执行了assign_default_content_type_and_charset
方法:
def assign_default_content_type_and_charset!
return if headers[CONTENT_TYPE].present?
@content_type ||= Mime::HTML
@charset ||= self.class.default_charset
type = @content_type.to_s.dup
type << "; charset=#{@charset}" unless @sending_file
headers[CONTENT_TYPE] = type
end
这段代码就是将Mime和Charset设置进Content-Type
。
handle_conditional_get!
则定义在actionpack-3.2.13/lib/action_dispatch/http/cache.rb
中的ActionDispatch::Http::Cache::Request
中:
def handle_conditional_get!
if etag? || last_modified? || !@cache_control.empty?
set_conditional_cache_control!
end
end
可以看到,当存在etag
或last_modified
或cache_control
的时候,调用set_conditional_cache_control!
方法:
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
NO_CACHE = "no-cache".freeze
PUBLIC = "public".freeze
PRIVATE = "private".freeze
MUST_REVALIDATE = "must-revalidate".freeze
def set_conditional_cache_control!
return if self[CACHE_CONTROL].present?
control = @cache_control
if control.empty?
headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL
elsif control[:no_cache]
headers[CACHE_CONTROL] = NO_CACHE
else
extras = control[:extras]
max_age = control[:max_age]
options = []
options << "max-age=#{max_age.to_i}" if max_age
options << (control[:public] ? PUBLIC : PRIVATE)
options << MUST_REVALIDATE if control[:must_revalidate]
options.concat(extras) if extras
headers[CACHE_CONTROL] = options.join(", ")
end
end
可以看到主要是设置Cache-Control
。
随后设置Set-Cookie
,如果Set-Cookie
实现join
方法的话,即调用该方法使数组按照\n
拼合在一起。
最后,如果返回值是204或是304,则去除返回值中的body部分,否则,将自身作为body返回。
至此,Controller执行彻底结束,下面将开始对View的源码解析。