Rails 源码探索 Rails 路由的处理

ane · 发布于 2017年09月13日 · 最后由 jasl 回复于 2017年09月14日 · 849 次阅读
8972
本帖已被设为精华帖!

首先Rails4+,默认的routes是’config/routes.rb’。要想拆分routes,可以往paths里加入:

class Application < Rails::Application
  config.paths['config/routes.rb'].concat ['config/routes2.rb']
end

在rails3里,采用的是config.paths['config/routes’]

Http的处理

ActionDispatch::Routing::Mapper::HttpHelpers 中定义了在route里可以设置的5种HTTP via

get 'bacon', to: 'food#bacon’
post 'bacon', to: 'food#bacon’
patch 'bacon', to: 'food#bacon’
put 'bacon', to: 'food#bacon’
delete 'broccoli', to: 'food#broccoli’

最终调用的还是match方法

match 'path' => 'controller#action', via: patch
match 'path', to: 'controller#action', via: :post
match 'path', 'otherpath', on: :member, via: :together

所以,直接写成match方法,似乎,也算少执行一些代码。

Rails::Application::RoutesReloader

<:application::routesreloader:0x007ff4cd431038> Rails路由的加载的地方,所有被拆分的路由文件放在@paths

ActionDispatch::Routing::Mapper::Base

ActionDispatch::Routing::Mapper::Base里定义了路由的匹配规则,具体可以看看代码注释 ActionDispatch::Routing::RouteSet

RouteSet其实就是一个rack

通过initializer :add_routing_paths的初始器,指定了routes.rb的文件位置,

initializer :add_routing_paths do |app|
  routing_paths = self.paths["config/routes.rb"].existent

  if routes? || routing_paths.any?
    app.routes_reloader.paths.unshift(*routing_paths)
    app.routes_reloader.route_sets << routes
  end
end

可以在app.routes_reloader.paths中加入多个routes文件。

set_routes_reloader_hook初始化器,开始执行routes文件里的代码,

initializer :set_routes_reloader_hook do |app|
  reloader = routes_reloader
  reloader.execute_if_updated
  reloaders << reloader
  app.reloader.to_run do
    # We configure #execute rather than #execute_if_updated because if
    # autoloaded constants are cleared we need to reload routes also in
    # case any was used there, as in
    #
    #   mount MailPreview => 'mail_view'
    #
    # This means routes are also reloaded if i18n is updated, which
    # might not be necessary, but in order to be more precise we need
    # some sort of reloaders dependency support, to be added.
    require_unload_lock!
    reloader.execute
  end
end
initializer :build_middleware_stack do
  build_middleware_stack
end

alias :build_middleware_stack :app

def app
  @app || @app_build_lock.synchronize {
    @app ||= begin
      stack = default_middleware_stack
      config.middleware = build_middleware.merge_into(stack)
      config.middleware.build(endpoint)
    end
  }
end
def endpoint
  self.class.endpoint || routes
end
def routes
  @routes ||= ActionDispatch::Routing::RouteSet.new_with_config(config)
  @routes.append(&Proc.new) if block_given?
  @routes
end

简而言之,就是:build_middleware_stack 初始化一个RouteSet,作为第一个rack加入middleware,:add_routing_paths指定了路由的path,:set_routes_reloader_hook执行路由文件,装配路由。

处理request

当发送请求时,http服务器会开始调用middleware的call方法,最底层的RouteSet,会根据env生成一个 ActionDispatch::Request对象。

def call(env)
  req = make_request(env)
  req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
  @router.serve(req)
end

@router 是一个 ActionDispatch::Journey::Router对象,里面包含一个@routes,是ActionDispatch::Journey::Routes的对象,@routes里包含许多ActionDispatch::Journey::Route对象,每一个都是一条http的请求匹配模式。

那么 @router 和 RouteSet怎么关联的?

每一个http请求先包装成一个ActionDispatch::Routing::Mapper对象,指定了@scope_level@concerns@scope

mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
@set.add_route(mapping, ast, as, anchor)

每一个Mapper对象,包装成ActionDispatch::Routing::Mapper::Mapping对象。 RouteSet的初始化方法

def initialize(config = DEFAULT_CONFIG)
       self.named_routes = NamedRouteCollection.new
       self.resources_path_names = self.class.default_resources_path_names
       self.default_url_options = {}

       @config                     = config
       @append                     = []
       @prepend                    = []
       @disable_clear_and_finalize = false
       @finalized                  = false
       @env_key                    = "ROUTES_#{object_id}_SCRIPT_NAME".freeze

       @set    = Journey::Routes.new
       @router = Journey::Router.new @set
       @formatter = Journey::Formatter.new self
     end

此处的@set就是RouteSet对象,@set.add_route里调用了Journey::Routes.new.add_route

def add_route(name, mapping)
  route = mapping.make_route name, routes.length
  routes << route
  partition_route(route)
  clear_cache!
  route
end
def make_route(name, precedence)
  route = Journey::Route.new(name,
                    application,
                    path,
                    conditions,
                    required_defaults,
                    defaults,
                    request_method,
                    precedence,
                    @internal)

  route
end

在Journey::Routes.new.add_route里,调用mapping.make_route,make_route生成一条http请求的匹配模式,加入到Journey::Routes中 Journey::Routes << Journey::Route RouteSet.@set = Journey::Routes RouteSet.@router = Journey::Router.new @set 至此RouteSet 和 @router, Routes,Route关联起来了。

当调用RouteSet的call方法,就调用@router的serve方法

def serve(req)
  find_routes(req).each do |match, parameters, route|
    set_params  = req.path_parameters
    path_info   = req.path_info
    script_name = req.script_name

    unless route.path.anchored
      req.script_name = (script_name.to_s + match.to_s).chomp("/")
      req.path_info = match.post_match
      req.path_info = "/" + req.path_info unless req.path_info.start_with? "/"
    end

    req.path_parameters = set_params.merge parameters

    status, headers, body = route.app.serve(req)

    if "pass" == headers["X-Cascade"]
      req.script_name     = script_name
      req.path_info       = path_info
      req.path_parameters = set_params
      next
    end

    return [status, headers, body]
  end

  return [404, { "X-Cascade" => "pass" }, ["Not Found"]]
end

此处的route.app是make_route创建route是,传入的application,是一个 Routing::Endpoint类,这里是它的一个子类ActionDispatch::Routing::RouteSet::Dispatcher对象

def serve(req)
  params     = req.path_parameters
  controller = controller req
  res        = controller.make_response! req
  dispatch(controller, params[:action], req, res)
rescue ActionController::RoutingError
  if @raise_on_name_error
    raise
  else
    return [404, { "X-Cascade" => "pass" }, []]
  end
end

此时 make_response定义在 ActionController::Base中,生成ActionDispatch::Response对象

dispatch调用到对应的action,返回rack的返回值

def dispatch(name, request, response) #:nodoc:
  set_request!(request)
  set_response!(response)
  process(name)
  request.commit_flash
  to_a
end
def rack_response(status, header)
  if NO_CONTENT_CODES.include?(status)
    [status, header, []]
  else
    [status, header, RackBody.new(self)]
  end
end

此处的self就是ActionDispatch::Response对象。

至此,路由的配置,和处理一个http请求的过程就完整了。

共收到 3 条回复
1107 jasl 将本帖设为了精华贴 09月13日 18:34
1107

对了 路由底层还隐藏着一个生成可视化路由状态迁移图的方法,可以挖挖,然后就可以清楚的了解URL是如何进行匹配的了

8972
1107jasl 回复

恩,我看过你之前写的那个,那老外的blog也看过

1107
8972ane 回复

嗯 journey 那层是你文章比更往底层的东西了,和 Rails 关系也不太大了

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册