Rails 路由的 API 版本控制

Posted

技术标签:

【中文标题】Rails 路由的 API 版本控制【英文标题】:API Versioning for Rails Routes 【发布时间】:2012-03-26 12:28:12 【问题描述】:

我正在尝试像 Stripe 那样对我的 API 进行版本控制。下面给出了最新的 API 版本是 2。

/api/users 将 301 返回到 /api/v2/users

/api/v1/users 在版本 1 中返回 200 个用户索引

/api/v3/users 将 301 返回到 /api/v2/users

/api/asdf/users 将 301 返回到 /api/v2/users

所以基本上任何没有指定版本的东西都会链接到最新的,除非指定的版本存在然后重定向到它。

这是我目前所拥有的:

scope 'api', :format => :json do
  scope 'v:api_version', :api_version => /[12]/ do
    resources :users
  end

  match '/*path', :to => redirect  |params| "/api/v2/#params[:path]" 
end

【问题讨论】:

【参考方案1】:

The original form of this answer is wildly different, and can be found here。只是证明给猫剥皮的方法不止一种。

我已经更新了答案,因为使用命名空间并使用 301 重定向——而不是默认的 302。感谢 pixeltrix 和 Bo Jeanes 对这些事情的提示。


您可能想要真正戴上坚固的头盔,因为这会让您大吃一惊

Rails 3 路由 API 非常邪恶。要为您的 API 编写路由,根据您的上述要求,您只需要这样:

namespace :api do
  namespace :v1 do
    resources :users
  end

  namespace :v2 do
    resources :users
  end
  match 'v:api/*path', :to => redirect("/api/v2/%path")
  match '*path', :to => redirect("/api/v2/%path")
end

如果你的思想在这之后仍然完好无损,让我解释一下。

首先,我们调用namespace,当您想要一堆路由范围为特定路径和名称相似的模块时,它非常方便。在这种情况下,我们希望 namespace 的块内的所有路由都被限定为 Api 模块内的控制器,并且对该路由内路径的所有请求都将以 api 为前缀。 /api/v2/users之类的请求,你知道吗?

在命名空间内,我们定义了另外两个命名空间(哇!)。这次我们定义了“v1”命名空间,所以这里控制器的所有路由都将位于Api 模块内的V1 模块内:Api::V1。通过在此路由中定义resources :users,控制器将位于Api::V1::UsersController。这是版本 1,您可以通过发出 /api/v1/users 之类的请求来实现。

版本 2 只是 微小 有点不同。而不是为它服务的控制器位于Api::V1::UsersController,而是现在位于Api::V2::UsersController。您可以通过发出/api/v2/users 之类的请求来到达那里。

接下来,使用match。这将匹配所有去往 /api/v3/users 之类的 API 路由。

这是我必须查找的部分。 :to => 选项允许您指定应将特定请求重定向到其他地方——我知道很多——但我不知道如何让它重定向到其他地方并传入原始请求的一部分和它一起。

为此,我们调用redirect 方法并将带有特殊插值%path 参数的字符串传递给它。当一个请求与这个最终的match 匹配时,它会将path 参数插入到字符串中%path 的位置,并将用户重定向到他们需要去的地方。

最后,我们使用另一个match 来路由所有以/api 为前缀的剩余路径,并将它们重定向到/api/v2/%path。这意味着像/api/users 这样的请求将转到/api/v2/users

我不知道如何让/api/asdf/users 匹配,因为您如何确定这是否应该是对/api/<resource>/<identifier>/api/<version>/<resource> 的请求?

无论如何,这很有趣,希望对您有所帮助!

【讨论】:

亲爱的瑞恩·比格。你太棒了。 衡量一个红宝石英雄的声誉并不仅仅是简单的。 Ryan... 我认为这实际上并不准确。这将使 /api 和 /api/v2 提供相同的内容,而不是具有单个规范 URL。 /api 应该重定向到 /api/v2(如原作者指定的那样)。我希望正确的路线看起来像gist.github.com/2044335(当然,我还没有测试过)。 只有 /api/v[12] 应该返回 200,/api 和 /api/ 应该返回 301s 到 /api/v2 值得注意的是,在路由文件中,301 已被设为默认重定向,这是有充分理由的。来自指南:Please note that this redirection is a 301 “Moved Permanently” redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible. 如果路径不正确,它不会创建无限重定向吗?例如,请求 /api/v3/path_that_dont_match_the_routes 将创建无限重定向,对吗?【参考方案2】:

补充几点:

您的重定向匹配不适用于某些路由 - *api 参数是贪婪的,会吞噬一切,例如/api/asdf/users/1 将重定向到 /api/v2/1。你最好使用像:api 这样的常规参数。诚然,它不会匹配 /api/asdf/asdf/users/1 这样的案例,但如果您的 api 中有嵌套资源,这是一个更好的解决方案。

Ryan 为什么你不喜欢namespace? :-),例如:

