实作 Web APIs
- 设计 Web APIs 的用途包括:
- 提供给手机 iOS, Android 应用程式的 Web API
- 提供 JavaScript MVC application 的 Web API
- 建立 API 平台,开放 APIs 给第三方开发者使用
Rails 5.0 提供了 API mode 可以产生一个 Rails 专案「只 Only」作为 API server,但这是不必要的,除非你想挤一点效能出来,但是你会关掉整个 ActionView。
拆开到 /api/v1,例如:
- Why? 保持 API Versioning 相容性,因应不随意变更 API 格式
- 也有人偏好不用 resources 语法,乖乖一条条将路由规格列出来。因为每个 API 都需要列出来与 client 端合作,写文件时仍会一条条列出来。
Controller 实作
- 和路由一样也是拆开,因为 API 需求跟 server-rending HTML 会差很多,例如 params 参数设计的格式就长的很不一样,后者会搭配 ActionView 的 Form helper 变成 ,前者则偏好简单设计成
params[:name]
- 也因此 business logic 尽量重构到 Model,这样才可以提高 code reusability
- 新加 ApiController 与 ApplicationController 拆开:因为不需要防御 CSRF,认证方式也不同:
# app/controllers/api_controller.rb
class ApiController < ActionController::Base
end
- 新增 API 专用的 Controller,并继承上述的 ApiController。例如
rails g controller api_v1::events
- 使用正确的 HTTP Status Code
- 例如
render :json => { :message => "your error message" }, :status => 400
- 例如
- Response Format 采用 JSON。但是要如何产生 JSON 格式呢? 有几种方法:
- to_json 方法
- 超级简单,直接在 controller 里面就可以 render :json => obj.to_json
- jbuilder 方式:
- 也是 Rails 内建
- template 里面可以根据不同条件组合,例如有登入没登入
- 可拆 partial,弹性高
- serializer 方式: https://github.com/rails-api/active_model_serializers
- 需额外安装 gem
- to_json 方法
- Request Format 支援哪些? (Client 端用什么格式送出资料?)
- Rails 支援 application/x-www-form-urlencoded、multipart/form-data 和 application/json
- Rails 可以吃 form data 也可以吃 JSON
- 浏览器表单是用 application/x-www-form-urlencoded,如果有档案上传(在 form attribute 加上 enctype=”multipart/form-data” 则改用 multipart/form-data。
- JSON 不能做档案上传,档案上传要用 multipart/form-data
重点包括:
- 如何输出 array 资料
- 可以使用 partial template 作 re-use
- 如何输出使用者上传档案(例如用 paperclip) 的网址
- 如何输出分页 paging 的资料,加上总共有几页等资讯
- 如何处理 inline relationship 资料
API 的自动化测试
手动测试的方式,参考
- 写 RSpec Request 测试,不然很难测试非 GET 的操作
- 测试中 Ruby Hash 的 symbol key 转 JSON 再转回来,key 会变成字串
- 测试中若要比对 Ruby Time 时间物件和
JSON.parse
出来的时间物件,前者要多转一次as_json
,不然会差一点。
- 实作 https://github.com/ihower/rails-exercise-ac8/blob/master/app/views/api_v1/topics/index.json.jbuilder
- 测试
实作 Web APIs 使用者认证
首先是 Model 部分,主要新增一个字段 authentication_token
字段,并用 Devise.friendly_token 产生乱数 token:
产生 Migration,指令是 rails g migration add_token_to_users
class AddTokenToUsers < ActiveRecord::Migration[5.1]
def change
User.find_each do |u|
puts "generate user #{u.id} token"
u.generate_authentication_token
u.save!
end
end
end
修改 User Model 加上generate_authentication_token
方法:
接着我们在 ApiController 上实作 before_action :authenticate_user_from_token!
,如果有带auth_token
就进行登入(但这里没有强制一定要登入):
class ApiController < ActionController::Base
def authenticate_user_from_token!
if params[:auth_token].present?
user = User.find_by_authentication_token( params[:auth_token] )
# Devise: 设定 current_user
sign_in(user, store: false) if user
end
end
end
上述的部分也可以安装现成的套件
接下来实作 API 的登入和登出,让使用者可以用帐号密码,或是 facebook access_token 来登入,来换得上述的 authentication_token
。也就是 先用 email 帐号密码登入,拿到 auth_token:
- 实作 Controller Code: https://github.com/ihower/rails-exercise-ac7/blob/master/app/controllers/api_v1/auth_controller.rb
- 实作 Model Code: (透过 get_fb_data 这个方法去验证用户传过来的 facebook token 是否正确)
- 测试 Request Spec: https://github.com/ihower/rails-exercise-ac7/blob/master/spec/requests/auth_spec.rb
用户端拿到 auth_token 后,之后的每个 request 都必须带入 auth_token。
上述的作法是整合 Devise 和 omniauth-facebook,如果不想整合 Devise 的话,也可以自己把 current_user
做出来,例如这份