iOS(Swift) 基于 Moya 的二次封装

我从来不是个天赋型的程序员,我只是站在巨人的脚下,仰望着巨人. 这篇文章,是以解读wildog的网络层封装后进行的描述 --致敬我心目中的大神, wildog.

Moya+Alamofire是现阶段大部分 Swift 项目所喜欢使用的网络层框架,其简洁明了的协议式接口设计,非常让人上头. 但是项目中,一般都会基于这个框架再进行二次封装,以适用于公司业务.本篇文章就是讲解下我司所封装的框架(大部分一致,但有部分是自己的修改)

目的

先说说网络层封装的最终目的,我们希望我们封装的请求框架,调用简单方便,封装简洁清晰易读,易拖展,本身已经具备了基础的加密,debug 打印,业务错误码处理等等功能. 以此为目的,一步步分析下如何封装.

步骤

最基础接入:

import Foundation
import Moya

enum MineAPI: TargetType {
    case user
}

extension MineAPI {
    var baseURL: URL {
        URL(string: "http://www.baidu.com" + "/appName")!
    }
    
    var path: String {
        switch self {
        case .user:
           return "/user/info"
        }
    }
    
    var method: Moya.Method {
        switch self {
        case .user:
            return .post
        }
    }
    
    var sampleData: Data {
        "".data(using: .utf8)!
    }
    
    var task: Task {
        switch self {
        case .user:
            return .requestPlain
        }
    }
    
    var headers: [String : String]? {
        nil
    }
}

这种调用弊端很大,我们一般会去做二次的封装,这里讲解下我司封装的网络层框架(我单独把公司框架网络层提取出来,自己做了一点修改).


API 文件夹目录

流程走,先封装 TargetType
这里对 targetType 进行拖展.我们不希望对外暴露 Moya 接口,所有关于 moya 的结构,都进行了二次封装.

protocol APIService: TargetType {
    
    /// The target's base `URL`.
    var baseURL: URL { get }

    /// The path to be appended to `baseURL` to form the full `URL`.
    var path: String { get }

    /// The HTTP method used in the request.
    var method: Moya.Method { get }

    /// Provides stub data for use in testing.
    var sampleData: Data { get }

    /// The type of HTTP task to be performed.
    var task: Task { get }

    /// The type of validation to perform on the request. Default is `.none`.
    var validationType: ValidationType { get }

    /// The headers to be used in the request.
    var headers: [String: String]? { get }
    
    var parameters: APIParameters? { get }
    /// API 路由和 HTTP 方法,不包含域名、服务名和版本号,
    ///
    /// 如一个 GET API 完整地址为 http://xx.com/message/v1/group/create
    /// 这里返回
    /// ```
    /// .get("/group/create")
    /// ```
    var route: APIRoute { get }
}

extension APIService {
    var url: URL {
        path.isEmpty ? baseURL : baseURL.appendingPathComponent(path)
    }
    
    var baseURL: URL {
        URL(string: Env.current.constants.baseUrl + servicePath)!
    }
    
    var servicePath: String {
        ""
    }
    
    var headers: [String: String]? {
        [
            "Accept": "*/*"
//            "Content-Type": "application/x-www-form-urlencoded; application/json; text/plain"
        ]
    }
    
    var sampleData: Data {
        fatalError("sampleData has not been implemented")
    }
    
    var task: Task {
        guard let params = self.parameters?.values else {
            return .requestPlain
        }
        let defaultEncoding: ParameterEncoding = self.method == .get ? URLEncoding.queryString : APIParamEncoding.default
        return .requestParameters(parameters: params, encoding: self.parameters?.encoding ?? defaultEncoding)
    }
    
    var route: APIRoute {
        fatalError("route has not been implemented")
    }
    
    var path: String {
        NSString.path(withComponents: [self.route.path])
    }
    
    var method: Moya.Method {
        route.method
    }
    
    var parameters: APIParameters? {
        nil
    }
    
    var identifier: String {
        route.method.rawValue + url.absoluteString
    }
    
    func makeHeaders() -> [String: String]? {
        var headers = self.headers ?? ["Accept": "application/json;application/x-www-form-urlencoded"]
//        if method == .get || method == .head || method == .delete {
//            headers["Content-Type"] = contentType ?? "application/json"
//        } else {
            headers["Content-Type"] = "application/x-www-form-urlencoded"
//        }
        return headers
//        return headers
    }
}

/// `APIProviderSharing` 为所有的 `APIService` 提供了一个
/// `APIProvider` 的单例用于执行请求和管理内部状态
protocol APIProviderSharing where Self: APIService {
    static var shared: APIProvider<Self> { get }
//    func make(_ duringOfObject: AnyObject?, behaviors: Set<APIRequestBehavior>?, hotPlugins: [APIPlugin]) -> SignalProducer<APIResult, APIError>
}

APIRoute 是对 method 的二次封装,顺便把 path 也封装进去

public enum APIRoute {
    case get(String)
    case post(String)
    case put(String)
    case delete(String)
    case options(String)
    case head(String)
    case patch(String)
    case trace(String)
    case connect(String)

