iOS 开发实践:如何构建一个网络层?

更新时间:2017-02-08 10:42:27 点击次数:1914次

如今的移动应用大多是“客户端-服务器”模式,某个应用中很可能就包含或大或小的网络层结构。迄今为止笔者见过的许多实现均有一些缺陷,新构建的这个或许仍有缺陷,但在手边的这两个项目中效果都很不错,而且测试覆盖率几乎达到100%。本文只讨论与单个后台通讯、发送 JSON 编码请求的网络层,这个网络层会与 AWS 通讯,发送一些文件,整体结构并不复杂,不过相应功能的扩展也应当十分简单。

思维流程

在构建相应网络层之前,我先提出一些问题:

存储后端 URL

首先,我们要了解后端 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 时曾尝试对端点执行硬编码的方式,并尝试了一些了解端点、便于实例化与注入的虚拟资源类对象,但并未找到需要的方案。然后得出了设想:创建知道要接入哪个端点,使用什么方法,该是 GETPOSTPUT 还是其它什么的 *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 只是个带有 GETPOSTPUTDELETE案例的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

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)
    }
}

NetworkServiceBackendService 和 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 对象,回应收到后会被解析转化为 Core Data 对象,这个解决方案非常不理想。

因此,新的设想是完全从网络层中获取Core Data。首先我们创建了对象创建的中间层,以便解析响应。

映射操作

响应映射的概念在于将解析逻辑与将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
}

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,所有请求都无法成功,放在这里只是为了方便大家了解网络层的构成方式。

封装

这种制作网络层的方式非常有用,而且简单方便:

本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

回到顶部
嘿,我来帮您!