如今的移動應用大多是“客戶端-服務器”模式,某個應用中很可能就包含或大或小的網絡層結構。迄今為止筆者見過的許多實現均有一些缺陷,新構建的這個或許仍有缺陷,但在手邊的這兩個項目中效果都很不錯,而且測試覆蓋率幾乎達到100%。本文只討論與單個后臺通訊、發送 JSON 編碼請求的網絡層,這個網絡層會與 AWS 通訊,發送一些文件,整體結構并不復雜,不過相應功能的擴展也應當十分簡單。
在構建相應網絡層之前,我先提出一些問題:
首先,我們要了解后端 URL 應當放在哪里?系統的其它部分怎么知道向哪里發送請求?這里我們更偏好創建存儲這類信息的BackendConfiguration 類。
import Foundation public final class BackendConfiguration { let baseURL: NSURL public init(baseURL: NSURL) {
self.baseURL = baseURL
} public static var shared: BackendConfiguration!
}
這種類易于測試,也易于配置,設定共享靜態變量之后,我們就能從網絡層的任意位置對其進行訪問,不需將這個變量發送到其它位置。
let backendURL = NSURL(string: "https://szulctomasz.com")!
BackendConfiguration.shared = BackendConfiguration(baseURL: backendURL)
在找到解決方案前,筆者在這個問題上做了頗有一陣子的實驗,在配置 NSURLSession 時曾嘗試對端點執行硬編碼的方式,并嘗試了一些了解端點、便于實例化與注入的虛擬資源類對象,但并未找到需要的方案。然后得出了設想:創建知道要接入哪個端點,使用什么方法,該是 GET、POST、PUT 還是其它什么的 *Request 對象,它要了解如何配置請求主體,以及要 pass 什么頭文件。
于是我得出了這樣的代碼:
protocol BackendAPIRequest {
var endpoint: String { get } var method: NetworkService.Method { get } var parameters: [String: AnyObject]? { get } var headers: [String: String]? { get } }
實現這個協議的類能夠提供構建請求所需的基本信息,NetworkService.Method 只是個帶有 GET、POST、PUT、DELETE案例的enum函數。
映射一個端點的請求示例如下:
final class SignUpRequest: BackendAPIRequest { private let firstName: String private let lastName: String private let email: String private let password: String init(firstName: String, lastName: String, email: String, password: String) { self.firstName = firstName self.lastName = lastName self.email = email self.password = password
} var endpoint: String { return "/users" } var method: NetworkService.Method { return .POST
} var parameters: [String: AnyObject]? {
return [ "first_name": firstName, "last_name": lastName, "email": email, "password": password ] }
var headers: [String: String]? {
return ["Content-Type": "application/json"] }
}
為了避免給每個 header 創建 dictionary,我們可以為 BackendAPIRequest 定義擴展。
extension BackendAPIRequest {
func defaultJSONHeaders() -> [String: String] { return ["Content-Type": "application/json"]
}
}
*Request 類利用所需參數成功創建了請求。我們要始終確保至少所需的參數都能 pass,否則就無法創建請求對象。定義端點非常簡單,如果要將對象的id包括在端點中,添加起來也是超級簡單的,因為這些id在屬性中有存儲。
private let id: String
init(id: String, ...) {
self.id = id
}
var endpoint: String { return "/users/\(id)" }
請求的方法從未變過,參數的body和header的構成與維護都很簡單,整個代碼測試起來也很容易。
有人在 Swift 中使用 AFNetworking(Objective-C) 和 Alamofire,這種方式很常見,不過由于 NSURLSession 也可以很好地完成工作,有時候不需要任何的第三方框架,否則只會讓應用框架更為復雜。
目前的解決方案包含兩個類—— NetworkService和 BackendService:
NetworkService:允許執行 HTTP 請求,包含 NSURLSession。每項網絡服務每次都只能執行一個請求,請求可以取消,成功和失敗都有回饋。
BackendService:負責接收與后臺相關的請求,包含 NetworkService。在目前使用的版本中,系統嘗試利用NSJSONSerializer 將響應數據序列化為 JSON。
class NetworkService { private var task: NSURLSessionDataTask?
private var successCodes: Range<Int> = 200..<299 private var failureCodes: Range<Int> = 400..<499 enum Method: String { case GET, POST, PUT, DELETE
}
func request(url url: NSURL, method: Method, params: [String: AnyObject]? = nil, headers: [String: String]? = nil, success: (NSData? -> Void)? = nil, failure: ((data: NSData?, error: NSError?, responseCode: Int) -> Void)? = nil) { let mutableRequest = NSMutableURLRequest(URL: url, cachePolicy: .ReloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: 10.0) mutableRequest.allHTTPHeaderFields = headers mutableRequest.HTTPMethod = method.rawValue if let params = params { mutableRequest.HTTPBody = try! NSJSONSerialization.dataWithJSONObject(params, options: []) } let session = NSURLSession.sharedSession() task = session.dataTaskWithRequest(mutableRequest, completionHandler: { data, response, error in // Decide whether the response is success or failure and call // proper callback.
}) task?.resume() } func cancel() { task?.cancel() }
}
class BackendService {
private let conf: BackendConfiguration private let service: NetworkService!
init(_ conf: BackendConfiguration) {
self.conf = conf
self.service = NetworkService() }
func request(request: BackendAPIRequest, success: (AnyObject? -> Void)? = nil,
failure: (NSError -> Void)? = nil) {
let url = conf.baseURL.URLByAppendingPathComponent(request.endpoint) var headers = request.headers
// Set authentication token if available.
headers?["X-Api-Auth-Token"] = BackendAuth.shared.token
service.request(url: url, method: request.method, params: request.parameters, headers: headers, success: { data in var json: AnyObject? = nil if let data = data { json = try? NSJSONSerialization.JSONObjectWithData(data, options: []) }
success?(json) }, failure: { data, error, statusCode in
// Do stuff you need, and call failure block.
})
}
func cancel() {
service.cancel() }
}
大家都知道,BackendService 是可以在頭文件中設置驗證 token 的,BackendAuth 對象只是簡單的存儲,將 token 存在NSUserDefaults 中,如果必要的話,可以將 token 存在 Keychain 中。
BackendService 將 BackendAPIRequest 作為請求的一個參數,從請求對象處提取必要的信息。由于封裝的很好,后臺服務只管使用就行了。
public final class BackendAuth { private let key = "BackendAuthToken" private let defaults: NSUserDefaults public static var shared: BackendAuth! public init(defaults: NSUserDefaults) {
self.defaults = defaults
} public func setToken(token: String) {
defaults.setValue(token, forKey: key)
} public var token: String? { return defaults.valueForKey(key) as? String
} public func deleteToken() {
defaults.removeObjectForKey(key)
}
}
NetworkService、BackendService 和 BackendAuth 測試維護起來都很容易。
這里涵蓋的問題包括:我們想用什么方式來執行網絡請求?如果想要一次執行多個請求,要怎樣操作?一般來說,要以什么方式獲得請求成功或失敗的通知?
我們決定采用 NSOperationQueue 以及 NSOperations 來執行網絡請求,因此在將 NSOperation 歸入子類之后,將其異步屬性覆蓋,以返回 true。
public class NetworkOperation: NSOperation { private var _ready: Bool public override var ready: Bool { get { return _ready } set { update({ self._ready = newValue }, key: "isReady") }
} private var _executing: Bool public override var executing: Bool { get { return _executing } set { update({ self._executing = newValue }, key: "isExecuting") }
} private var _finished: Bool public override var finished: Bool { get { return _finished } set { update({ self._finished = newValue }, key: "isFinished") }
} private var _cancelled: Bool public override var cancelled: Bool { get { return _cancelled } set { update({ self._cancelled = newValue }, key: "isCancelled") }
} private func update(change: Void -> Void, key: String) {
willChangeValueForKey(key)
change()
didChangeValueForKey(key)
} override init() {
_ready = true _executing = false _finished = false _cancelled = false super.init()
name = "Network Operation" } public override var asynchronous: Bool { return true } public override func start() { if self.executing == false {
self.ready = false self.executing = true self.finished = false self.cancelled = false }
} /// Used only by subclasses. Externally you should use `cancel`. func finish() {
self.executing = false self.finished = true } public override func cancel() {
self.executing = false self.cancelled = true }
}
之后,由于希望通過 BackendService執行網絡調用,筆者將 NetworkOperation 歸入子類,并創建了 ServiceOperation。
public class ServiceOperation: NetworkOperation { let service: BackendService public override init() {
self.service = BackendService(BackendConfiguration.shared)
super.init()
} public override func cancel() {
service.cancel()
super.cancel()
}
}
由于類中內部生成 BackendService,就無需在每個子類中分別創建了。
下面列出了登錄操作的示例代碼:
public class SignInOperation: ServiceOperation { private let request: SignInRequest public var success: (SignInItem -> Void)? public var failure: (NSError -> Void)? public init(email: String, password: String) {
request = SignInRequest(email: email, password: password)
super.init()
} public override func start() {
super.start()
service.request(request, success: handleSuccess, failure: handleFailure)
} private func handleSuccess(response: AnyObject?) { do { let item = try SignInResponseMapper.process(response)
self.success?(item)
self.finish()
} catch {
handleFailure(NSError.cannotParseResponse())
}
} private func handleFailure(error: NSError) {
self.failure?(error)
self.finish()
}
}
在 start 方法中,服務會執行操作的構造函數內部生成的請求,將 handleSuccess 與 handleFailure 方法作為服務的request(_:success:failure:) 方法,發送回調函數。這樣代碼更干凈,并且仍保有可讀性。
系統會將操作單獨發送給 NetworkQueue 對象,并分別插入隊列。我們令其盡可能簡單化:
public class NetworkQueue { public static var shared: NetworkQueue! let queue = NSOperationQueue() public init() {} public func addOperation(op: NSOperation) {
queue.addOperation(op)
}
}
在同一個地方執行操作的優點是什么?
這是這個版本不得不延遲發布的原因:在之前版本的網絡層中,操作返回 Core Data 對象,回應收到后會被解析轉化為 Core Data 對象,這個解決方案非常不理想。
NSManagedObjectContext 參數,才能知道應當執行哪部分內容。
因此,新的設想是完全從網絡層中獲取Core Data。首先我們創建了對象創建的中間層,以便解析響應。
NSManagedObjectContext 發送給操作;
響應映射的概念在于將解析邏輯與將JSON映射到有用項目這兩點分開。我們能夠區別這兩類解析器:種只返回特定類型的單個對象,第二種是解析這類項目數組的解析器。
首先定義所有項目的公共協議:
public protocol ParsedItem {}
現在有一些對象是映射的結果:
public struct SignInItem: ParsedItem { public let token: String public let uniqueId: String } public struct UserItem: ParsedItem { public let uniqueId: String public let firstName: String public let lastName: String public let email: String public let phoneNumber: String?
}
我們定義一下解析出錯時會拋出的錯誤類型。
internal enum ResponseMapperError: ErrorType { case Invalid case MissingAttribute
}
Invalid:當 JSON 為 nil 或不為 nil 時,或者當是一組對象而不是單個對象的 JSON 時拋出。
MissingAttribute——顧名思義,就是 JSON 中有漏 key,或者解析后的值為或應為 nil 時。
ResponseMapper 可能會像下面這樣:
class ResponseMapper<A: ParsedItem> {
static func process(obj: AnyObject?, parse: (json: [String: AnyObject]) -> A?) throws -> A {
guard let json = obj as? [String: AnyObject] else { throw ResponseMapperError.Invalid } if let item = parse(json: json) { return item
} else {
L.log("Mapper failure (\(self)). Missing attribute.") throw ResponseMapperError.MissingAttribute
}
}
}
后臺返回一個 obj ——在本例中是一個 JSON,以及消費這個 obj 的解析方式,并返回一個符合 ParsedItem 的對象。
現在,有了這個通用型的 mapper 之后,我們就能創建具體的 mapper 了。我們先來看一下回應登錄操作解析的 mapper。
protocol ResponseMapperProtocol {
associatedtype Item
static func process(obj: AnyObject?) throws -> Item
}
final class SignInResponseMapper: ResponseMapper<SignInItem>, ResponseMapperProtocol {
static func process(obj: AnyObject?) throws -> SignInItem { return try process(obj, parse: { json in let token = json["token"] as? String
let uniqueId = json["unique_id"] as? String if let token = token, let uniqueId = uniqueId { return SignInItem(token: token, uniqueId: uniqueId)
} return nil
})
}
}
ResponseMapperProtocol 是由具體 mapper 所實現的協議,因此解析回應的方法一致。
在成功的操作模塊中,我們也會使用這樣的 mapper,可使用特定類型的具體對象來代替 dictionary。這樣的對象容易使用,也容易測試。
后是解析數組的響應 mapper。
final class ArrayResponseMapper<A: ParsedItem> {
static func process(obj: AnyObject?, mapper: (AnyObject? throws -> A)) throws -> [A] {
guard let json = obj as? [[String: AnyObject]] else { throw ResponseMapperError.Invalid }
var items = [A]() for jsonNode in json {
let item = try mapper(jsonNode)
items.append(item)
} return items
}
}
這串代碼負責接收 mapping 函數,如果一切解析正常的話,就會返回數組。如果有單獨的內容無法解析,或者更甚之返回空數組的話,可以根據情況拋出錯誤。mapper 會希望這個對象(從后臺獲取回應)是一個 JSON 元素的數組。
下面的圖表展示了網絡層結構:
由于在后端使用了偽 URL,所有請求都無法成功,放在這里只是為了方便大家了解網絡層的構成方式。
這種制作網絡層的方式非常有用,而且簡單方便:
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。