    public var path: String {
        switch self {
        case .get(let path): return path
        case .post(let path): return path
        case .put(let path): return path
        case .delete(let path): return path
        case .options(let path): return path
        case .head(let path): return path
        case .patch(let path): return path
        case .trace(let path): return path
        case .connect(let path): return path
        }
    }

    public var method: Moya.Method {
        switch self {
        case .get: return .get
        case .post: return .post
        case .put: return .put
        case .delete: return .delete
        case .options: return .options
        case .head: return .head
        case .patch: return .patch
        case .trace: return .trace
        case .connect: return .connect
        }
    }
}

Env 是环境配置,属于公司业务范畴,这里不作展示.

Provider封装

我司使用的响应式框架为 ReactiveSwift
Moya 对此的拖展函数为

func request(_ token: Base.Target, callbackQueue: DispatchQueue? = nil) -> SignalProducer<Response, MoyaError> {
        return SignalProducer { [weak base] observer, lifetime in
            let cancellableToken = base?.request(token, callbackQueue: callbackQueue, progress: nil) { result in
                switch result {
                case let .success(response):
                    observer.send(value: response)
                    observer.sendCompleted()
                case let .failure(error):
                    observer.send(error: error)
                }
            }

            lifetime.observeEnded {
                cancellableToken?.cancel()
            }
        }
    }

在此基础上,我们进行封装


    func make(_ target: Target, behaviors: Set<APIRequestBehavior>? = nil, hotPlugins: [APIPlugin] = [], within: AnyObject? = nil) -> SignalProducer<APIResult, APIError> {
        
        let pluginApplied = apiPlugins + hotPlugins
        // 拿到初始的moya 请求signalprducer
        let originalSignalProducer = self.reactive.request(target)
        // 拿到初始的moya response, 并转成 apiresult
        let responseMapped = originalSignalProducer.observe(on: QueueScheduler()).map { response -> Moya.Response in
            let decodeResponse = Moya.Response(statusCode: response.statusCode, data: response.data, request: response.request, response: response.response)
            return decodeResponse
        }.map(APIResult.self).observe(on: QueueScheduler.main)

        // 错误匹配
        let apiErrorMapped = responseMapped.mapError { APIError(from: $0) }
        
        // 尝试去验证业务错误
        let  validateMapped = apiErrorMapped.attempt { result in
            let errors = pluginApplied.compactMap { $0.validate(result, behaviors: behaviors) }
            if errors.count > 0 {
                return .failure(errors.first!)
            } else {
                return .success(())
            }
        }
        
        // 插件执行生命周期
        var lifetimeObsered = validateMapped.on(started: {
            pluginApplied.forEach { $0.didStart(target: target, behaviors: behaviors)}
        }, failed: { error in
            pluginApplied.forEach { $0.didEnd(target: target, behaviors: behaviors, result: nil, error: error) }
        }, interrupted: {
            pluginApplied.forEach { $0.didEnd(target: target, behaviors: behaviors, result: nil, error: nil)}
        }, value: { response in
            pluginApplied.forEach { $0.didEnd(target: target, behaviors: behaviors, result: response, error: nil)}
        })
        
        // 生命周期监听
        if let obj = within {
            lifetimeObsered = lifetimeObsered.take(duringLifetimeOf: obj)
        }
        
        return lifetimeObsered
    }

我做了一些业务筛减,保留存储的请求处理,说明都在code 里,这里有几个参数定义

target: Target
操作目标,用于生成节点,节点最后会转化成 request
behaviors: Set<APIRequestBehavior>? = nil
定义本次请求的行为,如不展示错误弹出,忽略警告报错,隐藏请求活动图标,自定义超时时间等等
hotPlugins: [APIPlugin]
本次请求额外的插件(预留插件)
behavior 属于功能比较小的插件,概念不用,本质和 plugin 差不多,都是对请求行为的补充

在 plugins 基础上,我们定义了一个新的概念,APIPlugin,并且生命周期由rac 控制,其实对 PluginType 做拖展也能做到(选择自己喜欢的即可)

插件

Moya 初始化函数为:

/// Initializes a provider.
    public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
                requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
                stubClosure: @escaping StubClosure = MoyaProvider.neverStub,
                callbackQueue: DispatchQueue? = nil,
                session: Session = MoyaProvider<Target>.defaultAlamofireSession(),
                plugins: [PluginType] = [],
                trackInflights: Bool = false) {

        self.endpointClosure = endpointClosure
        self.requestClosure = requestClosure
        self.stubClosure = stubClosure
        self.session = session
        self.plugins = plugins
        self.trackInflights = trackInflights
        self.callbackQueue = callbackQueue
    }

moya 接收一个[PluginType]的插件数组初始化,并提供了基础的 log 插件NetworkLoggerPlugin
PluginType 生命周期函数为

public protocol PluginType {
    /// 配置 request
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest

    /// 请求发送前
    func willSend(_ request: RequestType, target: TargetType)

