rails5_api_tutorial, 关于 Hartl 5教程的Michael Rails,学习如何构建一个现代的API

分享于 

42分钟阅读

GitHub

  繁體 雙語
Learn how to build a modern API on Michael Hartl's Rails 5 tutorial
  • 源代码名称:rails5_api_tutorial
  • 源代码网址:http://www.github.com/vasilakisfil/rails5_api_tutorial
  • rails5_api_tutorial源代码文档
  • rails5_api_tutorial源代码下载
  • Git URL:
    git://www.github.com/vasilakisfil/rails5_api_tutorial.git
    Git Clone代码到本地:
    git clone http://www.github.com/vasilakisfil/rails5_api_tutorial
    Subversion代码到本地:
    $ svn co --depth empty http://www.github.com/vasilakisfil/rails5_api_tutorial
    Checked out revision 1.
    $ cd repo
    $ svn up trunk
    
    现在,在你的Rails 应用程序中构建一个 API。 ( Rails 5版本)

    注意:如果你查找常规自述文件,它是在这里的

    注意:你可以通过打开问题或者发送请求请求来对本教程进行贡献 !

    注意:3,我创建了 API,在Ember中创建了相同的应用程序。

    在不改变现有应用程序代码的情况下,我将展示如何扩展 Rails 应用程序并构建一个 API。 在中,我们将使用hartl教程的Rails,这是一个经典的Rails 应用程序插件,并通过构建一个API来扩展它。

    设计我们的API

    设计一个API不是一个容易的过程。 通常很难预先知道客户需要什么。 但是,我们将尽可能支持大多数客户的需求:

    • 使用流行的JSONAPI规范的resty方法
    • 将超媒体用于相关资源,而不是嵌入它们
    • 在相同的响应数据中,否则在客户端需要许多请求

    对了,我们对REST的含义有一个很长的讨论。 就像JSONAPI一样,REST fielding? 绝对不是,但是它比常规的JSON响应更加 resty,而且它在库中具有广泛的支持。

    继续,让我们添加第一个资源,让它成为用户。 但是在添加控制器之前,我们先添加路线:

    #api namespace :apido namespace :v1do resources :sessions, only: [:create, :show]
     resources :users, only: [:index, :create, :show, :update, :destroy] do post :activate, on::collection resources :followers, only: [:index, :destroy]
     resources :followings, only: [:index, :destroy] do post :create, on::memberend resource :feed, only: [:show]
     end resources :microposts, only: [:index, :create, :show, :update, :destroy]
     endend

    每个记录的所有REST路由,仅获取集合(。Rails muddles向上 Collection REST路由与相同控制器中的元素REST路由)的方法和一对自定义路由。

    就像你看到的我们有很多路线。 这样做的思想是,教程大部分会触摸并显示,你会设法理解并查看代码 inside 中的其余部分,如图5 所示。 我认为扩展教程很无聊:)。 但是,如果你发现某些奇怪的事情,或者不理解某些事情,那么开启问题并询问:

    创建用户API控制器并添加对单个记录的获取方法支持:

    添加我们的第一个API资源

    首先我们要做的是把我们的API与应用程序的其他部分分离。 为此,我们将在不同的命名空间下创建一个新的控制器。 考虑到有版本的API是好的,让我们开始在 app/controllers/api/v1/ 下创建我们的第一个控制器

    classApi::V1::BaseController <ActionController::APIend

    就像你看到的,我们从 ActionController::API 而不是 ActionController::Base 来 inherit。 前者减少了一些特性,不需要使它更快速和 LESS 内存的疲劳。

    现在,让我们添加 users#show 操作:

    classApi::V1::UsersController <Api::V1::BaseControllerdefshow user =User.find(params[:id])
     render jsonapi: user, serializer:Api::V1::UserSerializerendend

    在 Rails 中,我喜欢构建api的一件事是控制器是超级干净的,默认情况下是。 我们只是请求数据库中的用户并使用AMS在JSON中呈现它。

    让我们在下面添加用户序列化程序 app/serializers/api/v1/user_serializer.rb 我们将使用 ActiveModelSerializers 进行JSON序列化。

    classApi::V1::UserSerializer <ActiveModel::Serializer attributes(*User.attribute_names.map(&:to_sym))
     has_many :followers, serializer:Api::V1::UserSerializer has_many :followings, key::followings, serializer:Api::V1::UserSerializerend

    如果我们现在请求一个用户,它也会呈现所有的追随者和以下的( 用户跟踪的用户)。 通常我们不希望这样,但是我们可能希望AMS只呈现客户端的url以异步获取数据。 让我们更改它,并为 Microposts ( 你可以在 active_model_serializers wiki上找到更多信息) 添加一个链接:

    classApi::V1::UserSerializer <ActiveModel::Serializer attributes(*User.attribute_names.map(&:to_sym))
     has_many :microposts, serializer:Api::V1::MicropostSerializerdo include_data(false)
     link(:related) {api_v1_microposts_path(user_id: object.id)}
     end has_many :followers, serializer:Api::V1::UserSerializerdo include_data(false)
     link(:related) {api_v1_user_followers_path(user_id: object.id)}
     end has_many :followings, key::followings, serializer:Api::V1::UserSerializerdo include_data(false)
     link(:related) {api_v1_user_followings_path(user_id: object.id)}
     endend

    还有一个需要修复的东西。 如果客户端要求用户在数据库中不存在,find 将引发 ActiveRecord::RecordNotFound 异常,Rails 将返回 500个错误。 但是我们真正想要的是返回 404个错误。 我们可以在 Api::V1::BaseController 中捕获异常并使 Rails 返回 404. 只需添加 Api::V1::BaseController:

     rescue_from ActiveRecord::RecordNotFound, with::not_founddefnot_foundreturn api_error(status:404, errors:'Not found')
     end

    由于客户端可以从 404状态代码中找出错误,body 部分中的"找不到"就足够了。

    提示:ruby 中的异常非常慢。 一种更快的方法是使用find_by请求用户使用,如果find_by返回了零,则返回 404.

    重要yuki24 打开 an一个问题,以阐明"rescue_from可能是所有时间中最糟糕的Rails 模式之一"。 ! 有关更多信息,请在中查看问题( 有关更多信息,请参阅:)

    如果现在发送请求 api/v1/users/1,我们将得到以下json响应:

    {
     "data": {
     "id": "1",
     "type": "users",
     "attributes": {
     "name": "Example User",
     "email": "example@railstutorial.org",
     "created-at": "2016-11-05T10:15:26Z",
     "updated-at": "2016-11-19T21:30:10Z",
     "password-digest": "$2a$10$or7HFYm/H07/uE79wDae3uXMmHOX3BvRKdgedPJ1SPceiMA40V25O",
     "remember-digest": null,
     "admin": true,
     "activation-digest": "$2a$10$X5IeDtGZPuZQEVQ.ZiUP4eUzfw9M9Pag/nR.0ONiXwAAp3w98iAuC",
     "activated": true,
     "activated-at": "2016-11-05T10:15:26.300Z",
     "reset-digest": null,
     "reset-sent-at": null,
     },
     "relationships": {
     "microposts": {
     "links": {
     "related": "/api/v1/microposts?user_id=1" }
     },
     "followers": {
     "links": {
     "related": "/api/v1/users/1/followers" }
     },
     "followings": {
     "links": {
     "related": "/api/v1/users/1/followings" }
     }
     }
     }
    }

    当然,我们需要在API上添加身份验证和授权,但是我们将在以后查看它们:

    添加索引方法

    现在,我们添加一个方法来检索所有用户。 方法索引的Rails 名称,按REST表示,它是一个作用于 users 集合的获取方法。

    classApi::V1::UsersController <Api::V1::BaseControllerdefindex users =User.all
     render jsonapi: users, each_serializer:Api::V1::UserSerializer,
     endend

    非常简单?

    添加身份验证

    对于认证,Rails 应用程序使用定制的实现。 这不是问题,因为我们构建了一个 API,我们需要重新实现身份验证终结点。 在api中,我们不使用 Cookies,也没有会话。 相反,当用户想要登录时,用她的用户名和密码向我们的API ( 在我们的例子中,它是 sessions 端点) 发送一个 HTTP POST 请求。 这个标记证明了她是谁。 在每个API请求中,Rails 根据发送的令牌查找用户。 如果没有找到带有接收令牌的用户,或者没有发送令牌,那么API应该返回 401 ( 未授权) 错误。

    让我们向用户添加令牌。

    首先我们添加一个回调,它向每个新用户创建一个令牌。

     before_validation :ensure_tokendefensure_tokenself.token = generate_hex(:token) unless token.present?
     enddefgenerate_hex(column)
     loopdo hex =SecureRandom.hex
     break hex unlessself.class.where(column => hex).any?
     endend

    然后,我们就创建了迁移:

    classAddTokenToUsers <ActiveRecord::Migration[5.0]
     defup add_column :users, :token, :stringUser.find_each{|user| user.save!}
     change_column_null :users, :token, falseenddefdown remove_column :users, :token, :stringendend

    并运行 bundle exec rails db:migrate 每个用户都有一个有效的非空标记。

    然后让我们添加 sessions 端点:

    classApi::V1::SessionsController <Api::V1::BaseControllerdefcreateif@user render(
     jsonapi:@user,
     serializer:Api::V1::SessionSerializer,
     status:201,
     include: [:user],
     scope:@user )
     elsereturn api_error(status:401, errors:'Wrong password or username')
     endendprivatedefcreate_params normalized_params.permit(:email, :password)
     enddefload_resource@user=User.find_by(
     email: create_params[:email]
     )&.authenticate(create_params[:password])
     enddefnormalized_paramsActionController::Parameters.new(
     ActiveModelSerializers::Deserialization.jsonapi_parse(params)
     )
     endend

    会话序列化程序:

    classApi::V1::SessionSerializer <Api::V1::BaseSerializer type :session attributes :email, :token, :user_id has_one :user, serializer:Api::V1::UserSerializerdo link(:self) {api_v1_user_path(object.id)}
     link(:related) {api_v1_user_path(object.id)}
     object
     enddefuser object
     enddefuser_id object.id
     enddeftoken object.token
     enddefemail object.email
     endend

    客户端可能只需要用户。电子邮件和令牌的id,但最好返回一些数据,以便更好地优化。 我们可以将我们从额外的请求保存到用户的端点:)

    {
     "data": {
     "id": "1",
     "type": "session",
     "attributes": {
     "email": "example@railstutorial.org",
     "token": "f42f5ccee3689209e7ca8e4f9bd830e2",
     "user-id": 1 },
     "relationships": {
     "user": {
     "data": {
     "id": "1",
     "type": "users" },
     "links": {
     "self": "/api/v1/users/1",
     "related": "/api/v1/users/1" }
     }
     }
     },
     "included": [
     {
     "id": "1",
     "type": "users",
     "attributes": {
     "name": "Example User",
     "email": "example@railstutorial.org",
     "created-at": "2016-11-05T10:15:26Z",
     "updated-at": "2016-11-19T21:30:10Z",
     "password-digest": "$2a$10$or7HFYm/H07/uE79wDae3uXMmHOX3BvRKdgedPJ1SPceiMA40V25O",
     "remember-digest": null,
     "admin": true,
     "activation-digest": "$2a$10$X5IeDtGZPuZQEVQ.ZiUP4eUzfw9M9Pag/nR.0ONiXwAAp3w98iAuC",
     "activated": true,
     "activated-at": "2016-11-05T10:15:26.300Z",
     "reset-digest": null,
     "reset-sent-at": null,
     "token": "f42f5ccee3689209e7ca8e4f9bd830e2",
     "microposts-count": 99,
     "followers-count": 37,
     "followings-count": 48,
     "following-state": false,
     "follower-state": false },
     "relationships": {
     "microposts": {
     "links": {
     "related": "/api/v1/microposts?user_id=1" }
     },
     "followers": {
     "links": {
     "related": "/api/v1/users/1/followers" }
     },
     "followings": {
     "links": {
     "related": "/api/v1/users/1/followings" }
     }
     }
     }
     ]
    }

    提示:是的,我们需要添加适当的授权: 只返回允许客户端看到的属性,稍后我们将处理该属性:)

    客户端拥有令牌后,将令牌和电子邮件发送给每个后续请求的API。 现在让我们在 Api::V1::BaseController 中添加 authenticate_user 过滤器 inside:!

    defauthenticate_user! token, _=ActionController::HttpAuthentication::Token.token_and_options(
     request
     )
     user =User.find_by(token: token)
     if user
     @current_user= user
     elseraiseUnauthenticatedErrorendend

    ActionController::HttpAuthentication::Token 解析保存令牌的授权 header。 实际上,授权 header 看起来像这样:

    Authorization: Token token="f42f5ccee3689209e7ca8e4f9bd830e2"

    现在我们已经设置了 current_user,现在应该继续进行授权了。

    添加授权

    我们将使用 Pundit,这是一个基于策略的简单而美妙的gem。 值得注意的是,无论API版本如何,授权应该是相同的,所以这里没有名称空间。 原来的Rails 应用程序没有授权 gem,但是使用自定义的一个( 没有错误) !

    添加 gem 并运行缺省策略生成生成器后,我们将创建用户策略:

    classUserPolicy <ApplicationPolicydefshow?returntrueenddefcreate?returntrueenddefupdate?returntrueif user.admin?
     returntrueif record.id == user.id
     enddefdestroy?returntrueif user.admin?
     returntrueif record.id == user.id
     endclassScope <ApplicationPolicy::Scopedefresolve scope.all
     endendend

    Pundit的问题是它有一种黑白的策略。 无论你是否允许查看资源,或者根本不允许。 我们希望有一个混合策略( 灰色的那个): 允许,但只允许特定的资源属性。

    在我们的应用程序中,我们将拥有 3个角色:

    • 一个 Guest,它在不进行身份验证的情况下请求API数据
    • Regular 用户
    • 一个 Admin,就像上帝一样,它可以访问一切

    因为我们将使用 FlexiblePermissions gem,它是在 Pundit 之上工作的。 基本上,除了通知控制器,如果允许用户拥有访问权限,你还会嵌入访问类型: 用户可以访问哪些属性。 如果用户没有请求特定字段,也可以指定默认值( 它是允许属性的子集)。 因此,首先让我们指定 User 角色的权限类:

    classUserPolicy <ApplicationPolicyclassAdmin <FlexiblePermissions::BaseclassFields <self::Fieldsdefpermittedsuper+ [
     :links ]
     endendendclassRegular <AdminclassFields <self::Fieldsdefpermittedsuper- [
     :activated, :activated_at, :activation_digest, :admin,
     :password_digest, :remember_digest, :reset_digest, :reset_sent_at,
     :token, :updated_at,
     ]
     endendendclassGuest <RegularclassFields <self::Fieldsdefpermittedsuper- [:email]
     endendendend

    就像你看到的,Admin 角色( 请求 User(s) 时) 可以访问所有内容,而且链接属性,这是一个compute属性定义序列化程序。

    然后我们拥有 Regular 角色( 请求 User(s) 时),它继承了 Admin,但是我们去掉了一些 private 属性。

    然后从 Guest 角色中删除更多的属性( 也就是说,电子邮件)。

    定义角色后,现在可以定义 User 资源的授权方法:

    classUserPolicy <ApplicationPolicydefcreate?returnRegular.new(record)
     enddefshow?returnGuest.new(record) unless user
     returnAdmin.new(record) if user.admin?
     returnRegular.new(record)
     endend

    这是典型的资源 CRUD。 你可以看到,对于用户创建,我们设置了 Regular 权限,无论。 对于( 这里仅显示 show 操作)的rest操作,我们在角色之间取决于用户。 让我们看看控制器现在变成了什么样子:

    defshow auth_user = authorize_with_permissions(User.find(params[:id]))
     render jsonapi: auth_user.record, serializer:Api::V1::UserSerializer,
     fields: {user: auth_user.fields}
     end

    在控制器中,我们指定允许序列化程序返回哪些属性,基于 authorize_with_permissions。 因此,对于客户机,响应变成:

    {
     "data": {
     "id": "1",
     "type": "users",
     "attributes": {
     "name": "Example User",
     "created-at": "2016-11-05T10:15:26Z" },
     "relationships": {
     "microposts": {
     "links": {
     "related": "/api/v1/microposts?user_id=1" }
     },
     "followers": {
     "links": {
     "related": "/api/v1/users/1/followers" }
     },
     "followings": {
     "links": {
     "related": "/api/v1/users/1/followings" }
     }
     }
     }
    }

    增加分页。速率限制和 CORS

    分页对于 2原因是必需的。 它为前端客户端添加了一些非常基本的超媒体,它增加了性能,因为它只呈现总资源的一部分。

    对于分页,我们将使用与Michael已经使用的相同的gem: 我们将只需要在以下 2种方法中使用它:

    defpaginate(resource)
     resource = resource.page(params[:page] ||1)
     if params[:per_page]
     resource = resource.per_page(params[:per_page])
     endreturn resource
     end#expects paginated resource!defmeta_attributes(object)
     {
     current_page: object.current_page,
     next_page: object.next_page,
     prev_page: object.previous_page,
     total_pages: object.total_pages,
     total_count: object.total_entries
     }
     end

    我应该注意到,你也可以使用 Kaminari插件,它们几乎相同。

    速率限制是过滤那些滥用我们API的无用的机器人或者用户的好方法。 它由 redis节流 gem 实现,NAME 表示它使用redis来存储基于用户的IP地址的限制。 我们只需要添加 gem 并在 config/rack_attack.rb 中的新文件中添加几行行行

    classRack::Attack redis =ENV['REDISTOGO_URL'] ||'localhost'Rack::Attack.cache.store =ActiveSupport::Cache::RedisStore.new(redis)
     throttle('req/ip', limit:1000, period:10.minutes) do |req|
     req.ip if req.path.starts_with?('/api/v1')
     endend

    并在 config/application.rb 中启用它:

     config.middleware.use Rack::Attack

    CORS 是一个规范,"支持许多资源( 比如。 网页上要从它的他域发出资源的域中要求的字体,JavaScript,等等 )。 实际上它允许我们将javascript客户端从API加载到另一个域中,并允许js向我们的API发送AJAX请求。

    对于 Rails,我们只需安装 rack-cors gem 并允许:

     config.middleware.insert_before 0, "Rack::Cors"do allow do origins '*' resource '*', headers::any, methods: [:get, :post, :put, :patch, :delete, :options, :head]
     endend

    我们允许从任何地方访问,作为一个适当的API。 我们可以通过指定来源的主机名来设置允许访问API的客户端限制。

    测试

    现在我们来写一些测试 ! 我们将使用 Rack::Test helper 方法来描述这里的 在构建api时,测试路径输入-> 控制器-> 模型-> 控制器-> 序列化程序-> 输出可以正常工作很重要。 这就是为什么我觉得API测试站在单元测试和集成测试之间。 注意,由于Michael已经添加了一些模型测试,我们不必对它 pedantic。 我们可以跳过模型,只测试API控制器。

    describe Api::V1::UsersController, type::apido context :showdo before do create_and_sign_in_user
     @user=FactoryGirl.create(:user)
     get api_v1_user_path(@user.id), format::jsonend it 'returns the correct status'do expect(last_response.status).to eql(200)
     end it 'returns the data in the body'do body =JSON.parse(last_response.body, symbolize_names:true)
     expect(body.dig(:data, :attributes, :name).to eql(@user.name)
     expect(body.dig(:data, :attributes, :email).to eql(@user.name)
     expect(body.dig(:data, :attributes, :admin).to eql(@user.admin)
     expect(body.dig(:data, :attributes, :updated_at)).to eql(@user.created_at.iso8601)
     expect(body.dig(:data, :attributes, :updated_at)).to eql(@user.updated_at.iso8601)
     endendend

    create_and_sign_in_user 方法来自我们的认证 helper:

    moduleAuthenticationHelperdefsign_in(user)
     header('Authorization', "Token token="#{user.token}"")
     enddefcreate_and_sign_in_user user =FactoryGirl.create(:user)
     sign_in(user)
     return user
     endalias_method:create_and_sign_in_another_user, :create_and_sign_in_userdefcreate_and_sign_in_admin admin =FactoryGirl.create(:admin)
     sign_in(admin)
     return admin
     endalias_method:create_and_sign_in_admin_user, :create_and_sign_in_adminendRSpec.configure do |config|
     config.includeAuthenticationHelper, type::apiend

    我们要测试什么?

    • 路径输入-> 控制器-> 模型-> 控制器-> 序列化器-> 输出实际工作正常
    • 控制器返回正确的错误状态
    • 控制器根据发出请求的用户角色对API属性作出响应

    实际上,我们在这里实现了RSpecs方法 respond_to和 rspec rails'be_valid 方法在更高级别上。 然而,断言API响应的每个属性与初始对象相等,需要花费太多的时间和空间。 如果更改序列化程序并使用HAL或者 JSONAPI,则会发生什么情况?

    相反,我们可以使用 rspec-api_helpers插件来自动化这里过程:

    require'rails_helper'describe Api::V1::UsersController, type::apido context :showdo before do create_and_sign_in_user
     FactoryGirl.create(:user)
     @user=User.last!
     get api_v1_user_path(@user.id)
     end it_returns_status(200)
     it_returns_attribute_values(
     resource:'user', model:proc{@user}, attrs: [
     :id, :name, :created_at, :microposts_count, :followers_count,
     :followings_count ],
     modifiers: {
     created_at:proc{|i| i.in_time_zone('UTC').iso8601.to_s},
     id:proc{|i| i.to_s}
     }
     )
     endend

    这个 gem 为你添加了一个自动化的方法来测试你的JSONAPI ( 或者其他任何API规范) respone: 一个简单的API来测试所有属性。

    此外,为了拥有更健壮的测试,我们可以添加 rspec-json_schema 测试响应是否遵循预先定义的JSON模式。 例如用于常规角色的JSON架构如下所示:

    
    {
    
    
    "$schema":"http://json-schema.org/draft-04/schema#",
    
    
    "type":"object",
    
    
    "properties": {
    
    
    "data": {
    
    
    "type":"object",
    
    
    "properties": {
    
    
    "id": {
    
    
    "type":"string"
    
    
     },
    
    
    "type": {
    
    
    "type":"string"
    
    
     },
    
    
    "attributes": {
    
    
    "type":"object",
    
    
    "properties": {
    
    
    "name": {
    
    
    "type":"string"
    
    
     },
    
    
    "email": {
    
    
    "type":"string"
    
    
     },
    
    
    "created-at": {
    
    
    "type":"string"
    
    
     }
    
    
     },
    
    
    "required": [
    
    
    "name",
    
    
    "email",
    
    
    "created-at",
    
    
     ]
    
    
     },
    
    
    "relationships": {
    
    
    "type":"object",
    
    
    "properties": {
    
    
    "microposts": {
    
    
    "type":"object",
    
    
    "properties": {
    
    
    "links": {
    
    
    "type":"object",
    
    
    "properties": {
    
    
    "related": {
    
    
    "type":"string"
    
    
     }
    
    
     },
    
    
    "required": [
    
    
    "related"
    
    
     ]
    
    
     }
    
    
     },
    
    
    "required": [
    
    
    "links"
    
    
     ]
    
    
     },
    
    
    "followers": {
    
    
    "type":"object",
    
    
    "properties": {
    
    
    "links": {
    
    
    "type":"object",
    
    
    "properties": {
    
    
    "related": {
    
    
    "type":"string"
    
    
     }
    
    
     },
    
    
    "required": [
    
    
    "related"
    
    
     ]
    
    
     }
    
    
     },
    
    
    "required": [
    
    
    "links"
    
    
     ]
    
    
     },
    
    
    "followings": {
    
    
    "type":"object",
    
    
    "properties": {
    
    
    "links": {
    
    
    "type":"object",
    
    
    "properties": {
    
    
    "related": {
    
    
    "type":"string"
    
    
     }
    
    
     },
    
    
    "required": [
    
    
    "related"
    
    
     ]
    
    
     }
    
    
     },
    
    
    "required": [
    
    
    "links"
    
    
     ]
    
    
     }
    
    
     },
    
    
    "required": [
    
    
    "microposts",
    
    
    "followers",
    
    
    "followings"
    
    
     ]
    
    
     }
    
    
     },
    
    
    "required": [
    
    
    "id",
    
    
    "type",
    
    
    "attributes",
    
    
    "relationships"
    
    
     ]
    
    
     }
    
    
     },
    
    
    "required": [
    
    
    "data"
    
    
     ]
    
    
    }
    
    
    
    

    requiredadditionalProperties 属性上注意,这些属性会使模式。 最后,show 操作的测试规范变成:

    require'rails_helper'describe Api::V1::UsersController, '#show', type::apido describe 'Authorization'do context 'when as guest'do before doFactoryGirl.create(:user)
     @user=User.last!
     get api_v1_user_path(@user.id)
     end it_returns_status(401)
     it_follows_json_schema('errors')
     end context 'when authenticated as a regular user'do before do create_and_sign_in_user
     FactoryGirl.create(:user)
     @user=User.last!
     get api_v1_user_path(@user.id)
     end it_returns_status(200)
     it_follows_json_schema('regular/user')
     it_returns_attribute_values(
     resource:'user', model:proc{@user}, attrs: [
     :id, :name, :created_at, :microposts_count, :followers_count,
     :followings_count ],
     modifiers: {
     created_at:proc{|i| i.in_time_zone('UTC').iso8601.to_s},
     id:proc{|i| i.to_s}
     }
     )
     end context 'when authenticated as an admin'do before do create_and_sign_in_admin
     FactoryGirl.create(:user)
     @user=User.last!
     get api_v1_user_path(@user.id)
     end it_returns_status(200)
     it_follows_json_schema('admin/user')
     it_returns_attribute_values(
     resource:'user', model:proc{@user}, attrs:User.column_names,
     modifiers: {
     [:created_at, :updated_at] => proc{|i| i.in_time_zone('UTC').iso8601.to_s},
     id:proc{|i| i.to_s}
     }
     )
     endendend

    考虑到JSON模式对于响应属性非常详细和具体,我认为所有这些技术组合可以给我们提供非常强大的测试。

    final API

    我们已经注意到了,我们跳过了一些类似于创建或者更新用户。 因为我不想给你造成过多的信息。 你可以深入代码,看看如何实现所有内容:)

    作为参考,这个API用于 Ember应用程序,它模仿 Rails 教程。

    我们使用 ember-simple-auth插件插件进行身份验证和授权,尽管我们还没有在应用程序中使用。 但这就是api的美妙之处: 你可以隐藏你的实现细节:)

    在下面的部分中,我将介绍构建api时应该考虑的一些重要方面。 所有的( 除了uuid和模型缓存) 都在 final API中实现,你将在 github repo 中找到它们。 我真的认为你应该采取一个驱动器,并尝试添加模型缓存( 我建议使用的shopify identity_cache。) 如果你想:

    奖金:一些优化和提示

    时间戳

    当资源包含一天时,最好使用UTC时间和iso8601格式。 通常,我们真的不希望在我们的API中包含时区。 如果我们清楚地指出我们的datetimes是用utc表示的并且只接受utc日期时间,客户端将负责将utc日期时间转换为它们的。

    计数器

    另外,在构建API时,我们应该始终从客户的角度考虑。 例如,如果客户端请求用户,它可以能希望知道用户的microposts。跟随者或者下列( 用户跟随的用户) 数。

    目前,这可以通过向每个资源发送额外的请求并检查响应中元素的total_count 来实现。 Having 发送更多请求对客户机不好,因为这对于我们来说也不好,因为这意味着更多的请求。

    我们可以为每个关联添加( 高速缓存) 计数器,并返回那些与用户信息一起返回的。 首先,我们需要为每个计数器创建一个列,然后告诉 Rails 缓存计数器的( 通过在每个关联中添加 counter_cache: assocation_count )。 我们来了

    首先,我们创建迁移:

    classAddCacheCounters <ActiveRecord::Migration[5.0]
     defchange add_column :users, :microposts_count, :integer, null:false, default:0 add_column :users, :followers_count, :integer, null:false, default:0 add_column :users, :followings_count, :integer, null:false, default:0endend

    然后是 inside Micropost 模型:

     belongs_to :user, counter_cache:true

    和 inside Relationship 模型:

     belongs_to :follower, class_name:"User", counter_cache::followings_count belongs_to :followed, class_name:"User", counter_cache::followers_count

    在常规 Rails 开发中应该注意,即使不是 Having,这些计数器缓存列也是添加的。 它有助于在数据库列中缓存它们,而不是在每次需要它时运行 SQL COUNT(*)

    跟随器/跟随状态

    好的,让我们从客户的角度。 假设客户端想要检索用户,因这里它会随着计数器获得用户信息。 但是,在大多数情况下,你都需要知道是否遵循这里用户,否则是否遵循这里用户。

    在一个常规的Rails 应用程序中,我们可以立即对查询进行( 即使是从) 操作,或者使用 helper。 这里我们需要采取不同的方法。 如果我们预先提供这些信息而不是为这些信息创建新端点,让客户端完成请求将成为 LESS。

    我们将在序列化程序中将这些状态作为计算属性添加:

     attribute :following_state attribute :follower_statedeffollowing_stateRelationship.where(
     follower_id: current_user.id,
     followed_id: object.id
     ).exists?
     enddeffollower_stateRelationship.where(
     follower_id: object.id,
     followed_id: current_user.id
     ).exists?
     end

    我们应该缓存这个信息( 但我留给你如何做:)

    即使用户很少使用这些信息,你仍然应该在用户资源中使用它,但是在默认情况下,如果用户指定了 JSONAPI fields 参数,你只能提供这些资源属性。 这就给我们带来了下一个话题: 通过构建现代API帮助客户。 记住,你并不为自己构建 API,而是为客户机构建。 客户端的API越好,你所拥有的API客户机就会越多:

    奖金:构建现代 API

    现代 API,即你使用的规范,至少应该具有属性:

    • 稀疏字段
    • 粒度权限
    • 按需关联
    • 默认值( 帮助客户) !
    • 排序&分页
    • 筛选集合
    • 聚合查询

    这些API属性将帮助客户端避免unessecary数据,并要求确切地帮助我们的( 因为我们不会计算未使用的数据)。 理想情况下,我们希望在HTTP之上给客户机一个 ORM。

    稀疏字段,粒度许可和按需关联

    我们已经使用 flexible_permissions 角色解决了粒度许可问题。 每个角色只允许特定的属性和关联。 同样的Gems 也允许我们只选择允许字段的子集。

    JSONAPI已经指定了客户端如何查询资源的特定字段/关联。 现在我们需要做的是将用户字段/associaions的请求链接到角色属性和关联的允许。

    默认值,排序&分页,筛选集合和聚合查询

    我们已经使用 flexible_permissions 设置了默认值。 我们还在响应中添加了分页。

    现在我们需要允许客户端请求特定的排序,通过发送定制查询并请求聚合数据( 例如用户的平均追随者数) 来过滤集合。

    对于那些我们将要使用 active_hash_relation gem,它在我们的索引方法中添加了一个完整的API ! 请一定要把它检查一下。 这就像添加 2行一样简单:

    classApi::V1::UsersController <Api::V1::BaseControllerincludeActiveHashRelationdefindex auth_users = policy_scope(@users)
     render jsonapi: auth_users.collection,
     each_serializer:Api::V1::UserSerializer,
     fields: {user: auth_users.fields},
     meta: meta_attributes(auth_users.collection)
     endend

    现在,使用ActiveHashRelation我们可以要求在特定日期或者使用特定电子邮件前缀的用户创建的用户,我们也可以要求特定的排序和聚合查询。

    然而,在性能和安全性方面,首先过滤允许的参数是一个好主意。

    奖金:添加自动部署

    没有自动部署的新 Rails 项目不太酷。 circleci和codeship帮助我们构建和部署更快的服务。 在这个项目中,我们将使用 codeship

    创建新项目后,我们需要在设置部分添加以下命令:

    
    rvm use 2.3.3
    
    
    bundle install
    
    
    bundle exec rake db:create
    
    
    bundle exec rake db:migrate
    
    
    
    

    在测试secion中,我们可以运行所有测试( michael的测试和测试):

    
    rake test
    
    
    bundle exec rspec spec
    
    
    
    

    然后我们需要创建一个heroku应用程序( 如果heroku是我们希望托管代码的内容) 并获取 Codeship ( 或者任何其他自动部署服务) 所需的API密钥( 我很惊讶heroku没有为它的API键提供任何权限列表:/) 来部署代码。 一旦我们有了它,我们就添加了heroku管道,我们已经准备好了。

    现在,如果我们提交master并且测试是绿色的,那么它将在heroku中推动并部署 repo,并运行迁移:)

    : 在发生中断更改时: 如何处理版本 2

    我们构建我们的API,我们将它的发布,所有的。 我们总是可以添加更多的端点或者增强当前的版本,只要我们没有断开的更改。 尽管如这里,尽管需要改变,但我们仍然可以能达到一个需要更改的点。 不要紧急我们只需定义相同的路由,但是对于V2命名空间,定义 inherit 控制器。

    classApi::V2::UsersController <Api::V1::UsersControllerdefindex#new overriden index hereendend

    这样我们就可以节省大量的时间和精力为我们的V2 ( 虽然对于移动API版本,你可能需要比单个端点更多的更改)。

    奖金:添加文档 !

    记录我们的API是至关重要的,即使它支持超媒体。 文档帮助用户加速他们的应用程序或者客户端开发。 Rails的文档工具有很多,比如 swaggerslate。

    在这里我们将使用 slate,因为它很容易启动。

    我想把它们放在一起,因为它生成了css和html文件,因为它生成了css和html文件,因为它们生成了css和html文件,因为它们是使用bundler命令产生的。

    创建一个 app/docs/ 目录,然后克隆板库存储库并且 delete的. git 目录( 我们不需要石板的修改)。 在 app/docs/config.rb 中,将生成目录设置为 public 文件夹:

    set :build_dir, '../public/docs/'

    然后开始写你的文档。 你可以从我们的文档中获得一些启发:)

    奖金:提前查看

    就像我提到的,有 2个未实现的东西,但是你应该尝试将它们实现为测试:)

    首先,当我们知道我们的应用程序将具有API时,使用uuid是一个好主意。 利用ids我们可以向攻击者公开敏感信息。 在使用uuid时对数据库有轻微的性能冲击,但可能是 GREATER 带来的好处。 你也可以在这篇文章中查看以获得更多信息。

    其次,我们没有添加任何缓存。 按照我的经验,一个 Rails 应用程序像 should应该在普通的heroku X2大约 stand/分钟,但是添加缓存应该将它放到 2500. 但是我没有测试过。 谁有兴趣告诉我他要达到多少? ( 带或者没有缓存) 我很乐意添加一个额外的节,只是为了从你们这里进行优化。 只需创建 PR: d

    所有的人

    现在就开始吧,现在你真的应该开始构建你的Rails API了,而不是明天,而不是明天。

    我现在准备制作 Ember教程。 在那之前小心点 !

    你知道你可以通过打开一个问题或者发送一个请求请求来帮助这个教程?


    API  构建  模式  rails  learn  教程  
    相关文章