转载

Swift 运用协议泛型封装网络层

swift 版本: 4.1

Xcode 版本 9.3 (9E145)

基于 Alamofire 和 Moya 再封装

代码 Github 地址: MoyaDemo

一、前言

最近进入新公司开展新项目,我发现公司项目的网络层很 OC ,最让人无法忍受的是数据解析是在网络层之外的,每一个数据模型都需要单独写解析代码。趁着项目才开始,我提议由我写一个网络层小工具来代替以前的网络层,顺便把加载菊花,缓存也封装到了里面。

二、Moya工具和Codable协议简介

这里只是展示一下 Moya 的基本使用方法和 Codable协议 的基本知识,如果对这两块感兴趣,读者可以自行去搜索研究。

2.1 Moya工具

使用 Moya 是因为笔者觉得它很方便,如果读者不想使用 Moya,也不影响你阅读这篇文章的内容。

Alamofire 这里就不作介绍了,如果没有接触过,你可以把它当做是 Swift 版本的 AFNetworking。 Moya 是一个对 Alamofire 进行了再次封装的工具库。如果只使用 Alamofire ,你的网络请求可能会是这样:

let url = URL(string: "your url")!
Alamofire.request(url).response { (response) in
    // handle response
}

当然读者也会基于它进行二次封装,不会仅仅是上面代码那么简单。

如果使用 Moya, 你首先做的不是直接请求,而是根据项目模块建立一个个文件定义接口。例如我喜欢根据模块的功能取名 模块名 + API,然后再在其中定义我们需要使用的接口,例:

import Foundation
import Moya

enum YourModuleAPI {
    case yourAPI1
    case yourAPI2(parameter: String)
}

extension YourModuleAPI: TargetType {
    var baseURL : URL {
        return URL(string: "your base url")!
    }
    
    var headers : [String : String]? {
        return "your header"
    }
    
    var path: String {
        switch self {
            case .yourAPI1:
                return "yourAPI1 path"
            case .yourAPI2:
                return "yourAPI2 path"
        }
    }
   
    var method: Moya.Method {
        switch self {
            case .yourAPI1:
                return .post
            default:
                return .get
        }
    }
   
    // 这里只是带参数的网络请求
    var task: Task {
        var parameters: [String: Any] = [:]
        switch self {
            case let .yourAPI1:
                parameters = [:]
            case let .yourAPI2(parameter):
                parameters = ["字段":parameter]
        }
        return .requestParameters(parameters: parameters,
                                    encoding: URLEncoding.default)
    }
   
    // 单元测试使用    
    var sampleData : Data {
        return Data()
    }
}

定义如上的文件后,你就可以使用如下方式进行网络请求:

MoyaProvider().request(YourModuleAPI.yourAPI1) { (result) in
    // handle result            
}

2.2 Codable协议

Codable协议 是 Swift4 才更新的,用来解析和编码数据,它是由编码协议和解码协议组成。

public typealias Codable = Decodable & Encodable

在 Swift 更新 Codable协议 之前,笔者一直用的 SwiftyJSON 来解析网络请求返回的数据。最近使用 Codable协议 后,发现还蛮好用的,就直接用上了。

不过 Codable协议 还是有一些坑点的,例如这篇文章所描述的:

When JSONDecoder meets the real world, things get ugly…

下面的 Person 模型类储存了一个简单的个人信息,这里只是使用了解码,所以只遵守了  Decodable协议:

struct Person: Decodable {
  var name: String
  var age: Int
}
String 和 Int 是系统默认的可编解码类型,所以我们无需再写其他代码了,编译器将默认为我们实现。
let jsonString = """
        {   "name": "swordjoy",
            "age": 99
        }
"""

if let data = jsonString.data(using: .utf8) {
    let decoder = JSONDecoder()
    if let person = try? decoder.decode(Person.self, from: data) {
        print(person.age)    // 99
        print(person.name)   // swordjoy
    }
}

只需要将 Person 类型传给 JSONDecoder 对象,它就能直接将 JSON 数据转换成 Person 数据模型对象。实际使用中由于解析规则的各种严格的限制,远远没有上面看着这么简单。

三、分析和解决方案

3.1.1 重复解析数据到模型

例如这里有两个接口,一个是请求商品列表,一个是请求商城首页。笔者以前是这样写的:

enum MallAPI {
    case getMallHome
    case getGoodsList
}
extension MallAPI: TargetType {
    // 略   
}
let mallProvider = MoyaProvider()
mallProvider.request(MallAPI.getGoodsList) { (response) in
    // 将 response 解析成 Goods 模型数组用 success 闭包传出去
}

mallProvider.request(MallAPI.getMallHome) { (response) in
    // 将 response 解析成 Home 模型用 success 闭包传出去
}

