iOS-swift-网络请求二次封装moya

1 github地址

https://github.com/chensx1993/moyaManager

这个应该是一个小姐姐,我看到我们项目用了这个三方库,所以这边分析下封装细节。

简单介绍下吧:

moya是对Alamofire的再次封装。它可以实现各种自定义配置,真正实现了对网络层的高度抽象。

还有一个优秀的网络框架(github地址),大家可以看看,跟moya对比一下。

有关moya的介绍可以看看: Moya的使用

2 框架架构

  • Core应该是核心模块,存放网络状态管理员,Network.swift应该是最关键的暴露给开发者使用的类吧,所有的请求都是经过这层转发的,Request就是内部请求相关,比如get和post请求层的封装吧。

  • Extension是扩展层,这里扩展了String,也扩展了网络请求基础返回体,这里可以根据项目来。

  • Plugin是插件层,这个可以自由添加。

  • Server是网络层,这里我们可以定义一些通过的请求参数,比如token啥的。

  • API层,这就和我们自己定义的Api相关了,怎么请求,怎么传参这些定义。

  • Error层,这里应该是异常层了。

3 实例分析

这里我们考虑用一个真实案例,来深入分析这个封装框架如何实现网络请求的。
这里就以登录接口为案例吧。

可以建立一个登录相关接口,比如修改密码啥的,跟登录有关的统一用一个Manager。
如下:

import Foundation
import Moya

// MARK: - 用户登录信息请求
let loginApiRequest = Networking<UserLoginAPIManagerService>()


enum UserLoginAPIManagerService {
    /// 账号密码登录
    case userAccountLogin(userName:String, passWord:String)
    /// 是否显示注册
    case isShowRegister
    /// 注册
    case register(_ params: [String: Any])
    /// 更新是否已经提醒用户修改密码标记
    case signNotFirstLogin(userId:String)
    /// 修改密码
    case changePassword(newPassword:String, oldPassword:String, username:String)
    /// 校验原密码
    case checkOldPassword(oldPassword:String, username:String)
    /// 登出
    case logOut

}

extension UserLoginAPIManagerService : MyServerType {
    
    //域名
    var baseURL: URL {
        switch self {
        case .register:
            return URL(string: mainHost)!
        case .isShowRegister:
            return URL(string: mainHost)!
        case .userAccountLogin:
            return URL(string: LoginHost)!
        case .signNotFirstLogin:
            return URL(string: mainHost)!
        case .changePassword,.checkOldPassword:
            return URL(string: mainHost)!
        default:
            return URL(string: LoginHost)!
        }
        
    }
    
    //接口路径
    public var path: String {
        switch self {
        case .userAccountLogin:
            return "oauth/token"
        case .register:
            return "v1/user/Register"
        case.isShowRegister:
            return "v1/user/iosIsEnabled"
        case .signNotFirstLogin:
            return "v1/exposure/password/flag"
        case .changePassword:
            return "v1/user/reset/myself"
        case .logOut:
            return "ssoLogout"
        case .checkOldPassword:
            return "v1/user/check/reset-password"
        }
    }
    
    //是否执行Alamofire验证
    var validate: Bool {
        return false
    }
    
    //验证方式
    var validationType: MyValidationType {
        return .none
    }
    
    //单元测试模拟的数据
    var sampleData: Data {
        return "{}".data(using: String.Encoding.utf8)!
    }
    
    //请求类型:get、post、delete、put
    var method: HTTPMethod {
        switch self {
        case .userAccountLogin(userName: _ , passWord: _),
                .register,
                .isShowRegister,
                .changePassword,
                .checkOldPassword:
            return .post
        default:
            return .get
        }
    }
    
    //请求任务:
    public var task: Task {
        switch self {
        case .userAccountLogin(let userName, let passWord):
            let secret = "1234".toMD5
            let params = ["client_id": clientId,
                          "client_secret":secret,
                          "username": userName,
                          "password": passWord] as [String : Any]
            return .requestParameters(parameters: params, encoding:URLEncoding.default)
        case .register(let params):
            return .requestParameters(parameters: params, encoding:JSONEncoding.default)
        case .isShowRegister:
            return .requestParameters(parameters: [: ], encoding: URLEncoding.default)
        case .logOut:
            return .requestPlain
        case .signNotFirstLogin(let userId):
            let params = ["userId": userId] as [String : Any]
            return .requestParameters(parameters: params, encoding:URLEncoding.default)
        case .changePassword(let newPassword, let oldPassword, let username):
            let params = ["newPassword":newPassword,
                          "oldPassword":oldPassword,
                          "username": username] as [String : Any]
            return .requestParameters(parameters: params, encoding: JSONEncoding.default)
        case .checkOldPassword(let oldPassword, let username):
            let params = ["oldPassword":oldPassword,
                          "username": username] as [String : Any]
            return .requestParameters(parameters: params, encoding: JSONEncoding.default)
        }
        
    }
    
