Rails 就是一个 Rack app. 实际上,基本上所有的 Ruby web framework 都是rack app
.
官网中列出的使用 Rack 的 web 框架:
基本上,Ruby 世界的 web, rack 已经一统天下了。有兴趣的童鞋可以看看ruby-toolbox
的web-app-framework
, 看看有哪些没有用到 rack 的:https://www.ruby-toolbox.com/categories/web_app_frameworks
Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between(the so-called middleware) into a single method call.
简单点说,rack 是 Ruby web 应用的简单的模块化的接口。它封装 HTTP 请求与响应,并提供大量的实用工具。它需要一个响应 call 方法的对象,接受 env. 返回三元素的数组:分别是 status code, header, body. 其中 status code 大于等于 100, 小于 600. header 是一个 hash, body 是一个响应 each 方法的数组。
理解上述接口标准,就可以写一个完整的 rack app 了。除了这些,rack 还提供了许多有工具。
gem install rack
gem install pry
pry
require 'rack'
cd Rack
ls
上述得到如下内容:
constants:
Auth CommonLogger Deflater Handler MethodOverride NullLogger Runtime ShowStatus
BodyProxy ConditionalGet Directory Head Mime Recursive Sendfile Static
Builder Config ETag Lint MockRequest Reloader Server URLMap
Cascade ContentLength File Lock MockResponse Request Session Utils
Chunked ContentType ForwardRequest Logger Multipart Response ShowExceptions VERSION
Rack.methods: release version
以上内容,除少部分是 Rack 自身运行的依赖外,大部分都是 Rack 提供的可选模块,它们封装了许多简单实用的方法,比如说处理静态文件,缓存,log, 非法内容 sanitize . 使用它们,Ruby web 开发才不会那么痛苦. 另外,还有许多 Rack middleware. 这里有详细列表: https://github.com/rack/rack/wiki/List-of-Middleware
这里不防用反证法,带大家看看若是没有 Rack, 我们应该如何开发一个 Ruby web app.
如果从零开始了解 web 世界。我们知道,浏览器与服务端通过 HTTP 协议交互。这是一个 request 与 response 的过程。
request 从客户端发出,包含了字符串的头文件,它包括请求的地址,请求方式 (get/post/put/delete 等). 浏览器发送的是格式化的字符串,作为服务端,我们需要分析这段字符串,如果没有 Rack, 我们得自己程序来分析这个 orgin string header.
同样的,按照 http 协议,Response 也是类似的字符串。浏览器接收到它们后,分析并 render, 最终生成页面。没有 rack, 这字符串也得自己来生成。
这里看一个例子,非常有意思的博文。博主去面试,让他写一个简单的 Ruby web server, 要求如下:
总计 4 条要求,其中第一二条实现了,后面两条却失败了 .事后好好研究了下这个,并写出了博客。这个是第三条的实现,就是自己手动输出 response string :
require "socket"
webserver = TCPServer.new('localhost', 2000)
base_dir = Dir.new(".")
while(session = webserver.accept)
session.print "HTTP/1.1 200/OK\r\nContent-type:text/html\r\n\r\n"
request = session.gets
trimmedrequest = request.gsub(/GET\ \//, '').gsub(/\ HTTP.*/, '')
if trimmedrequest.chomp != ""
base_dir = Dir.new("./#{trimmedrequest}".chomp)
end
session.print "
#{trimmedrequest}
"
session.print("#{base_dir}")
if Dir.exists? base_dir
base_dir.entries.each do |f|
if File.directory? f
session.print("<a href="#{f}"> #{f}</a>")
else
session.print("
#{f}
")
end
end
else
session.print("Directory does not exists!")
end
session.close
end
原文地址:A Simple Web Server in Ruby
就像terminal
端的 Ruby code 运行时通过 gets 方法来获取用户的输入一样,真是简陋得可以。
仔细看一下博主的实现,就是将request
当字符串处理 (其实 request 本来就是格式化的字符串), 通过正则来获取其REQUEST_METHOD
, PATH_INFO
等等,将Content-Type
设置为text/html
, 最后将信息打包,作为response
发送给客户端.
至于第四条,则需要手动去设置mimi-type
,这样才能以完整的方式将图片等内容在 web 页面中正常展现。
看看 http request header fields: http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields 数十条,各有其含义。从头至尾写一个 web server, 你得分析它们,作不同的响应。
再看看 http response fields: http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields 同样数十条,浏览器会根据你返回的 header 来决定如何 render page. 如果手动去生成,那是头皮发麻的工作量。
但是,使用 Rack, 以及它提供的工具的话,实现博主接受的考题要求,我们可以这样做,只需要两行代码:
require 'rack'
Rack::Handler::Thin.run Rack::Directory.new('./'), :Port => 9292
当然,是否允许这样做就不得而知了。
rack app
gem install rack
cd to/your/path
touch app.rb
app.rb
内容如下;
#app.rb
require 'rack'
class HelloWorld
def call(env)
[200, {"Content-Type" => "text/html"}, ["Hello Rack!"]]
end
end
Rack::Handler::Mongrel.run HelloWorld.new, :Port => 9292
在 terminal 里运行ruby app.rb
, 而后在浏览器里打开http://localhost:9292
就可以看到返回的内容了。
#config.ru
# 将 body 标签的内容转换为全大写.
class ToUpper
def initialize(app)
@app = app
end
def call(env)
status, head, body = @app.call(env)
upcased_body = body.map{|chunk| chunk.upcase }
[status, head, upcased_body]
end
end
# 将 body 内容置于标签, 设置字体颜色为红色, 并指明返回的内容为 text/html.
class WrapWithRedP
def initialize(app)
@app = app
end
def call(env)
status, head, body = @app.call(env)
red_body = body.map{|chunk| "<p style='color:red;'>#{chunk}</p>" }
head['Content-type'] = 'text/html'
[status, head, red_body]
end
end
# 将 body 内容放置到 HTML 文档中.
class WrapWithHtml
def initialize(app)
@app = app
end
def call(env)
status, head, body = @app.call(env)
wrap_html = <<-EOF
<!DOCTYPE html>
<html>
<head>
<title>hello</title>
<body>
#{body[0]}
</body>
</html>
EOF
[status, head, [wrap_html]]
end
end
# 起始点, 只返回一行字符的 rack app.
class Hello
def initialize
super
end
def call(env)
[200, {'Content-Type' => 'text/plain'}, ["hello, this is a test."]]
end
end
use WrapWithHtml
use WrapWithRedP
use ToUpper
run Hello.new
直接运行rackup
就可以运行上述 app.
use 与 run 本质上没有太大的差别,只是 run 是最先调用的。它们生成一个 statck, 本质上是先调用 Hello.new#call, 而后返回 ternary-array, 而后再将之交给另一个 ToUpper, ToUpper 干完自己的活,再交给 WrapWithRedP, 如此一直到 stack 调用完成。
use ToUpper; run Hello.new
本质上是完成如下调用:
ToUpper.new(Hello.new.call(env)).call(env)
以上只是简单的举例,实际的 web 项目中,有无数的场景需求。
比如说,你可以随时需要更改 status code, 或者,你需要判断当前请求是什么类型的,比如说是 get 还是 post, 在 rails 中 resource 生成的同样的 path,如 /products/:id
可以是get/put/delete
. 但是 Rails 如何知道调用show/update/destroy
中的哪一个?这时,这个时候可以去看看 env['REQUEST_METHOD'], 而后判断。这们做虽然不用去分析原始的 head 文件,但是也是愚蠢的行为,这个时候就可以直接用 rack 提供的一些工具了。
require 'rack'
class Hello
def get_request
@request ||= Rack::Request.new(@env)
end
def response(text, status=200, head={})
raise "respond" if @respond
text = [text].flatten
@response = Rack::Response.new(text, status, head)
end
def get_response
@response || response([])
end
%W{get? put? post? delete? patch? trace?}.each do |md|
define_method md do
get_request.send(md.intern)
end
end
%W{body headers length= status= body= header length
redirect status content_length content_type}.each do |md|
define_method md do |*arg|
get_response.send(md.intern, *arg)
end
end
def call(env)
@env = env
content_type = 'text/plain'
if get?
body= ['you send a get request']
else
status= 403
body= ['we do not support request method except get, please try another.']
end
[status, headers, body]
end
end
Rack::Handler::Thin.run Hello.new, :Port => 9292
若想测试非 get 方法,可以通过curl -X POST http://localhost:9292/
来测试.
以上示例是对 Rack::Request, Rack::Response 的非常愚蠢的封装,只是展示下使用它们的便捷之处。想看高质的封装代码,不仿看看 sinatra 的:
https://github.com/bmizerany/sinatra/blob/work/lib/sinatra/base.rb#L15
Rails 中使用的 rack middleware stack:
cd to/your/rails/project/path
rake middleware
得到的内容如下:
use Rack::Sendfile
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x000000029a0838>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run Rails.application.routes
可以看出,Rails 重度依赖 Rack. 理解 Rails, 不防从 Rack 开始。按上述列表,先从Rails.application.routes
开始
一直走到最后一步.
关于在 Rails 如何使用 rack, 具体请参照 http://guides.rubyonrails.org/rails_on_rack.html
另外这里有xdite
一篇博文,讲的是 Rails 如何支持 Rack, 又为什么要支持 Rack, 非常清晰明了。详细可以看看这里:http://wp.xdite.net/?p=1557
:)
想更深入了解 Rack, 可以参见: http://rack.github.io/ http://wp.xdite.net/?p=1557 http://m.onkey.org/ruby-on-rack-1-hello-rack http://guides.rubyonrails.org/rails_on_rack.html http://rubylearning.com/blog/a-quick-introduction-to-rack/ https://www.digitalocean.com/community/tutorials/a-comparison-of-rack-web-servers-for-ruby-web-applications http://codecondo.com/12-small-ruby-frameworks/ 这里特别推荐 railscasts-china.com 的一篇演讲:http://railscasts-china.com/episodes/the-rails-initialization-process-by-kenshin54