關鍵詞:iOS
開發語言:Swift
使用到的庫:Alamofire,SwiftyJSON,ObjectMapper
背景
最近重構 API 相關代碼,參考了 RocketChat.iOS 項目,獲益匪淺故分享記錄一下。RocketChat 對 API 的編寫思路很有意思,這里只是對相關設計做了簡單的整理。如果對此感興趣,可以直接查看其開源項目。(文末附有地址)
API 設計
API 設計屬于網咯層開發十分重要的一部分。API 設計是否合理,直接影響數據層的邏輯。接下來我將從 RESTful API 來分析和講解優化方案。
整體結構
先來看一下目錄結構
主要的文件是 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(::)。