你可能听说过 Rails 、Sinatra 这些 Web 框架,也使用过其中一二。但如果你不知道 Rack ,那只能说你 Web 开发停留在表面上。Rack 是这些 Web 框架的基础,它们正是构建在 Rack 之上。了解 Rack 可以让更好你理解 Rails、Sinatra 这些 Web 框架背后运作机制,处理问题时得心应手。下面就开始我们的 Rack 之旅吧~
什么是 Rack Rack 提供一组轻量接口,实现了 Web 服务器与 Rack 应用间的交互。其交互过程下图所示:
当用户在 User Agent 发起 HTTP 请求后,Rack Web Server 接收请求并调起 Rack App (如 Rails、Sinatra 等)响应, 最后 Web Server 将响应返回给 User Agent。
那么一个 Rack 应用如何实现呢?下面是一个最简单的 Rack 应用:
1 2 3 4 5 app = proc do |env| [200 , {'Content-Type' => 'text/plain' }, ['hello rack' ]] end
一个 Rack 应用是一个能响应 call 方法的对象,其中 call 方法接收一个 env 参数,并返回含有三个元素数组:
第一个元素是 HTTP 状态码
第二个元素是 HTTP Headers,使用 hash 定义
第三个元素是 HTTP Body,使用一个响应 each 方法的对象定义
为了让上面的 Rack 应用能成功运行,还需加上两行代码,引入 Rack Server,代码如下:
1 2 3 4 5 6 7 8 9 require 'rack' app = proc do |env| [200 , {'Content-Type' => 'text/plain' }, ['hello rack' ]] end Rack::Handler::WEBrick.run(app)
如果你已经安装过 Rails、Sinatra,那么 rack 已被默认安装了。还没有? 赶紧 gem install rack
吧。
执行 ruby app.rb
,访问 localhost:8080 hello rack ~
Rack Middleware Rack 真的怎么这么简单么? 其实从 Rack 应用定义中已经透露了 Rack 无限的可能性(Rack Middleware):
任何能响应 call 方法并接收指定参数,返回特定数组的对象都是 Rack 应用
下面是一个简单的 Rack Middleware
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Timing def initialize (app) @app = app end def call (env) ts = Time.now status, headers, body = @app.call(env) ts = Time.now - ts [status, headers, ["app call #{ts} s" ]] end end
在 Rack app 中使用 Timing Middleware
1 2 3 4 5 6 7 8 9 10 require 'rack' require './timing' app = proc do |env| [200 , {'Content-Type' => 'text/plain' }, ['hello rack' ]] end Rack::Handler::WEBrick.run(Timing.new(app))
快试试我们 Rack 应用起了什么变化吧~
在上面例子中, Timing Middleware 包装了原本的 Rack app,其接管了从 Rack Server 发给 Rack app 的请求。当 Timing 接收到 Rack Server 请求后,记录当前时间并调用 Rack app 响应,最后将执行时间显示处理。
从中我们可发现,Rack Middleware 其实也是一个 Rack 应用,其可以一层层嵌套。最外层的 Middleware 最早接收到 Rack Server 请求,最后作出响应。故在 Rack Middleware 栈中最上的 Middleware 更具有话语权。
利用 Rack Middleware 我们可以很好地实现用户鉴权等各种各样的业务场景。
Rack::Builder、rackup Rack::Builder 是 Rack 内置的模块,通常与命令 rackup 一起使用。
让我们用 Rack::Builder 改造下之前 rack app
1 2 3 4 5 6 7 8 9 10 11 require 'rack' require './timing' app = Rack::Builder.new do use Timing run proc {|env| [200 , {'Content-Type' => 'text/plain' }, ['hello rack' ]]} end Rack::Handler::WEBrick.run(app)
上面使用 Rack::Builder 去挂载 Timing、 app。
你说和之前什么差别?接着往下看哈
1 2 3 4 5 6 7 class App def self .call (env) [200 , {'Content-Type' => 'text/plain' }, ['hello rack' ]] end end
1 2 3 4 5 6 7 require './app' require './timing' use Timing run App
代码结构是不是更清晰了,完美~ 赶紧执行 rackup
看看吧
你还可以在 config.ru
中配置路由!!
我们先添加下面的 app2
1 2 3 4 5 6 7 class App2 def self .call (env) [200 , {'Content-Type' => 'text/plain' }, ['Hello App2' ]] end end
在 config.ru
中定义路由 /app2
指向 App2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 require './app' require './app2' require './timing' map '/app2' do run App2 end map '/' do use Timing run App end
这样以 /app2
开头的路由都会去访问 App2,其他路由则访问 App
另外 Rack Middleware 也可添加参数,下面对 Timing 添加 options 参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Timing def initialize (app, options = {}, &block) @app = app @options = options yield if block_given? end def call (env) ts = Time.now status, headers, body = @app.call(env) ts = Time.now - ts [status, headers, ["app run pid #{Process.pid if @options[:pid ]} , executed time #{ts} s" ]] end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 require './app' require './app2' require './timing' map '/app2' do run App2 end map '/' do use Timing, pid: true do puts "hello Timing" end run App end
Rails on Rack 我们之前提到 Rails 是一个 Rack 应用,在 Rails 项目根目录下你可以发现 config.ru
文件
1 2 3 4 5 require_relative 'config/environment' run Rails.application
Rails 没有使用 Middleware? 不存在的
执行 rails middleware
,你会发现 Rails 默认已启用了很多 Middleware
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 use Rack::Sendfile use ActionDispatch::Static use ActionDispatch::Executor use ActiveSupport::Cache::Strategy::LocalCache::Middleware use Rack::Runtime use Rack::MethodOverride use ActionDispatch::RequestId use Sprockets::Rails::QuietAssets use Rails::Rack::Logger use ActionDispatch::ShowExceptions use WebConsole::Middleware use ActionDispatch::DebugExceptions use ActionDispatch::RemoteIp use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::Migration::CheckPending use ActionDispatch::Cookies use ActionDispatch::Session::CookieStore use ActionDispatch::Flash use Rack::Head use Rack::ConditionalGet use Rack::ETag run Milog::Application.routes
Rack Env 前面提到 Rack 应用接收了一个 env 变量作为输入。其实这个 env 变量包含了 HTTP 请求的所有信息。
现在让我们检看下其有什么信息,在 app 中打印 env 信息
1 2 3 4 5 6 7 class App def self .call (env) env.each {|key, value| puts "#{key} =#{value} " } [200 , {'Content-Type' => 'text/plain' }, ['hello rack' ]] end end
其包含以下信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 GATEWAY_INTERFACE=CGI/1.1 PATH_INFO=/favicon.ico QUERY_STRING= REMOTE_ADDR=::1 REMOTE_HOST=::1 REQUEST_METHOD=GET REQUEST_URI=http://localhost:9292/favicon.ico SCRIPT_NAME= SERVER_NAME=localhost SERVER_PORT=9292 SERVER_PROTOCOL=HTTP/1.1 SERVER_SOFTWARE=WEBrick/1.4.2 (Ruby/2.6.0/2018-08-25) HTTP_HOST=localhost:9292 HTTP_CONNECTION=keep-alive HTTP_USER_AGENT=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36 HTTP_ACCEPT=image/webp,image/apng,image/*,*/*;q=0.8 HTTP_DNT=1 HTTP_REFERER=http://localhost:9292/ HTTP_ACCEPT_ENCODING=gzip, deflate, br HTTP_ACCEPT_LANGUAGE=zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7 HTTP_COOKIE=user_locale=zh-CN; Hm_lvt_24f17767262929947cc3631f99bfd274=1532421006,1533536332,1533791498,1533867618; oschina_new_user=false rack.version=[1, 3] rack.input=#<Rack::Lint::InputWrapper:0x000055b88351bc60> rack.errors=#<Rack::Lint::ErrorWrapper:0x000055b88351bc38> rack.multithread=true rack.multiprocess=false rack.run_once=false rack.url_scheme=http rack.hijack?=true rack.hijack=#<Proc:0x000055b88351bf30@/home/jinhu/.rvm/gems/ruby-head/gems/rack-2.0.5/lib/rack/lint.rb:525> rack.hijack_io= HTTP_VERSION=HTTP/1.1 REQUEST_PATH=/favicon.ico rack.tempfiles=[]
可以发现这些信息分为大写的 CGI 变量 以及 rack 变量
References
Rack
Ruby Rack 及其应用 (上)