封装一个 Swift-Style 的网络模块

2018-09-10 12:21:27 浏览数 (1)

Swift 跟 OC 有着完全不同的设计哲学,它鼓励你使用 protocol 而不是 super class,使用 enum 和 struct 而不是 class,它支持函数式特性、范型和类型推导,让你可以轻松封装异步过程,用链式调用避免 callback hell。如果你还是用 OC 的思维写着 Swift 代码,那可以说是一种极大的资源浪费,你可能还会因为 Swift 弱鸡的反射而对它感到不满,毕竟 Swift 在强类型和安全性方面下足了功夫,如果不使用 OC 的 runtime,在动态性方面是远不如 OC 的。

OOP 和消息传递非常适合 UI 编程,在这方面来说 OC 是非常称职的,整个 Cocoa Touch 框架也都是面向对象的,所以对于 iOS 开发来说,不管你使用什么语言,都必须熟悉 OOP。在 UI 构建方面,无论是 Swift 还是 OC,无非都是调用 API 罢了,在有自动提示的情况下,其实编码体验都差不多。那 Swift 相比于 OC 的优势到底体现在什么地方呢,我认为是 UI 以外的地方,跟 UI 关系越小,Swift 能一展拳脚的余地就越大,譬如网络层。

讲到网络层就绕不开 Alamofire,Alamofire 几乎是现在用 Swift 开发 iOS App 的标配,它是个很棒的库,几乎能满足所有网络方面的日常需求,但如果对它再封装一下的话,不仅使用起来更得心应手,而且能将第三方库与业务代码解耦,以后万一要更换方案会更加方便。

Alamofire 使用 Result 来表示请求返回的结果,它是个 enum,长这样:

代码语言:javascript复制
public enum Result<Value, Error : ErrorType> {
    case Success(Value)
    case Failure(Error)
    /// Returns `true` if the result is a success, `false` otherwise.
    public var isSuccess: Bool { get }
    /// Returns `true` if the result is a failure, `false` otherwise.
    public var isFailure: Bool { get }
    /// Returns the associated value if the result is a success, `nil` otherwise.
    public var value: Value? { get }
    /// Returns the associated error value if the result is a failure, `nil` otherwise.
    public var error: Error? { get }
}

我们可以对它进行扩展,让它支持链式调用:

代码语言:javascript复制
import Foundation
import Alamofire

extension Result {

    // Note: rethrows 用于参数是一个会抛出异常的闭包的情况,该闭包的异常不会被捕获,会被再次抛出,所以可以直接使用 try,而不用 do-try-catch

    // U 可能为 Optional
    func map<U>(@noescape transform: Value throws -> U) rethrows -> Result<U, Error> {
        switch self {
        case .Failure(let error):
            return .Failure(error)
        case .Success(let value):
            return .Success(try transform(value))
        }
    }

    // 若 transform 的返回值为 nil 则作为异常处理
    func flatMap<U>(@noescape transform: Value throws -> U?) rethrows -> Result<U, Error> {
        switch self {
        case .Failure(let error):
            return .Failure(error)
        case .Success(let value):
            guard let transformedValue = try transform(value) else {
                return .Failure(SYError.errorWithCode(.TransformFailed) as! Error)
            }
            return .Success(transformedValue)
        }
    }

    // 适用于 transform(value) 之后可能产生 error 的情况
    func flatMap<U>(@noescape transform: Value throws -> Result<U, Error>) rethrows -> Result<U, Error> {
        switch self {
        case .Failure(let error):
            return .Failure(error)
        case .Success(let value):
            return try transform(value)
        }
    }

    // 处理错误,并向下传递
    func mapError(@noescape transform: Error throws -> NSError) rethrows -> Result<Value, NSError> {
        switch self {
        case .Failure(let error):
            return .Failure(try transform(error))
        case .Success(let value):
            return .Success(value)
        }
    }

    // 处理数据(不再向下传递数据,作为数据流的终点)
    func handleValue(@noescape handler: Value -> Void) {
        switch self {
        case .Failure(_):
            break
        case .Success(let value):
            handler(value)
        }
    }

    // 处理错误(终点)
    func handleError(@noescape handler: Error -> Void) {
        switch self {
        case .Failure(let error):
            handler(error)
        case .Success(_):
            break
        }
    }
}

有了这个扩展我们就可以定义一个parseResult的方法,对返回结果进行处理,像这样:

代码语言:javascript复制
func parseResult(result: Result<AnyObject, NSError>, responseKey: String) -> Result<AnyObject, NSError> {
    return result
        .flatMap { $0 as? [String: AnyObject] }
        .flatMap(self.checkJSONDict) // 解析错误信息并进行打印,然后继续向下传递,之后业务方可自由选择是否进一步处理错误
        .flatMap { $0.valueForKey(responseKey) }
}

checkJSONDict用来处理服务器返回的错误信息,具体的处理逻辑不同项目都不一样,主要看跟服务器的约定,我就不细说了。valueForKey是对Dictionary的扩展,可以通过字符串拿到返回的 JSON 数据中需要的部分(先转换成[String: AnyObject]),支持用"."分隔 key,从而取得嵌套对象。譬如这样一个东西:

代码语言:javascript复制
{
  key1: value1,
  key2: { nest: value2 }
  key3: { nest1: { nest2: value3 } }
}

你可以用"key2.nest"拿到value2,用"key3.nest1.nest2"拿到value3。我用reduce实现了这个功能:

