1. 什么是 API 应用程序?
传统上,当人们说他们将 Rails 用作“API”时,他们指的是在 Web 应用程序旁边提供可编程访问的 API。例如,GitHub 提供了一个 API,你可以从自己的自定义客户端使用它。
随着客户端框架的出现,越来越多的开发人员使用 Rails 构建一个在 Web 应用程序和其他原生应用程序之间共享的后端。
例如,X 在其 Web 应用程序中使用其公共 API,该应用程序作为一个静态站点构建,消费 JSON 资源。
许多开发人员不再使用 Rails 生成通过表单和链接与服务器通信的 HTML,而是将其 Web 应用程序视为仅仅是一个作为 HTML 和 JavaScript 交付的 API 客户端,它消费 JSON API。
本指南涵盖了构建一个 Rails 应用程序,该应用程序向 API 客户端(包括客户端框架)提供 JSON 资源。
2. 为什么将 Rails 用于 JSON API?
许多人在考虑使用 Rails 构建 JSON API 时,首先会问的问题是:“使用 Rails 输出一些 JSON 是不是有点大材小用?我难道不应该只使用像 Sinatra 这样的东西吗?”
对于非常简单的 API,这可能是真的。然而,即使在 HTML 含量很高的应用程序中,应用程序的大部分逻辑也存在于视图层之外。
大多数人使用 Rails 的原因是它提供了一组默认设置,允许开发人员快速上手,而无需做出许多琐碎的决策。
让我们看看 Rails 开箱即用的一些功能,这些功能仍然适用于 API 应用程序。
在中间件层处理
- 重新加载:Rails 应用程序支持透明重新加载。即使你的应用程序变得很大,并且每次请求都重新启动服务器变得不可行,这也有效。
- 开发模式:Rails 应用程序带有智能的开发默认设置,使开发变得愉快,而不会影响生产时的性能。
- 测试模式:同开发模式。
- 日志:Rails 应用程序记录每个请求,详细程度适合当前模式。开发中的 Rails 日志包括有关请求环境、数据库查询和基本性能信息。
- 安全性:Rails 检测并阻止IP 欺骗攻击,并以时序攻击感知的方式处理加密签名。不知道什么是 IP 欺骗攻击或时序攻击?这就对了。
- 参数解析:想将参数指定为 JSON 而不是 URL 编码字符串?没问题。Rails 会为你解码 JSON 并将其在
params中可用。想使用嵌套的 URL 编码参数?也可以。 - 条件 GET:Rails 处理条件
GET(ETag和Last-Modified)请求头,并返回正确的响应头和状态码。你只需在控制器中使用stale?检查,Rails 将为你处理所有 HTTP 细节。 - HEAD 请求:Rails 会透明地将
HEAD请求转换为GET请求,并在返回时只返回头。这使得HEAD在所有 Rails API 中都能可靠工作。
虽然你显然可以根据现有 Rack 中间件构建这些,但这个列表表明,即使你“只是生成 JSON”,默认的 Rails 中间件栈也提供了很多价值。
在 Action Pack 层处理
- 资源路由:如果你正在构建 RESTful JSON API,你将需要使用 Rails 路由器。HTTP 到控制器的清晰和常规映射意味着无需花费时间思考如何在 HTTP 中建模你的 API。
- URL 生成:路由的另一面是 URL 生成。一个基于 HTTP 的优秀 API 包括 URL(参见GitHub Gist API 作为示例)。
- 头和重定向响应:
head :no_content和redirect_to user_url(current_user)派上用场。当然,你可以手动添加响应头,但为什么要这么做呢? - 缓存:Rails 提供页面、动作和片段缓存。片段缓存在构建嵌套 JSON 对象时特别有用。
- 基本、摘要和令牌认证:Rails 开箱即用支持三种 HTTP 认证。
- 仪表:Rails 有一个仪表 API,它为各种事件触发注册的处理程序,例如动作处理、发送文件或数据、重定向和数据库查询。每个事件的有效负载都带有相关信息(对于动作处理事件,有效负载包括控制器、动作、参数、请求格式、请求方法和请求的完整路径)。
- 生成器:通常方便的是,通过一个命令生成资源,并为你创建模型、控制器、测试存根和路由,以便进一步调整。迁移和其他也一样。
- 插件:许多第三方库都支持 Rails,这降低或消除了设置和连接库与 Web 框架的成本。这包括覆盖默认生成器、添加 Rake 任务和遵守 Rails 选择(如日志记录器和缓存后端)。
当然,Rails 启动过程还将所有注册组件连接起来。例如,Rails 启动过程在配置 Active Record 时使用你的 config/database.yml 文件。
简而言之:你可能没有考虑即使删除视图层,Rails 的哪些部分仍然适用,但答案是:大部分都适用。
3. 基本配置
如果你正在构建一个以 API 服务器为首要任务的 Rails 应用程序,你可以从一个更有限的 Rails 子集开始,并根据需要添加功能。
3.1. 创建新应用程序
你可以生成一个新的 API Rails 应用程序
$ rails new my_api --api
这将为你完成三件主要事情
- 配置你的应用程序以比正常情况下更有限的中间件集启动。具体来说,默认情况下它不会包含任何主要用于浏览器应用程序的中间件(例如 cookie 支持)。
- 使
ApplicationController继承自ActionController::API而不是ActionController::Base。与中间件一样,这将省略任何主要由浏览器应用程序使用的 Action Controller 模块。 - 配置生成器以在你生成新资源时跳过生成视图、助手和资产。
3.2. 生成新资源
要了解我们新创建的 API 如何处理生成新资源,让我们创建一个新的 Group 资源。每个组都将有一个名称。
$ bin/rails g scaffold Group name:string
在使用我们脚手架的代码之前,我们需要更新我们的数据库方案。
$ bin/rails db:migrate
现在,如果我们打开 GroupsController,我们会注意到对于 API Rails 应用程序,我们只渲染 JSON 数据。在索引动作中,我们查询 Group.all 并将其赋值给名为 @groups 的实例变量。将其与 :json 选项一起传递给 render 将自动将组渲染为 JSON。
# app/controllers/groups_controller.rb
class GroupsController < ApplicationController
before_action :set_group, only: %i[ show update destroy ]
# GET /groups
def index
@groups = Group.all
render json: @groups
end
# GET /groups/1
def show
render json: @group
end
# POST /groups
def create
@group = Group.new(group_params)
if @group.save
render json: @group, status: :created, location: @group
else
render json: @group.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /groups/1
def update
if @group.update(group_params)
render json: @group
else
render json: @group.errors, status: :unprocessable_entity
end
end
# DELETE /groups/1
def destroy
@group.destroy
end
private
# Use callbacks to share common setup or constraints between actions.
def set_group
@group = Group.find(params[:id])
end
# Only allow a list of trusted parameters through.
def group_params
params.expect(group: [:name])
end
end
最后,我们可以从 Rails 控制台向数据库添加一些组
irb> Group.create(name: "Rails Founders")
irb> Group.create(name: "Rails Contributors")
在应用程序中有了数据后,我们可以启动服务器并访问 https://:3000/groups.json 以查看我们的 JSON 数据。
[
{"id":1, "name":"Rails Founders", "created_at": ...},
{"id":2, "name":"Rails Contributors", "created_at": ...}
]
3.3. 更改现有应用程序
如果你想将现有应用程序更改为 API 应用程序,请阅读以下步骤。
在 config/application.rb 中,在 Application 类定义顶部添加以下行
config.api_only = true
在 config/environments/development.rb 中,设置 config.debug_exception_response_format 以配置在开发模式下发生错误时响应中使用的格式。
要渲染包含调试信息的 HTML 页面,请使用值 :default。
config.debug_exception_response_format = :default
要渲染保留响应格式的调试信息,请使用值 :api。
config.debug_exception_response_format = :api
默认情况下,当 config.api_only 设置为 true 时,config.debug_exception_response_format 设置为 :api。
最后,在 app/controllers/application_controller.rb 中,而不是
class ApplicationController < ActionController::Base
end
请这样做
class ApplicationController < ActionController::API
end
4. 选择中间件
API 应用程序默认包含以下中间件
ActionDispatch::HostAuthorizationRack::SendfileActionDispatch::StaticActionDispatch::ExecutorActionDispatch::ServerTimingActiveSupport::Cache::Strategy::LocalCache::MiddlewareRack::RuntimeActionDispatch::RequestIdActionDispatch::RemoteIpRails::Rack::LoggerActionDispatch::ShowExceptionsActionDispatch::DebugExceptionsActionDispatch::ActionableExceptionsActionDispatch::ReloaderActionDispatch::CallbacksActiveRecord::Migration::CheckPendingRack::HeadRack::ConditionalGetRack::ETag
有关它们的更多信息,请参阅 Rack 指南的内部中间件部分。
其他插件,包括 Active Record,可能会添加额外的中间件。一般来说,这些中间件与你正在构建的应用程序类型无关,并且在纯 API Rails 应用程序中有意义。
你可以通过以下方式获取应用程序中所有中间件的列表
$ bin/rails middleware
4.1. 使用 Rack::Cache
当与 Rails 一起使用时,Rack::Cache 使用 Rails 缓存存储作为其实体和元存储。这意味着,如果你的 Rails 应用程序使用 memcache,例如,内置的 HTTP 缓存将使用 memcache。
要使用 Rack::Cache,你首先需要将 rack-cache gem 添加到 Gemfile,并将 config.action_dispatch.rack_cache 设置为 true。要启用其功能,你需要在控制器中使用 stale?。这是一个使用 stale? 的示例。
def show
@post = Post.find(params[:id])
if stale?(last_modified: @post.updated_at)
render json: @post
end
end
调用 stale? 将把请求中的 If-Modified-Since 头与 @post.updated_at 进行比较。如果头比上次修改时间新,此操作将返回“304 Not Modified”响应。否则,它将渲染响应并在其中包含 Last-Modified 头。
通常,此机制是按客户端使用的。Rack::Cache 允许我们在客户端之间共享此缓存机制。我们可以在调用 stale? 时启用跨客户端缓存
def show
@post = Post.find(params[:id])
if stale?(last_modified: @post.updated_at, public: true)
render json: @post
end
end
这意味着 Rack::Cache 会将 URL 的 Last-Modified 值存储在 Rails 缓存中,并向该 URL 的任何后续入站请求添加 If-Modified-Since 头。
可以将其视为使用 HTTP 语义的页面缓存。
4.2. 使用 Rack::Sendfile
当你在 Rails 控制器中使用 send_file 方法时,它会设置 X-Sendfile 头。Rack::Sendfile 负责实际发送文件。
如果你的前端服务器支持加速文件发送,Rack::Sendfile 将把实际的文件发送工作卸载到前端服务器。这使得 Rails 能够更快地完成请求处理并释放资源。
你可以在相应的环境配置文件中使用 config.action_dispatch.x_sendfile_header 配置前端服务器用于此目的的头名称。
你可以在 Rack::Sendfile 文档中了解有关如何将 Rack::Sendfile 与流行前端一起使用的更多信息。
以下是某些流行服务器的此头的一些值,一旦这些服务器配置为支持加速文件发送
# Apache and lighttpd
config.action_dispatch.x_sendfile_header = "X-Sendfile"
# Nginx
config.action_dispatch.x_sendfile_header = "X-Accel-Redirect"
请务必按照 Rack::Sendfile 文档中的说明配置你的服务器以支持这些选项。
4.3. 使用 ActionDispatch::Request
ActionDispatch::Request#params 将以 JSON 格式从客户端获取参数,并使其在你的控制器中的 params 内可用。
要使用此功能,你的客户端需要使用 JSON 编码的参数发出请求,并将 Content-Type 指定为 application/json。
这是一个例子
fetch('/people', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ person: { firstName: 'Yehuda', lastName: 'Katz' } })
}).then(response => response.json())
ActionDispatch::Request 将看到 Content-Type,你的参数将是
{ person: { firstName: "Yehuda", lastName: "Katz" } }
4.4. 使用会话中间件
以下用于会话管理的中间件被排除在 API 应用程序之外,因为它们通常不需要会话。如果你的一个 API 客户端是浏览器,你可能想重新添加其中一个
ActionDispatch::Session::CacheStoreActionDispatch::Session::CookieStoreActionDispatch::Session::MemCacheStore
重新添加这些中间件的诀窍在于,默认情况下,它们在添加时会传递 session_options(包括会话键),因此你不能只添加一个 session_store.rb 初始化器,添加 use ActionDispatch::Session::CookieStore 并使会话像往常一样工作。(明确地说:会话可能有效,但你的会话选项将被忽略——即会话键将默认为 _session_id)
你需要将相关选项设置在中间件构建之前(例如 config/application.rb),然后将它们传递给你首选的中间件,而不是初始化器,就像这样
# This also configures session_options for use below
config.session_store :cookie_store, key: "_your_app_session"
# Required for all session management (regardless of session_store)
config.middleware.use ActionDispatch::Cookies
config.middleware.use config.session_store, config.session_options
4.5. 其他中间件
Rails 附带了许多其他中间件,你可能想在 API 应用程序中使用它们,特别是如果你的一个 API 客户端是浏览器
Rack::MethodOverrideActionDispatch::CookiesActionDispatch::Flash
任何这些中间件都可以通过以下方式添加
config.middleware.use Rack::MethodOverride
4.6. 删除中间件
如果你不想使用纯 API 中间件集中默认包含的中间件,你可以使用以下方法将其删除
config.middleware.delete ::Rack::Sendfile
请记住,删除这些中间件将删除 Action Controller 中某些功能的支持。
5. 选择控制器模块
API 应用程序(使用 ActionController::API)默认包含以下控制器模块
ActionController::UrlFor |
使 url_for 和类似助手可用。 |
ActionController::Redirecting |
支持 redirect_to。 |
AbstractController::Rendering 和 ActionController::ApiRendering |
基本的渲染支持。 |
ActionController::Renderers::All |
支持 render :json 及相关功能。 |
ActionController::ConditionalGet |
支持 stale?。 |
ActionController::BasicImplicitRender |
确保在没有明确响应时返回空响应。 |
ActionController::StrongParameters |
支持参数过滤与 Active Model 批量赋值结合使用。 |
ActionController::DataStreaming |
支持 send_file 和 send_data。 |
AbstractController::Callbacks |
支持 before_action 和类似助手。 |
ActionController::Rescue |
支持 rescue_from。 |
ActionController::Instrumentation |
支持 Action Controller 定义的仪表钩子(有关此内容的更多信息,请参阅仪表指南)。 |
ActionController::ParamsWrapper |
将参数哈希包装成嵌套哈希,这样你在发送 POST 请求时就不必指定根元素。 |
ActionController::Head |
支持返回无内容、仅带头的响应。 |
其他插件可能会添加额外的模块。你可以在 rails 控制台中获取所有包含在 ActionController::API 中的模块列表
irb> ActionController::API.ancestors - ActionController::Metal.ancestors
=> [ActionController::API,
ActiveRecord::Railties::ControllerRuntime,
ActionDispatch::Routing::RouteSet::MountedHelpers,
ActionController::ParamsWrapper,
... ,
AbstractController::Rendering,
ActionView::ViewPaths]
5.1. 添加其他模块
所有 Action Controller 模块都知道它们的依赖模块,因此你可以随意将任何模块包含到你的控制器中,并且所有依赖项也将被包含和设置。
你可能想要添加的一些常见模块
AbstractController::Translation: 支持l和t本地化和翻译方法。- 支持基本、摘要或令牌 HTTP 认证
ActionController::HttpAuthentication::Basic::ControllerMethodsActionController::HttpAuthentication::Digest::ControllerMethodsActionController::HttpAuthentication::Token::ControllerMethods
ActionView::Layouts: 渲染时支持布局。ActionController::MimeResponds: 支持respond_to。ActionController::Cookies: 支持cookies,其中包括对签名和加密 cookie 的支持。这需要 cookie 中间件。ActionController::Caching: 支持 API 控制器的视图缓存。请注意,你需要手动在控制器中指定缓存存储,如下所示class ApplicationController < ActionController::API include ::ActionController::Caching self.cache_store = :mem_cache_store endRails 不会自动传递此配置。
添加模块的最佳位置是你的 ApplicationController,但你也可以将模块添加到单个控制器中。