Ruby 入门指南 / 第 18 章:Sinatra 轻量 Web
第 18 章:Sinatra 轻量 Web
“少即是多。” —— Ludwig Mies van der Rohe
18.1 Sinatra 概述
18.1.1 为什么选择 Sinatra
| 特性 | Sinatra | Rails |
|---|
| 学习曲线 | 低 | 高 |
| 项目规模 | 小型/微服务 | 中大型应用 |
| 配置 | 几乎无 | 约定配置 |
| 启动速度 | 极快 | 较慢 |
| 灵活性 | 极高 | 约定优先 |
| 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.rb | rackup 或 ruby 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 动手练习
- 创建天气 API
# 创建一个简单的天气查询 API
# GET /weather/:city → 返回天气信息
- 创建短链接服务
# POST /shorten { url: "..." } → 返回短链接
# GET /:code → 重定向到原链接
- 添加中间件
18.10 本章小结
| 要点 | 说明 |
|---|
| Sinatra | 轻量级 DSL 风格 Web 框架 |
| 路由 | 基于 HTTP 方法的简洁路由 |
| 模板 | 支持 ERB、Haml、Slim 等 |
| 中间件 | Rack 中间件实现插件功能 |
| REST API | 适合构建轻量级 API 服务 |
| 部署 | 通过 config.ru 使用 Rack 服务器 |
📖 扩展阅读
上一章:← 第 17 章:Rails 入门
下一章:第 19 章:并发编程 →