以上是简化的实用场景,每一个网络请求都会单独的写一次将返回的数据解析成数据模型或者数据模型数组。就算是将数据解析的功能封装成一个单例工具类,也仅仅是稍稍好了一些。

笔者想要的是指定数据模型类型后,网络层直接返回解析完成后的数据模型供我们使用。

3.1.2 运用泛型来解决

泛型就是用来解决上面这种问题的,

使用泛型创建一个网络工具类,并给定泛型的条件约束:遵守 Codable 协议。

struct NetworkManager where T: Codable {
   
}

这样我们在使用时,就可以指定需要解析的数据模型类型了。

NetworkManager().reqest...
NetworkManager().reqest...

细心的读者会发现这和 Moya 初始化 MoyaProvider 类的使用方式一样。

3.2.1 使用Moya后,如何将加载控制器和缓存封装到网络层

由于使用了 Moya 进行再次封装,每对代码进行一次封装的代价就是自由度的牺牲。如何将加载控制器&缓存功能和 Moya 契合起来呢?

一个很简单的做法是在请求方法里添加是否显示控制器和是否缓存布尔值参数。看着我的请求方法参数已经5,6个,这个方案立马被排除了。看着 Moya 的 TargetType 协议,给了我灵感。

3.2.2 运用协议来解决

既然 MallAPI 能遵守 TargetType 来实现配置网络请求信息,那当然也能遵守我们自己的协议来进行一些配置。

自定义一个 Moya 的补充协议

protocol MoyaAddable {
    var cacheKey: String? { get }
    var isShowHud: Bool { get }
}

这样 MallAPI 就需要遵守两个协议了

extension MallAPI: TargetType, MoyaAddable {
    // 略   
}

四、部分代码展示和解析

完整的代码,读者可以到 Github 上去下载。

4.1 封装后的网络请求

通过给定需要返回的数据类型,返回的 response 可以直接调取 dataList 属性获取解析后的 Goods 数据模型数组。错误闭包里面也能直接通过 error.message 获取报错信息,然后根据业务需求选择是否使用弹出框提示用户。

NetworkManager().requestListModel(MallAPI.getOrderList, 
completion: { (response) in
    let list = response?.dataList
    let page = response?.page
}) { (error) in
    if let msg = error.message else {
        print(msg)
    }
}

4.2 返回数据的封装

笔者公司服务端返回的数据结构大致如下:

{
    "code": 0,
    "msg": "成功",
    "data": {
        "hasMore": false,
        "list": []
    }
}

出于目前业务和解析数据的考虑,笔者将返回的数据类型封装成了两类,同时也将解析的操作放在了里面。

后面的请求方法也分成了两个,这不是必要的,读者可以根据自己的业务和喜好选择。

  • 请求列表接口返回的数据

  • 请求普通接口返回的数据

class BaseResponse {
    var code: Int { ... } // 解析
    var message: String? { ... } // 解析
    var jsonData: Any? { ... } // 解析
   
    let json: [String : Any]
    init?(data: Any) {
        guard let temp = data as? [String : Any] else {
            return nil
        }
        self.json = temp
    }
    
    func json2Data(_ object: Any) -> Data? {
        return try? JSONSerialization.data(
        withJSONObject: object,
        options: [])
    }
}

class ListResponse: BaseResponse where T: Codable {
    var dataList: [T]? { ... } // 解析
    var page: PageModel? { ... } // 解析
}

class ModelResponse: BaseResponse where T: Codable {
    var data: T? { ... } // 解析
}

这样我们直接返回相应的封装类对象就能获取解析后的数据了。

4.3 错误的封装

网络请求过程中,肯定有各种各样的错误,这里使用了 Swift 语言的错误机制。

// 网络错误处理枚举
public enum NetworkError: Error  {
    // 略...
    // 服务器返回的错误
    case serverResponse(message: String?, code: Int)
}

extension NetworkError {
    var message: String? {
        switch self {
            case let .serverResponse(msg, _): return msg
            default: return nil
        }
    }
   
    var code: Int {
        switch self {
            case let .serverResponse(_, code): return code
            default: return -1
        }
    }
}

这里的扩展很重要,它能帮我们在处理错误时获取错误的 message 和 code.

4.4 请求网络方法

最终请求的方法

private func request(
    _ type: R,
    test: Bool = false,
    progressBlock: ((Double) -> ())? = nil,
    modelCompletion: ((ModelResponse?) -> ())? = nil,
    modelListCompletion: ((ListResponse?) -> () )? = nil,
    error: @escaping (NetworkError) -> () )
    -> Cancellable?
{}

这里的 R 泛型是用来获取 Moya 定义的接口,指定了必须同时遵守 TargetType 和 MoyaAddable 协议,其余的都是常规操作了。

和封装的返回数据一样,这里也分了普通接口和列表接口。

