Swift Swift基础

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

2021-06-23  本文已影响0人  简单coder

我从来不是个天赋型的程序员,我只是站在巨人的脚下,仰望着巨人. 这篇文章,是以解读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,感觉写起来好麻烦,写着写着就不想写了,最近事情确实忙,忙了接近一年,没时间也没精力去写这种总结性的文章,虽然知道写起来对我自己有帮助,但是确实太忙了,唉~这篇文章写的也很水,本来想把框架里的每个文件都介绍一下,写着写着,就懒得写了,希望后续有所改善吧.

上一篇 下一篇

猜你喜欢

热点阅读