实作 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,认证方式也不同:
    1. # app/controllers/api_controller.rb
    2. class ApiController < ActionController::Base
    3. 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
    • 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 的自动化测试

    手动测试的方式,参考 https://ihower.tw/cs/web-apis.html#sec3

    • 写 RSpec Request 测试,不然很难测试非 GET 的操作
    • 测试中 Ruby Hash 的 symbol key 转 JSON 再转回来,key 会变成字串
    • 测试中若要比对 Ruby Time 时间物件和 JSON.parse 出来的时间物件,前者要多转一次 as_json,不然会差一点。

    实作 Web APIs 使用者认证

    首先是 Model 部分,主要新增一个字段 authentication_token 字段,并用 Devise.friendly_token 产生乱数 token:

    产生 Migration,指令是 rails g migration add_token_to_users

    1. class AddTokenToUsers < ActiveRecord::Migration[5.1]
    2. def change
    3. User.find_each do |u|
    4. puts "generate user #{u.id} token"
    5. u.generate_authentication_token
    6. u.save!
    7. end
    8. end
    9. end

    修改 User Model 加上generate_authentication_token方法:

    接着我们在 ApiController 上实作 before_action :authenticate_user_from_token! ,如果有带auth_token就进行登入(但这里没有强制一定要登入):

    1. class ApiController < ActionController::Base
    2. def authenticate_user_from_token!
    3. if params[:auth_token].present?
    4. user = User.find_by_authentication_token( params[:auth_token] )
    5. # Devise: 设定 current_user
    6. sign_in(user, store: false) if user
    7. end
    8. end
    9. end

    上述的部分也可以安装现成的套件

    接下来实作 API 的登入和登出,让使用者可以用帐号密码,或是 facebook access_token 来登入,来换得上述的 authentication_token。也就是 先用 email 帐号密码登入,拿到 auth_token:

    用户端拿到 auth_token 后,之后的每个 request 都必须带入 auth_token。

    上述的作法是整合 Devise 和 omniauth-facebook,如果不想整合 Devise 的话,也可以自己把 current_user 做出来,例如这份