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

swift 版本: 4.1

Xcode 版本 9.3 (9E145)

基于 AlamofireMoya 再封装

代码 Github 地址: MoyaDemo

一、前言

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

二、Moya工具和Codable协议简介

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

2.1 Moya工具

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

Alamofire 这里就不作介绍了,如果没有接触过,你可以把它当做是 Swift 版本的 AFNetworkingMoya 是一个对 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<YourModuleAPI>().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
}

StringInt 是系统默认的可编解码类型,所以我们无需再写其他代码了,编译器将默认为我们实现。

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<MallAPI>()
mallProvider.request(MallAPI.getGoodsList) { (response) in
    // 将 response 解析成 Goods 模型数组用 success 闭包传出去
}

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

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

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

3.1.2 运用泛型来解决

泛型就是用来解决上面这种问题的,
使用泛型创建一个网络工具类,并给定泛型的条件约束:遵守 Codable 协议。

struct NetworkManager<T> where T: Codable {
    
}

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

NetworkManager<Home>().reqest...
NetworkManager<Goods>().reqest...

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

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

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

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

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<Goods>().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<T>: BaseResponse where T: Codable {
    var dataList: [T]? { ... } // 解析
    var page: PageModel? { ... } // 解析
}

class ModelResponse<T>: 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
        }
    }
}

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

4.4 请求网络方法

最终请求的方法

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

这里的 R 泛型是用来获取 Moya 定义的接口,指定了必须同时遵守 TargetTypeMoyaAddable 协议,其余的都是常规操作了。
和封装的返回数据一样,这里也分了普通接口和列表接口。

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

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

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

// Demo里没有这个方法
func requestCustom<R: TargetType & MoyaAddable>(
    _ 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<R: TargetType & MoyaAddable>(
    _ type: R,
    modelCompletion: ((Response<T>?) -> ())? = nil,
    modelListCompletion: ( (ListResponse<T>?) -> () )? = nil,
    model: (Response<T>?, ListResponse<T>?))
{
    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<T: TargetType & MoyaAddable>(
    type: T,
    test: Bool) 
    -> MoyaProvider<T> 
{
    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<T>(
        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<R: TargetType & MoyaAddable>(_ 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<R: TargetType & MoyaAddable>(_ type: R) {
    switch type.task {
        case let .requestParameters(parameters, _):
            let key = type.path + parameters.description
            barrierQueue.sync(flags: .barrier) {
                fetchRequestKeys.remove(key)
            }
        default:
            // 不会调用
            ()
    }
}

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

五、后记

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

转载评论留转载地址即可转载。
文章掘金地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,857评论 6 13
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,299评论 8 265
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,417评论 25 707
  • 有了与春天相遇的惊喜,必定有与它告别的伤心。所谓怀春,怀的即是春天,又是少年。 韩愈《春雪》诗说,新年都未有芳华,...
    热水c阅读 197评论 0 1
  • 1、适当有氧运动,一定不可以做剧烈的运动,以免关节过劳。最好选择游泳、太极拳、乒乓球之类的有氧运动,以此减轻体重。...
    Fun_Day阅读 221评论 0 0