项目剖析03-swift 网络请求Moya+HandyJSON+RxSwift

项目第一版网络框架用的是siesta,它的缓存与自动刷新确实很好用而且代码很简洁,但是在文件的上传与下载以及对返回类型需要精确匹配要求这方面就很不友好,所以在第二版的我选择了Moya,它是一个网络抽象层,它在Alamofire基础上提供了一系列的抽象接口方便维护。关于Moya的使用介绍很多,我就不再赘述了。我主要记录一下我在使用过程中学到的处理方式。我的网络框架是搭着HandyJSONRxSwift一起构建的。

1 Moya

  • 1 代码
import Foundation
import enum Result.Result
import Alamofire

//设置请求超时时间
private let requestTimeoutClosure = { (endpoint: Endpoint, done: @escaping MoyaProvider<ApiManager>.RequestResultClosure) in
    do {
        var request = try endpoint.urlRequest()
        request.timeoutInterval = 60
        done(.success(request))
    } catch {
        return
    }
}
let ApiManagerProvider = MoyaProvider<ApiManager>(endpointClosure: endpointMapping, requestClosure: requestTimeoutClosure, plugins:[])

// MARK: 取消所有请求
func cancelAllRequest() {
    WTOtherProvider.manager.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
        dataTasks.forEach { $0.cancel() }
        uploadTasks.forEach { $0.cancel() }
        downloadTasks.forEach { $0.cancel() }
    }
    
    WTLoginProvider.manager.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
        dataTasks.forEach { $0.cancel() }
        uploadTasks.forEach { $0.cancel() }
        downloadTasks.forEach { $0.cancel() }
    }
    ……
 }


public func endpointMapping<Target: TargetType>(target: Target) -> Endpoint {
    WTDLog("请求连接:\(target.baseURL)\(target.path) \n方法:\(target.method)\n参数:\(String(describing: target.task.self)) ")
    return MoyaProvider.defaultEndpointMapping(for: target)
}

final class RequestAlertPlugin: PluginType {
    
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
        return request
    }
    
    func willSend(_ request: RequestType, target: TargetType) {
        //实现发送请求前需要做的事情
    }
    
    public func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {

        switch result {
        case .success(let response):
            guard response.statusCode == 200 else {
                if response.statusCode == 401 {
                    if isJumpLogin == false {
                        cancelAllRequest()
                        // 退出登录
                        if let nvc = (WTNavigationManger.Nav as? WTMainViewController) {
                            nvc.login()
                        }
                    }
                }
                return
            }
            var json = try? JSON(data: response.data)
            WTDLog("请求状态码\(json?["status"] ?? "")")
            
            guard let codeString = json?["status"] else {return}
             if codeString == 401 {// 退出登录
                if isJumpLogin == false {
                    cancelAllRequest()
                    if let nvc = (WTNavigationManger.Nav as? WTMainViewController) {
                        nvc.login()
                    }
                }
                break
            }

        case .failure(let error):
            WTDLog(error)
            let myAppdelegate = UIApplication.shared.delegate as! AppDelegate
            myAppdelegate.listenNetwork()
            break
        }
    }
}

struct AuthPlugin: PluginType {
    let token: String
}


enum ApiManager {
}

extension ApiManager: TargetType {
    var headers: [String : String]? {
        var dict = ["ColaLanguage": ("common.isChinese".L() == "YES") ? "CN" : "EN"]
        if let authToken =  WTLoginInfoManger.shareDataSingle.model?.accessToken {
            dict["Authorization"] = authToken
        }
        return dict
    }
    
    var baseURL: URL {
        return URL.init(string: AppURLHOST.MyPublicBaseURL)!
    }
    
    var path: String {
        return ""
    }
    
    var method: Moya.Method {
        return .get
    }
    
    var task: Task {
        return .requestPlain
    }
    
    var validate: Bool {
        return false
    }
    var sampleData: Data {
        return "".data(using: String.Encoding.utf8)!
    }
}
/// 数据 转 模型
extension ObservableType where E == Response {
    public func mapHandyJsonModel<T: HandyJSON>(_ type: T.Type) -> Observable<T> {
        return flatMap { response -> Observable<T> in
            return Observable.just(response.mapHandyJsonModel(T.self))
        }
    }
}
/// 数据 转 模型
extension Response {
    func mapHandyJsonModel<T: HandyJSON>(_ type: T.Type) -> T {
        let jsonString = String.init(data: data, encoding: .utf8)
        if let modelT = JSONDeserializer<T>.deserializeFrom(json: jsonString) {
            return modelT
        }
        return JSONDeserializer<T>.deserializeFrom(json: "{\"msg\":\"\("common.try".L())\"}")!
    }
}

/// 自定义插件
public final class NetworkLoadingPlugin: PluginType {
    public func willSend(_ request: RequestType, target: TargetType) {
    }
    public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
    }
}
  • 2 模式Target -> Endpoint -> Request


    来自GitHub图片

Moya虽然是基于Alamofire的但是我们在代码中却不会和Alamofire打交道,它是通过枚举来管理API的。我在项目中定义来一个API基类,然后为每一个模块定义了一个API管理类。

enum HomeApiManager {
    case getBanner // 获取轮播
    case getAnnouncement(per_page: String) // 获取公告
}

对于请求类型的改变和对于URL的改变也是通过枚举

var method: Moya.Method {
        switch self {
        case .orderCreate:
            return .post
        case .orderCancelById, .orderCancelByPair:
            return .delete
        default:
            return .get
        }
    }

var path: String {
        switch self {
        case .getKline:
            return "/api/kline"
        case .transGetByID(let orderId):
            return "/api/\(orderId)"
        }
    }

