今天是伟大的爵士乐大师法兰克.辛纳区(Frank Sinatra)诞辰一百周年。天时地利人和,正是翻译这篇文章的好日子,错过再等一百年。
杭州此刻在下雨,阴冷潮湿。耳边是摇曳的爵士乐,我把猫关进了阳台,打开电脑,开始胡说八道。
(可以跳过正文直接看最后的完整代码)
原文链接:https://robots.thoughtbot.com/lets-build-a-sinatra
构建一个Sinatra
Sinatra
是一个基于Ruby的快速开发Web应用程序基于特定域(domain-specific)语言。在一些小项目中使用过后,我决定一探究竟。
Sinatra是由什么组成的?
Sinatra的核心是Rack。我写过一篇文章关于Rack,如果你对Rack的工作原理有些困惑,那篇文章值得一读。Sinatra在Rack之上提供了一个给力的DSL。来看个例子:
get "/hello" do
[200, {}, "Hello from Sinatra!"]
end
post "/hello" do
[200, {}, "Hello from a post-Sinatra world!"]
end
当这段代码执行的时候,我们发送一个GET
给/hello
,将看到Hello from Sinatra!
;发送一个POST
请求给/hello
将看到Hello from a post-Sinatra world!
。但这个时候,任何其他请求都将返回404.
结构
Sinatra 的源码,我们一起提炼出一个类似Sinatra的结构。
我们将创造一个基于Sinatra那种可继承可扩展的类。它保存请求路由表(GET /hello handleHello),当收到GET /hello
请求的时候,能去调用handleHello
函数。事实上,它能很好的处理所有的请求。当收到请求的时候,它会遍历一遍路由表,如果没有合适的请求,就返回404。
OK,开搞。
就叫它Nancy
吧,别问我为什么。
第一步要做的事是:创建一个类,它有一个get
方法,能捕获请求的path
并找到对应的函数。
# nancy.rb
require "rack"
module Nancy
class Base
def initialize
@routes = {}
end
attr_reader :routes
def get(path, &handler)
route("GET", path, &handler)
end
private
def route(verb, path, &handler)
@routes[verb] ||= {}
@routes[verb][path] = handler
end
end
end
route
函数接收一个动词(HTTP请求方法名),一个路径和一回调方法并保存到一个Hash结构中。这样设计可以让POST /hello
和GET /hello
不会混乱。
然后在下面加一些测试代码:
nancy = Nancy::Base.new
nancy.get "/hello" do
[200, {}, ["Nancy says hello"]]
end
puts nancy.routes
可以看到,Nancy使用了nancy.get
替代了Sinatra的get
显得没那么简洁,本文最后会解决这个问题。
如果我们这时执行程序,会看到:
{ "GET" =\> { "/hello" =\> \#\<Proc:0x007fea4a185a88@nancy.rb:26\> } }
这个返回结果,我们的路由表工作的很好。
引入了 Rack 的 Nancy
现在我们给Nancy增加调用Rack的call
方法,让它成为一个最小的Rack
程序。这些代码是我的另一篇Rack文章中的:
# nancy.rb
def call(env)
@request = Rack::Request.new(env)
verb = @request.request_method
requested_path = @request.path_info
handler = @routes[verb][requested_path]
handler.call
end
首先,我们从Rack的请求的env环境变量参数中的得到请求方法(HTTP/GET等)和路径(/the/path),然后根据这些信息去路由表中招对应的回调方法并调用它。回调方法需返回一个固定的结构,这个结构包含状态码、HTTP Header和返回的内容,这个结构正是Rack的Call所需要的,它会经由Rack返回给用户。
我们增加一个这样的回调给Nancy::Base
:
nancy = Nancy::Base.new
nancy.get "/hello" do
[200, {}, ["Nancy says hello"]]
end
# This line is new!
Rack::Handler::WEBrick.run nancy, Port: 9292
现在这个Rack App已经能运行了。我们使用WEBrick
作为服务端,它是Ruby内置的。
nancy = Nancy::Base.new
nancy.get "/hello" do
[200, {}, ["Nancy says hello"]]
end
# This line is new!
Rack::Handler::WEBrick.run nancy, Port: 9292
执行ruby nancy.rb
,访问http://localhost:9292/hello
,一切工作的很好。需要注意,Nancy不会自动重新加载,你所做的任何改动都必须重新启动才会生效。Ctrl+C
能在终端中停止它。
错误处理
访问路由表中处理的路径它能正常的工作,但是访问路由表中不存在的路径比如http://localhost:9292/bad
你只能看到Rack返回的默认错误信息,一个不友好的Internal Server Error
页面。我们看下如何自定义一个错误信息。
我们需要修改call方法
:
def call(env)
@request = Rack::Request.new(env)
verb = @request.request_method
requested_path = @request.path_info
- handler = @routes[verb][requested_path]
-
- handler.call
+ handler = @routes.fetch(verb, {}).fetch(requested_path, nil)
+ if handler
+ handler.call
+ else
+ [404, {}, ["Oops! No route for #{verb} #{requested_path}"]]
+ end
end
现在,如果请求一个路由表中没有定义的路径回返回一个404状态码和错误信息。
从 HTTP 请求中得到更多信息
nancy.get
现在只能得到路径,但要想正常工作,它需要得到更多的信息,比如请求的参数等。有关请求的环境变量被封装在Rack::Request
的params
中。
我们给Nancy::Base
增加一个新的方法params
:
module Nancy
class Base
#
# ...other methods....
#
def params
@request.params
end
end
end
需要这些请求信息的回调处理中,可以访问这个params
方法来得到。
访问 params
再来看一下刚刚添加的这个params
实例方法。
修改调用回调这部分代码:
if handler
- handler.call
+ instance_eval(&handler)
else
[404, {}, ["Oops! Couldn't find #{verb} #{requested_path}"]]
end
这里面有一些小把戏让人困惑,为啥要用instance_eval
替代call
呢?
handler
是一个没有上下文的lambda如果我们使用
call
去调用这个lambda,它是没法访问Nancy::Base
的实例方法的。使用
instance_eval
替代call
来调用,Nancy::Base
的实例信息会被注入进去,它可以访问Nancy::Base
的实例变量和方法(上下文)了。
所以,现在我们能访问params
在handler block中了。试试看:
nancy.get "/" do
[200, {}, ["Your params are #{params.inspect}"]]
end
访问http://localhost:9292/?foo=bar&hello=goodbye
,有关请求的信息,都会被打印出来。
支持任意的 HTTP 方法
到目前为止,nancy.get
能正常的处理GET请求了。但这还不够,我们要支持更多的HTTP
方法。支持它们的代码和get
很相似:
# nancy.rb
def post(path, &handler)
route("POST", path, &handler)
end
def put(path, &handler)
route("PUT", path, &handler)
end
def patch(path, &handler)
route("PATCH", path, &handler)
end
def delete(path, &handler)
route("DELETE", path, &handler)
end
通常在POST
和PUT
请求中,我们会想访问请求的内容(request body)。既然现在在回调中,我们已经可以访问Nancy::Base的实例方法和变量了,让@request变得可见就好(迷糊的去翻上面的call方法代码):
attr_reader :request
访问requrest
实例变量在回调中:
nancy.post "/" do
[200, {}, request.body]
end
访问测试:
$ curl --data "body is hello" localhost:9292
body is hello
现代化进程
我们来做以下优化:
使用params实例方法来替代直接调用request.params
def params
request.params
end
允许回调方法返回一个字符串
if handler
- instance_eval(&handler)
+ result = instance_eval(&handler)
+ if result.class == String
+ [200, {}, [result]]
+ else
+ result
+ end
else
[404, {}, ["Oops! Couldn't find #{verb} #{requested_path}"]]
end
这样处理回调就简化很多:
nancy.get "/hello" do
"Nancy says hello!"
end
使用代理模式继续优化 Nancy::Application
在使用Sinatra
的时候,我们使用get
,post
来进行请求处理优雅强大又直观。它是怎么做到的呢?先考虑Nancy的结构。它执行的时候,我们调用Nancy::Base.new
得到一个新的实例,然后添加处理path的函数,然后执行。那么,如果有一个单例,就可以实现Sinatra
的效果,将文件中处理路径的方法添加给这个单例并执行即可。(译者注:这段的译文和原文没关系,纯属杜撰。如果迷惑,请参考原文)
是时候考虑将nancy.get
优化为get
了。
增加Nancy::Base
单例:
module Nancy
class Base
# methods...
end
Application = Base.new
end
增加回调:
nancy_application = Nancy::Application
nancy_application.get "/hello" do
"Nancy::Application says hello"
end
# Use `nancy_application,` not `nancy`
Rack::Handler::WEBrick.run nancy_application, Port: 9292
增加代理器(这部分代码来自Sinatra的源码):
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
引入Nancy::Delegate
到 Nancy
模块:
include Nancy::Delegator
Nancy::Delegator
提供代理如get
,patch
,post
,等一系列方法。当在Nancy::Application
中调用这些方法的时候,它会按图索骥找到代理器的这些方法。我们实现了和Sinatra一样的效果。
现在可以删掉那些创建Nancy::Base::new
和nancy_application
的代码啦!Nancy的使用已经无限接近Sinatra了:
t "/bare-get" do
"Whoa, it works!"
end
post "/" do
request.body.read
end
Rack::Handler::WEBrick.run Nancy::Application, Port: 9292
还能使用rackup
来进行调用:
# config.ru
require "./nancy"
run Nancy::Application
Nancy的完整代码:
# nancy.rb
require "rack"
module Nancy
class Base
def initialize
@routes = {}
end
attr_reader :routes
def get(path, &handler)
route("GET", path, &handler)
end
def post(path, &handler)
route("POST", path, &handler)
end
def put(path, &handler)
route("PUT", path, &handler)
end
def patch(path, &handler)
route("PATCH", path, &handler)
end
def delete(path, &handler)
route("DELETE", path, &handler)
end
def head(path, &handler)
route("HEAD", path, &handler)
end
def call(env)
@request = Rack::Request.new(env)
verb = @request.request_method
requested_path = @request.path_info
handler = @routes.fetch(verb, {}).fetch(requested_path, nil)
if handler
result = instance_eval(&handler)
if result.class == String
[200, {}, [result]]
else
result
end
else
[404, {}, ["Oops! No route for #{verb} #{requested_path}"]]
end
end
attr_reader :request
private
def route(verb, path, &handler)
@routes[verb] ||= {}
@routes[verb][path] = handler
end
def params
@request.params
end
end
Application = Base.new
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
include Nancy::Delegator
Nancy的使用代码:
# app.rb
# run with `ruby app.rb`
require "./nancy"
get "/" do
"Hey there!"
end
Rack::Handler::WEBrick.run Nancy::Application, Port: 9292
我们来回顾一下都发生了什么:
起名为N(T)an(i)c(r)y Sinatra(别问为什么)
实现一个以来Rack的Web App
简化
nancy.get
为get
支持子类化
Nancy::Base
来实现更丰富的自定义。
课外阅读
Sinatra的代码几乎全部都在base.rb
。代码密度有点大,阅读完本文再去看,更容易理解一些了。从call!
开始是个不错的选择。然后是Response
类,它是Rack::Response
的子类,请求返回的信息封装在这里。还有Sinatra
是基于类的,Nancy
是基于对象,有些在Nancy
中的示例方法,在Sinatra
中是作为类方法实现的,这也是需要注意的一点。