current_api_routes = lambda do
  resources :users
end

namespace :api do
  scope :module => :v2, &current_api_routes
  namespace :v2, &current_api_routes
  namespace :v1, &current_api_routes
  match ":api/*path", :to => redirect("/api/v2/%path")
end

这具有版本化和通用命名路由的额外好处。另一个注意事项 - 使用 :module 时的约定是使用下划线表示法,例如:api/v1 而不是 'Api::V1'。有一次后者不起作用,但我相信它已在 Rails 3.1 中修复。

此外,当您发布 API 的 v3 时,路由将按如下方式更新:

current_api_routes = lambda do
  resources :users
end

namespace :api do
  scope :module => :v3, &current_api_routes
  namespace :v3, &current_api_routes
  namespace :v2, &current_api_routes
  namespace :v1, &current_api_routes
  match ":api/*path", :to => redirect("/api/v3/%path")
end

当然,您的 API 可能在版本之间有不同的路由,在这种情况下您可以这样做:

current_api_routes = lambda do
  # Define latest API
end

namespace :api do
  scope :module => :v3, &current_api_routes
  namespace :v3, &current_api_routes

  namespace :v2 do
    # Define API v2 routes
  end

  namespace :v1 do
    # Define API v1 routes
  end

  match ":api/*path", :to => redirect("/api/v3/%path")
end

【讨论】:

你会如何处理最后的案例?即/api/asdf/users?/api/users/1?我无法在我更新的答案中弄清楚这一点,所以想你可能知道一种方法 没有简单的方法来做到这一点 - 您必须在全部捕获之前定义所有重定向,但您只需要为每个父资源执行每个操作,例如/api/users/*path => /api/v2/users/%path【参考方案3】:

如果可能的话,我建议重新考虑您的网址,以便版本不在网址中,而是放入接受标头中。这个堆栈溢出答案很好地解决了它:

Best practices for API versioning?

这个链接准确地展示了如何使用 Rails 路由来做到这一点:

http://freelancing-gods.com/posts/versioning_your_ap_is

【讨论】:

这也是一种很好的方法,并且可能也可以满足“/api/asdf/users”请求。【参考方案4】:

如果没有明确请求某个版本,我不确定您为什么要重定向到特定版本。似乎您只是想定义一个默认版本,如果没有明确请求版本,则会提供该版本。我也同意 David Bock 的观点,即在 URL 结构之外保留版本是支持版本控制的一种更简洁的方式。

无耻插件:版本主义者支持这些用例(以及更多)。

https://github.com/bploetz/versionist

【讨论】:

【参考方案5】:

我不喜欢按路线进行版本控制。我们构建了VersionCake 来支持更简单的 API 版本控制形式。

通过在我们各自视图(jbuilder、RABL 等)的文件名中包含 API 版本号,我们保持版本控制不显眼,并允许轻松降级以支持向后兼容性(例如,如果视图的 v5 不支持存在,我们渲染视图的 v4)。

【讨论】:

【参考方案6】:

Ryan Bigg 的回答对我有用。

如果您还想通过重定向保留查询参数,您可以这样做:

match "*path", to: redirect |params, request| "/api/v2/#params[:path]?#request.query_string" 

【讨论】:

【参考方案7】:

今天实现了这一点,并在RailsCasts - REST API Versioning 上找到了我认为的“正确方法”。很简单。如此可维护。很有效。

添加lib/api_constraints.rb(甚至不必更改vnd.example。)

class ApiConstraints
  def initialize(options)
    @version = options[:version]
    @default = options[:default]
  end

  def matches?(req)
    @default || req.headers['Accept'].include?("application/vnd.example.v#@version")
  end
end

像这样设置config/routes.rb

require 'api_constraints'

Rails.application.routes.draw do

  # Squads API
  namespace :api do
    # ApiConstaints is a lib file to allow default API versions,
    # this will help prevent having to change link names from /api/v1/squads to /api/squads, better maintainability
    scope module: :v1, constraints: ApiConstraints.new(version:1, default: true) do
      resources :squads do
        # my stuff was here
      end
    end
  end

  resources :squads
  root to: 'site#index'

编辑您的控制器(即/controllers/api/v1/squads_controller.rb

module Api
  module V1
    class SquadsController < BaseController
      # my stuff was here
    end
  end
end

然后,您可以将应用中的所有链接从 /api/v1/squads 更改为 /api/squads,您可以轻松实现新的 api 版本,甚至无需更改链接

【讨论】:

以上是关于Rails 路由的 API 版本控制的主要内容,如果未能解决你的问题,请参考以下文章

如何让 Devise 和 JWT 与我的版本化 API Rails 控制器一起工作?

REST API 版本控制 - 为啥不对模型进行版本控制

用于 REST API 的 Rails 控制器

Symfony中的API版本控制

版本控制 PHP Lithium API

Swagger 版本控制不起作用。它显示所有端点,尽管选择了 API 版本