强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 18 章:Sinatra 轻量 Web

第 18 章:Sinatra 轻量 Web

“少即是多。” —— Ludwig Mies van der Rohe


18.1 Sinatra 概述

18.1.1 为什么选择 Sinatra

特性SinatraRails
学习曲线
项目规模小型/微服务中大型应用
配置几乎无约定配置
启动速度极快较慢
灵活性极高约定优先
ORM自选ActiveRecord
模板多种选择ERB/Haml

18.1.2 安装

gem install sinatra

# 或在 Gemfile 中
gem "sinatra"
gem "sinatra-contrib"  # 扩展功能
gem "puma"             # Web 服务器
gem "thin"             # 备选服务器

18.2 基础应用

18.2.1 最小应用

# app.rb
require "sinatra"

get "/" do
  "Hello, World!"
end

get "/hello/:name" do
  "Hello, #{params[:name]}!"
end
ruby app.rb
# => http://localhost:4567

18.2.2 模块化应用

# app.rb
require "sinatra/base"

class MyApp < Sinatra::Base
  get "/" do
    "Hello from MyApp"
  end

  get "/hello/:name" do
    "Hello, #{params[:name]}!"
  end

  # 只在直接运行时启动服务器
  run! if app_file == $0
end

18.2.3 经典 vs 模块化

特性经典风格模块化风格
定义require "sinatra"require "sinatra/base"
隐式全局继承 Sinatra::Base
运行ruby app.rbrackupruby app.rb
适用快速原型生产应用
测试困难容易

18.3 路由

18.3.1 HTTP 方法路由

# 基本路由
get "/" do
  "GET request"
end

post "/items" do
  "POST request"
end

put "/items/:id" do
  "PUT request for #{params[:id]}"
end

patch "/items/:id" do
  "PATCH request for #{params[:id]}"
end

delete "/items/:id" do
  "DELETE request for #{params[:id]}"
end

# 多方法
route "GET", "/path" do ... end
route ["GET", "POST"], "/path" do ... end

# 所有方法
# Sinatra::Application 在经典模式下

18.3.2 路由参数和通配符

# 命名参数
get "/users/:id" do
  "User: #{params[:id]}"
end

# 多个参数
get "/users/:user_id/posts/:post_id" do
  "User #{params[:user_id]}, Post #{params[:post_id]}"
end

# 通配符
get "/say/*/to/*" do
  # 匹配 /say/hello/to/world
  "Say #{params[:splat][0]} to #{params[:splat][1]}"
end

# 正则路由
get %r{/hello/([\w]+)} do
  "Hello, #{params[:captures][0]}!"
end

# 条件路由
get "/", provides: "json" do
  content_type :json
  { message: "Hello" }.to_json
end

get "/", provides: "html" do
  "<h1>Hello</h1>"
end

18.3.3 路由过滤器

# Before 过滤器
before do
  @start_time = Time.now
  content_type :json
end

# 特定路由的过滤器
before "/admin/*" do
  authenticate!
end

# After 过滤器
after do
  logger.info "#{request.request_method} #{request.path} - #{Time.now - @start_time}s"
end

# 条件过滤器
before "/api/*", provides: :json do
  @format = :json
end

18.4 请求和响应

18.4.1 请求对象

get "/request_info" do
  {
    method:    request.request_method,
    url:       request.url,
    path:      request.path_info,
    params:    params,
    headers:   request.env.select { |k, v| k.start_with?("HTTP_") },
    ip:        request.ip,
    user_agent: request.user_agent,
    content_type: request.content_type,
    body:      request.body.read
  }.to_json
end

18.4.2 参数处理

# params 对象
post "/users" do
  # 表单参数
  name = params[:name]
  
  # JSON 请求体
  json_body = JSON.parse(request.body.read)
  
  # 文件上传
  file = params[:file]
  if file
    File.open("uploads/#{file[:filename]}", "wb") do |f|
      f.write(file[:tempfile].read)
    end
  end
  
  "Created"
end

# 参数验证
post "/api/users" do
  content_type :json
  
  halt 400, { error: "Name required" }.to_json unless params[:name]
  halt 400, { error: "Email required" }.to_json unless params[:email]
  
  user = User.create(name: params[:name], email: params[:email])
  status 201
  user.to_json
end

18.4.3 响应控制

# 状态码
get "/not-found" do
  status 404
  "Not Found"
end

# 响应头
get "/custom-header" do
  headers "X-Custom" => "value"
  "Response with custom header"
end

# 重定向
get "/old-path" do
  redirect "/new-path"
end

get "/redirect-with-status" do
  redirect "/new-path", 301
end

# Content-Type
get "/data.json" do
  content_type :json
  { key: "value" }.to_json
end

get "/data.xml" do
  content_type :xml
  "<root><key>value</key></root>"
end

# 流式响应
get "/stream" do
  stream do |out|
    10.times do |i|
      out << "Line #{i}\n"
      sleep 0.5
    end
  end
end

18.5 模板

18.5.1 ERB 模板

# views/layout.erb
<!DOCTYPE html>
<html>
<head>
  <title><%= @title || "My App" %></title>
</head>
<body>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
  </nav>
  <%= yield %>
</body>
</html>

# views/index.erb
<h1>Welcome</h1>
<p>Hello, <%= @name %>!</p>

<ul>
  <% @items.each do |item| %>
    <li><%= item[:name] %> - $<%= item[:price] %></li>
  <% end %>
</ul>
# app.rb
get "/" do
  @name = "World"
  @items = [
    { name: "Item 1", price: 10 },
    { name: "Item 2", price: 20 }
  ]
  erb :index