    //请求头
    var appendHeaders: [String : String]? {
        switch self {
        case .register:
            return ["Content-Type": "application/json"]
        case .signNotFirstLogin,
                .checkOldPassword,
                .logOut:
            return [: ]
        case .changePassword:
            return ["menuPath": MenuPath.changePassword.info]
        default:
            return [: ]
        }
    }
    
}

首先是建立了一个常量,一个Networking包装了MyServerType,当然这个MyServerType和Networking都是封装好的。主要操作就是自定义这个MyServerType了。

这个接口基本就写好了。
请求头可配置,参数可配置,接口路径可配置,后端怎么改都不怕了。

然后就是我们调用的地方了。

在登录的地方这样用:

 loginApiRequest.requestJson(.userAccountLogin(userName: userName, passWord: safePassWord)) {result in
    let json = JSON.init(rawValue: result)
    if json?["code"].intValue == 1000 {
        MBProgressHUD.hide()
        guard let model = UserInformationData.deserialize(from: json?["data"].jsonString) else { return }
        self.saveLoginSuccessUserInformation(model: model, userName: userName, passWord: passWord)
        callBack(model)
        
        self.requestUploadLog(userName,1,"登录成功")
    }else{
        MBProgressHUD.hide()
        let str = json?["message"].stringValue ?? "登录失败"
        GMToast.showFailure(str)
        self.requestUploadLog(userName,0,str)
    }
    
} failure: { error in
    MBProgressHUD.hide()
    GMToast.showFailure()
}

loginApiRequest就是我们前面定义的登录类接口的常量。

因为这是一个Networking,所有它有requestJson方法,这个方法也是框架自己二次封装好的,等下具体分析下这里面的代码即可。

注意到这里有传参,需要关注下.userAccoutLogin是啥东西?

原来就是我们定义的泛型类,MyServerType,这里.userAccountLogin是一个枚举也是一个MyServerType,我们可以在这里面添加请求参数,这样就关联起来了。

enum UserLoginAPIManagerService {
    /// 账号密码登录
    case userAccountLogin(userName:String, passWord:String)
    /// 是否显示注册
    case isShowRegister
    /// 注册
    case register(_ params: [String: Any])
    /// 更新是否已经提醒用户修改密码标记
    case signNotFirstLogin(userId:String)
    /// 修改密码
    case changePassword(newPassword:String, oldPassword:String, username:String)
    /// 校验原密码
    case checkOldPassword(oldPassword:String, username:String)
    /// 登出
    case logOut

}

果然就是第一个“账号密码登录”。

所有对于使用还是相当方便的,只需要写一个Manager,就可以很方便请求接口了。

4 细节分析

继续上面的案例,当我们发送json请求时,走了一个框架封装好的方法,看下:

@discardableResult
    public func requestJson(_ target: T,
                            callbackQueue: DispatchQueue? = DispatchQueue.main,
                            progress: ProgressBlock? = .none,
                            success: @escaping JsonSuccess,
                            failure: @escaping Failure) -> Cancellable {
        return self.request(target, callbackQueue: callbackQueue, progress: progress, success: { (response) in
            do {
                let result = try JSON.init(data: response.data)
                let vc = UIViewController.current()
                if vc is LoginPageViewController {}else{
                    if response.statusCode == 403 || response.statusCode == 401 {
                        //token过期
                        if tokenInvalidHandle != nil {
                            tokenInvalidHandle()
                            return
                        }
                    }
                }
                #if DEBUG
                    print(result)
                #endif
                success(result)
            } catch (let error) {
                failure(error as? NetworkError ?? NetworkError.invalidURL)
            }
        }) { (error) in
            #if DEBUG
                print(error)
            #endif
            failure(error)
        }
    }

这里继续走了内部的self.request方法,T就是我们自定义的MyServerType。
里面回调是我们自己的逻辑,就是解析了下Json,然后就是403或401的时候,跳转了登录页,这里的逻辑不需要关注。

