RESTful API 優化設計(Swift 版)

關鍵詞:iOS
開發語言:Swift
使用到的庫:Alamofire,SwiftyJSON,ObjectMapper

背景

最近重構 API 相關代碼,參考了 RocketChat.iOS 項目,獲益匪淺故分享記錄一下。RocketChat 對 API 的編寫思路很有意思,這里只是對相關設計做了簡單的整理。如果對此感興趣,可以直接查看其開源項目。(文末附有地址)

API 設計

API 設計屬于網咯層開發十分重要的一部分。API 設計是否合理,直接影響數據層的邏輯。接下來我將從 RESTful API 來分析和講解優化方案。

整體結構

先來看一下目錄結構


api_design.jpeg

主要的文件是 API、APIRequest、APIResource、APIParameters ,我們先從依賴關系最少的文件開始看起。

APIRequest

import Alamofire

protocol APIRequest {
  associatedtype APIResourceType: APIResource 
  
  var path: String { get }
  var method: HTTPMethod { get }
  var parameters: Parameters? { get }
}

APIRequest 是一個協議,用來提供請求所需的漏油,HTTP請求類型,表單參數,以及返回數據的 APIResourceType。在后文 APIRequest 擴展中將詳細說明各個屬性的作用。在這里只需要知道,APIRequest 是我們管理請求所需參數的地方

APIResource

APIResource 在上面已經出現過,但沒有講她的具體作用。通過閱讀代碼發現 APIResource 僅存放了一個 JSON(SwiftyJSON 的基本類型) 類型的 raw。那我們就先認為 APIResource 就是 API 請求返回的 json 數據好了。

import SwiftyJSON

class APIResource {
  let raw: JSON?

  required init(raw: JSON?) {
    self.raw = raw
  }
}

// 基本擴展
/**
API 請求返回的最基本數據結構:
{
  status: "error",
  message: "xxxx"    // 錯誤原因 
}
*/
extension APIResource {
  enum Status: String {
    case error
    case success
  }
  var status: Status? {
    return Status.init(rawValue: (raw?["status"].string)!)
  }
  
  var message: String? {
    return raw?["message"].string
  }
}

基本擴展新增了只讀屬性 status 和 message。在這里你可以根據自身的情況設計此模型

APIParameters

APIParameters 協議只有一個方法,為 Alamofire 提供 Parameters。我們也可以在子類中存放請求所需的表單參數,下文也會對其擴展做說明。

import Alamofire

protocol APIParameters {
  func toParameters() -> Parameters?
}

// 基本擴展
extension APIParameters {
  func toParameters() -> Parameters? {
    let paramDic = ModelToDic(model: self as AnyObject)
    return paramDic as? Parameters
  }
  
  /**
   對象轉換為字典
   - parameter model: 對象
   - returns: 轉化出來的字典
   */
  private func ModelToDic(model:AnyObject) -> NSDictionary{
    let redic = NSMutableDictionary()
    
    let mirror: Mirror = Mirror(reflecting: model)
    
    for p in mirror.children{
      if(p.label! != ""){
        redic.setValue("\(p.value)", forKey: p.label!)
      }
    }
    
    return redic
  }
}

上面三個類都是為最后的重頭戲 API 文件服務,話不多說一起來看看她吧

API

import Alamofire
import SwiftyJSON

typealias APIFetchCompletionBlock<R: APIRequest> = (R.APIResourceType) -> Void

final class API {
  
  let host: String // http 接口地址
  var authToken: String? // 根據需要添加
  var userId: String? // 根據需要添加
  
  init(host: String) {
    self.host = host
  }
  
  func fetch<R: APIRequest>(_ request: R, completion: (APIFetchCompletionBlock<R>)? = nil) {
    // 根據需要自行取舍
    let headers: HTTPHeaders = [
      "X-Auth-Token" : authToken!,
      "X-User-Id": userId!
    ]

    // 拼接 APIRequest 屬性, 使用  Alamofire 請求 HTTP
    Alamofire.request(host + request.path, method: request.method,parameters: request.parameters, headers: headers).responseJSON { (response) in
      guard let jsonStr = response.result.value else {
        #if DEBUG
        print("[API.fetch][\(request.path)]: No data.")
        #endif
        
        return
      }
      
      #if DEBUG
      print("[API.fetch][request: \(response.request)] [params: \(request.parameters)]]")
      print("[respons: \(response.result.value)")
      #endif
      
      let json = JSON(jsonStr) // 將返回的 json 字符串轉換成 JSON 對象
      //如果 Alamofire.request 沒有在主線程中使用,下面需要切換到主線程調用block
      completion?(R.APIResourceType(raw: json)) // 使用APIRequest 中設置的 APIResourceType 來創建 APIResource 對象
    }
  }
}