end

get "/about" do
  erb :about, layout: :custom_layout
end

18.5.2 Haml 模板

# Gemfile
gem "haml"

# views/index.haml
%h1 Welcome
%p Hello, #{@name}!

%ul
  - @items.each do |item|
    %li= "#{item[:name]} - $#{item[:price]}"
get "/" do
  @name = "World"
  haml :index
end

18.5.3 Slim 模板

# Gemfile
gem "slim"

# views/index.slim
h1 Welcome
p Hello, #{@name}!

ul
  - @items.each do |item|
    li = "#{item[:name]} - $#{item[:price]}"

18.5.4 局部模板

# views/_user.erb
<div class="user">
  <h3><%= user[:name] %></h3>
  <p><%= user[:email] %></p>
</div>

# views/users.erb
<% @users.each  do |user| %>
  <%= erb :_user, locals: { user: user } %>
<% end  %>

18.6 中间件

18.6.1 使用 Rack 中间件

# app.rb
require "sinatra"
require "rack/cache"

# 使用中间件
use Rack::Cache
use Rack::CommonLogger
use Rack::ContentLength

# 自定义中间件
class RequestTimer
  def initialize(app)
    @app = app
  end

  def call(env)
    start = Time.now
    status, headers, body = @app.call(env)
    elapsed = Time.now - start
    headers["X-Runtime"] = elapsed.to_s
    [status, headers, body]
  end
end

use RequestTimer

get "/" do
  "Hello"
end

18.6.2 CORS 中间件

# Gemfile
gem "rack-cors"

# config.ru
use Rack::Cors do
  allow do
    origins "*"
    resource "*",
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options]
  end
end

18.6.3 认证中间件

class ApiAuth
  def initialize(app)
    @app = app
  end

  def call(env)
    request = Rack::Request.new(env)
    token = request.env["HTTP_AUTHORIZATION"]&.split(" ")&.last

    unless valid_token?(token)
      return [401, { "content-type" => "application/json" }, 
              [{ error: "Unauthorized" }.to_json]]
    end

    env["current_user"] = find_user(token)
    @app.call(env)
  end

  private

  def valid_token?(token)
    token && User.find_by(api_token: token)
  end

  def find_user(token)
    User.find_by(api_token: token)
  end
end

use ApiAuth

18.7 RESTful API

18.7.1 完整的 API 示例

# api.rb
require "sinatra/base"
require "json"

class TodoAPI < Sinatra::Base
  configure do
    set :show_exceptions, false
    mime_type :json, "application/json"
  end

  before do
    content_type :json
  end

  helpers do
    def json_params
      JSON.parse(request.body.read, symbolize_names: true)
    rescue JSON::ParserError
      halt 400, { error: "Invalid JSON" }.to_json
    end

    def find_todo(id)
      Todo.find(id)
    rescue ActiveRecord::RecordNotFound
      halt 404, { error: "Todo not found" }.to_json
    end
  end

  # GET /api/todos
  get "/api/todos" do
    todos = Todo.all
    todos = todos.where(done: false) if params[:active]
    todos.to_json
  end

  # GET /api/todos/:id
  get "/api/todos/:id" do
    find_todo(params[:id]).to_json
  end

  # POST /api/todos
  post "/api/todos" do
    data = json_params
    todo = Todo.new(data)
    
    if todo.save
      status 201
      todo.to_json
    else
      status 422
      { errors: todo.errors.full_messages }.to_json
    end
  end

  # PATCH /api/todos/:id
  patch "/api/todos/:id" do
    todo = find_todo(params[:id])
    data = json_params

    if todo.update(data)
      todo.to_json
    else
      status 422
      { errors: todo.errors.full_messages }.to_json
    end
  end

  # DELETE /api/todos/:id
  delete "/api/todos/:id" do
    find_todo(params[:id]).destroy
    status 204
  end

  # 错误处理
  error ActiveRecord::RecordNotFound do
    status 404
    { error: "Resource not found" }.to_json
  end

  error do |e|
    status 500
    { error: e.message }.to_json
  end
end

18.8 配置和部署

18.8.1 配置

class MyApp < Sinatra::Base
  # 环境配置
  configure :development do
    enable :logging
    set :database, "sqlite3:///dev.db"
  end

  configure :production do
    set :database, ENV["DATABASE_URL"]
    disable :show_exceptions
  end

  configure do
    set :public_folder, "public"
    set :views, "views"
    set :port, 4567
    set :bind, "0.0.0.0"
  end
end

18.8.2 config.ru

# config.ru
require "./app"

run MyApp
# 使用 Rack 启动
rackup

# 指定端口
rackup -p 9292

# 使用 Puma
rackup -s puma

18.9 动手练习

  1. 创建天气 API
# 创建一个简单的天气查询 API
# GET /weather/:city → 返回天气信息
  1. 创建短链接服务
# POST /shorten { url: "..." } → 返回短链接
# GET /:code → 重定向到原链接
  1. 添加中间件
# 添加日志、认证、CORS 中间件

18.10 本章小结

要点说明
Sinatra轻量级 DSL 风格 Web 框架
路由基于 HTTP 方法的简洁路由
模板支持 ERB、Haml、Slim 等
中间件Rack 中间件实现插件功能
REST API适合构建轻量级 API 服务
部署通过 config.ru 使用 Rack 服务器

📖 扩展阅读


上一章← 第 17 章:Rails 入门 下一章第 19 章:并发编程 →