看下内部的request方法吧:

@discardableResult
    public func request(_ target: T,
                        callbackQueue: DispatchQueue? = DispatchQueue.main,
                        progress: ProgressBlock? = .none,
                        success: @escaping Success,
                        failure: @escaping Failure) -> Cancellable {
        return self.provider.request(target, callbackQueue: callbackQueue, progress: progress) { (result) in
            switch result {
            case let .success(response):
                #if DEBUG
                if let body = response.request?.httpBody,
                   let param = String(data: body, encoding: .utf8),
                   let interface = response.request?.url {
                    print("✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅")
                    print("接口地址:\n\(interface)")
                    print("✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅")
                    print("请求参数:\n\(param)")
                    print("✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅")
                }
                #endif
                success(response);
            case let .failure(error):
                #if DEBUG
                print("❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌")
                print(error)
                #endif
                let err = NetworkError.init(error: error)
                failure(err);
                break
            }
        }
    }

这里的回调队列,进度,都有默认实例,当然可以自由传递。
另外就是成功和失败的逃逸闭包了。

其实内部继续走了self.provider.request才是重中之重。

4.1 生产Moya Provider

首先看下provider怎么来的:

public struct Networking<T: MyServerType> {
    public let provider: MoyaProvider<T>
    
    public init(provider: MoyaProvider<T> = newDefaultProvider()) {
        self.provider = provider
    }
}

public static func newDefaultProvider() -> MoyaProvider<T> {
        return newProvider(plugins: plugins)
}

/// 如何新建MoyaProvider
func newProvider<T>(plugins: [PluginType],session: Session = newManager()) -> MoyaProvider<T> where T: MyServerType {
    
    return MoyaProvider(endpointClosure: Networking<T>.endpointsClosure(),
                        requestClosure: Networking<T>.endpointResolver(),
                        stubClosure: Networking<T>.APIKeysBasedStubBehaviour,
                        session: session,
                        plugins: plugins,
                        trackInflights: false
    )
}

/// 新建Provider需要的参数1
func newManager(delegate: SessionDelegate = SessionDelegate()) -> Session {
//    let configuration = URLSessionConfiguration.default
//    configuration.httpAdditionalHeaders = Alamofire.SessionManager.defaultHTTPHeaders
    let configuration = Alamofire.Session.default.session.configuration
    let session = Alamofire.Session(configuration: configuration, delegate: delegate, startRequestsImmediately: false)
    return session
}

/// 新建Provider需要的参数2
static var plugins: [PluginType] {
        let activityPlugin = NewNetworkActivityPlugin { (state, targetType) in
            switch state {
            case .began:
                if targetType.isShowLoading { //这是我扩展的协议
                    // 显示loading
                }
            case .ended:
                if targetType.isShowLoading { //这是我扩展的协议
                    // 关闭loading
                }
            }
        }
        
        return [
            activityPlugin, myLoggorPlugin
        ]
    }

这里创建的时候,就默认传了一个默认的Provider,这个是Moya官方的哦。

另外新建这个MoyaProvider的时候还需要3个闭包,我们通过Networking来生产:

static func endpointsClosure<T>() -> (T) -> Endpoint where T: MyServerType {
    return { target in
        var headers: [String: String] = target.headers ?? [:]
        if apiEnvironment == .product {
            //生产环境灰度
            if canary == true {
                headers["canary"] = "true"
            }
        }
        //生产环境api路径设置
        var str = ""
        str = URL(target: target).absoluteString
        if apiEnvironment != .test && apiEnvironment != .dev {
            str = str.replacingOccurrences(of: "-test", with: "", options: .literal, range: nil)
        }
        let absoluteString = str
        let defaultEndpoint = Endpoint(
            url: absoluteString,
            sampleResponseClosure: { target.sampleResponse },
            method: target.method,
            task: target.task,
            httpHeaderFields: headers
        )
        return defaultEndpoint;
    }
}

//测试网络错误,如超时等.
static func endpointResolver() -> MoyaProvider<T>.RequestClosure {
    return { (endpoint, closure) in
        do {
            var request = try endpoint.urlRequest()
            request.httpShouldHandleCookies = false
            request.timeoutInterval = WebService.shared.timeoutInterval
            closure(.success(request))
        } catch let error {
            closure(.failure(MoyaError.underlying(error, nil)))
        }
    }
}   

static func APIKeysBasedStubBehaviour<T>(_ target: T) -> Moya.StubBehavior where T: MyServerType {
        return target.stubBehavior;
    }

