Rails路由的API版本控制

151

我正在尝试像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
7个回答

288

这个答案的原始形式与现在截然不同,可以在此处找到。只是证明了有多种方法可以解决问题。

由于pixeltrix和Bo Jeanes的提示,我已更新答案以使用命名空间并使用301重定向,而不是默认的302。


你可能需要戴一顶非常坚固的头盔,因为这将让你恍然大悟。

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::UsersController,通过在此路线内定义resources :users, 它会匹配到版本1的控制器。通过像/api/v1/users这样的请求,您可以到达那里。
版本2只有一个微小的差异。控制器不再被称为Api::V1::UsersController,而是现在称为Api::V2::UsersController。通过像/api/v2/users这样的请求,您可以到达那里。
接下来,使用match。这将匹配所有前往类似于/api/v3/users的API路由。
这是我不得不查询的部分。 :to =>选项允许您指定应该将特定请求重定向到其他位置--我知道这一点--但我不知道如何将其重定向到其他位置并传递原始请求的一部分。
为此,我们调用redirect方法并传递一个带有特殊插值的字符串%{path}参数。当匹配到最后一个match时,它将把路径参数插值到字符串中%{path}的位置,并将用户重定向到他们需要去的地方。
最后,我们使用另一个match来路由所有以/api为前缀的剩余路径,并将它们重定向到/api/v2/%{path}。这意味着像/api/users这样的请求将转到/api/v2/users
我无法弄清楚如何匹配/api/asdf/users,因为怎么确定它是/api/<resource>/<identifier>的请求还是/api/<version>/<resource>的请求呢?

26
亲爱的Ryan Bigg,你很出色。 - maletor
19
评价一位 Ruby 英雄的声望并不是一件简单的事情。 - Waseem
1
Ryan...我认为这并不准确。这将使/api和/api/v2提供相同的内容,而不是具有单个规范URL。/api应该重定向到/api/v2(如原始作者所指定的)。我希望正确的路由看起来像https://gist.github.com/2044335(尽管我没有测试过)。只有/api/v[12]应返回200,/api和/api/<bad version>应返回301到/api/v2。 - Bo Jeanes
2
值得注意的是,在路由文件中,301已成为默认重定向,并且有充分的理由。来自指南的说明:请注意,此重定向是301“永久移动”重定向。请记住,一些Web浏览器或代理服务器将缓存此类型的重定向,使旧页面无法访问。 - maletor
3
如果路径不正确,它是否会创建无限重定向?例如,请求 /api/v3/path_that_dont_match_the_routes 会创建一个无限重定向,对吗? - Robin
显示剩余8条评论

38

需要补充几点:

你的重定向匹配无法适用于某些路由 - *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?我在我的更新答案中无法解决这个问题,所以想知道你是否知道解决方法。 - Ryan Bigg
没有简单的方法可以做到这一点 - 你需要在捕获所有请求之前定义所有的重定向,但你只需要为每个父资源定义一次,例如:/api/users/*path => /api/v2/users/%{path}。 - pixeltrix

13

这也是一个很好的方法,可能也适用于“/api/asdf/users”请求。 - Ryan Bigg

9
我不太喜欢通过路由进行版本控制。我们创建了VersionCake来支持更简单的API版本控制方式。
通过在每个视图文件名(jbuilder,RABL等)中包含API版本号,我们可以保持版本控制的低调,并且可以轻松地降级以支持向后兼容性(例如,如果视图的v5不存在,则渲染v4的视图)。

8
我不确定为什么你想要在没有明确请求版本的情况下重定向到特定版本。似乎你只是想定义一个默认版本,如果没有明确请求版本,则提供该版本。我也同意David Bock的观点,即将版本保持在URL结构之外是支持版本控制的更清晰的方式。
无耻地插入广告: Versionist支持这些用例(以及更多)。

https://github.com/bploetz/versionist


3
今天我实现了这个功能,并找到了我认为是“正确的方式”在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版本,而无需甚至更改链接。


2

Ryan Bigg的答案对我有用。

如果你也想通过重定向保留查询参数,可以像这样操作:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接