@discardableResult
func requestModel(
    _ type: R,
    test: Bool = false,
    progressBlock: ((Double) -> ())? = nil,
    completion: @escaping ((ModelResponse?) -> ()),
    error: @escaping (NetworkError) -> () )
    -> Cancellable?
{
    return request(type,
                    test: test,
                    progressBlock: progressBlock,
                    modelCompletion: completion,
                    error: error)
}

@discardableResult
func requestListModel(
    _ type: R,
    test: Bool = false,
    completion: @escaping ((ListResponse?) -> ()),
    error: @escaping (NetworkError) -> () )
    -> Cancellable?
{
    return request(type,
                    test: test,
                    modelListCompletion: completion,
                    error: error)
}

我综合目前项目和 Codable 协议的坑点考虑,将这里写得有点死板,万一来个既是列表又有其他数据的就不适用了。不过到时候可以添加一个类似这种方法,将数据传出去处理。

// Demo里没有这个方法
func requestCustom(
    _ type: R,
    test: Bool = false,
    completion: (Response) -> ()) -> Cancellable? 
{
    // 略
}

4.5 缓存和加载控制器

想到添加 MoyaAddable 协议后,其他就没什么困难的了,直接根据 type 获取接口定义文件中的配置做出相应的操作就行了。

var cacheKey: String? {
    switch self {
        case .getGoodsList:
            return "cache goods key"
        default:
            return nil
    }
}

var isShowHud: Bool {
    switch self {
        case .getGoodsList:
            return true
        default:
            return false
    }
}

这就添加了 getGoodsList 接口请求中的两个功能

  • 请求返回数据后会通过给定的缓存 Key 进行缓存

  • 网络请求过程中自动显示和隐藏加载控制器。

如果读者的加载控制器有不同的样式,还可以添加一个加载控制器样式的属性。甚至缓存的方式是同步还是异步,都可以通过这个 MoyaAddable 添加。

// 缓存
private func cacheData(
    _ type: R,
    modelCompletion: ((Response?) -> ())? = nil,
    modelListCompletion: ( (ListResponse?) -> () )? = nil,
    model: (Response?, ListResponse?))
{
    guard let cacheKey = type.cacheKey else {
        return
    }
    if modelComletion != nil, let temp = model.0 {
        // 缓存
    }
    if modelListComletion != nil, let temp = model.1 {
        // 缓存
    }
}

加载控制器的显示和隐藏使用的是 Moya 自带的插件工具。

// 创建moya请求类
private func createProvider(
    type: T,
    test: Bool) 
    -> MoyaProvider 
{
    let activityPlugin = NetworkActivityPlugin { (state, targetType) in
        switch state {
        case .began:
            DispatchQueue.main.async {
                if type.isShowHud {
                    SVProgressHUD.showLoading()
                }
                self.startStatusNetworkActivity()
            }
        case .ended:
            DispatchQueue.main.async {
                if type.isShowHud {
                    SVProgressHUD.dismiss()
                }
                self.stopStatusNetworkActivity()
            }
        }
    }
    let provider = MoyaProvider(
        plugins: [activityPlugin,
        NetworkLoggerPlugin(verbose: false)])
    return provider
}

4.6 避免重复请求

定义一个数组来保存网络请求的信息,一个并行队列使用 barrier 函数来保证数组元素添加和移除线程安全。

// 用来处理只请求一次的栅栏队列
private let barrierQueue = DispatchQueue(label: "cn.tsingho.qingyun.NetworkManager",
attributes: .concurrent)
// 用来处理只请求一次的数组,保存请求的信息 唯一
private var fetchRequestKeys = [String]()
private func isSameRequest(_ type: R) -> Bool {
    switch type.task {
        case let .requestParameters(parameters, _):
            let key = type.path + parameters.description
            var result: Bool!
            barrierQueue.sync(flags: .barrier) {
                result = fetchRequestKeys.contains(key)
                if !result {
                    fetchRequestKeys.append(key)
                }
            }
            return result
        default:
            // 不会调用
            return false
    }
}

private func cleanRequest(_ type: R) {
    switch type.task {
        case let .requestParameters(parameters, _):
            let key = type.path + parameters.description
            barrierQueue.sync(flags: .barrier) {
                fetchRequestKeys.remove(key)
            }
        default:
            // 不会调用
            ()
    }
}

这种实现方式目前有一个小问题,多个界面使用同一接口,并且参数也相同的话,只会请求一次,不过这种情况还是极少的,暂时没遇到就没有处理。

五、后记

目前封装的这个网络层代码有点强业务类型,毕竟我的初衷就是给自己公司项目重新写一个网络层,因此可能不适用于某些情况。不过这里使用泛型和协议的方法是通用的,读者可以使用同样的方式实现匹配自己项目的网络层。如果读者有更好的建议,还希望评论出来一起讨论。

作者:swordjooy

链接:https://www.jianshu.com/p/2cdf1c969f5f

正文到此结束
Loading...