这里我们自己生产了3个闭包给moya,也是moya需要的3个闭包。

4.2 继续走Provider的request

/// Designated request-making method. Returns a `Cancellable` token to cancel the request later.
    @discardableResult
    open func request(_ target: Target,
                      callbackQueue: DispatchQueue? = .none,
                      progress: ProgressBlock? = .none,
                      completion: @escaping Completion) -> Cancellable {

        let callbackQueue = callbackQueue ?? self.callbackQueue
        return requestNormal(target, callbackQueue: callbackQueue, progress: progress, completion: completion)
    }

这个是moya三方库里面的代码了,简单看下就好,不是我们这期重点。

4.3 如何封装MyServerType

个人感觉这个也是非常关键,我们外部使用,需要建立一个枚举,同时也是一个MyServerType,这个携带了请求参数,请求方法之类的,总之它包装了一切我们请求需要的东西。

import Foundation
import Moya

public typealias HTTPMethod = Moya.Method
public typealias MyValidationType = Moya.ValidationType
public typealias MySampleResponse = Moya.EndpointSampleResponse
public typealias MyStubBehavior = Moya.StubBehavior

public protocol MyServerType: TargetType {
    var isShowLoading: Bool { get }
    /// 附加请求头(如需添加额外的,使用这个)
    var appendHeaders: [String : String]? { get }
    var parameters: [String: Any]? { get }
    var stubBehavior: MyStubBehavior { get }
    var sampleResponse: MySampleResponse { get }
}

首先,它这个继承了Moya自己的TargetType,还增加了自己额外的一些协议。

extension MyServerType {
    public var base: String { return WebService.shared.rootUrl}
    
    public var baseURL: URL { return URL(string: base)! }
    /// 请求头 (默认请求头 + appendHeaders),需求添加额外的改appendHeaders
    public var headers: [String : String]? {
        var result: [String : String]? = WebService().headers
        guard let appendHeaders = appendHeaders else { return result }
        result = WebService().headers?.merging(appendHeaders, uniquingKeysWith: { (_, new) in new })
        return result
    }
    public var appendHeaders: [String : String]? { return nil }
    public var parameters: [String: Any]? { return WebService.shared.parameters }
    
    public var isShowLoading: Bool { return false }
    
    public var task: Task {
        let encoding: ParameterEncoding
        switch self.method {
        case .post:
            encoding = JSONEncoding.default
        default:
            encoding = URLEncoding.default
        }
        if let requestParameters = parameters {
            return .requestParameters(parameters: requestParameters, encoding: encoding)
        }
        return .requestPlain
    }
    
    
    public var method: HTTPMethod {
        return .post
    }
    
    public var validationType: MyValidationType {
        return .successCodes
    }
    
    public var stubBehavior: StubBehavior {
        return .never
    }
    
    public var validate: Bool {
        return false
    }
    
    public var sampleData: Data {
        return "response: test data".data(using: String.Encoding.utf8)!
    }
    
    public var sampleResponse: MySampleResponse {
        return .networkResponse(200, self.sampleData)
    }
}

这里应该是实现了默认值的设定。
当然我们是可以更改的。

func myBaseUrl(_ path: String) -> String {
    if path.isCompleteUrl { return path }
    return WebService.shared.rootUrl;
}

func myPath(_ path: String) -> String {
    if path.isCompleteUrl { return "" }
    return path;
}

extension String {
    var isCompleteUrl: Bool {
        let scheme = self.lowercased()
        if scheme.contains("http") { return true }
        return false
    }
}

然后是其它工具方法。

这里有用到一个WebService类,这里面存放的也是一些默认值设定:

import Foundation
import UIKit
import AdSupport
import Alamofire
import Moya

class WebService: NSObject {
    
    var rootUrl: String = mainHost
    var headers: [String: String]? = defaultHeaders()
    var parameters: [String: Any]? = defaultParameters()
    var timeoutInterval: Double = 30.0
    //和服务器时间相差的时间戳:备注每次app恢复活跃状态需更新一次
    static var serviceTimeSpace: String?
    
    static let shared = WebService()
    override init() {}
    
