您当前的位置:首页 > 计算机 > 编程开发 > Other

深入淺出 rack

时间:12-14来源:作者:点击数:

rack 是一个协定(protocol),是一个接口(interface),来传递 HTTP request/response。透过 rack 能够连结到我们的 Ruby web app。

一个简单的 rack app

现在我们先来写个简单的 rack app,如果你的还没装 rack,可以下指令 gem install rack 来安装。

# first_rack_app.rbrequire "rack"class FirstRackApp  def call(env)    status = 200    headers = {"Content-Type" => "text/html"}    body = ["This is my first web app"]    return [status, headers, body]  endendRack::Handler::WEBrick.run(FirstRackApp.new, Port:3001)

执行 ruby first_rack_app.rb 后,去 http://localhost:3001/ 就能看到 This is my first web app 这行字了,若想要让它中断下指令 ctrl c 即可。

由于 Ruby 可以省略 return 以及 () 的特性,所以这段 code 可以更简洁写成

# first_rack_app.rbrequire "rack"class FirstRackApp  def call(env)    status = 200    headers = {"Content-Type" => "text/html"}    body = ["This is my first web app"]    [status, headers, body]  endendRack::Handler::WEBrick.run FirstRackApp.new, Port:3001

这段code很简单,我写了一个rack app,裡面定义了一个 call method,它回传一个 array 。依序是

  1. Status Code:200
  2. Header:一个hash,我请求的Content-Type是text,于是我可以在body写一串text
  3. Body:一个array,这array裡面有著一串string

若是不懂 Status code、Header 是什麽,可以参考下面两篇

于是,你可以在一些 rack 文章上面看到更简洁的写法

# first_rack_app.rbrequire "rack"class FirstRackApp  def call(env)    [200, {"Content-Type" => "text/html"}, ["This is my first web app"]]  endendRack::Handler::WEBrick.run FirstRackApp.new, Port:3001

rack app的结构很简单:

  • 一个 call method
  • 回传一个array
    • HTTP status
    • header
    • body

env 是什麽?

我们来改一下code,看看这个 env 是什麽吧

# first_rack_app.rbrequire "rack"class FirstRackApp  def call(env)    [200, {}, [env.inspect]]  endendRack::Handler::WEBrick.run FirstRackApp.new, Port:3001

接著,我们在Terminal下开一个分页,然后到 first_rack_app.rb 所在的路径位置,下指令 curl http://localhost:3001/ ,于是我们看到了一大串hash

$ curl http://localhost:3001/{  "GATEWAY_INTERFACE"=>"CGI/1.1",  "PATH_INFO"=>"/",  "QUERY_STRING"=>"",  "REMOTE_ADDR"=>"::1",  "REMOTE_HOST"=>"::1",  "REQUEST_METHOD"=>"GET",  "REQUEST_URI"=>"http://localhost:3001/",  "SCRIPT_NAME"=>"",  "SERVER_NAME"=>"localhost",  "SERVER_PORT"=>"3001",  "SERVER_PROTOCOL"=>"HTTP/1.1",  "SERVER_SOFTWARE"=>"WEBrick/1.3.1(Ruby/2.4.0/2016-12-24)",  "HTTP_HOST"=>"localhost:3001",  "HTTP_USER_AGENT"=>"curl/7.43.0",  "HTTP_ACCEPT"=>"*/*",  "rack.version"=>[1, 3],  "rack.input"=>#<StringIO:0x007fd4a894fde8>,  "rack.errors"=>#<IO:<STDERR>>,  "rack.multithread"=>true,  "rack.multiprocess"=>false,  "rack.run_once"=>false,  "rack.url_scheme"=>"http",  "rack.hijack?"=>true, "rack.hijack"=>#<Proc:0x007fd4a894fb18@/Users/nicholas/.rvm/gems/ruby-2.4.0/gems/rack-2.0.1/lib/rack/handler/webrick.rb:74 (lambda)>,  "rack.hijack_io"=>nil,  "HTTP_VERSION"=>"HTTP/1.1",  "REQUEST_PATH"=>"/"}

env 也就是environment是一个hashenv 被传到 call method裡去,于是Rack environment知道了这个request的很多资讯。我们可以看到,这个request是送出一个 GET 请求( "REQUEST_METHOD"=>"GET" )。,也可以看到我们在code裡定义的port 3001( "SERVER_PORT"=>"3001" ),欲了解这些Rack environment的定义可以看Rack doc

