[转]构建 Rack-based 框架 (Rails/Grape/Sinatra) 的几个 Tricks

Posted by baicai on November 25, 2015

原文请前往Let’s Build Sinatra

曾经看过一本书《Rebuilding Own Rails》</a>,土豪请支持作者。对于中国读者来说,此书确实挺贵。

看了这两本书以后,略作总结,顺便和大家分享一下。

Trick #1 Rack

无论是Rails、还是Sinatra这些基于Rack的框架,原理都是调用Rack的call函数,此函数接受一个参数,env。 Rack的基本原理,是把对Rack对象发送HTTP请求的“环境“, 变量env,作为参数来调用call方法,然后把返回值当作是对请求的回应。 在我们生成的Rails应用程序里面,有个文件<strong>config.ru</strong>,这个文件就是为Rack准备的。 我们可以试试如下代码(节选至《代码的未来》,210页):

# hello.rb
class HelloApp
   def call(env)
       [ 200,                                  # 依次是 HTTP 状态码,200表示成功;
         {"Content-Type" => "text/plain"},    #  HTTP 响应头部(即HTML文件中header部分),放在Hash中;
         ["Hello, Rack World!"]              # HTTP 响应主体(即HTML文件中body部分,也就是我们打开浏览器看到的内容)
                                                  #必须是字符串数组。(为什么是字符串数组,而不是字符串?)
       ]       
   end
end

 

call函数的返回值,最后一个body部分,因为在Ruby 1.9里,如果是字符串,就会导致程序崩溃,所以必须是Enumerator类型的,(可接受each方法),具体前往Rack SPEC

另外需要准备一个配置文件,以<strong>.ru</strong>结尾

# hello.ru
require  'rubygems'
require 'rack'
require 'hello'

#至为关键的一句
run HelloApp.new

 

然后在命令行终端运行:

$ rackup hello.ru

在Rails项目里,相应的配置文件名为<strong>config.ru</strong>

# config.ru

# This file is used by Rack-based servers to start the application.

require ::File.expand_path('../config/environment',  __FILE__)   # 包含启动文件

run Rails.application  #启动程序

 

如果你在Rails应用的目录下运行以下命令,会有如下输出:

diancai.in master % rackup config.ru
[2015-10-12 21:36:37] INFO  WEBrick 1.3.1
[2015-10-12 21:36:37] INFO  ruby 2.2.3 (2015-08-18) [x86_64-darwin15]
[2015-10-12 21:36:37] INFO  WEBrick::HTTPServer#start: pid=6490 port=9292

神奇吧!

好了,说完Rack的基础知识,再看看Sinatra是怎么使用Rack的。

Trick #2 Application = Sinatra::Base.new

这个是用来简化代码,让框架看起来更像是DSL。 以下代码来自于<a href=”https://robots.thoughtbot.com/lets-build-a-sinatra” target=”_blank”>Let’s Build Sinatra</a>

module Nancy
     class Base
     end

     Application = Base.new
end

 

这个其实很容易理解,但是对简化代码来说,不得不说是一个巧妙的设计。

Trick #3 Delegator

这一点稍微有点让人费解

module Nancy
  module Delegator
    def self.delegate(*methods, to:)
      Array(methods).each do |method_name|
        define_method(method_name) do |*args, &block|
          to.send(method_name, *args, &block)
        end

        private method_name
      end
    end

    delegate :get, :patch, :put, :post, :delete, :head, to: Application
  end
end

 

这段代码采用元编程的技术,让我们在调用函数时不必这样写:

Application.get "/", {}

而可以直接写成


get '/', {}

上面的代码利用define_method动态定义了:get, :patch等,而且把这些方法作为Application (即:Nancy::Base.new实例的实例方法)。

它的真正的trick在于,在利用这个框架,写的应用程序, 如:

# app.rb
# run with `ruby app.rb`
require "./nancy"

get "/" do
  "Hey there!"
end

 

中,表面上好像直接调用Object的get方法,于是你查看所有ruby文档也没有get这个方法,你可能有点崩溃了,不知道它是来自于那里,干什么用的。 其实,就是因为在nancy.rb里面有一句<code>include Nancy::Delegator</code>, 我们把<code>get</code>方法补全,就一下看明白这个故意设计的trick;

# app.rb
# run with `ruby app.rb`
require "./nancy"

Nancy::Application.get "/" do
  "Hey there!"
end

 

或者更近一步:

# app.rb
# run with `ruby app.rb`
require "./nancy"

Nancy::Base.new.get "/" do
  "Hey there!"
end

 

这样写,就清楚明了了,但是可能写两句你就很不乐意了。

总结,无论是Rails,还是Sinatra,在使用了这些元编程技术之后,让我们的代码更加简洁,也让我们对于代码所要完成的任务更加清晰明了。