Rack为编写Web应用以及Web框架提供了很多便利的工具,那么这一节,我们实现一个最简单的Web框架。
一、Web框架应该具备什么功能
- 对request和response的存取
- 路由:根据不同URL执行不同程序
- 能够处理cookies
- 能够存取session
- 能够生成日志
- ......
看上去挺麻烦的,是吧?其实,则不然
前面我们已经知道Rack::Request和Rack::Response这两个类可以用来处理request和 response,那么我们该如何实现路由呢?Rack提供Rack::URLMap类来处理路由,我们这里可以不直接使用这个类,如果大家仔细看Rack::Builder的实现,大家会发现这个类提供了一个map方法以及generate_map方法:
map
def map(path, &block) @map ||= {} @map[path] = block end
generate_map
def generate_map(default_app, mapping) mapped = default_app ? {'/' => default_app} : {} mapping.each { |r,b| mapped[r] = self.class.new(default_app, &b).to_app } URLMap.new(mapped) end
路由的作用其实就是根据URL请求的路径去调用对应的程序,因此map方法它接受路径path和 对应的程序&block,然后用hash保存其对应关系generate_map这个方法是在to_app中调用的,它主要通过URLMap建立起路由和所执行应用程序的关系。
def to_app app = @map ? generate_map(@run, @map) : @run fail "missing run or map statement" unless app app = @use.reverse.inject(app) { |a,e| e[a] } @warmup.call(app) if @warmup app end
前面我们知道to_app方法是为了组合中间件以及应用程序的,通过这段代码我们可以看出来, 在组合中间件之前需要先根据路由找到真正的run,然后再组装对应的中间件。
二、一个简单的Web框架
下面是一个简单的web框架,具备对request、response的处理并且具有路由、日志功能。
require 'rack' app = Rack::Builder.new { use Rack::ContentLength map '/hello' do use Rack::CommonLogger map '/user1' do run lambda {|env| [200, {"Content-type" => "text/html"}, ["from user1", "SCRIPT_NAME=#{env["SCRIPT_NAME"]}", "PATH_INFO=#{env["PATH_INFO"]}"]] } end map '/everyone' do run lambda {|env| [200, {"Content-type" => "text/html"}, ["from everyone", "SCRIPT_NAME=#{env["SCRIPT_NAME"]}", "PATH_INFO=#{env["PATH_INFO"]}"]] } end map '/' do run lambda {|env| [200, {"Content-type" => "text/html"}, ["from hello catch all", "SCRIPT_NAME=#{env["SCRIPT_NAME"]}", "PATH_INFO=#{env["PATH_INFO"]}"]] } end end map '/' do run lambda {|env| [200, {"Content-type" => "text/html"}, ["root"]] } end }.to_app Rack::Handler::WEBrick.run app, :Port => 3000
通过上面代码可以看出来:
- map是可以嵌套的
- 可以对不同的路由使用不同的中间件组合
三、rackup
前面我们的代码中最后一行总是这样写的:
Rack::Handler::WEBrick.run xxx, :Port => 3000
这样做写死了所要用的服务器,不灵活。
Rack提供了rackup命令,允许我们用一个配置文件去执行我们的应用程序,rackup使用很简单,我们只需要提供一个后缀为.ru的配置文件即可,然后运行 rackup xxx.ru就ok了,我们把 前面代码改为:
map '/hello' do map '/user1' do run lambda {|env| [200, {"Content-type" => "text/html"}, ["from user1", "SCRIPT_NAME=#{env["SCRIPT_NAME"]}", "PATH_INFO=#{env["PATH_INFO"]}"]] } end map '/everyone' do run lambda {|env| [200, {"Content-type" => "text/html"}, ["from everyone", "SCRIPT_NAME=#{env["SCRIPT_NAME"]}", "PATH_INFO=#{env["PATH_INFO"]}"]] } end map '/' do run lambda {|env| [200, {"Content-type" => "text/html"}, ["from hello catch all", "SCRIPT_NAME=#{env["SCRIPT_NAME"]}", "PATH_INFO=#{env["PATH_INFO"]}"]] } end end map '/' do run lambda {|env| [200, {"Content-type" => "text/html"}, ["root"]] } end
保存上述代码到config.ru文件然后运行 rackup config.ru即可。