更多内容请访问 rubyonrails.org:

将 Rails 用于纯 API 应用程序

在本指南中你将学到

  • Rails 为纯 API 应用程序提供了什么
  • 如何配置 Rails 以在不使用任何浏览器功能的情况下启动
  • 如何决定要包含哪些中间件
  • 如何决定在控制器中使用哪些模块

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 处理条件 GETETagLast-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_contentredirect_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::HostAuthorization
  • Rack::Sendfile
  • ActionDispatch::Static
  • ActionDispatch::Executor
  • ActionDispatch::ServerTiming
  • ActiveSupport::Cache::Strategy::LocalCache::Middleware
  • Rack::Runtime
  • ActionDispatch::RequestId
  • ActionDispatch::RemoteIp
  • Rails::Rack::Logger
  • ActionDispatch::ShowExceptions
  • ActionDispatch::DebugExceptions
  • ActionDispatch::ActionableExceptions
  • ActionDispatch::Reloader
  • ActionDispatch::Callbacks
  • ActiveRecord::Migration::CheckPending
  • Rack::Head
  • Rack::ConditionalGet
  • Rack::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::CacheStore
  • ActionDispatch::Session::CookieStore
  • ActionDispatch::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::MethodOverride
  • ActionDispatch::Cookies
  • ActionDispatch::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::RenderingActionController::ApiRendering 基本的渲染支持。
ActionController::Renderers::All 支持 render :json 及相关功能。
ActionController::ConditionalGet 支持 stale?
ActionController::BasicImplicitRender 确保在没有明确响应时返回空响应。
ActionController::StrongParameters 支持参数过滤与 Active Model 批量赋值结合使用。
ActionController::DataStreaming 支持 send_filesend_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: 支持 lt 本地化和翻译方法。
  • 支持基本、摘要或令牌 HTTP 认证
    • ActionController::HttpAuthentication::Basic::ControllerMethods
    • ActionController::HttpAuthentication::Digest::ControllerMethods
    • ActionController::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
    end
    

    Rails 不会自动传递此配置。

添加模块的最佳位置是你的 ApplicationController,但你也可以将模块添加到单个控制器中。



回到顶部