Skip to content

Latest commit

 

History

History
469 lines (394 loc) · 18 KB

05.controller.md

File metadata and controls

469 lines (394 loc) · 18 KB

An Http Request Through Rails

05. Controller

当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-Cascadepass即可处理下一个搜索结果。

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.rbprocess的实现是:

# 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_pathhash_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::Dispatcherdispatch方法执行前就已经执行,因此这里的@_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

可以看到,当存在etaglast_modifiedcache_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的源码解析。