如今的移动应用大多是“客户端-服务器”模式,某个应用中很可能就包含或大或小的网络层结构。迄今为止笔者见过的许多实现均有一些缺陷,新构建的这个或许仍有缺陷,但在手边的这两个项目中效果都很不错,而且测试覆盖率几乎达到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,所有请求都无法成功,放在这里只是为了方便大家了解网络层的构成方式。
这种制作网络层的方式非常有用,而且简单方便:
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。