为何 rack app 要定义 call method

上面的范例,我们的rack app裡面,只有定义一个 call method,为何rack要定义这个 call method呢?

在其他Rack的文章你可能会常看到,rack app接一个proc或lambda,像是:

app = proc do |env|  ['200', {'Content-Type' => 'text/html'}, ['Hello, Rack!']]end

application = lambda do |env|  [200, { "Content-Type" => "text/html" }, ["Yay, your first web application! <3"]]end

这就是Rack会定义 call method的主要原因。

当我们的Ruby web app 被Rack app呼叫( call )时,Rack app 的实体(instance)其实是一个proc或lambda instance,然后它调用了proc与lambda的default method,也就是 call 。不得不说Rack的作者真的很聪明,想到这写法。

如果对proc或lambda不熟,可以参考我写的block、proc、yield、lambda之间的关係

更细腻地操作 call :语意化 method 与 path

Rack app裡头只有一个 call method, env 是一大串hash,当我们需要hash里头的一些特定资讯时,我的 call method要怎麽写呢?

在此,以 env 中的 "REQUEST_METHOD""PATH_INFO" 为例,我现在定义一个Rack app

# some_rack_app.rbclass SomeRackApp  def call    handle_request(env['REQUEST_METHOD'], env['PATH_INFO'])  end  private  def handle_request(method, path)    if method == "GET"      get(path)    else      method_not_allowed(method)    end  end  def get(path)    [200, {"Content-Type" => "text/html"}, ["You have requested the path #{path}, using GET"]]  end  def method_not_allowed(method)    [405, {}, ["Method not allowed: #{method}"]]  endend

这段 code 很简单,当 rack app 调用 call 时,会把 env hash得到的 REQUEST_METHODPATH_INFO 传进我们自己定义的 handle_request method裡面。

接著用一个 if..else 做判断,如果 REQUEST_METHOD 是GET,就调用自定义的 get method来回传一组array,如果 REQUEST_METHOD 不是GET,就用自定义的 method_not_allowed ,回传另一组array

如果我们把所有的逻辑都写在 call 裡头,时间久了后再去读code就很难读,但我们把它语意化包成一个个method,就会非常直觉了

private 在Ruby裡面是一个method,在Ruby的 private method其实不只Class自己内部可以存取,它的SubClass也可以存取。

如果对private的意义不熟,可以参考这篇文章

Rack::Handler

回到最前面最简单的rack app, first_rack_app.rb 最结尾有一行 Rack::Handler::WEBrick.run FirstRackApp.new, Port:3001 ,这个 Rack::Handler 是什麽? WEBrick 是什麽?

Rack用handler来跑( run ) Rack app,在此我们选的是 WEBrick 这个handler。

我们可以在env hash中看到 "SERVER_SOFTWARE"=>"WEBrick/1.3.1(Ruby/2.4.0/2016-12-24)"

WEBrick是ruby内建的HTTP server,其他还有些常见的还有:Puma、Unicorn, Thin, Apache via Passenger...。

假如我们要用Puma的话,就写

require "rack/handler/puma"...Rack::Handler::Puma.run FirstRackApp.new, Port:3001

Rack::Builder 与  config.ru

原本要启动rack app,必须写一个Ruby档,然后 require "rack" ,启动时也要写一大串 Rack::Handler::WEBrick.run

若想要用更简洁地写法启动rack app,则我们可以透过 Rack::Builder 。首先新增一个 config.ru

# config.ruclass FirstRackApp  def call(env)    [200, {"Content-Type" => "text/html"}, ["This is my first web app"]]  endendrun FirstRackApp.new

然后在 config.ru 所在的位置,下指令 rackup -p 3001 ,接著去浏览器 http://localhost:3001/ 就能看到 "This is my first web app"

config.ru 中的 ru 就是指 rackup ,所以才会在Rebuilding rails上看到用 rackup -p 3001 这指令来启动我们的Ruby web app。

Rack::Builder is a pattern that allows us to more succinctly define our application and the middleware stack surrounding it.

ref

推荐这篇文章 Rack::Builder 这节,说明了不用 config.ru 是怎麽写 Rack::Builder

in config.ru , use adds middleware to the stack, run dispatches to an application. You can use map to construct a Rack::URLMap in a convenient way.

ref