请求任务

    var task: Task {
        switch self {
        case .securityPostGoogleAuth(let tokenKey, let oldGoogleCode, let googleCode, let captcha):
            return .requestParameters(parameters: ["captcha": captcha], encoding: JSONEncoding.default) // post请求

        case .getReward(let type, let cursor, let limit):
            return .requestParameters(parameters: ["type": type], encoding: URLEncoding.default) // 其它请求

        case .uploadImage(let imageArry):
            let formDataAry:NSMutableArray = NSMutableArray()
            for (index,image) in imageArry.enumerated() {
                //图片转成Data
                let data:Data = image.jpegData(compressionQuality: 0.7)!
                //根据当前时间设置图片上传时候的名字
                var dateStr: String = "yyyy-MM-dd-HH:mm:ss".timeStampToString(timeStamp: Date().timeIntervalSince1970)
                //别忘记这里给名字加上图片的后缀哦
                dateStr = dateStr.appendingFormat("-%i.jpg", index)
                let formData = MultipartFormData(provider: .data(data), name: "file\(index)", fileName: dateStr, mimeType: "image/jpeg")
                formDataAry.add(formData)
            }
            return .uploadCompositeMultipart(formDataAry as! [MultipartFormData], urlParameters: [
                :])
            
        default:
            return .requestPlain
        }
    }
  • 3 插件机制
    Moya的另一个强大的功能就是它的插件机制,提供了两个接口,willSendRequest 和 didReceiveResponse,它可以在请求前和请求后做一些额外的操作而和主功能是解耦的,比如可以在请求前开始加载动画请求结束后移除加载动画,还可以自定义插件。
final class RequestAlertPlugin: PluginType {
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
        return request
    }
    func willSend(_ request: RequestType, target: TargetType) {
        现发送请求前需要做的事情
        if target.headers?["isHiddentLoading"] != "true" {
            currentView?.addSubview(activityIndicatorView)
            activityIndicatorView.center = currentView!.center
            activityIndicatorView.startAnimating()
        }
    }
    public func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
        if activityIndicatorView.isAnimating {
            activityIndicatorView.stopAnimating()
            activityIndicatorView.removeFromSuperview()
        }
    }
}

/// 自定义插件
public final class NetworkLoadingPlugin: PluginType {
    public func willSend(_ request: RequestType, target: TargetType) {
    }
    public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
    }
}

Moya默认有4个插件

  1. AccessTokenPlugin // 管理AccessToken的插件
  2. CredentialsPlugin // 管理认证的插件
  3. NetworkActivityPlugin // 管理网络状态的插件
  4. NetworkLoggerPlugin // 管理网络log的插件

3 RxSwift

这里的RxSwift不是完整的RxSwift,而是为Moya定制的一个扩展(pod 'Moya/RxSwift')在数据请求回来后进行处理。

  1. request() 传入API
  2. asObservable() 是Moya为RxSwift提供的扩展方法,返回可监听序列
  3. mapHandyJsonModel() 也是Moya RxSwift的扩展方法进行自定义的,可以把返回的数据解析成model
  4. subscribe() 是对处理过的 Observable 订阅一个 onNext 的观察者,一旦得到JSON格式的数据,就会经行相应的处理
  5. disposed() 是RxSwift的一个自动内存处理机制,类似ARC,会自动处理不需要的对象
/// 数据 转 模型
extension ObservableType where E == Response {
    public func mapHandyJsonModel<T: HandyJSON>(_ type: T.Type) -> Observable<T> {
        return flatMap { response -> Observable<T> in
            return Observable.just(response.mapHandyJsonModel(T.self))
        }
    }
}
/// 数据 转 模型
extension Response {
    func mapHandyJsonModel<T: HandyJSON>(_ type: T.Type) -> T {
        let jsonString = String.init(data: data, encoding: .utf8)
        if let modelT = JSONDeserializer<T>.deserializeFrom(json: jsonString) {
            return modelT
        }
        return JSONDeserializer<T>.deserializeFrom(json: "{\"msg\":\"\("common.try".L())\"}")!
    }
}
extension WTApiManager {
    class func NetExchangeRequest<T: BaseModel>(disposeBag: DisposeBag,type: ExchangeApiManager, model: T.Type, isBackFail: Bool = false, Success:@escaping (T)->(), Error: @escaping ()->()) {
        WTExchangeProvider.rx.request(type)
            .asObservable()
            .mapHandyJsonModel(model)
            .subscribe { (event) in
                switch event {
                case let .next(data):
                    if isBackFail {
                        Success(data)
                        break
                    }
                    guard data.status == 200 else {
                        WTProgressHUD.show(error: data.message ?? "common.try".L(), toView: nil)
                        Error()
                        break
                    }
                    Success(data)
                    break
                case let .error(error):
                    WTDLog(error)
                    Error()
                    break
                default:
                    break
                }
            }.disposed(by: disposeBag)
    }
}

4 HandyJSON

class BaseModel: HandyJSON {
    var status: Int = 0
    var message: String? = nil // 服务端返回提示
    required init(){}
}

class WTBaseModel<T: HandyJSON>: BaseModel {
    var data: T? // 具体的data的格式和业务相关,故用泛型定义
}
struct WTCurrencyBalanceModel: HandyJSON {
    var coinCode: String = ""
    let balanceAvailable: Double = 0.0
    let balanceFrozen: Double = 0.0
    let worth: Double = 0.0
}
// 网络请求 传入对应model
WTApiManager.NetOtherRequest(disposeBag: disposeBag, type: .getMarketsPrice, model: WTBaseModel<WTRateModel>, Success: {(model) in
}) {}

5 以上就是我在项目中使用Moya+HandyJSON+RxSwift的方法,如果有错误或者不足之处还望指正,谢谢

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

推荐阅读更多精彩内容