    static func defaultHeaders() -> [String : String]? {
        var headers = ["x-flag": "iOS", "serverName": "APP", "clientId": clientId]
        if let token = Defaults[key: DefaultsKeys.access_token] {
            headers["Authorization"] = "Bearer" + token
        }
        let isMonopoly = Defaults[key: DefaultsKeys.isMonopoly] ?? false
        /// appVersion 字段,(1表示专业版,0表示简易版)
        headers["appVersion"] = isMonopoly ? "0" : "1"
        return headers
    }
    
    static func defaultParameters() -> [String : Any]? {
        return ["platform" : "ios",
            "version" : "1.2.3",
        ]
    }
    
    /**
     获取当前app版本号
     */
    static func getAppShortVersion() -> String {
        let infoDictionary = Bundle.main.infoDictionary!
        let version = infoDictionary["CFBundleShortVersionString"] as! String
        return version
    }
    
    /**
    获取当前请求User-Agent
     */
    static func getUserAgent() -> String {
        let infoDictionary = Bundle.main.infoDictionary!
        let version = infoDictionary["CFBundleShortVersionString"] as! String
        let userAgent = String(format: "GREEMall%@(%@; iOS %@; Scale/%.2f)", version, UIDevice.current.model, UIDevice.current.systemVersion, UIScreen.main.scale)
        
        return userAgent
    }
    
    /**
    获取服务器时间和计算时间差
     */
    static func setServiceTimeInterval(timeInterval:TimeInterval) {
        serviceTimeSpace = String(format: "%@", timeInterval)
    }
    
    /**
     获取服务器时间戳
     */
    static func getServiceTimeInterval() -> Int {
        return Int(serviceTimeSpace!)!
    }
    
    /**
     设置请求统一参数
     */
    static func getSystemParameterData() -> Dictionary<String, String> {
        var data = [String: String]()
        data["source"] = "iOS"
        var timeSpace: String = ""
        if serviceTimeSpace != nil {
            let now: TimeInterval = Date.init().timeIntervalSince1970
            let appTime: TimeInterval = now + Double(CLongLong(serviceTimeSpace!)!)
            timeSpace = String(format:"%@", appTime)
        } else {
            timeSpace = String(format:"%@", Date.init().timeIntervalSince1970)
        }
        data["t"] = timeSpace
        data["version"] = self.getAppShortVersion()
        data["ios_idfa"] = ASIdentifierManager.shared().advertisingIdentifier.uuidString
        return data
    }
    
    /**
     签名MD5处理
     */
    static func appendSign(paramterDic: Dictionary<String, String>, secrekey:String) -> String {
        var sign:String = ""
        var keys:Array = Array(paramterDic.keys)
        keys = keys.sorted()
        for key:String in keys {
            if key == "sign" {
                continue
            } else {
                let keyValue: String = String(format: "%@", paramterDic[key]!)
                sign.append(String(format: "%@%@", key, keyValue.EscapesValr))
            }
        }
        sign.append(secrekey)
        return sign.td.md5.uppercased()
    }
}

大致就是这样了。

5 总结

  • 这个二次封装主要是基于Moya,简化moya流程,提供了一个MyServerType,封装了一些请求参数,请求方法,方便我们进行接口配置。

  • 使用方法很简单,不过需要将一类接口单独放置,全部放一起不方便管理,这里比如登录相关接口统一用一个Manger配置,接口参数都写在这个Manager里面,其实也是由一定好处的。

  • 这里将moya更加封装成一个系统了,我们需要的结果无非就是接口成功或失败,传参也是我们必要的,将必要的都封装起来了,总体上使用还是比较便捷。

  • 如果有特殊请求,比如下载文件这种,需要我们单独抽一个方法处理,这里稍微增加了一点复杂度吧,一般的请求还是可以直接使用的。


   转载规则


《iOS-swift-网络请求二次封装moya》 Jason 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
iOS swift 混编Flutter iOS swift 混编Flutter
1 集成Flutter原因最除是项目人员调动,导致产品和开发数量不匹配。领导打算使用跨平台方案,但原有项目都是基于原生实现,如果想转Flutter,肯定得先考虑混编方案,因为需求也是持续迭代的,而且市场上也是由先例,可行性肯定是没问题的。
2023-02-01
下一篇 
iOS swift 实现简易地图功能 iOS swift 实现简易地图功能
1 需求定义产品需要我们实现这样一个需求,就是根据一个地址(可能有经纬度也可能没有),然后跳转到地图页面去,地图中间会显示这个地址。运行用户自行点击定位,这样中间会显示当前位置。允许左右滑动,地图中心也会随之变更。支持搜索功能,根据用户搜索
2023-01-31
  目录