这样写,并不只是为了更简洁,而是要带入middleware的概念

Middleware 的概念

Middleware本身就是一个rack app。

config.ru 裡可以用 use 调用先前编写的一支支Middlewares,例如:

# config.rurequire './app.rb'require './middleware1.rb'require './middleware2.rb'require './middleware3.rb'use Middleware1use Middleware2use Middleware3run App.new

如上面的code所示,Rack::Builder依序使用了三个middleware,最后才执行App。用这样的方式产生出一个Rack app

这段code的效果等同如下

rack_app = Middleware1.new(Middleware2.new(Middleware3.new(App)))

文章开头时,我们看到下面这张图,帮助我们理解rack与user以及我们的Ruby web app的关係。

我们可以看到user送出的request先通过rack server然后才送到Ruby web app。

也就是说,user送出的request先经过rack app的处理后,才送进我们的ruby web app,透过下面这张图,能直观地了解rack app做了什麽事

一层又一层的Middleware拦截了我们的request并对他做处理与修改,如此一来Ruby web app所收到的request,就只会有这app所需要的资讯,而不会有其他多馀的资讯。

这样的模式称之为pipeline design pattern,推荐阅读下面两篇,尤其是维基百科的那张图,看了会很有感觉

Rack middleware is more than "a way to filter a request and response" - it's an implementation of the pipeline design pattern for web servers using Rack.

It very cleanly separates out the different stages of processing a request - separation of concerns being a key goal of all well designed software products.

Middleware 与 config.ru

config.ru 裡,可以用 runmapuse 这三个method

  • run :执行一个proc或lambda的instance
  • use :告诉我们的Rack app要使用什麽middleware
  • map :告诉Rack app什麽路径( path ),要用什麽middleware与rack app来处理

在这边给一个 map 很具体的例子

# config.rurequire './main_rack_app.rb'require './admin_rack_app.rb'require './first_middleware.rb'require './second_middleware.rb'map '/' do  use FirstMiddleware  run MainRackApp.newendmap '/admin' do  use SecondMiddleware  run AdminRackApp.newend

当送过来的requets,它的 "REQUEST_PATH"=>"/" 就给 FirstMiddlewareMainRackApp 处理。

当送过来的requets,它的 "REQUEST_PATH"=>"/admin" 就给 SecondMiddlewareAdminRackApp 处理。

如此一来,我们就能依照不同路由,来给我们的request通过不同的middleware与rack app进行处理。

rack也有提供不少包好的middleware让人使用

Middleware 的具体细节

这边就直接给一个简单的例子,延续先前的 config.ru 范例

原本我们知道的 config.ru 大概会这样写

# 原始config.ruclass MyApp  def call(env)    [200, {"Content-Type" => "text/html"}, ["This is my app"]]  endendrun FirstRackApp.new

现在我把rack app的部分独立成 my_app.rb

# my_app.rbclass MyApp  def call(env)    [200, {"Content-Type" => "text/html"}, ["This is my app."]]  endend

这个时候, config.ru 应该改写成

# config.rurequire "./my_app.rb"app = MyApp.newrun app

接著我写一个简单的middleware, my_app_middleware.rb

Middleware会用class包起来,先定义 initialize method,再定义 call method。

# my_app_middleware.rbclass MyAppMiddleware  def initialize(app)    @app = app  end  def call(env)    code, headers, body = @app.call(env)    message = "This is my app middleware."    body << message    [code, headers, body]  endend

如上面code所示,我先透过 initialize method去捞出rack app,接著再定义 call method。

在middleware的 call method裡,一开始先用 @app.call 去捞出我原本在 my_app.rb 裡定义call食捞出的array,然后存成 codeheadersbody

接著我塞一串字串进到body裡去,最后再回传一个array。

我回到 config.ru 来使用我刚刚写好的middleware

require "./my_app.rb"require "./my_app_middleware.rb"app = MyApp.newuse MyAppMiddlewarerun app

然后一样下指令 rackup -p 3001 ,接著你就能在 http://localhost:3001/ 看到 " This is my app.This is my app middleware. "

再来个 map 的具体实例吧。

现在我创造另一个middleware hello_nick_middleware.rb

# hello_nick_middleware.rbclass HelloNickMiddleware  def initialize(app)    @app = app  end  def call(env)    code, headers, body = @app.call(env)    body <<  "Hello, Nick"    [code, headers, body]  endend

