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

rocLv · 2015年10月12日 · 最后由 roclv 回复于 2015年11月22日 · 7528 次阅读
本帖已被管理员设置为精华贴

原文请前往Let's Build Sinatra

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

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

Trick #1 Rack

无论是 Rails、还是 Sinatra 这些基于 Rack 的框架,原理都是调用 Rack 的 call 函数,此函数接受一个参数,env。 Rack 的基本原理,是把对 Rack 对象发送 HTTP 请求的“环境“,变量 env,作为参数来调用 call 方法,然后把返回值当作是对请求的回应。 在我们生成的 Rails 应用程序里面,有个文件config.ru,这个文件就是为 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

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

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

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

然后在命令行终端运行:

$ rackup hello.ru

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

# 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。 以下代码来自于Let's Build Sinatra

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里面有一句include Nancy::Delegator, 我们把get方法补全,就一下看明白这个故意设计的 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,在使用了这些元编程技术之后,让我们的代码更加简洁,也让我们对于代码所要完成的任务更加清晰明了。

thoughtbot 那篇看完了 sinatra 的源码结合社区的几篇帖子一下就懂了~~~

经常说 web 框架是MVC架构的. 其实,我们应该说是RMVC. 即前面加上路由router. 可以MVC分得不清不楚,但是你得通过router解析请求,导向不同的 C, 而后由 C 找 V, 中间再用到 M. 如果是ruby社区来谈这个话题,那应该是RRMVC, 一切前面再加上个Rack才是正解。 好吧,你们说单只是这样,没有数据库交互的本质上都是静态站,功能上真是弱爆了。你说得加上ORM, ActiveRecord, Sequel, Mongoid之类的都是干这事的。ORM 上没法一个字母就给总结出来,本质上都与数据库相关,用 D(atabase) 表示。 于是我们的领域进一步拓展,变成了RRMVCD

#2 楼 @suffering 你说的 router 和 Database 都是构成 MVC 的构建。 怎么样把一个 MVC 框架组合起来?这时候需要 router; 对于 Model 来说,Database 也是它的一部分。 所以直接说 MVC 框架我觉得还是更贴切些。

#3 楼 @roclv 并不是这个意思呢。我这只是吐槽。自己试着去实现MVC框架这种事我也干过,发现来来去去,核心就那几个。处理好几个核心 feature , 一个理论上的 MVC 框架就出来了。这个是我的一个玩具项目 https://github.com/suffering/learnmvc

#4 楼 @suffering 恩,是的。除了对自己的锻炼外,关键是新的框架能解决什么问题?重框架学习成本高,但是一旦掌握了,很多东西都是现成的; 轻框架,学习成本低,但是很多东西要自己完成。

表示写过一个 100 行类 rails 框架:myrails

def self.delegate(*methods, to:)

这个 to 的用法第一次看到,是不是 ruby 该补课了。

#7 楼 @5swords 和下面的代码是一样的,不算是新内容。

def self.delegate(*methods, {  to:  "" })

Hash 作为最后的参数时,可以省略 { } 因为不需要传入默认值,所以就写成这样了。

《Rebuilding Rails》的作者是前同事啊!https://www.linkedin.com/in/noahgibbs

#8 楼 @roclv 这个能理解,但是看下面 to 拿过来直接当变量用,没有 hash 的样子啊。

def testit(*params, a:, b:)
  puts "params:#{params}"
  puts "a:#{a}, b:#{b}"
end

testit 'a',1,2,b:1,a:6

# params:["a", 1, 2]
# a:6, b:1

def self.delegate(*methods, to:) 大神 我这里跑 报错啊。。 @roclv

2.0.0 会报错,2.1.5 正常 https://robots.thoughtbot.com/ruby-2-keyword-arguments

#11 楼 @alex_marmot Hash 以前的语法是 { key => value }, 2.1 以后才添加了 { key: value }这种形式的声明。

👍 👍🏻 👍🏼 👍🏽 👍🏾 👍🏿

文章最后不敢苟同,尤其是最后一段代码

#encoding:utf-8

require "./nancy"
$b = Nancy::Base.new

$b.get '/' do
  'Hey there!'
end

$b.post '/login' do
  # do something
end

实用环境这样才是合理的,然而,只是多打三个字符,我却觉得比元编程方式更清楚自己在干什么

#16 楼 @shy07 欢迎不同意见

以前看见 config.ru 觉得挺奇怪,Ruby 文件多是 rb 后缀,后来才知道是 rackup

#19 楼 @pynix 我是 react 党,但我不反对用 jquery 来处理一下 dom 😄

24 楼 已删除

非常赞 读了这些资料后对 Rack 以及 Sinatra 的源代码理解深入了更多 Ruby 的知识也提升不少~

能否把你的标题改为“构建 Rack-based 框架 (Rails/Grape/Sinatra) 的几个 Tricks”

现在你这个标题有点长,导致主页看着不舒服,下面被挤开了。。。

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

建议修改措辞,使语义表达得更加准确。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号