经过一番试验和考虑...一,我尝试了一些思维导图工具(MindMapper,FREEMIND),但我始终没有找到一种好的方式将自己学习Rails
源代码的思路表述出来,就此作罢(顺便问问,有研究思维导图的同学么?能否推荐两个自己觉得用起来比较顺手的工具)。二,不再打算整理代码运行顺序图,对
不熟悉Rails源代码的同学们来说,这个图可能的确没什么帮助,甚至会把人搞晕。我现在打算从Rails源代码功能点的角度出发,根据具体功能点,结合
Rails源代码进行学习,整理,总结。如果某些源代码比较复杂,牵涉类比较繁多,我仍然打算整理一个类图,从一个高的层次了解系统内部对象的关系。
前面三篇文章,我们看到了Rails启动的大致功能和流程,包括初始化多种环境变量,初始化Route表,启动Web服务器开始侦听客户端请求。。。那么
接下来,当然是开门迎客,等待客户端(浏览器)的请求,并进行处理,最终将结果返回客户端(浏览器)呈现。那么熟悉Rails的同学都知道,首先,
Rails必须根据客户端的一个请求,决定将要执行哪个Controller的哪个Action,这也是本文的主要目的。
首先,我们先来看一看Rails通过客户端请求,查找Controller的大致流程图
(一)生成DisapatchServlet实例,开始服务吧
源代码:gems/rails-2.0.2/lib/webrick_server.rb
在第一篇文章,讲解Rails的启动时,我提到webrick_server.rb中定义了DisapatchServlet类,此类启动了WEBrick,开始侦听客户端请求。当有客户端请求到达时,会生成一个DispatchServlet实例,具体代码如下:
class DispatchServlet < WEBrick::HTTPServlet::AbstractServlet
def initialize(server, options) #:nodoc:
@server_options = options
@file_handler = WEBrick::HTTPServlet::FileHandler.new(server, options[:server_root])
# Change to the RAILS_ROOT, since Webrick::Daemon.start does a Dir::cwd("/")
# OPTIONS['working_directory'] is an absolute path of the RAILS_ROOT, set in railties/lib/commands/servers/webrick.rb
Dir.chdir(OPTIONS['working_directory']) if defined?(OPTIONS) && File.directory?(OPTIONS['working_directory'])
super
end
...
end
初始化参数server是web服务器的类型,当然,在我的环境中是WEBRick::HTTPServer。option是一个hash,包含了一些列的环境参数,这里,我将一些比较重要的参数罗列出来:
名称
|
类型
|
参考值
|
port |
Fixnum |
3000 |
ip |
String |
0.0.0.0(因为我是本机操作) |
environment |
String |
development |
charset |
String |
UTF-8 |
working_directory |
String |
D:\Project\Ruby\blog |
初始化中,首先将option参数赋DispatchServlet的@server_options变量,然后生成一个FileHandler对象,这
个对象的具体作用马上会提到。紧接着将Rails的工作目录设置为“working_directory”,也就是前面文章提到过的RAIL_ROOT。
至此,DsipatchServlet的初始化工作完成了。WEBRick会执行此Servlet的service方法。
(二)是否存在相应html
源代码:gems/rails-2.0.2/lib/webrick_server.rb
第一步生成了Servlet实例,并且,开始执行service方法,我们先来看看service方法的具体内容:
class DispatchServlet < WEBrick::HTTPServlet::AbstractServlet
def service(req, res) #:nodoc:
unless handle_file(req, res)
begin
REQUEST_MUTEX.lock unless ActionController::Base.allow_concurrency
unless handle_dispatch(req, res)
raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found."
end
ensure
unless ActionController::Base.allow_concurrency
REQUEST_MUTEX.unlock if REQUEST_MUTEX.locked?
end
end
end
end
...
end
此方法算是处理一个Request的最高层次描述,首先是方法handle_file。这个方法会使用初始化生成的FileHandler对象,查找针对客户端请求的path,在RAILS_ROOT/public目录下是否存在相应的html。例如客户端的请求是http://localhost:3000/posts
,那么首先Rails就使用FileHandler查找在public根目录下面是否存在posts.html,如果存在的话,则直接向客户端呈现这个html,如果不存在,OK,开始寻找Controller吧。
(这里,值得一提是并发控制,默认情况下,Rails只允许一次dispatch一个request,当然,我们可以通过在程序配置文件中设置ActionController::Base.allow_concurrency来改变这个默认的行为。)
(我想你应该知道很多Rails书籍提到过,如果你在routes.rb中通过map.root
:controller=>'posts'的方式,使得当用户通过http://www.yoursite.com
访问站点时,显示相应的功能页面。但是你必须把public下的index.html删除掉,就是这个原因。)
(handle_file源代码不列出,因为他十分简单,只是调用FileHandler的相应方法,而WEBRick暂不在研究范围内。)
(三)开始Dispatch吧
源代码:gems/rails-2.0.2/lib/webrick_server.rb
gems/actionpack-2.0.2/lib/action_controller/dispatcher.rb
第二步说了,如果没有相应的html存在的话,Rails将执行Dispatch过程。我们先来看一看handle_dispatch方法:
def handle_dispatch(req, res, origin = nil) #:nodoc:
data = StringIO.new
Dispatcher.dispatch(
CGI.new("query", create_env_table(req, origin), StringIO.new(req.body || "")),
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS,
data
)
...
end
这里,可以看到Dispatch的主角Dispatcher对象开始登场了。要执行dispatch,首先生成一个CGI对象(默认CGI类型是
“query”,并且将环境配置传递给CGI对象,包括:主机名称,查询字符串,字符集,Path信息...等,以及默认的Session管理方式),其
中的data表示对用户的返回数据(StringIO请参考相应的API)。然后执行Dispatcher的类方法dispatch。此方法内容如下:
class Dispatcher
class << self
# Backward-compatible class method takes CGI-specific args. Deprecated
# in favor of Dispatcher.new(output, request, response).dispatch.
def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
new(output).dispatch_cgi(cgi, session_options)
end
...
end
此类方法将生成一个Dispatcher实例,并调用其dispatch_cgi实例方法(从前面的方法调用,我想不难看出每一个参数是什么)。我们继续接着看dispatch_cgi方法:
def dispatch_cgi(cgi, session_options)
if cgi ||= self.class.failsafe_response(@output, '400 Bad Request') { CGI.new }
@request = CgiRequest.new(cgi, session_options)
@response = CgiResponse.new(cgi)
dispatch
end
rescue Exception => exception
failsafe_rescue exception
end
前面也有request和reponse,这里又生成了一个request和response。我是这样理解的,前面handle_dispatch接收
的req和res是“原生”的对象----WEBRick::HTTPRequest和WEBRick::HTTPResponse(是WEBRick和
Rails的通讯方式),而这里的request和response是CgiRequest和CgiResponse对象,是针对Dispatch的通讯
(CgiRequest和CgiResponse的细节这里先略过,我们看看主流程)。有了request和response对象,真正的
dispatch过程开始了:
def dispatch
run_callbacks :before
handle_request
rescue Exception => exception
failsafe_rescue exception
ensure
先来看一看run_callbacks:
def run_callbacks(kind, enumerator = :each)
callbacks[kind].send!(enumerator) do |callback|
case callback
when Proc; callback.call(self)
when String, Symbol; send!(callback)
when Array; callback[1].call(self)
else raise ArgumentError, "Unrecognized callback #{callback.inspect}"
end
end
end
其中的callbacks(hash)是Dispatcher的类属性,用来执行一些dispatch前,后,准备的工作。这里,我们直接看看他的值
{:before=>[:reload_application,:prepare_application],:after=>[:flush_logger,:
cleanup_application],:prepare=>[:activerecord_instantiate_observers,"Proc"]}。
就这里而言,我们要执行所有:before的callback,不一一列出,只看其中一个:
def reload_application
if Dependencies.load?
Routing::Routes.reload
self.unprepared = true
end
end
这段代码揭示了在程序运行时,我们改动了routes.rb中的路由信息后,下一次request马上就能生效的原理。另外
prepare_application的功能是require我们熟悉的Controller/application.rb,并且验证
ActiveRecord的数据库连接是否正常(当然,你需要使用AR框架的话)。好了,这里稍微偏离了主线,接下来,让我们回到dispatch方法
中,看看下面的调用handle_request:
def handle_request
@controller = Routing::Routes.recognize(@request)
@controller.process(@request, @response).out(@output)
end
上面的代码非常直观,首先通过Routing系统,根据客户端的request找到相应的controller,然后执行并且将返回数据写入到@output中(这也是我前面提到的那个StringIO对象)。至于如何具体找到controller的,进入下一步吧。
(四)寻找controller
源代码:/actionpack-2.0.2/lib/action_controller/routing.rb
从前面的方法调用中,我们看出寻找controller的入口是RouteSet对象的recognize方法(还记得Routing::Routes是
一个RouteSet的对象实例吗?要理解Rails中的Routing子系统,我在第二篇文章中整理的那张类图十分重要!)。下面看看此方法的具体内
容:
def recognize(request)
params = recognize_path(request.path, extract_request_environment(request))
request.path_parameters = params.with_indifferent_access
"#{params[:controller].camelize}Controller".constantize
end
首先调用recognize_path方法,其中request.path是客户端请求的路径(比如:如果客户端访问地址是http://localhost:3000/posts
,
那么此参数就是/posts,extract_request_environment(request)方法只是得到请求的http方法(get,
post,put,delete),当然,此方法返回的结果params便是我们的Controller和Action。我们知道,Routing系统通
过path和http verb就可以确定应该使用哪个Controller的哪个Action,下面看看他是怎么做到的吧:
def recognize_path(path, environment={})
routes.each do |route|
result = route.recognize(path, environment) and return result
end
allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } }
if environment[:method] && !HTTP_METHODS.include?(environment[:method])
raise NotImplemented.new(*allows)
elsif !allows.empty?
raise MethodNotAllowed.new(*allows)
else
raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}"
end
end
如果你看过我的第二,三篇文章,我想你应该知道这里的routes数组就是Routing系统中庞大的路由表,routes数组的元素是Route对象,里面记录了相应的path
pattern对应于哪个Controller的哪个Action方法。这里,通过path,和environment(http
verb)参数,调用每一个Route对象的recognize方法,如果找到相应的Controller,则返回;如果未找到,则进行接下来的错误处理,这里我们可以看到很熟悉的“No
route
matches...”。那我们再来看看Route对象是如何通过Path和environment来识别Controller的,先来看看Route类的recognize方法:
def recognize(path, environment={})
write_recognition
recognize path, environment
end
在recognize方法中调用recognize方法?传说中的死循环?呵呵。当然不是了,看完write_recognition你就知道是怎么回事了:
def write_recognition
# Create an if structure to extract the params from a match if it occurs.
body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"
# Build the method declaration and compile it
method_decl = "def recognize(path, env={})\n#{body}\nend"
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
method_decl
end
这里,Rails利用instance_eval重写了该对象(此Route对象)的recognize方法(可不是override哦)。完成重写后,
将再次调用recognize方法,此时,这个方法已经是动态生成的了。那么现在我们来看看这个动态方法长什么样,这里,我假设客户端访问url为http://localhost:3000/posts
,并且,在routes.rb中,我们已经通过map.resources
:posts建立了一系列针对posts的Route,其中当然包括“Get
/posts/”(对应的Controller是posts,对应的Action是index)。那么在调用这个Route对象的write_recognition方法时,将会动态生成如下代码:
def recognize(path, env={})
if (match = /\A\/posts\/?\Z/.match(path)) && conditions[:method] === env[:method]
params = parameter_shell.dup
params
end
end
逻辑很简单,只是通过正则表达式来判断此Route的pattern是否与客户端请求的path一致,并且http
verb也匹配,如果是的话,则将parameter_shell方法的结果dup出来,并且返回。
(注意,这里if子句的条件是通过recognition_conditions方法根据不同Route的不同condition动态生成的。因此针对每
个Route,此条件都不同。另外recognition_extraction方法我一直没搞懂他干什么用的-_-!)
这里,我们还是看一看parameter_shell方法:
def parameter_shell
@parameter_shell ||= returning({}) do |shell|
requirements.each do |key, requirement|
shell[key] = requirement unless requirement.is_a? Regexp
end
end
end
无非就是将requirements(包括controller和action)塞到shell数组,然后返回。
好啦,针对路由表中的每一个Route,调用其recognize方法,知道找到匹配的Route,然后将结果(controller和action数
组)返回(如未找到匹配的,则进行错误处理),接下来,我们的思路得回到RouteSet对象的recognize方法,最终,使用#{params[:
controller].camelize}Controller".constantize,将controller参数转换为首字符大写的形式,并且
加上“Controller”字符串,最终将整个字符串(“PostsController”),转换为一个常量(PostsController,表示
控制器对象),并且,调用此Controller的process方法(此方法其实是ActionController::Base的类方法),接下来的
事,后续文章会继续分析。
(这个过程也揭示了Rails中的“约定甚于配置”的精髓)
(就目前而言,我们都在Rails的主线上游走,我觉得下篇文章,应该暂停一下,来看一些细节的东西,已达到更深入了解Rails的目的)
相关推荐
在Ruby on Rails框架中,`Rails 2.0.2`是一个较早的版本,而分页功能在那个时期并不像现在的Rails应用那样内置在框架内。为了实现分页,开发者通常需要安装并使用第三方插件,比如"will_paginate"。这个插件允许你在...
在本文中,我们将深入探讨如何在您的计算机上安装和配置Ruby 1.8.6、Rails 2.0.2、RadRails 0.7.2 IDE以及MySQL数据库。这是一个适用于初学者和有一定经验的开发者的技术指南,旨在帮助您创建一个稳定的开发环境,...
在命令行工具中执行以下命令来安装Rails 2.0.2: ```bash gem install rails -v 2.0.2 ``` 该命令会从Ruby的包管理库中查找并安装指定版本的Rails。安装完成后,同样通过`rails -v`来检查是否安装成功。 **知识点...
`rails-documentation-2-0-2.chm` 文件详细涵盖了这些概念,包含了关于Rails 2.0.2的API参考、教程和指南。通过仔细阅读和实践,开发者能够深入理解Rails的工作原理,并有效地开发出高效、可维护的Web应用。
rails-hackernews-reddit-producthunt-clone, 黑客 news/reddit/social 链接分享网站 用 Rails 构建 Rails 上的 Reddit-Hackernews-ProductHunt克隆演示 这是一个 readme.md的Ruby on Rails 应用程序,模仿了 Hacker...
《Rails Exporter 源码解析》 Rails Exporter 是一个用于 Rails 应用程序的开源工具,主要用于数据导出功能。源码分析将帮助我们深入理解其内部工作原理,以便更好地利用它来优化我们的应用。 一、Rails 框架基础 ...
RoR遵循MVC(Model-View-Controller)架构模式,旨在简化Web应用开发,降低开发者的工作负担,提高开发效率。该框架的核心设计理念包括“Don’t Repeat Yourself”(DRY)和“Convention Over Configuration”(CoC...
gem 'rails-controller-testing' 然后执行: $ bundle 或将其自己安装为: $ gem install rails-controller-testing 规范 参见 。 从3.5.0版开始,rspec-rails会自动与该gem集成。 将gem添加到您的Gemfile就足够...
rails3-mongoid-devise, 示例 Rails 3.2应用,带有数据 Mongoid,用于验证 Rails 4.1有关设计的Rails 4.1示例应用程序,请参见:rails设计有一个用于设计的教程:Rails 设计教程。类似示例和教程这是来自 RailsApps...
《Agile Web Development with Rails-Second Edition-Beta》是一本专注于使用Ruby on Rails框架进行敏捷Web开发的书籍。这本书的实例部分提供了丰富的代码示例,帮助读者深入理解Rails的架构和开发流程。在本压缩包...
本文将深入探讨"rails-react-components-源码.rar"中的关键知识点,帮助开发者理解如何在Rails应用中集成React组件。 1. **React组件化开发** React的核心概念是组件,它允许我们将UI拆分为独立、可重用的部分。在...
在这个压缩包中,`awesome-rails-gem-zh_CN-master`可能是项目源码或文档的主目录。以下是一些可能包含在列表中的关键Rails Gem及其功能简介: 1. **Devise**:这是一个灵活的身份认证解决方案,提供了一套完整的...
标题 "rails-documentation-1-2-1.zip" 暗示这是一份关于 Ruby on Rails 框架的文档,版本为 1.2.1。Ruby 是一种面向对象的编程语言,而 Rails 是一个基于 Ruby 的开源 Web 应用程序框架,遵循 Model-View-...
【标题】"rails-yelp-mvp-源码" 指的是一个基于Rails框架开发的类似于Yelp(美国知名餐饮评论网站)的最小可行产品(Minimum Viable Product, MVP)的源代码。Rails是Ruby编程语言的一个流行Web开发框架,以其“约定...
rails-documentation-2-0-2
Rails是Ruby语言的一个著名Web应用框架,以敏捷开发和“约定优于配置”...通过对这些版本的源码进行分析和学习,开发者不仅可以提升对Rails框架的理解,还能在实际项目中运用这些知识,编写出更高效、更安全的Web应用。
upmin-admin 是一个为 Rails 应用开发的开源管理框架。用来管理 Rails 应用中各种对象(如 Model、View 和 Controller )。 标签:upmin
在"rails-builds-test-源码.rar"这个压缩包中,我们很显然会接触到一个使用Rails框架构建的测试项目。接下来,我们将深入探讨Rails的几个关键知识点,以及如何通过源码来理解其工作原理。 1. **Gemfile与Gemfile....