Rails 源码探索 Rails 路由的处理

ane · 2017年09月13日 · 最后由 jasl 回复于 2017年09月14日 · 5591 次阅读
本帖已被管理员设置为精华贴

首先 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

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 请求的过程就完整了。

jasl 将本帖设为了精华贴。 09月13日 18:34

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

jasl 回复

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

ane 回复

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

zhuoerri Rails 路由 Journey 与 有限状态自动机 提及了此话题。 03月04日 13:01
需要 登录 后方可回复, 如果你还没有账号请 注册新账号