0. 开发Rails
为什么要重建Rails?
What I cannot create, I do not understand.
Feynman
了解软件的最深层次的实现可以让你掌握它。它是你无法假装的一种特殊的完成式。你必须 足够了解它,然后才能重新构建它。那么假如你真的重建了它会怎么样?这种努力值得吗?
这本书将引导你从零开始构建一个和Ruby on Rails类似的框架(不知道的同学可以自己百度一下~ 世界上最早的MVC框架,影响了一代Web 框架),使用同样的Ruby特性和结构,它们让 Rails变得如此有趣、好玩。
Ruby onRails 以“魔法”著称。当你真的使用这些Ruby特性来构建和Rails相似的框架时,你会 发现这些没法并不是滥用,它们都是有道理的。
另外,Ruby on Rails是一个固执己见的框架;Rails团队自己也这样的观点高调的宣扬Rails。Rails也是惯例先于配置的实践框架,假 如你有不同的想法时怎么办?你也可以构建一个Rails一样的框架,然后你可以能耗费大量的时间,来按自己的想法添加各种特性。
总之,无论你是想深入理解Rails,还是想要构建你自己的个人版本,这本书都可以帮上忙。
谁应该重建Rails?
你需要有一点Ruby基础。假如你构建过几个小的Ruby应用程序,或者一个中型的Rails应用, 你应该可以了。当你阅读的时候,你参考一下鹤嘴锄书(国内可以搜《Ruby编程》)也可以。你应该可以不用太麻 烦就能用Ruby写一些基本的程序。
假如你想要复习一下Rails,Michael Hartl的指南非常好:“http://ruby.railstutorial.org/ruby-on-rails-tutorial-book”。HTML版本是免费的,或 者你可以购买电子版或视频。本书用到的一些概念,假如你已经在Rails里见过的话,对这些 概念就会更明白。
在大部分章节里,我们将使用一点Ruby魔法。我们会边学习,边解释这些魔法。这些魔法都 很好理解。只是会让你觉得Ruby还让我这样用!
亲手实现
每章都是关于在和 Rails 类似的框架里构建系统,我们喊它为Rulers(就像Ruby on Rulers)。Rulers比最近版本的的Rails更简单。但是一旦你实现了这个简化版,你就知道复杂的版本做些什么了,以及它们是怎么工作的。
后面的文章里有一个源代码的链接 -- 那是本书每章前一章结束时标准的Rulers的代码。你可以下载源代码,然后动手实现任何你觉得好奇的一章。
在每章的末尾是建议的特性和练习。略过它们很容易,它们是可选的。但是假如你在每章结束后停下来,然后想想在你框架里你还想实现那些功能,你将会收获更多。你想要知道关于 Rails的那些实现?那些是Rails没有的,而你自己却非常想要的功能?Sinatra(另一个Ruby 框架)有一些精彩的内容吗?你想要要添加特性就是最好的特性!
作弊
你可以从GitHub下载下一章的示例代码,不用一章一章的敲代码。假如你自己敲代码的话就 会学到更多,敲错代码了,是的,然后苦逼的调试代码。但是假如有一些关卡你就是过不去 ,没关系,使用示例代码继续下一章。下次再看到的时候就会容易一些了。
让每件事情完美可能的解决可能需要几次才行。回到那些内容太多的代码和练习。跳过那些 ,后面再回去,再在它们上面花点时间。假如这本书的版本对你来说太难,你可以去看看对 应的Rails,或者其他简单一点的框架,如Sinatra。有时你只是需要看看不同版本的概念解 释。
哦,通常你不会得到练习的源代码,只有主要的章节才有。甚至对作弊的也留了一点挑战。不用客气。
在本章的最后是每个系统的Rails版本的源代码。阅读Rails源代码是可选的。但是,即使主 要的组件(ActiveRecord, ActionPack)也有25000行代码 -和其他大部分框架比较的话还是 挺短的,可读性也较高。通常你正寻找一些特殊的更小的组件,大概就是一百到一千行左右。
假如你花时间读好的源代码你也会试一个更好的Rails程序员。Rails代码是Ruby技巧的宝藏 ,也有一些很有趣的元编程示例。
环境搭建可以参考另一篇文章,0.5章开发环境设置 -- 构建自己的MVC框架
1. 从零到它工作了!
既然你已经安装好了,到了开始构建的时候了。像Rails一样,你的框架将是一个gem(Ruby 的库),gem就像其他语言的库,可以包含、引用。整本书中,我们的框架的名字都是Rules
,即“Ruby on Rails”。
在本书的开始,有些命令你可能不太熟悉。没关系,跟着书在命令行敲进去就可以了。
开垦“荒地”
首先创建一个新的gem:
gem相当于其他语言的库,lib,或者jar包等。是一个其他程序员开发好以后打包好的源代码文件。
$ bundle gem rulers create rulers/Gemfile create rulers/Rakefile create rulers/LICENSE.txt create rulers/README.md create rulers/.gitignore create rulers/rulers.gemspec create rulers/lib/rulers.rb create rulers/lib/rulers/version.rb Initializating git repo in src/rulers”
Rulers是我们创建的gem(库),在自动生成的rulers.gemspec文件里定义它所需的其他依赖 。在你的文本编辑器里打开那个文件。假如你喜欢,你可以个性化你的名字,gem的功能描述 等等。你可以像这样自定义多个部分:
# rulers.gemspec gem.name = "rulers" gem.version = Rulers::VERSION gem.authors = ["Singleton Ruby-Webster"] gem.email = ["webster@singleton-rw.org"] gem.homepage = "" gem.summary = %q{A Rack-based Web Framework} gem.description = %q{A Rack-based Web Framework, but with extra awesome.}
按照惯例,摘要(summary)和描述(description)差不多,就是更短一些。摘要通常只有一样,然而描述有时会长达4-5行。
记住一定要把这个文件里FIXME
和TODO
这样的字符替换掉,gem build
不喜欢它们,而且它们对用户也不友好。
你需要在底部添加依赖库。它们可能和下面的内容差不多:
gem.add_development_dependency "rspec" gem.add_runtime_dependency "rest-client" gem.add_runtime_dependency "some_gem", "1.3.0" gem.add_runtime_dependency "other_gem", ">0.8.2”
每一行添加一个运行时依赖(在运行gem的时候需要)或者开发时需要的依赖(在开发或测试 的时候用到的gem)。目前,只需添加下面的:
gem.add_runtime_dependency "rack"
Rack也是一个gem,是你的框架和Ruby应用程序服务器,例如Mongrel、Thin、Lighttpd、Passenger、WEBrick或Unicorn,之间的接口。应用服务器是特殊类型的网页服务器,运行服务器程序,通常是用Ruby写的。在真实的生产环境,会在应用程序服务器前再运行像Apache或者NGinX这样的网页服务器。但是在开发环境我只运行应用程序服务器,其他的都不运行。幸运的是,应用程序服务器作为网页服务器效果也还不错。
我们会在控制器那章覆盖更多Rack的细节,然后在中间件(Middleware)再次覆盖一部分。现在你只需要知道Ruby通过Rack把HTTP请求变成在你的服务器上运行的代码。
修改lib/rulers/version.rb
文件,将0.1.0
改为0.1.1
。
让我们打包你的gem(译者注:Ruby是动态语言,不需要编译),然后安装它:
$ gem build reulers.gemspec $ gem install rulers-0.0.1.gem
最终我们将从开发目录使用Bundler的技巧来使用你的gem。但是现在我们只是用简单的方法 来安装它。知道完成一个任务最简单的方法总是好的--当聪明的技巧不起作用的适合,你可 以回头使用它。
Hello World,或多或少
Rails就像你刚才打包的Gem,但是它运行的是什么程序?我们将从一个很简单的app开始,你可以提交最爱的名言,用户可以给它们打分。如果你用的是Rails,那么Rails自带生产新的APP的命令,不过我们现在只能手动构建。
先创建一个目录和一些子目录,注意目录结构:
$ mkdir best_quotes $ cd best_quotes $ git init Initialized empty Git repository in src/best_quotes/.git/ $ mkdir config $ mkdir app”
你也想使用你的自己写库rulers。添加Gemfile:
# best_quotes/Gemfile source 'http://gems.ruby-china.com/' gem 'rulers' #你创建的gem
然后运行“bundle install”来创建Gemfile.lock,确保所有的依赖都可以使用。
我们先构建一个小小的Rack程序。创建config.ru文件:
# best_quotes/config.ru run proc { [200, {'Content-Type' => 'text/html'}, ['Hello, World!']] }
使用Rack创建Rulers
在你的Rulers的目录,打开lib/rulers.rb。修改如下:
# rulers/lib/rulers.rb requrie 'rulers/version' module Rulers class Application def call(env) [200, {'Content-Type' => 'text/html'}, ['Hello from Ruby on Rulers']] end end end
重新打包安装
$ gem build rulers.gemspec $ gem install rulers-0.0.1.gem
现在切换到应用程序目录: best_quotes。
现在你可以使用Rulers::Application
类了。在config
目录下新建文件config/application.rb
,然后添加以下代码:
# best_quotes/config/application.rb require 'rulers' module BestQuotes class Application < Rulers::Application end end
“BestQuotes”对象应该使用你的Rulers框架,当你使用它的时候会显示“Hello from Ruby on Rulers”。为了使用它,打开你的config.ru
,然后修改如下:
require './config/application' run BestQuotes::Application.new
现在当你在命令行输入rackup -p 3001
然后在浏览器输入http://localhost:3001
,你应该看到“Hello from Ruby on Rulers!”。
你已经用你自己的框架创建了一个应用程序!
复习
在这章,你创建了一个可重复使用的Ruby库,作为gem。你把你的gem包含进示例应用程序。你也建立了一个简单的Rack程序,你可以使用Rackup文件config.ru
来创建。你学到了Rack基本用法,然后把这些东西连接起来以便让它们工作。
从这里开始,你将逐渐添加一些新东西进去。但是这章是唯一的一次从白板开始,从什么也 没有到有一点功能。
厉害吧。
在Rails里
默认Rails包含不止一个,而是五个可以重复使用的gem。真正的rails
gem仅有很少的代码。相反,它把请求分发给支持的gem。Rails本身只是把它们连接在一起。
Rails允许你改变许多组件 - 你可以使用不同的ORM,不同的测试库,不同的Ruby模板库或者不同的Javascript库。所以 描述对于严重自定义的程序来说不总是100%准确。
下面是基础的rails gem -- 没有依赖和库,只有rails本身基础的一部分内容。
- ActiveSupport 是一个Ruby扩展库,它里面的方法不是专为Rails设计的。你会看见 一些不是Rail的库里也使用了ActiveSupport。ActiveSuppot包含了一些像Rails怎么变换单 词的单复数,或怎么把驼峰(CamelCase)名称转换成蛇形(snack_case)名称。它也包括了 比Ruby标准库本身显著提高的时间和日期支持。
- ActiveModel 卷入你的模型的特性。它不止针对数据库 -例如,假如你想要某个模型的URL,ActiveRecord也可以帮你实现。它只是包装了许多不 同ActiveModel的实现来告诉Rails怎样使用他们。更宽泛的说,ActiveModel实现是ORM(见 ActiveRecord,下面提到),但是他们也能使用非关系型存储,像MongoDB,Redis, Memcached或者甚至只是本地机器存储。
- ActiveRecord 是一个对象-关系映射(ORM)。这意味着它在Ruby对象和SQL数据库表之间 相互映射。当你在Rails里从SQL数据库查询或者写入数据,你都是通过ActiveRecord。ActiveRecord也实现了ActiveModel。ActiveRecord支持MySQL和SQLite、也支持JDBC、 Oracle、PostgreSQL和许多其他的关系型数据库。
- ActionPack 负责路由 - 输入的URL到Rails里的控制器(Controller)和动作(Action) 之间的映射。它也建立你的控制器和视图,然后通过它的控制动作引导请求,然后渲染视 图。这里中的一些,ActionPack通过Rack来实现。模板引入其他的外部的gem来渲染自己, 像Eruby渲染.erb模板、Haml渲染.haml模板。ActionPack也处理动作 - 或视图为中心的功能,如视图缓存。
- ActionMailer 用来发送邮件,尤其基于模板的email。它具有你期望Rails email能具有的 功能,有控制器、动作、和“视图” - 对于email来是说基于文本的模板,不是普通的网页模板。
当然,Rails里不是啥都有。
一些你在这章里构建的功能是在程序里,不是在Rulers里。继续向前,创建新的Rails 4 应用 - 输入 rails new test_app
。假如你看看config/application.rb,你将看见Rails 建立了一个Rails Application对象,非常像Rulers Application对象。现在该是打开config目录,看看Rails Application默认生成了些什么。你看到一些现在觉得更合理的代码了吗?
练习
练习1. 重新加载Rulers
让我们给Rulers框架添加一点调试代码。
# rulers/lib/rulers.rb module Rulers class Application def call(env) `echo debug > debug.txt`; [200, {'Content-Type' => 'text/html'}, ['Hello from Ruby on Rulers!']] end end end
当这个运行时,它应该在best_quotes文件夹下创建一个名为debug.txt的文件,也就是你运 行rackup的那个目录。
试着重启你的服务器,刷新你的浏览器页面“http://localhost:3001”。但是你找不到debug 文件!
试试重新打包gem,然后重新安装它(gem build rulers; gem install rulers-0.0.1.gem)。刷新浏览器。你仍看不见它。最后,再次重启你的服务器,刷新浏览器。现在,你终于看见debug文件了。
Rails和Rulers都很难调试。在第三章我们将看到Bundler的:path选项,可以让我们调试容易 点。现在你需要重新安装gem和重启rack服务器来让新的Rulers代码执行。当方便的方式行 不通的时候,你也知道怎么用笨方法。
练习2. 你的库的库
你可以开始一个简单的可复用的库,就像ActiveSupport。当程序使用你的Rulers gem,然后require它(“require rulers”),程序会自动得到那个文件里所有方法。试试添加lib/rulsers/array.rb的文件, 然后加入以下代码:
# rulers/lib/array.rb class Array def sum(start = 0) inject(start, &:+) end end
你以前见过“&:+”吗?它是个有趣的技巧。“:+”意思是“符号+”就像“:foo”意思是“符号foo”。“&”意思是“当作块传递”-- 在圆括号里的代码快通常跟在后面。所以你正把符号当作代码块 来传递。Ruby知道把符号转换为proc,然后调用它。当你用“加”的时候,你得到“把这些加 起来”。
现在,把require 'rulers/array'
添加到lib/rulers.rb
的顶部。它会在所有Rulers应用里包含它。
在你打包gem前,你需要进入rulers目录,运行git add -A
(git add -A; gem build rulers.gemspec; gem install rulers-0.0.1.gem)。那是因为rulers.gemspec实际调用git来查找包含进你gem里的文件。你可以在rulers.gemspec文件里看到一行:
gem.files = `git ls-files`.split($/)
“git ls-files”仅仅显示git知道的文件 -- split只是为了得到输出的每一行。加入你创建 了一个新文件,确信你重新打包前运行了git add -A
,否则你看不到它!
现在有了新的rulers/array.rb文件,任何保含了Rulers的程序都能写[1, 2, 37, 9].sum, 得到数组的和。继续,添加更多的那些使用你的框架的程序可能会有用的方法。
练习3. 早点测试,经常测试
因为我们正创建Rack应用,rack-test gem可以很方便的测试它。让我们来用用。
把rack-test添加到你的gemspec文件里中的开发(不是运行时runtime)依赖:
# rulers/rulers.gemspec, 底部附近 # ... (译者注:...表示省略,下文同,不再做说明) gem.add_runtime_dependency 'rack' gem.add_development_dependency 'rack-test' gem.add_development_dependency 'test-unit' end
(在Ruby 2.0或者更高版本,你需要显示地声明Test/Unit依赖--不再自动包含了。)
当你有了Gemfile的适合,为什么还要使用Gemspec?应用程序和依赖你的gem的库考虑 gemspec,所以这是个好习惯。目前,它是开发依赖,所以没太大关系。
现在运行bundle install
来确保你已经安装了rack-test。我们为Rulers添加一个可用的测试。后面你会写 更多的测试。
创建测试目录:
# 在Rulers的目录下 $ mkdir test
现在我们将创建测试辅助方法:
# rulers/test/test_helper.rb require 'rack/test' require 'test/unit' # Always use local Rulers first d = File.join(File.dirname(__FILE__), '..', 'lib') $LOAD_PATH.unshift File.expand_path(d) require 'rulers'
这里唯一让人惊奇的一点就是$LOAD_PATH
魔法。它确保require的‘rulers’将require当前目 录本地的而不是,就是说,你作为gem安装的那个。它通过在$LOAD_PATH前添加本地路径,以 便在任何别的路径前提前检查本地路径。
我们也expand_path(扩展路径)以便它是绝对路径,而不是相对路径。这点很重要,因为有 可能改变当前目录。
测试不同的本地变化对一个已经安装了gem可能有点麻烦--你已经安装了什么?正使用什么?通过显示的在本地路径前添加,你可以确定优先使用本地代码,这个代码在使用前不必安装。
现在你需要测试,我们把它放在test_application.rb里:
#rulers/test_application.rb require_relative 'test_helper' class TestApp < Rulers::Application end class RulersAppTest < Test::Unit::TestCase include Rack::Test::Methods def app TestApp.new end def test_request get '/' assert last_response.ok? body = last_response.body assert body['Hello'] end end
require_relative
只是意味着“require,但是从这个文件的目录检查,不是加载路径”。它是一个有趣,简单的技巧。
这个测试创建了新的TestApplication类,创建了一个实例,然后在那个实例上get '/'。它用last_response.ok?
检查错误,检查返回的body包含“Hello”。
为了运行测试,输入“ruby test/test_application.rb" 。它应该运行测试,显示一个和下 面类似的消息:
# Running tests: . Finished test“in 0.007869s, 127.0810 tests/s, 254.1619 assertions/s. 1 tests, 2 assertions, 0 failures, 0 errors, 0 skips”
上面的那行get '/'
也可以是post '/my/url'
,假如你更偏好这种方式,或者任何其他 HTTP方法和URL。
现在,写起码一个以上的测试。
2. 你的第一个控制器
在这章你将写你的第一个控制器,也开始明白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 -b chapter_2_mine chapter2”来创建一个新的名为“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,或者其他别的东西。
最后,是内容。这里我们只是包含字符串的部分。所以浏览器将显示“Hello!”
不久,我们将解释Rack的“env”对象,它是一串有趣的hash值。现在,你需要知道的是那些之中一个名为PATH_INFO的值,它是URL中服务器名称后面不包括查询参数的的那部分。它也是Rails应用程序里告诉Rails使用哪个控制器的那个动作的部分。
路由
一个请求到达你的网页服务器或应用程序服务器,Rack把它传递给你的代码。Rulers将需要 路由请求 -- 那意味着它从那个请求了取走URL,回答了这个问题,“什么控制器和什么动作处理这个请求?”我们将从简单的路由的开始。
具体来说,我们准备从Rails的默认路由开始。那意味着形如“http://host.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从第一个斜线前分开,给“_”分配了空的字符串,然后是控 制器,然后是动作,然后是剩下的一起分给最后一个变量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的控制器一样。你需要手 动把它添加它到程序里,因为你还没有为控制器实现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 reulers.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:inconst_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 end
非常糟糕的hack?肯定了。不过最后我们会解决它的。现在,我们就能看见真正的错误,没 有了这种浏览器特性引起的错误了。
复习
你刚刚建立了很基础的路由,还有你可以路由到控制器的动作。假如添加过多控制器动作, 你可以得到更多路由。Rulers 0.0.2只是刚刚后建立一个超级简单的网站。后面我们将添加 更多特性。
你已经学习了一点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章,我们将建立更多配置的路由,更像真正的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”。它 里面满满都是你可以应用在自己的框架里的内容。有些在本书的后面内容里也会添加。