extension API {
  // 初始化 API 對象
  static func current() -> API? {
    let api = API(host: "\(MeteorConfig.domain)/api/v1")
    
    api.authToken = MeteorWrapper.shared.getMeteorToken()
    api.userId = MeteorWrapper.shared.getUserId()
    
    return api
  }
}

總的來說,上面的代碼難度不大。值得一說到就是 R.APIResourceType(raw: json) 的使用。R 為 APIRequest 的實現,APIResourceType 為 R 對應的關聯類型。聽起來有點繞,我們重新組織一下語言。
R.APIResourceType(raw: json) :創建 APIRequest 子類 R 關聯類型 APIResourceType 的實例,該實例需要傳入 json 字符串。
下面分別給出 APIRequest、APIResource、APIParameter 的擴展,方便理解。

APIRequest 擴展

我們實現一個處理會話的 ThreadRequest

import Alamofire
import RealmSwift

class ThreadRequest: APIRequest {
  // ThreadRequest 可以處理的事件
  enum Action {
    case addMembers(threadId: String, members: [String])
    case loadMissed(time: String, limit: Int?)
    case delete(threadId: String)
    case update(threadId: String, params: UpdateThreadInfoParameters)
  }
  
  /**  
    API 中的 R.APIResourceType(raw: json) 實際調用的下面這個類型。
    所以我們在實現 APIRequest 的時候需要對 APIResourceType 參數做相應的修改
  */
  typealias APIResourceType = ThreadResource
  
  var path: String
  
  var method: HTTPMethod
  
  var parameters: Parameters?
  
  init(action: Action) {
    switch action {      
    case .delete(let threadId):
      self.method = .delete
      self.path = "/threads/\(threadId)"
      
    case .update(let threadId, let params):
      self.method = .put
      self.path = "/threads/\(threadId)"
      self.parameters = params.toParameters()
      
    case .addMembers(let threadId, let members):
      self.method = .put
      self.path = "/threads/\(threadId)/members/\(members.joined(separator: ","))"

    case .loadMissed(let time, let limit):
      self.method = .get
      
      let paramLimit = limit != nil ? "&limit=\(limit!)" : ""
      
      self.path = "/threads/load_missed?time=\(time)\(paramLimit)"
    }
  }
  
  class ThreadResource: APIResource {
  // 根據你的需要解析 APIResource.raw 的 json 字符串,推薦盡量使用只讀屬性,統一結構
  }
}

APIParameters 擴展

import Alamofire

class UpdateThreadInfoParameters: APIParameters {
  // 需要傳入的參數
  var subject: String?
  var announcement: String?
  
  init(subject: String? = nil, announcement: String? = nil) {
    self.subject = subject
    self.announcement = announcement
  }
  
/** 
Alamofire 需要的 Parameters 是字典類型,所以這里需要將 APIParameters 子類的所有屬性轉換成 Parameters 。
細心的你一定觀察到,APIParameters 的基本擴展中已經實現了 toParameters() 方法。
這里復寫是為本實現做了定制
*/
  func toParameters() -> Parameters? {
    if subject != nil, announcement != nil {
      return ["subject": subject!, "announcement": announcement!]
      
    } else if subject != nil, announcement == nil {
      return ["subject": subject!]
      
    } else if subject == nil, announcement != nil {
      return ["announcement": announcement!]
      
    } else {
      return nil
    }
  }
}

思考

以上 API 的優化設計就做完了。是不是覺得十分清爽,擴展新接口的成本幾乎可以忽略。
回顧整個優化過程,APIRequest、APIResource、APIParameter 的抽離。使得API 接口組件化,我們可以根據需要定制 APIRequest,并將其傳遞給 API.current().fetch(::)。

參考

RocketChat.iOS

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網絡請求組件 FMDB本地數據庫組件 SD...
    陽明AGI閱讀 16,009評論 3 119
  • 小雨過后,慢悠悠的走在回家的路上,怡人的氣溫,加上雨后清新的空氣,讓我放慢了回家的腳步。路兩旁的紫薇開了,不那么濃...
    青青的子衿閱讀 274評論 0 3
  • 我叫涂安,我的祖父是大周慶烈將軍,曾跟隨周主南征北戰,殺得西南蠻軍龜縮邊陲,戮得東海倭賊遠遁海外,戰北國草莽于邊土...
    卓別馬閱讀 573評論 2 1
  • 我怎么如此幸運,又學到了楊力老師的一些知識,是關于講“虛”的 。現在的人們經常將“虛”掛在嘴邊,要知道這...
    國粹堂秦國強閱讀 226評論 0 0
  • 我們要把自己的生活過得像SVN一樣,隨時更新,隨時上傳,隨時解決沖突! 我們要接觸更多的形形色色的人,去經歷更多的...
    流年印記閱讀 218評論 0 0