今天是伟大的爵士乐大师法兰克.辛纳区(Frank Sinatra)诞辰一百周年。天时地利人和,正是翻译这篇文章的好日子,错过再等一百年。git
杭州此刻在下雨,阴冷潮湿。耳边是摇曳的爵士乐,我把猫关进了阳台,打开电脑,开始胡说八道。github
(能够跳过正文直接看最后的完整代码)ruby
原文连接:https://robots.thoughtbot.com/lets-build-a-sinatraapp
Sinatra
是一个基于Ruby的快速开发Web应用程序基于特定域(domain-specific)语言。在一些小项目中使用事后,我决定一探究竟。dom
Sinatra的核心是Rack。我写过一篇文章关于Rack,若是你对Rack的工做原理有些困惑,那篇文章值得一读。Sinatra在Rack之上提供了一个给力的DSL。来看个例子:curl
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的结构。post
咱们将创造一个基于Sinatra那种可继承可扩展的类。它保存请求路由表(GET /hello handleHello),当收到GET /hello
请求的时候,能去调用handleHello
函数。事实上,它能很好的处理全部的请求。当收到请求的时候,它会遍历一遍路由表,若是没有合适的请求,就返回404。测试
就叫它Nancy
吧,别问我为何。fetch
第一步要作的事是:建立一个类,它有一个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\> } }
这个返回结果,咱们的路由表工做的很好。
如今咱们给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状态码和错误信息。
nancy.get
如今只能获得路径,但要想正常工做,它须要获得更多的信息,好比请求的参数等。有关请求的环境变量被封装在Rack::Request
的params
中。
咱们给Nancy::Base
增长一个新的方法params
:
module Nancy class Base # # ...other methods.... # def params @request.params end end end
须要这些请求信息的回调处理中,能够访问这个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
,有关请求的信息,都会被打印出来。
到目前为止,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
在使用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
中是做为类方法实现的,这也是须要注意的一点。