    /// 接受到响应
    func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)
}

核心调用顺序位置为, moya 实现了alamofire 的RequestInterceptor

func adapt(_ urlRequest: URLRequest, for session: Alamofire.Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        // prepare
        let request = prepare?(urlRequest) ?? urlRequest
        // willSend
        willSend?(request)
        completion(.success(request))
    }

加密

数据加密请求,请求头的通用参数,我们可以通过插件形式实现
自定一个插件,实现

final class RequestTransformation: PluginType {
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
          //业务加密代码,aes,rsa,md5,base64,爱咋搞咋搞
    }
}

用插件实现,结构会非常的清晰

log

自定义一个 log 插件

public final class NetworkLoggerPlugin {

    public var configuration: Configuration

    /// Initializes a NetworkLoggerPlugin.
    public init(configuration: Configuration = Configuration()) {
        self.configuration = configuration
    }
}

注意一点,Moya 框架自带了一个NetworkLoggerPlugin,如果不想自定义的话,可以使用它,但是注意它接受一个 Configuration参数

public final class NetworkLoggerPlugin {

    public var configuration: Configuration

    /// Initializes a NetworkLoggerPlugin.
    public init(configuration: Configuration = Configuration()) {
        self.configuration = configuration
    }
}

这个参数里面有个Formatter,记得将 data->string, 使用.prettyPrinted,这样打印出来的结果会好看点,我司使用的是自定义,为了区分打印 globalParam, 业务param等等,不过实现原理和 Moya 自带的差不多

static func ResponseLoggingDataFormatter(_ data: Data) -> Data {
        do {
            let dataAsJSON = try JSONSerialization.jsonObject(with: data)
            let prettyData =  try JSONSerialization.data(withJSONObject: dataAsJSON, options: .prettyPrinted)
            return prettyData
        } catch {
            return data
        }
    }

业务错误码

从此插件开始,后续均为 APIPlugin

class APIResponseValidation: APIPlugin {
    
    static let shared = APIResponseValidation()
    private init() {}
    
    func validate(_ result: APIResult, behaviors: Set<APIRequestBehavior>?) -> APIError? {
        APIError(from: result)
    }
}

核心是,在 APIProvider 中,我们在执行插件 didEnd之前

// 尝试去验证业务错误
        let  validateMapped = apiErrorMapped.attempt { result in
            let errors = pluginApplied.compactMap { $0.validate(result, behaviors: behaviors) }
            if errors.count > 0 {
                return .failure(errors.first!)
            } else {
                return .success(())
            }
        }

APIResponseValidation去 validate APIResult 的业务,如果业务上有特殊需要,可以对特殊的 code,进行错误抛出,比如业务上code: 8888,尽管状态码200,但是我们仍然认为是不成功的一次请求,走的是failed,从而走插件 的 didEnd业务处理(Toast 啥的),而不会进入 success

Toast插件

final class ToastErrorHandler: APIPlugin {
    static let shared = ToastErrorHandler()
    private init() {}

    func didEnd(target: TargetType, behaviors: Set<APIRequestBehavior>?, result: APIResult?, error: APIError?) {
        guard let error = error else { return }
        var shouldEmitToast = true
        var shouldBalance = true
        if let behaviors = behaviors {
            for behavior in behaviors {
                if case let .suppressMessage(codes) = behavior {
                    if let codes = codes {
                        if codes.contains(error.errorCode) {
                            shouldEmitToast = false
                        }
                        if codes.contains(APIError.balanceCode) {
                            shouldBalance = false
                        }
                    } else {
                        shouldEmitToast = false
                    }
                    break
                }
            }
        }
        
        if case let .balance(message) = error, shouldBalance {
            log("\(message ?? "")")
        } else if case let .notVIP(message) = error {
            log("\(message ?? "")")
        } else {
            if error.errorCode == -6 { // AFN特有的-6网络请求失败
                return
            }
            
            if shouldEmitToast {
                DispatchQueue.main.async {
                    print("\(error.description)")
                }
            }
        }
        
    }
}

toast 我简单处理了下,根据自己的业务处理弹窗即可
提到 toast,这里再埋一个坑,toast 大家很常用,但是 toast 封装也很重要,如果有时间,我会抽出我司封装的 toast 组件,非常非常 nice!
稍微透露下

enum Behavior {
        case replaceAll // 全部消失,清空队列(当前显示的和队列中的所有的spinner会被保留并延后)
        case replaceCurrent // 只消失当前显示(当前显示的spinnner会被保留并延后),不清空队列
        case queue(_ priority: Operation.QueuePriority = .normal) // 加入队列
}

ps:太久没写 blog,感觉写起来好麻烦,写着写着就不想写了,最近事情确实忙,忙了接近一年,没时间也没精力去写这种总结性的文章,虽然知道写起来对我自己有帮助,但是确实太忙了,唉~这篇文章写的也很水,本来想把框架里的每个文件都介绍一下,写着写着,就懒得写了,希望后续有所改善吧.

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

推荐阅读更多精彩内容