第三部分,我们要做三件事情

  1. 版本管理
  2. 序列化
  3. 分页

在开始之前,我们先设置seed,假的数据,方便后面的测试

检查Gemfile

gem 'faker'

编辑 db/seeds.rb

user = User.create(name: "hongli", email: "yhlssdone@gmail.com", password: "password")

50.times do
  todo = Todo.create(title: Faker::Lorem.word, created_by: User.first.id)
  todo.items.create(name: Faker::Lorem.word, done: false)
end

然后,本地开发环境重置数据库

rails db:migrate:reset
rails db:seed

如果代码已经提交到 heroku,那么,heroku 重置数据库

heroku pg:reset DATABASE
heroku run rake db:migrate
heroku run rake db:seed
heroku restart

好的,准备工作完成,开始进入正题

版本管理

最常见的场景,客户端1.0用到了一个 API,但是下个版本1.1中,需要服务端改变同个 API 中的返回字段,但是用户群在客户端发布1.1以后不选择升级,那么服务端就需要适配两个版本的接口,需要引入版本管理。在 Rails 中,如果你需要做这件事情,你只需要做两件事:1. 添加路由约束,这会依据每个 request 的 header 选择不同的接口版本。2. 给控制器命名空间,controller 中不同的 namespace 处理不同的版本

编辑 app/lib/api_version.rb

class ApiVersion
  attr_reader :version, :default

  def initialize(version, default = false)
    @version = version
    @default = default
  end

  # check whether version is specified or is default
  def matches?(request)
    check_headers(request.headers) || default
  end

  private

  def check_headers(headers)
    # check version from Accept headers; expect custom media type `todos`
    accept = headers[:accept]
    accept && accept.include?("application/vnd.todos.#{version}+json")
  end
end

依据标准写法,检查这个外链,很明显 header 里面有对应版本号的字段。从 request 对象,进入 Accept 的 headers,然后检查版本信息,找到对应的接口,这个过程叫做 content negotiation,外链,简单讲就是同样的 URI 对应不同的版本,服务端驱动的版本管理。依据 Media Type Specification,你可以通过 vendor tree 自己定义 media types,比如 application/vnd.example.resource+json

好的,现在我们已经有了约束的类,接下去需要改变路由。由于我们不会改变 URI 来做版本管理(顺便说一句这是错误的做法,是反设计的,但很多公司仍然在使用😄),所以我们会使用 module scope 和 namespace。

编辑 config/routes

Rails.application.routes.draw do
  # namespace the controllers without affecting the URI
  scope module: :v1, constraints: ApiVersion.new('v1', true) do
    resources :todos do
      resources :items
    end
  end

  post 'auth/login', to: 'authentication#authenticate'
  post 'signup', to: 'users#create'
end

version constraint 是全局有效的,v1 是默认版本,也就是说没指定版本就是 v1。如果我们指定新的版本,Rails 会从高到低去匹配版本号,使用那个 matches 方法。然后我们看到 controller,建立文件夹

mkdir app/controllers/v1

把相关文件移动到这个文件夹下面

mv app/controllers/{todos_controller.rb,items_controller.rb} app/controllers/v1

还没完,记得加 module 的版本号,看到 app/controllers/v1/todos_controller.rb

module V1
  class TodosController < ApplicationController
  # [...]
  end
end

同样的 app/controllers/v1/items_controller.rb

module V1
  class ItemsController < ApplicationController
  # [...]
  end
end

接下去是v2的版本

rails g controller v2/todos

编辑config/routes.rb

Rails.application.routes.draw do

  # module the controllers without affecting the URI
  scope module: :v2, constraints: ApiVersion.new('v2') do
    resources :todos, only: :index
  end

  scope module: :v1, constraints: ApiVersion.new('v1', true) do
    # [...]
  end
  # [...]
end

注意顺序,不是默认的版本要在默认的版本上方

编辑 app/controllers/v2/todos_controller.rb

class V2::TodosController < ApplicationController
  def index
    json_response({ message: 'Hello There'})
  end
end

因为只是测试用,所以就给条消息吧

rails-todo-api-login

用 v1 登陆,拿到 token 用给 v2 的 todos 发 get 请求,是有效的也是正确的

序列化

这是个很重要的特性,经常有这么个段子,客户端问服务端,我要新加个字段,你要多久。服务端说,是已经有的字段吧,几分钟的事情。嗯?这么快,为什么?那是因为序列化😄

这里举个例子,我需要一个 todo 的内容,还有他对应的所有 items 数组,总不至于请求两次 API 吧。使用 Active model serializers ,检查 Gemfile

gem 'active_model_serializers', '~> 0.10.0'

bundle install 以后

rails g serializer todo

会有一个新的目录 app/serializers,以及一个新的文件 todo_serializer.rb,编辑

class TodoSerializer < ActiveModel::Serializer
  # attributes to be serialized  
  attributes :id, :title, :created_by, :created_at, :updated_at
  # model association
  has_many :items
end

item 的哪些属性需要序列化,以及 item 和 todo 的关系。用 httpie 再测试下吧

rails-todo-api-httpie-login

rails-todo-api-httpie-add-item

rails-todo-httpie-todo-serializer

看到我请求了 todo,返回了 todo 的属性,还有这个 todo 下的所有 items

分页

已经接近完成了,由于在真实环境中,数据可能非常多,我们不可能一次返回所有数据,所以我们需要一个功能叫做分页,让数据批量次序返回,对应于前端的上拉加载更多功能

检查Gemfile:

  gem 'will_paginate', '~> 3.1.0'

记得 bundle install

编辑 app/controllers/v1/todos_controller.rb

module V1
  class TodosController < ApplicationController
  # [...]
  # GET /todos
  def index
    # get paginated current user todos
    @todos = current_user.todos.paginate(page: params[:page], per_page: 20)
    json_response(@todos)
  end
  # [...]
end

可见,每次返回20条数据,用 httpie 试一下是不是20条数据,这里就不贴图了

http :3000/todos page==1 Accept:'application/vnd.todos.v1+json' Authorization:'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE0OTUxMjc5MTF9.eab5fH5bfz2lZsJw5jTFeRLlwyx3n-ggpsHPaYF7F64'

注意如果page==0,那么返回所有数据

使用 Rails 5写 RESTful API,真的很高效,赶快实践下吧,小伙伴们😄

这是我部署在heroku上的应用(https://desolate-sea-54055.herokuapp.com/)

完整的git项目地址(https://github.com/HongliYu/todos_api)