Ruby on Rails 實戰聖經

使用 Rails 5.0+ 及 Ruby 2.3+

電子書製作中,歡迎留下 E-mail,有消息將會通知您。若您有任何意見、鼓勵或勘誤,也歡迎來信給我。願意贊助支持的話,这是我的支付宝微信 和乙太幣 ETH 地址
0x232b7245EBE02900c21682be1e6Ad4e839751F6a

實作 Web APIs

Good code is its own best documentation. As you’re about to add a comment, ask yourself, ‘How can I improve the code so that this comment isn’t needed?’ Improve the code and then document it to make it even clearer. – Steve McConnell, Code Complete 作者

  • 設計 Web APIs 的用途包括:
    • 提供給手機 iOS, Android 應用程式的 Web API
    • 提供 JavaScript MVC application 的 Web API
    • 建立 API 平台,開放 APIs 給第三方開發者使用

Rails 5.0 提供了 API mode 可以產生一個 Rails 專案「只 Only」作為 API server,但這是不必要的,除非你想擠一點效能出來,但是你會關掉整個 ActionView。

Router 路由實作

拆開到 /api/v1,例如:

scope :path => '/api/v1/', :module => "api_v1", :as => 'v1', :defaults => { :format => :json } do
  resources :topics
end
  • Why? 保持 API Versioning 相容性,因應不隨意變更 API 格式
  • 也有人偏好不用 resources 語法,乖乖一條條將路由規格列出來。因為每個 API 都需要列出來與 client 端合作,寫文件時仍會一條條列出來。

Controller 實作

  • 和路由一樣也是拆開,因為 API 需求跟 server-rending HTML 會差很多,例如 params 參數設計的格式就長的很不一樣,後者會搭配 ActionView 的 Form helper 變成 params[:event][:name],前者則偏好簡單設計成 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
# app/controllers/api_v1/events_controller.rb
class ApiV1::EventsController < ApiController
end
  • 使用正確的 HTTP Status Code
    • 例如 render :json => { :message => "your error message" }, :status => 400
  • Response Format 採用 JSON。但是要如何產生 JSON 格式呢? 有幾種方法:
    • to_json 方法
      • 超級簡單,直接在 controller 裡面就可以 render :json => obj.to_json
      • 彈性低,不好擴充。比較適合簡單的情況,例如 400 時。
    • jbuilder 方式: https://github.com/rails/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
    • 四種常見的POST 提交數據方式
    • Rails 可以吃 form data 也可以吃 JSON
    • 瀏覽器表單是用 application/x-www-form-urlencoded,如果有檔案上傳(在 form attribute 加上 enctype=”multipart/form-data” 則改用 multipart/form-data。
    • JSON 不能做檔案上傳,檔案上傳要用 multipart/form-data

jBuilder 錦囊妙計

重點包括:

  • 如何輸出 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 使用者認證

不像瀏覽器有 cookie,每個 request 都必須帶有我們自行設計的 token 參數,我們才可以識別使用者。

首先是 Model 部分,主要新增一個欄位 authentication_token 欄位,並用 Devise.friendly_token 產生亂數 token:

產生 Migration,指令是 rails g migration add_token_to_users

class AddTokenToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :authentication_token, :string
    add_index :users, :authentication_token, :unique => true

    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方法:

class User < ApplicationRecord
  #.....
  before_create :generate_authentication_token

  def generate_authentication_token
     self.authentication_token = Devise.friendly_token
  end
end

接著我們在 ApiController 上實作 before_action :authenticate_user_from_token! ,如果有帶auth_token就進行登入(但這裡沒有強制一定要登入):

class ApiController < ActionController::Base
  before_action :authenticate_user_from_token!

    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

如果是強制一定要登入的action,用法跟之前 Device 一樣,例如以下整個 EventsController 就一定要登入了,不然會回傳錯誤:

class ApiV1::EventsController < ApiController

  before_action :authenticate_user!

end

上述的部分也可以安裝現成的套件 https://github.com/gonzalo-bulnes/simple_token_authentication

接下來實作 API 的登入和登出,讓使用者可以用帳號密碼,或是 facebook access_token 來登入,來換得上述的 authentication_token。也就是 POST /api/v1/login 先用 email 帳號密碼登入,拿到 auth_token:

用戶端拿到 auth_token 後,之後的每個 request 都必須帶入 auth_token。

上述的作法是整合 Devise 和 omniauth-facebook,如果不想整合 Devise 的話,也可以自己把 current_user 做出來,例如這份 Example code

其他議題

》回到頁首