编写扩展

Sinatra 包含一个 API,供扩展作者使用,以帮助确保为应用程序开发人员提供一致的行为。

背景

编写良好的扩展需要了解 Sinatra 的内部设计。本节将概述 Sinatra 设计核心中的类和习惯用法。

Sinatra 有两种不同的使用模式,扩展应该注意这些模式

  1. “经典”风格,其中应用程序在主/顶层定义 - 大多数示例和文档都针对这种用法。经典应用程序通常是单文件、独立的应用程序,可以直接从命令行运行,或者使用最小的 rackup 文件运行。当经典应用程序需要扩展时,期望所有扩展功能都应该存在,而无需应用程序开发人员进行额外的设置(例如包含/扩展模块)。

  2. “模块化”风格,其中 Sinatra::Base 被显式地子类化,并且应用程序在子类的范围内定义。这些应用程序通常作为库捆绑在一起,并在更大的基于 Rack 的系统中用作组件。模块化应用程序必须通过在应用程序类的范围内调用 register ExtensionModule 来显式地包含任何所需的扩展。

大多数扩展都与这两种风格相关,但扩展作者必须注意确保扩展在每种风格下都能正常工作。扩展 API (Sinatra.registerSinatra.helpers) 提供给扩展作者,以帮助他们完成此任务。

重要:以下关于 Sinatra::BaseSinatra::Application 的说明仅供背景参考 - 扩展作者无需直接修改这些类。

Sinatra::Base

The Sinatra::Base 类为 Sinatra 应用程序中的所有评估提供上下文。顶层的 DSL 式内容存在于类范围内,而请求级内容存在于实例范围内。

应用程序在 Sinatra::Base 子类的类范围内定义。 “DSL”(例如,getpostbeforeconfigureset 等)只是一组在 Sinatra::Base 上定义的类方法。 扩展 DSL 是通过向 Sinatra::Base 或其子类添加类方法来实现的。 但是,基类不应使用 extend 扩展;为此任务提供了 Sinatra.register 方法(如下所述)。

请求在新的 Sinatra::Base 实例中进行评估 - 路由、前置过滤器、视图、助手和错误页面都共享相同的上下文。 默认的请求级助手方法集(例如,erbhamlhaltcontent_type 等)是在 Sinatra::Base 上或在包含在 Sinatra::Base 中的模块中定义的简单实例方法。 在请求级别提供新功能是通过向 Sinatra::Base 添加实例方法来实现的。

与 DSL 扩展一样,助手模块不应通过扩展作者使用 include 直接添加到 Sinatra::Base 中;为此任务提供了 Sinatra.helpers 方法(如下所述)。

Sinatra::Application

Sinatra::Application 类为经典风格应用程序提供默认的执行上下文。 它是一个简单的 Sinatra::Base 子类,它提供默认选项值和其他针对顶级应用程序定制的行为。 当运行经典风格应用程序时,所有 Sinatra::Application 公共类方法都会导出到顶级。

扩展规则

  1. 切勿直接修改 Sinatra::Base。 您不应包含或扩展、更改选项值或以其他方式修改其行为。 模块化风格应用程序将使用 register 方法在它们的子类中显式包含您的扩展。

  2. 切勿在您的扩展中 require 'sinatra'。 您只需要 require 'sinatra/base'。 这样做的原因是 require 'sinatra' 会触发经典风格 - 扩展永远不会触发经典风格。

  3. 尽可能使用下面描述的 API。您不应该直接包含或扩展模块,也不应该直接在核心 Sinatra 类上定义方法。下面的专用方法会为您处理所有这些操作。

  4. 扩展应该Sinatra 模块下的单独模块中定义。例如,一个添加基本身份验证原语的扩展可能被命名为 Sinatra::BasicAuth

使用 Sinatra.helpers 扩展请求上下文

最常见的扩展类型是在路由、视图和辅助方法中添加方法的扩展。

例如,假设您想编写一个扩展,添加一个 h 方法,该方法可以转义保留的 HTML 字符(例如在其他流行的 Ruby Web 框架中找到的那些字符)。

require 'sinatra/base'

module Sinatra
  module HTMLEscapeHelper
    def h(text)
      Rack::Utils.escape_html(text)
    end
  end

  helpers HTMLEscapeHelper
end

Sinatra.helpers 的调用将模块包含在 Sinatra::Application 中,使模块中定义的所有方法都可用于经典风格的应用程序。在经典风格的应用程序中使用此扩展非常简单,只需需要扩展并使用新方法即可