然后再修改一下 config.ru

# config.rurequire "./my_app.rb"require "./my_app_middleware.rb"require "./hello_nick_middleware.rb"require "./sao.rb"app = MyApp.newsao = SAO.newmap '/' do  use MyAppMiddleware  run appendmap '/nick' do  use HelloNickMiddleware  run saoend

rackup 之后,你可以去 http://localhost:3001/nick 看到我们第二个middleware所插入进去的字串

另外要注意一下, config.ru 的top-level context只能有一个 run ,像是

# config.ru... #上面略map '/admin' do  use MyAppMiddleware  run appendmap '/nick' do  use HelloNickMiddleware  run saoendrun myapp

top-level所 run 的rack app,会预设是 / 路径的request,依此逻辑在top level裡 run 两个不同的rack app就没有意义了。

若不清楚什麽是top level可以参考下面两篇文章

一些常用的东西

在前面的“更细腻地操作 call :语意化method与path”这节,曾看到用 env["REQUEST_METHOD"] 来查看用什麽request method,rack提供更便利的方式来操作 env 这hash裡的 key-value pairs。

request = Rack::Request.new(env)

Rack::Request 提供了一些method让我们不用直接操作 env 的hash

# 等同于 env["REQUEST_METHOD"]request.request_method# 等同于 env["rack.request.query_hash"] + env["rack.input"]request.params# 等同于 env["REQUEST_METHOD"] == "GET"request.get?

最后简单两个范例常见的 Rack:RequestRack:Response

Rack:Request 的例子

建一个 my_app1.rb

require 'rack'class MyApp1  def call(env)    # 创建一个request对象,就能享受Rack::Request的便捷操作了    @request = Rack::Request.new env    method = @request.request_method    info = @request.path_info    puts "This website request method is `#{method}` "    puts "This website path info is `#{info}`"    [200, {}, ['hello world']]  endendRack::Handler::WEBrick.run MyApp1.new, Port:3001

执行 ruby my_app1.rb 然后去 http://localhost:3001/ ,接著你可以在Terminal看到

$ ruby my_app1.rb[2017-03-13 16:14:58] INFO  WEBrick 1.3.1[2017-03-13 16:14:58] INFO  ruby 2.2.2 (2015-04-13) [x86_64-darwin14][2017-03-13 16:14:58] INFO  WEBrick::HTTPServer#start: pid=61940 port=3001This website request method is `GET`This website path info is `/`localhost - - [13/Mar/2017:16:15:01 CST] "GET / HTTP/1.1" 200 11- -> /This website request method is `GET`This website path info is `/favicon.ico`

我们确实可以看到这个request用的method与path information

This website request method is `GET`This website path info is `/`

PS:会看到 /favicon.ico 是因为浏览器会去抓网站的logo,可以参考下面两篇

Rack::Response 的例子

建一个 my_app2.rb

require 'rack'class MyApp2  def call(env)    # 创建一个request对象,就能享受Rack::Request的便捷操作了    @response = Rack::Response.new env    @response.set_cookie('token', 'xxxxxxx123')    @response.headers['Content-Type'] = 'text/plain'    puts @response.inspect    [200, {}, ['hello world']]  endendRack::Handler::WEBrick.run MyApp2.new, Port:3001

执行 ruby my_app2.rb 然后去 http://localhost:3001/ ,接著你可以在Terminal看到很长一大串

$ ruby my_app2.rb[2017-03-13 16:33:03] INFO  WEBrick 1.3.1[2017-03-13 16:33:03] INFO  ruby 2.2.2 (2015-04-13) [x86_64-darwin14][2017-03-13 16:33:03] INFO  WEBrick::HTTPServer#start: pid=62319 port=3001#<Rack::Response:0x007f957b99b520 @status=200, @header={"Content-Length"=>"7805", "Set-Cookie"=>"token=xxxxxxx123", "Content-Type"=>"text/plain"}, @writer=#<Proc:0x007f957b99aad0@/Users/nicholas/.rvm/gems/ruby-2.4.0/gems/rack-2.0.1/lib/rack/response.rb:32 (lambda)>,......(后面省略)

不过你可以看到,我们在程式裡写的cookie与header都成功写到response裡去了

更多 method 可以参考 Rack doc

相关链接

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
    无相关信息
栏目更新
栏目热门
本栏推荐