在这章我们将动手写我们自己的第一个控制器,通过学习,我们也可以理解Rails是怎样实现路由请求的。
我们现在已经有了一个很基础的gem
和应用程序,gem也已经安装到本地。假如你没有这些,或者你不喜欢你在第一章写的代码,你可以下载示例代码。
我们将依据本书的每章依次在版本号上加1。假如你正用你自己的代码,你可以修改它,或者 不修改,都可以。
为了改变gem的版本,打开rulers/lib/rulers/version.rb,把常数从“0.0.1”改到“0.0.2”。
下次你重新安装你的gem的时候,你需要输入:
$ gem build rulers.gemspec $ gem insall rulers-0.0.2.gem
你应该删除rulers/rulers-0.0.1.gem
,这样你就不会错误地安装和运 行旧的版本。
示例源代码
所有章节的示例源代码均保存在Github上:http://github.com/noahgibbs/rulershttp://github.com/noahgibbs/best_quotes
一旦你克隆了仓库,在每个项目的目录下面运行
$ git checkout chapter2 $ git checkout -b chapter_2_mine
来创建一个新的名为“chapter_2_mine”的分支。
关于Rack
上一章提到的Rack的复杂的返回值可能需要好好解释一番。
所以让我们解释一下吧。
这是其一:
[200, {'Content-Type' => 'text/html'}, ['Hello!']]
让我们把它分解开来。第一个数字,200,是HTTP状态码。假如你返回了404,然后浏览器将 显示404信息--找不到页面。假如你返回500,浏览器将提示服务器错误。
下一个hash是header。你可以返回所有的header来设置cookie,改变安全设置以及其他别的东西。现在对我们来说最重要的一个是'Content-Type',它必须是‘text/html’。它告诉浏览器按照HTML来渲染文本,而不是JSON、XML、RSS,或者其他别的东西。
最后,是Body。这里我们只是包含字符串的部分。所以浏览器将显示“Hello!”
后面我们将解释Rack的“env”对象,它是一串有趣的hash值。现在,你需要知道的是它们之中一个名为PATH_INFO的值,它是URL中服务器名称后面不包括查询参数的的那部分。它也是Rails应用程序里告诉Rails使用哪个控制器的那个动作的部分。
路由
一个请求到达你的网页服务器或应用程序服务器,Rack把它传递给你的代码。Rulers将需要 路由请求 -- 那意味着它从那个请求了取走URL,回答了这个问题,“什么控制器和什么动作处理这个请求?”我们将从简单的路由的开始。
具体来说,我们准备从Rails的默认路由开始。那意味着形如“http://yourhost.com/category/action”的URL将被路由到CategoryController#action
。(译者注:按照Rails的惯例,通常控制器使用控制器名称最后的名词的复数形式,即CategoriesController。)
在Rulers的目录下,打开lib/rulers.rb
# rulers/lib/rulers.rb require "rulers/version" require "rulers/routing" module Rulers class Application def call(env) klass, act = get_controller_and_action(env) controller = klass.new(env) text = controller.send(act) [200, {'Content-Type' => 'text/html'}, [text]] end end class Controller def initialize(env) @env = env end def env @env end end end
我们的Application#call
方法现在根据URL获取到了控制器和动作,然后创建了一个新的控制器,然后把它发送给动作。对于像“http://mysite.com/people/create”这样的URL,你会希望klass值为PeopleController
,action
值为create
。
我们会在rulers/routing.rb
里实现如下。
控制器只是保存了我们给出它的环境。我们将在后面使用它。
现在在lib/rulers/routing.rb
里:
# rulers/lib/rulers/routing.rb module Rulers class Application def get_controller_and_action(env) _, cont, action, after = env['PATH_INFO'].split('/', 4) cont = cont.capitalize # 'People' cont += 'Controller' # 'PeopleController' [Object.const_get(cont), action] end end end
这是很简单的路由,因为我们只是让控制器和动作尽可能的简单。我们把URL从“/”分开。“4” 的意思是“分开不要超过4次”。所以split
从第一个斜线前分开,给“_”分配了空的字符串,然后是Controller,然后是action,剩下的一起分给最后一个变量after。现在我们把第二个“/”后面的全扔了 - 但是它仍然在环境里,所以它不是真正的被抛弃了。
“const_get”方法是Ruby魔法的一员 - 它的意思是查找任何大写字母开头的名称(译者注:在Ruby中,首字母大写意味着是常量)- 这里,就是指你的控制器类。
另外,有时你会看见下划线,常常表示“一个我不关心的值”, 就像上面我所做的那样。它 实际上是个普通的变量,如果你喜欢,你可以使用它,但是许多Rubyist(Ruby程序员的爱 称)喜欢用它表示“一些我正忽视或不想要的东西”。
现在,你要在best_quotes里创建一个控制器。在app/controllers的目录下,创建一个名 为quotes_controller.rb的文件:
# best_quotes/app/controllers/quotes_controler.rb class QuotesController < Rullers::Controller def a_quote "There is nothing eigher good or bad" + "but thinking makes it so." end end
虽然看起来像一个Rails App 的控制器,但是我们需要手 动把它添加到App里,因为你还没有为Controller实现Rails那样魔法般的自动加载文件功能。打开best_quotes/config/application.rb。你要在require 'rulers'
后面添加下面一行:
# best_quotes/config/application.rb (片段) $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'app', 'controllers') require 'quotes_controller'
LOAD_PATH
让你从“app/controllers”加载文件,只通过查询他们的名称就可以了,就像Rails一样。
然后你需要新控制器。
现在,进入rulers的目录,输入“git add .;gem build rulers.gemspec; gem install rulers-0.0.2.gem”。然后在best_quotes目录下,输入“rackup -p 3001”。最后,打开你的 浏览器,在地址栏输入“http://localhost:3001/quotes/a_quote”查看效果。
假如你之前没出错,你应该看到哈姆莱特的名言。你也正看见你的第一个控制器的第一个动 作。
假如你没有看到,请确定你在URL地址里包含了“quotes/a_quote”,就像你上面看见的一样。
几乎搞定了!
现在,看看你运行rackup的控制台。看看屏幕。看见那个错误了吗?在有些浏览器上可能看 不到,但是你也可能有像这样一个错误:
NameError: wrong constant name Favicon.icoController .../gems/rulers-0.0.3/lib/rulers/routing.rb:9:in `const_get' .../gems/rulers-0.0.3/lib/rulers/routing.rb:9:in `get_controller_and_action' .../gems/rulers-0.0.3/lib/rulers.rb:7:in `call' .../gems/rack-1.4.1/lib/rack/lint.rb:48:in ` _call' (...more lines...) 127.0.0.1 - - [21/Feb/2012 19:46:51] "GET / favicon.ico HTTP/1.1" 500 42221 0.0092
这是浏览器请求一个文件失败的错误信息...嗯...看看最后一行...哦, favicon.ico。
大部分的浏览器会自动请求这个文件。最终我们会让我们的框架或者网页服 务器负责服务像这样的静态文件。但是现在,我们将只是很糟糕的实现一下。
打开rulers/lib/rulers.rb
,看看Rulers::Application#call
。
我们检查一下PATH_INFO
是不是/favicon.ico
,如果是的话就返回404:
# rulers/lib/rulers.rb module Rulers class Application def call(env) if env['PATH_INFO'] == '/favicon.ico' return [404, { 'Content-Type' => 'text/html' }, []] end klass, act = get_controller_and_action(env) controller = klass.new(env) text = controller.senc(act) [200, {'Content-Type' => 'text/html'}, [text]] end end end
非常糟糕的hack?肯定了。不过最后我们会解决它的。现在,我们就能看见真正的错误,没 有了这种因为浏览器特性引起的错误了。
复习
你刚刚建立了一个功能很基础的路由,还有你可以路由到Controller的action。假如添加更多的控制器动作,你就可以得到更多路由。rulers 0.0.2
只是刚刚创建的一个超级简单的gem
。后面我们将添加更多特性。
你已经学习了一点关于Rack的知识 -- 在这章的“Rails”一节还会看到一些。你已经通过LOAD_PATH和const_get见识了Rails的一点魔法,我们在后面也还会看到它们。
练习
练习1. 调试Rack环境
打开app/controllers/quotes_controller.rb
,修改成下面这样:
# best_quotes/app/controllers/quotes_controller.rb class QuotesController < Rulers::Controller def a_quote "There is nothing either good or bad" + "but thinking makes it so." + "\n<pre>\n#{env}\n</pre>" end end
现在,重启服务器 -- 假如你只是修改了应用程序(best_quotes),你不需要重建gem. 刷新一下浏览器,你应该看见一个大的hash表,充满了好多有趣的信息。它只有一行,所以为了 更清晰,我为你重新把它格式化了:
{"GATEWAY_INTERFACE"=>"CGI/1.1", "PATH_INFO"=>"/ quotes/a_quote", "QUERY_STRING"=>"", "REMOTE_ADDR"=>"127.0.0.1", "REMOTE_HOST"=>"localhost", "REQUEST_METHOD"=>"GET", "REQUEST_URI"=>"http://localhost:3001/quotes/a_quote", "SCRIPT_NAME"=>"", "SERVER_NAME"=>"localhost", "SERVER_PORT"=>"3001", "SERVER_PROTOCOL"=>"HTTP/1.1", "SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/1.9.3/2012-11-10)", "HTTP_HOST"=>"localhost:3001", "HTTP_CONNECTION"=>"keep-alive", "HTTP_CACHE_CONTROL"=>"max-age=0", "HTTP_USER_AGENT"=>"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11", "HTTP_ACCEPT"=>"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "HTTP_ACCEPT_ENCODING"=>"gzip,deflate,sdch", "HTTP_ACCEPT_LANGUAGE"=>"en-US,en;q=0.8", "HTTP_ACCEPT_CHARSET"=>"ISO-8859-1,utf-8;q=0.7,*;q=0.3", "rack.version"=>[1, 1], "rack.input"=>#>, "rack.errors"=>#>>, "rack.multithread"=>true, "rack.multiprocess"=>false, "rack.run_once"=>false, "rack.url_scheme"=>"http", "HTTP_VERSION"=>"HTTP/1.1", "REQUEST_PATH"=>"/quotes/a_quote"}
看起来内容很多,对不对?你的程序从Rack返回的所有信息都有了。何时你的Rails控制器支持像“post”这样的动作?,通过查看Rack的环境变量揭开它的面纱。
假如你的控制器检查是否env['REQUEST_METHOD'] == 'POST'
,你可以很容易的实现你自己的“post”方法?
现在好了,你现在能看见Rails必须用到的每个内容。Rails需要知道的的进来的请求中的每 一项都是从这个hash中抽取出来的。
练习2. 调试异常
让我们给我们的控制器中添加一个新方法,让它抛出异常:
# best_quotes/app/controllers/quotes_controller.rb class QuotesController < Rulers::Controller def exception raise 'It's a bad one!' end end
重新运行rackup
,然后在浏览器里访问“http://localhost:3001/quotes/exception”,它应该抛出一个一次。
你可能看到美化了的页面,在quotes/exception
有一个RuntimeError
和 一大的堆栈跟踪信息。
在第八章,我们将深入Rack中间件,你看到那些的内容的原因。它不是内建在你的浏览器的, 你可以通过设置RACK_ENV
为production
(生产环境)来关闭它。一般来说,
它是开发环境才有的调试工具,你正受益于此。
然而,用它不是必须的。你可以在你的Rulers应用程序里添加begin/rescue/end
块来处理异常。
打开rulers/lib/rulers.rb
,然后在你的call
方法里,把controller.send()
函数添加进begin/rescue/end
块里。现在你必须决定怎么处理抛出的异常 -- 你可以从一个简单的错误页面开始,或者自定义500页面,或者其他你喜欢的方式。
你的框架还可以对错误作出哪些处理呢?
练习3. 根路径和路由
你不能通过直接访问“http://localhost:3001”看程序工作正不正常, 这点很不方便。只是得到异常不能告诉你是否程序出错。
回到rulers/lib/rulers.rb
。在检查favicon.ico
的代码下面,你可以再添加一个判断,看看PATH_INFO
是不是“/”。
首先,试着硬编码,让它总是重定向到“/quotes/a_quote” 然后再你的浏览器里测试。然后,试试下面的这个方法:
- 返回已知文件的内容 -- 比如public/index.html?
- 查找HomeController和index动作
- 额外得分:试试浏览器重定向。这个请求返回200或404以外的代码,然后设置header。
在第9章,我们将建立更多配置的路由,让Rulers更像真正的Rails。到那时,在你的框架里用Ruby hack你的路由就非常容易了。
在Rails里
在Rails里ActionPack包含控制器。Rails也有ApplicationController,它继承了控制器基类,然后每个控制器继承它。
你的框架也可以!
不同的Rails版本的路由实现有实质上的不同。你可以通过以下网址了解一下现在的 “http://guides.rubyonrails.org/routing.html”,“Rails路由从外到内”。Rulers现在的路由和旧版本Rails1.2的路由(也是Rails2.3默认的方式)相似。它会自动查找控制器和动作,不需要指定单个的路由。
虽然不太安全,但是对于开始来说并不太差。Rails 3默认的路由通过生成许多单个的匹配的路由来实现,所以和这个不是很一样。
Rails把Rack信息打包到“request”对象,而不是只把hash包含到相应的控制器。当你想要抽象一点的时候,这个是个好主意--为某个特殊的变量一般化值,例如,或者读和设置cookie来储存session数据。
Rails做了许多工作,在第6章,你会看见Rulers怎么实现它。Rails在底层也使用Rack,所以用到的基础信息和你是一样的。
Rack是简单的、CGI一样的接口,比你想象的功能要少。
假如你好奇,看看“http://rack.rubyforge.org/doc/SPEC.html”,包含了所有细节。
虽然有点晦涩,但是它可以告诉你你想要了解的的所有知识。
随着后面的学习,你将会了解更多关于Rack的知识。但是对于没有耐心的读者,Rails包含了 一篇关于怎样使用Rack的指南“http://guides.rubyonrails.org/rails_on_rack.html”。
它里面满满都是你可以应用在自己的框架里的内容。有些在本书的后面内容里也会添加。