require 'sinatra'
require 'sinatra/htmlescape'

get "/hello" do
  h "1 < 2"     # => "1 &lt; 2"
end

另一方面,Sinatra::Base 子类必须使用 helpers 方法显式地需要包含模块

require 'sinatra/base'
require 'sinatra/htmlescape'

class HelloApp < Sinatra::Base
  helpers Sinatra::HTMLEscapeHelper

  get "/hello" do
    h "1 < 2"
  end
end

使用 Sinatra.register 扩展 DSL(类)上下文

扩展还可以使用 Sinatra.register 方法扩展 Sinatra 的类级 DSL。这是一个添加 block_links_from 宏的扩展,该宏检查每个请求的引用器以查找应用程序提供的模式,并在检测到匹配时发送 403 Forbidden 响应

require 'sinatra/base'

module Sinatra
  module LinkBlocker
    def block_links_from(host)
      before {
        halt 403, "Go Away!" if request.referer.match(host)
      }
    end
  end

  register LinkBlocker
end

Sinatra.register 将模块中给出的所有公共方法作为类方法添加到 Sinatra::Application 上。它还处理在执行经典风格的应用程序时将公共方法导出到顶层。

经典风格的应用程序将按如下方式使用此扩展

require 'sinatra'
require 'sinatra/linkblocker'

block_links_from 'digg.com'

get '/' do
  "Hello World"
end

模块化风格的应用程序必须在其 Sinatra::Base 子类中显式地注册扩展

require 'sinatra/base'
require 'sinatra/linkblocker'

class Hello < Sinatra::Base
  register Sinatra::LinkBlocker

  block_links_from 'digg.com'

  get '/' do
    "Hello World"
  end
end

设置选项和其他扩展设置

扩展可以通过在扩展模块上定义一个 registered 方法来定义选项、路由、前置过滤器和错误处理程序。 Module.registered 方法在扩展模块被添加到 Sinatra::Base 子类之后立即被调用,并且传递给该模块注册的类。

以下示例创建了一个非常简单的扩展,它添加了基本的会话身份验证支持。 添加了用户名和密码的选项,定义了登录的路由,并提供了用于确定用户是否已授权的辅助方法。

require 'sinatra/base'

module Sinatra
  module SessionAuth

    module Helpers
      def authorized?
        session[:authorized]
      end

      def authorize!
        redirect '/login' unless authorized?
      end

      def logout!
        session[:authorized] = false
      end
    end

    def self.registered(app)
      app.helpers SessionAuth::Helpers

      app.set :username, 'frank'
      app.set :password, 'changeme'

      app.get '/login' do
        "<form method='POST' action='/login'>" +
        "<input type='text' name='user'>" +
        "<input type='text' name='pass'>" +
        "</form>"
      end

      app.post '/login' do
        if params[:user] == options.username && params[:pass] == options.password
          session[:authorized] = true
          redirect '/'
        else
          session[:authorized] = false
          redirect '/login'
        end
      end
    end
  end

  register SessionAuth
end

一个经典的应用程序将通过要求扩展库、覆盖选项和使用提供的辅助方法来使用此扩展。

require 'sinatra'
require 'sinatra/sessionauth'

set :password, 'hoboken'

get '/public' do
  if authorized?
    "Hi. I know you."
  else
    "Hi. We haven't met. <a href='/login'>Login, please.</a>"
  end
end

get '/private' do
  authorize!
  'Thanks for logging in.'
end

模块化应用程序的不同之处在于它必须显式注册扩展模块。

require 'sinatra/base'
require 'sinatra/sessionauth'

class MyApp < Sinatra::Base
  register Sinatra::SessionAuth

  set :password, 'hoboken'

  get '/public' do
    if authorized?
      "Hi. I know you."
    else
      "Hi. We haven't met. <a href='/login'>Login, please.</a>"
    end
  end

  get '/private' do
    authorize!
    'Thanks for logging in.'
  end
end

构建和打包扩展

Sinatra 扩展应该构建为单独的库,并打包为 gem 或单个文件,这些文件可以包含在应用程序的 lib 目录中。 使用扩展的理想过程是安装 gem 并要求单个文件。

以下是一个典型的打包为 gem 的扩展的文件布局示例。

sinatra-fu
|-- README
|-- LICENSE
|-- Rakefile
|-- lib
|   `-- sinatra
|       `-- fu.rb
|-- test
|   `-- spec_sinatra_fu.rb
`-- sinatra-fu.gemspec