rails 返回json
In part two of this tutorial, we added token-based authentication with JWT (JSON Web Tokens) to our todo API.
在本教程的第二部分中,我们向Todo API中添加了基于JWT (JSON Web令牌)的基于令牌的身份验证。
In this final part of the series, we'll wrap with the following:
在本系列的最后一部分,我们将包装以下内容:
- Versioning 版本控制
- Serializers 序列化器
- Pagination 分页
版本控制 ( Versioning )
When building an API whether public or internal facing, it's highly recommended that you version it. This might seem trivial when you have total control over all clients. However, when the API is public facing, you want to establish a contract with your clients. Every breaking change should be a new version. Convincing enough? Great, let's do this!
构建公共或内部API时,强烈建议您对其进行版本控制。 当您完全控制所有客户端时,这似乎微不足道。 但是,当API面向公众时,您希望与客户建立合同。 每个重大更改都应该是一个新版本。 有说服力吗? 太好了,让我们做吧!
In order to version a Rails API, we need to do two things:
为了对Rails API进行版本控制,我们需要做两件事:
- Add a route constraint - this will select a version based on the request headers 添加路由约束-这将根据请求标头选择版本
- Namespace the controllers - have different controller namespaces to handle different versions. 控制器的命名空间-具有不同的控制器命名空间来处理不同的版本。
Rails routing supports advanced constraints. Provided an object that responds to matches?
, you can control which controller handles a specific route.
Rails路由支持高级约束 。 提供了对matches?
做出响应的对象matches?
,您可以控制哪个控制器处理特定路由。
We'll define a class ApiVersion
that checks the API version from the request headers and routes to the appropriate controller module. The class will live in app/lib
since it's non-domain-specific.
我们将定义一个ApiVersion
类,该类检查请求标头中的API版本,并路由到适当的控制器模块。 由于该类不是特定于域的,因此将存在于app/lib
。
# create the class file
$ touch app/lib/api_version.rb
Implement ApiVersion
实施ApiVersion
# 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
The ApiVersion
class accepts a version and a default flag on initialization. In accordance with Rails constraints, we implement an instance method matches?
. This method will be called with the request object upon initialization. From the request object, we can access the Accept
headers and check for the requested version or if the instance is the default version. This process is called content negotiation. Let's add some more context to this.
ApiVersion
类在初始化时接受版本和默认标志。 按照Rails的约束,我们实现一个实例方法matches?
。 初始化时将与请求对象一起调用此方法。 从请求对象中,我们可以访问Accept
标头并检查请求的版本或实例是否为默认版本。 此过程称为内容协商。 让我们为此添加更多的上下文。
内容协商 (Content Negotiation)
REST is closely tied to the HTTP specification. HTTP defines mechanisms that make it possible to serve different versions (representations) of a resource at the same URI. This is called content negotiation.
REST与HTTP规范紧密相关。 HTTP定义了可以在同一URI上为资源的不同版本( 表示形式 )提供服务的机制。 这称为内容协商 。
Our ApiVersion
class implements server-driven content negotiation where the client (user agent) informs the server what media types it understands by providing an Accept HTTP header.
我们的ApiVersion
类实现了服务器驱动的内容协商,其中客户端(用户代理)通过提供Accept HTTP标头来通知服务器它理解哪种媒体类型。
According to the Media Type Specification, you can define your own media types using the vendor tree i.e. application/vnd.example.resource+json
.
根据媒体类型规范 ,您可以使用供应商树定义自己的媒体类型,即application/vnd.example.resource+json
。
The vendor tree is used for media types associated with publicly available products. It uses the "vnd" facet.
供应商树用于与公共可用产品关联的媒体类型。 它使用“ vnd”构面。
Thus, we define a custom vendor media type application/vnd.todos.{version_number}+json
giving clients the ability to choose which API version they require.
因此,我们定义了一个定制的供应商媒体类型application/vnd.todos.{version_number}+json
使客户可以选择所需的API版本。
Cool, now that we have the constraint class, let's change our routing to accommodate this. Since we don't want to have the version number as part of the URI (this is argued as an anti-pattern), we'll make use of the module scope to namespace our controllers.
很酷,现在我们有了约束类,让我们更改路由以适应这一点。 由于我们不想将版本号作为URI的一部分(这被认为是反模式),因此我们将利用模块作用域为控制器命名空间。
Let's move the existing todos and todo-items resources into a v1
namespace.
让我们将现有的待办事项和待办事项资源移动到v1
命名空间中。
# config/routes
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
# 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
We've set the version constraint at the namespace level. Thus, this will be applied to all resources within it. We've also defined v1
as the default version; in cases where the version is not provided, the API will default to v1
. In the event we were to add new versions, they would have to be defined above the default version since Rails will cycle through all routes from top to bottom searching for one that matches
(till method matches?
resolves to true).
我们已经在名称空间级别设置了版本约束。 因此,这将应用于其中的所有资源。 我们还将v1
定义为默认版本。 如果未提供版本,则该API将默认为v1
。 如果我们要添加新版本,则它们必须在默认版本之上进行定义,因为Rails将从头到尾遍历所有路由以寻找matches
路由(直到方法matches?
解析为true)。
Next up, let's move the existing todos and items controllers into the v1 namespace. First, create a module directory in the controllers folder.
接下来,让我们将现有的待办事项和项目控制器移至v1名称空间。 首先,在controllers文件夹中创建一个模块目录。
$mkdir app/controllers/v1
Move the files into the module folder.
将文件移到模块文件夹中。
$mv app/controllers/{todos_controller.rb,items_controller.rb} app/controllers/v1
That's not all, let's define the controllers in the v1 namespace. Let's start with the todos controller.
不仅如此,让我们在v1名称空间中定义控制器。 让我们从todos控制器开始。
# app/controllers/v1/todos_controller.rb
module V1
class TodosController < ApplicationController
# [...]
end
end
Do the same for the items controller.
对项目控制器执行相同的操作。
# app/controllers/v1/items_controller.rb
module V1
class ItemsController < ApplicationController
# [...]
end
end
Let's fire up the server and run some tests.
让我们启动服务器并运行一些测试。
# get auth token
$ http :3000/auth/login email=foo@bar.com password=foobar
# get todos from API v1
$ http :3000/todos Accept:'application/vnd.todos.v1+json' Authorization:'ey...AWH3FNTd3T0jMB7HnLw2bYQbK0g'
# attempt to get from API v2
$ http :3000/todos Accept:'application/vnd.todos.v2+json' Authorization:'ey...AWH3FNTd3T0jMB7HnLw2bYQbK0g'
In case we attempt to access a nonexistent version, the API will default to v1 since we set it as the default version. For testing purposes, let's define v2.
如果我们尝试访问不存在的版本,则由于我们将其设置为默认版本,因此该API将默认为v1。 为了进行测试,让我们定义v2。
Generate a v2 todos controller
生成v2 todos控制器
$ rails g controller v2/todos
Define the namespace in the routes.
在路由中定义名称空间。
#config/routes.rb
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
# 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
Remember, non-default versions have to be defined above the default version.
请记住,非默认版本必须在默认版本之上定义。
Since this is test controller, we'll define an index controller with a dummy response.
由于这是测试控制器,因此我们将定义一个具有虚拟响应的索引控制器。
# app/controllers/v2/todos_controller.rb
class V2::TodosController < ApplicationController
def index
json_response({ message: 'Hello there'})
end
end
Note the namespace syntax, this is shorthand in Ruby to define a class within a namespace. Great, now fire up the server once more and run some tests.
注意名称空间语法,这是Ruby中在名称空间内定义类的简写。 太好了,现在再次启动服务器并运行一些测试。
# get todos from API v1
$ http :3000/todos Accept:'application/vnd.todos.v1+json' Authorization:'eyJ0e...Lw2bYQbK0g'
# get todos from API v2
$ http :3000/todos Accept:'application/vnd.todos.v2+json' Authorization:'eyJ0e...Lw2bYQbK0g'
Voila! Our API responds to version 2!
瞧! 我们的API响应版本2!
序列化器 ( Serializers )
At this point, if we wanted to get a todo and its items, we'd have to make two API calls. Although this works well, it's not ideal.
此时,如果我们要获取待办事项及其项目,则必须进行两次API调用。 尽管此方法效果很好,但并不理想。
We can achieve this with serializers. Serializers allow for custom representations of JSON responses. Active model serializers make it easy to define which model attributes and relationships need to be serialized. In order to get todos with their respective items, we need to define serializers on the Todo model to include its attributes and relationships.
我们可以使用序列化器来实现。 序列化程序允许自定义表示JSON响应。 主动模型序列化程序使定义需要序列化的模型属性和关系变得容易。 为了获得待办事项及其各自的项目,我们需要在Todo模型上定义序列化器以包括其属性和关系。
First, let's add active model serializers to the Gemfile:
首先,让我们将活动模型序列化器添加到Gemfile中:
# Gemfile
# [...]
gem 'active_model_serializers', '~> 0.10.0'
# [...]
Run bundle to install it:
运行捆绑软件进行安装:
$ bundleinstall
Generate a serializer from the todo model:
从todo模型生成序列化器:
$ rails g serializer todo
This creates a new directory app/serializers
and adds a new file todo_serializer.rb
. Let's define the todo serializer with the data that we want it to contain.
这将创建一个新目录app/serializers
,并将一个新文件添加到todo_serializer.rb
。 让我们用我们要包含的数据定义待办事项序列化器。
# 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
We define a whitelist of attributes to be serialized and the model association (only defined attributes will be serialized). We've also defined a model association to the item model, this way the payload will include an array of items. Fire up the server, let's test this.
我们定义了要序列化的属性和模型关联的白名单 (仅定义的属性将被序列化)。 我们还定义了与项目模型的模型关联,这样,有效载荷将包含项目数组。 启动服务器,让我们测试一下。
# create an item for todo with id 1
$ http POST :3000/todos/1/items name='Listen to Don Giovanni' Accept:'application/vnd.todos.v1+json' Authorization:'ey...HnLw2bYQbK0g'
# get all todos
$ http :3000/todos Accept:'application/vnd.todos.v1+json' Authorization:'ey...HnLw2bYQbK0g'
This is great. One request to rule them all!
这很棒。 一个要求将它们全部统治的请求!
分页 ( Pagination )
Our todos API has suddenly become very popular. All of a sudden everyone has something to do. Our data set has grown substantially. To make sure the requests are still fast and optimized, we're going to add pagination; we'll give clients the power to say what portion of data they require.
我们的todos API突然变得非常流行。 突然每个人都有事要做。 我们的数据集已经大大增加。 为了确保请求仍然快速且经过优化,我们将添加分页; 我们将使客户有权说出他们需要哪一部分数据。
To achieve this, we'll make use of the will_paginate gem.
为了实现这一点,我们将使用will_paginate gem。
Let's add it to the Gemfile:
让我们将其添加到Gemfile中:
# Gemfile
# [...]
gem 'will_paginate', '~> 3.1.0'
# [...]
Install it:
安装它:
$ bundleinstall
Let's modify the todos controller index action to paginate its response.
让我们修改todos控制器索引操作以分页其响应。
# 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
The index action checks for the page number in the request params. If provided, it'll return the page data with each page having twenty records each. As always, let's fire up the Rails server and run some tests.
索引操作检查请求参数中的页码。 如果提供的话,它将返回页面数据,每个页面有二十个记录。 与往常一样,让我们启动Rails服务器并运行一些测试。
# request without page
$ http :3000/todos Accept:'application/vnd.todos.v1+json' Authorization:'eyJ0...nLw2bYQbK0g'
# request for page 1
$ http :3000/todos page==1 Accept:'application/vnd.todos.v1+json' Authorization:'eyJ0...nLw2bYQbK0g'
# request for page 2
$ http :3000/todos page==2 Accept:'application/vnd.todos.v1+json' Authorization:'eyJ0...nLw2bYQbK0g'
The page number is part of the query string. Note that when we request the second page, we get an empty array. This is because we don't have more that 20 records in the database. Let's seed some test data into the database.
页码是查询字符串的一部分。 请注意,当我们请求第二页时,我们得到一个空数组。 这是因为我们数据库中没有超过20条记录。 让我们将一些测试数据播种到数据库中。
Add faker and install faker gem. Faker generates data at random.
添加fakerr并安装fakerr gem。 Faker随机生成数据。
# Gemfile
# [...]
gem 'faker'
# [...]
In db/seeds.rb
let's define seed data.
在db/seeds.rb
我们定义种子数据。
# db/seeds.rb
# seed 50 records
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
Seed the database by running:
通过运行以下命令来播种数据库:
$ rake db:seed
Awesome, fire up the server and rerun the HTTP requests. Since we have test data, we're able to see data from different pages.
太棒了,启动服务器并重新运行HTTP请求。 由于我们拥有测试数据,因此我们可以查看来自不同页面的数据。
结论 ( Conclusion )
Congratulations for making it this far! We've come a long way! We've gone through generating an API-only Rails application, setting up a test framework, using TDD to implement the todo API, adding token-based authentication with JWT, versioning our API, serializing with active model serializers, and adding pagination features.
恭喜! 我们已经走了很长一段路! 我们已经完成了生成纯API的Rails应用程序,设置测试框架,使用TDD来实现todo API,使用JWT添加基于令牌的身份验证,对API进行版本控制,使用活动模型序列化程序进行序列化以及添加分页功能的过程。
Having gone through this series, I believe you should be able to build a RESTful API with Rails 5. Feel free to leave any feedback you may have in the comments section below. If you found the tutorial helpful, don't hesitate to hit that share button. Cheers!
完成本系列文章后,我相信您应该能够使用Rails 5构建RESTful API。请随时在下面的评论部分中留下您的任何反馈。 如果您发现本教程对您有帮助,请立即点击该“共享”按钮。 干杯!
翻译自: https://scotch.io/tutorials/build-a-restful-json-api-with-rails-5-part-three
rails 返回json