代码语言:javascript复制
extension Dictionary {
    var dictObject: AnyObject? { return self as? AnyObject }

    func valueForKey(key: Key) -> Value? {
        guard let stringKey = key as? String 
            where stringKey.containsString(".") else { return self[key] }

        let keys = stringKey.componentsSeparatedByString(".")
        guard !keys.isEmpty else { return nil }

        let results: AnyObject? = keys.reduce(dictObject, combine: fetchValueInObject)
        return results as? Value
    }
}

func fetchValueInObject(object: AnyObject?, forKey key: String) -> AnyObject? {
    return (object as? [String: AnyObject])?[key]
}

有了parseResult之后,我们就可以轻松封装请求过程了:

代码语言:javascript复制
/**
 Fetch raw object

 - parameter api:              API address
 - parameter method:           HTTP method, default = POST
 - parameter parameters:       Request parameters, default = nil
 - parameter responseKey:      Key of target value, use '.' to get nested objects, e.g. "data.vehicle_list"
 - parameter jsonArrayHandler: Handle result with raw object

 - returns: Optional request object which is cancellable.
 */
func fetchDataWithAPI(api: API,
                   method: Alamofire.Method = .POST,
               parameters: [String: AnyObject]? = nil,
              responseKey: String,
 networkCompletionHandler: NetworkCompletionHandler) -> Cancellable? {

    guard let url = api.url else {
        printLog("URL Invalid: (api.rawValue)")
        return nil
    }

    let params = configParameters(parameters)

    return Alamofire.request(method, url, parameters: params).responseJSON {
        networkCompletionHandler(self.parseResult($0.result, responseKey: responseKey))
    }
}

API是一个枚举,有一个url的计算属性,用来返回 API 地址,configParameters用来配置请求参数,也跟具体项目有关,就不展开了,method可以设置一个项目中常用的 HTTP Method 作为默认参数。这个方法会返回一个Cancellable,长这样:

代码语言:javascript复制
protocol Cancellable {
    func cancel()
}

extension Request: Cancellable {}

Request本来就实现了cancel方法,所以只要显式地声明一下它遵守Cancellable协议就行了,使用的时候像这样:

代码语言:javascript复制
let task = NetworkManager.defaultManager
    .fetchDataWithAPI(.ModelList, responseKey: "data.model_list") {
        // ...
}

在请求完成之前,随时可以调用task?.cancel() 来取消这个网络任务。

当然如果你想在网络模块中把 JSON 直接转化成 Model 也是可以的,我个人倾向于使用 ObjectMapper 来构建网络 Model 层,于是就可以对外提供两个直接取得 Model 和 Model 数组的方法:

代码语言:javascript复制
/**
 Fetch JSON model

 - parameter api:              API address
 - parameter method:           HTTP method, default = POST
 - parameter parameters:       Request parameters, default = nil
 - parameter responseKey:      Key of target value, use '.' to get nested objects, e.g. "data.vehicle_list"
 - parameter jsonArrayHandler: Handle result with model

 - returns: Optional request object which is cancellable.
 */
func fetchJSONWithAPI<T: Mappable>(api: API,
                                method: Alamofire.Method = .POST,
                            parameters: [String: AnyObject]? = nil,
                           responseKey: String,
                           jsonHandler: Result<T, NSError> -> Void) -> Cancellable? {

    return fetchDataWithAPI(api, method: method, parameters: parameters, responseKey: responseKey) {
        jsonHandler($0.flatMap(=>))
    }
}

/**
 Fetch JSON array

 - parameter api:              API address
 - parameter method:           HTTP method, default = POST
 - parameter parameters:       Request parameters, default = nil
 - parameter responseKey:      Key of target value, use '.' to get nested objects, e.g. "data.vehicle_list"
 - parameter jsonArrayHandler: Handle result with model array

 - returns: Optional request object which is cancellable.
 */
func fetchJSONArrayWithAPI<T: Mappable>(api: API,
                                     method: Alamofire.Method = .POST,
                                 parameters: [String: AnyObject]? = nil,
                                responseKey: String,
                           jsonArrayHandler: Result<[T], NSError> -> Void) -> Cancellable? {

    return fetchDataWithAPI(api, method: method, parameters: parameters, responseKey: responseKey) {
        jsonArrayHandler($0.flatMap(=>))
    }
}

=>是我自定义的操作符,它有两个重载版本,都满足flatMap的参数要求:

代码语言:javascript复制
postfix operator => {}

postfix func =><T: Mappable>(object: AnyObject) -> T? {
    return Mapper().map(object)
}

postfix func =><T: Mappable>(object: AnyObject) -> [T]? {
    return Mapper().mapArray(object)
}

于是就可以在业务代码中直接这样:

代码语言:javascript复制
class TableViewController: UITableViewController {
    // ...
    var results: [Demo]? {
        didSet {
            tableView.reloadData()
        }
    }

    func fetchData() {
        let task = NetworkManager.defaultManager
            .fetchJSONArrayWithAPI(.Demo, responseKey: "data.demo_list") { 
                self.results = $0.value
        }
    }
}

到此一个简洁方便的网络模块就差不多成型了,别忘了为你的模块添加单元测试,这会让模块的使用者对你的代码更有信心,而且在测试过程中会让你发现一些开发过程中的思维盲区,还能帮你优化设计,毕竟良好的可测试性在某种程度上就意味着良好的可读性和可维护性。

有什么建议欢迎在评论中指出 ^ ^

0 人点赞