- 浏览: 41087 次
- 性别:
- 来自: 北京
最新评论
-
lijingtx:
为什么我报错了。in `alias_method`:undef ...
Rails中如何更加优雅的处理文件上传 -
gigix:
woody_420420 写道rainchen 写道几时cuc ...
让测试并行起来吧 -
woody_420420:
rainchen 写道几时cucumber也能并发跑scena ...
让测试并行起来吧 -
rainchen:
有时为了保证测试环境和开发、生产的特性一致,减少非必要的环境差 ...
让测试并行起来吧 -
woody_420420:
是的。耗时主要在数据库访问上。
内存数据库,我倒真没想过用这个 ...
让测试并行起来吧
-
前言
经过一番试验和考虑...一,我尝试了一些思维导图工具(MindMapper,FREEMIND),但我始终没有找到一种好的方式将自己学习Rails源代码的思路表述出来,就此作罢(顺便问问,有研究思维导图的同学么?能否推荐两个自己觉得用起来比较顺手的工具)。二,不再打算整理代码运行顺序图,对不熟悉Rails源代码的同学们来说,这个图可能的确没什么帮助,甚至会把人搞晕。我现在打算从Rails源代码功能点的角度出发,根据具体功能点,结合Rails源代码进行学习,整理,总结。如果某些源代码比较复杂,牵涉类比较繁多,我仍然打算整理一个类图,从一个高的层次了解系统内部对象的关系。
前面三篇文章,我们看到了Rails启动的大致功能和流程,包括初始化多种环境变量,初始化Route表,启动Web服务器开始侦听客户端请求。。。那么接下来,当然是开门迎客,等待客户端(浏览器)的请求,并进行处理,最终将结果返回客户端(浏览器)呈现。那么熟悉Rails的同学都知道,首先,Rails必须根据客户端的一个请求,决定将要执行哪个Controller的哪个Action,这也是本文的主要目的。
-
寻找Controller
首先,我们先来看一看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的目的)
评论
通过你的文章学习到很多东西!搞明白了RAILS的一个大概流程!谢谢
不知道下面的帖子是否对你有帮助
http://www.iteye.com/post/299671?page=3
关于rails的问题:就是我模型里面创建了一个回调的方法,功能是:比如:我要创建一个部落,用回调方法实现自动增加了创建者为酋长和管理员!但是我用夹具创建了一个部落,这些都没有显示出来!是怎么回事?是夹具在加载数据时饶过回调方法了吗?还是直接饶过模型,直接把数据加载到数据库里面?你有关于夹具的源代码吗?我想知道是怎么回事!
发表评论
-
Rack Middleware Profile
2009-05-26 21:27 1535Rack是一个高效,简洁的框架(Webserver Int ... -
column_timestamp plugin
2008-10-16 00:00 1057有些时候,我们可能需要记录某些列的更新时间,类似于rail ... -
为Rails中的validation error增加error_code
2008-08-05 22:49 1951各位同学对model中一 ... -
Rails中如何更加优雅的处理文件上传
2008-07-19 22:23 2226通常,在rails中处理文件上传,我们会这么做,在view ... -
慎用typo(theme_support)的换肤机制
2008-07-17 23:29 1809前言 本文提到的typo版本是目前最新的5.0.3 ... -
Ruby中&&操作符的妙用(旁门左道)
2008-07-09 22:30 1799几乎所有的现代编程 ... -
Ruby生成斐波拉契数列
2008-07-09 13:52 1809不管你是用c,c++,c#,java。。。不管你是用循环, ... -
Ruby On Rails-2.0.2源代码分析(3)-named route和resource
2008-03-21 00:28 2680前言 在《Routing的载入》中,我大致介绍了一 ... -
netbean调试ActiveSupport::OptionMerger需注意的一个问题
2008-03-18 15:08 1663这两天,在调试Rails ... -
Ruby On Rails-2.0.2源代码分析(2)-Routing的载入
2008-03-16 22:58 3860前言 在前一篇 ... -
Ruby On Rails-2.0.2源代码分析(1)-Rails的启动
2008-03-12 23:32 5450前言 本文主要是 ...
相关推荐
Ruby on Rails Guides v2 - Ruby on Rails 4.2.5
A cheatsheet for Ruby on Rails
rails-dev-box, 面向 Ruby on Rails 核心开发的虚拟机 用于 Ruby on Rails 核心开发的虚拟机简介注意:这个虚拟机不是为 Rails 应用程序开发而设计的,只是为。 这个项目自动设置开发环境,以便在 Ruby on Rails ...
《Ruby on Rails Tutorial》中文版(原书第2版,涵盖 Rails 4) Ruby 是一门很美的计算机语言,其设计原则就是“让编程人员快乐”。David Heinemeier Hansson 就是看重了这一点,才在开发 Rails 框架时选择了 Ruby...
Ruby on Rails安装,有人说难,但其实也很方便。要基于ruby开发应用程序,我们必须安装ruby、gem、rails、mongrel。
请结合我上次上传的“Ruby中文文档”,学习了解之后,再来实际操作,理解“Ruby On Rails”框架开发web程序,这个是很不错的实例,能够在短期内实现Rails的web开发。
Ruby on Rails入门经典代码,非常适合新手学习使用
Ubuntu系统ruby on rails安装 Ubuntu系统ruby on rails安装 Ubuntu系统ruby on rails安装 Ubuntu系统ruby on rails安装 Ubuntu系统ruby on rails安装 Ubuntu系统ruby on rails安装 Ubuntu系统ruby on rails安装 ...
Ruby On Rails中文教材(PDF)
Ruby on Rails源代码
ruby on rails社区网站开发源码
Ruby on Rails入门经典-例子,有很多rails工程实例。
原文是Web版本,已经导出成PDF版本供大家查看。原版是英文版的《Ruby on Rails Tutorial》,特别适合有其他语言开发经验的Rails入门。
Ruby on Rails与MongoDB 您可以在MongoDB的帮助下轻松... rails new ruby-on-rails-with-mongodb --skip-active-record从您的Gemfile中删除sqlite3(如果存在),将Mongoid添加到您的Gemfile中,然后运行“ bundle”。
ruby_on_rails 开发者实战 源代码上 1-12章 来之不易啊。仅供学习.
Ruby on Rails Web开发学习实录 内容简介: 在目前的主流web开发技术中,基于ruby语言的rails框架是做网站开发速度最快的工具。它可以达到j2ee框架开发速度的5~10倍,并且代码量也非常少。另外由于代码量的大幅度...
Ruby On Rails 框架自它提出之日起就受到广泛关注,在“不要重复自己”,“约定优于配置”等思想的指导下,Rails 带给 Web 开发者的是极高的开发效率。 ActiveRecord 的灵活让你再也不用配置繁琐的 Hibernate 即可...
Beginning Ruby on rails 源代码
ruby on rails对mongodb的操作ruby on rails对mongodb的操作ruby on rails对mongodb的操作ruby on rails对mongodb的操作