GithubHelp home page GithubHelp logo

enxibaba / apiservice Goto Github PK

View Code? Open in Web Editor NEW

This project forked from coder-star/apiservice

0.0 0.0 0.0 593 KB

Swift 网络请求 抽象层

License: MIT License

Shell 0.88% JavaScript 0.83% Ruby 2.35% Swift 70.79% SCSS 17.56% Mustache 7.58%

apiservice's Introduction

CSAPIService

Version License Platform Doc

CSAPIService 是一个轻量的 Swift 网络抽象层框架,将请求、解析等流程工作分成几大角色去承担,完全面向协议实现,利于扩展。

使用方式

pod 'CSAPIService'

其实原来的名称为APIService,但是因为该名在CocoaPods已经被占用了,就加了前缀,但是在使用时,模块名称依然是APIService

代码注释比较完备,部分细节可以直接查看代码。

特性

  • 层次清晰,按需引入;
  • 支持Request级别拦截器,并支持对响应进行异步替换;
  • 支持插件化拦截器;
  • 面向协议,角色清晰,方便功能扩展;
  • 缓存可拆卸,缓存包含内存、磁盘两级缓存;

框架组成

APIService

箭头指的是发送流程,实心点指的是数据回调流程; 高清图可见 链接

框架按照网络请求流程中涉及的步骤将其内部分为几个角色:

  • 请求者(APIRequest):更准确的叫法应该是构建请求者,其主要作用就是将外部传入的相关参数经过相关处理构造成一个URLRequest实例,并且提供请求拦截器以及回调拦截器两种拦截器;
  • 发送者(APIClient):实际的网络请求发送者,目前默认是Alamofire,你也可以实现协议,灵活的对发送者进行替换;
  • 解析者(APIParsable):一般与Model是同一个角色,由Model实现协议从而实现从数据到实体这一过程的映射;
  • 缓存(APICache、APICacheTool):缓存相关;
  • 服务提供者(APIService):整个框架的服务提供者,提供最外层的API,可以传入插件;

整个框架是按照POP的**进行设计,将相关角色都尽量抽象成协议,方便扩展;

APIRequest 协议

描述一个URLRequest实例需要的信息,并提供相应的拦截服务,在构造中我们可以设置返回的ResponseModel类型;

我们应当以领域服务为单位来提供对应的APIRequest领域服务大部分会按照域名不同来划分,即 A 域名对应AAPIRequest,B 域名对应BAPIRequest

APIRequest的拦截器应用场景主要是整个领域服务级别的,一般添加的逻辑都是统一的逻辑。如:

  • 发送前加上统一参数,Header 等信息;
  • 数据回调到业务之前统一对一些 code 进行判断,如未登录自动弹出登录框等统一逻辑;
/// 拦截参数,在参数编码之前
func intercept(parameters: [String: Any]?) -> [String: Any]?

/// 请求发送之前
func intercept(urlRequest: URLRequest) throws -> URLRequest

/// 数据回调给业务之前
/// 利用 replaceResponseHandler 我们可以替换返回给业务的数据,还可以用作一些重试机制上等;
/// 需要注意的是一旦实现该方法,需要及时使用 replaceResponseHandler 将 response 返回给业务方。
func intercept<U: APIRequest>(request: U, response: APIResponse<Response>, replaceResponseHandler: @escaping APICompletionHandler<Response>)

APICache

APICache是一个struct,用来配置一个API请求时的缓存设置,相关设置包括:

相关属性作用请看注释。

public struct APICache {
    public init() { }

    /// 读取缓存模式
    public var usageMode: APICacheUsageMode = .none

    /// 写入缓存模式
    public var writeNode: APICacheWriteMode = .none

    /// 只有 writeNode 不为 .none 时,后面参数有效

    /// 额外的缓存key部分
    /// 可添加app版本号、用户id、缓存版本等
    public var extraCacheKey = ""

    /// 自定义缓存key
    public var customCacheKeyHandler: ((String) -> String)?

    /// 缓存过期策略类型
    public var expiry: APICacheExpiry = .seconds(0)
}

public protocol APIRequest {
    // MARK: - 缓存相关

    /// 缓存
    /// 目前 taskType 为 request 才生效
    var cache: APICache? { get }

    /// 是否允许缓存
    /// 可根据业务实际情况控制:比如业务code为成功,业务数据不为空
    /// 这个闭包之所以不放入 APICache 内部的原因是 享受泛型的回调
    var cacheShouldWriteHandler: ((APIResponse<Response>) -> Bool)? { get }

    /// 过滤不参与缓存key生成的参数
    /// 如果一些业务场景不想统一参数参与缓存key生成,可在此配置
    var cacheFilterParameters: [String] { get }
}

底层的缓存实现是可以通过设置delegate的方式进行替换。

/// 可以通过调整 cacheTool 的实现来更换缓存底层实现
APIConfig.shared.cacheTool = CacheTool.shared

框架内部对于缓存的读写采用的是 同步读,异步存 的方式。

使用方式:

enum HomeBannerAPI {
    struct HomeBannerRequest: CSAPIRequest {
        typealias DataResponse = HomeBanner

        var parameters: [String: Any]? {
            return nil
        }

        var path: String {
            return "/config/homeBanner"
        }

        /// 设置缓存
        var cache: APICache? {
            var cache = APICache()
            cache.readMode = .cancelNetwork
            cache.writeNode = .memoryAndDisk
            cache.expiry = .seconds(10)
            return cache
        }
    }
}


let request = HomeBannerAPI.HomeBannerRequest()
APIService.sendRequest(request, plugins: [networkActivityPlugin], cacheHandler: { response in
     /// 缓存回调
    switch response.result.validateResult {
        case let .success(info, _):
            debugPrint(info)
        case let .failure(_, error):
            debugPrint(error)
    }
}, completionHandler: { response in
    /// 网络回调
    switch response.result.validateResult {
        case let .success(info, _):
            debugPrint(info)
        case let .failure(_, error):
            debugPrint(error)
    }
})

我们可以看到,sendRequest 为缓存单独加了一个回调,而不是和原来的completionHandler使用同一个,目的是想让业务方可以明确的感知到该次回调是来自网络还是缓存,也是呼应 APICacheReadMode 这个配置。

APIClient 协议

负责发送一个Request请求,我们可以调整APIClient的实现方式; 目前默认实现方式为Alamofire,其中使用别名等方式做了隔离。

/// 网络请求任务协议
public protocol APIRequestTask {
    /// 恢复
    func resume()

    /// 取消
    func cancel()
}

/// 网络请求客户端协议
public protocol APIClient {
    func createDataRequest(
        request: URLRequest,
        progressHandler: APIProgressHandler?,
        completionHandler: @escaping APIDataResponseCompletionHandler
    ) -> APIRequestTask

    func createDownloadRequest(
        request: URLRequest,
        to: @escaping APIDownloadDestination,
        progressHandler: APIProgressHandler?,
        completionHandler: @escaping APIDownloadResponseCompletionHandler
    ) -> APIRequestTask
}

APIClient 协议比较简单,就是根据请求类型区分不同的方法,其返回值也是一个协议(APIRequestTask),支持resume以及cancel操作。

目前操作只保留数据请求、下载请求两种方式,其他方式后续版本再补充;

APIParsable 协议

public protocol APIParsable {
    static func parse(data: Data) throws -> Self
}

如上所示,APIParsable协议其实很简单,实现者通常是Model,就是将Data类型的数据映射成实体类型。

这是最上层的协议,在该协议下方目前还有APIJSONParsable协议,其继承了APIParsable协议,如下所示:

public protocol APIJSONParsable: APIParsable {}

extension APIJSONParsable where Self: Decodable {
    public static func parse(data: Data) throws -> Self {
        do {
            let model = try JSONDecoder().decode(self, from: data)
            return model
        } catch {
            throw APIResponseError.invalidParseResponse(error)
        }
    }
}

目前协议的默认实现方式是通过Decodable的方式将JSON转为Model

当然我们可以根据项目数据交换协议扩展对应的解析方式,如 XML、Protobuf等;

public typealias APIDefaultJSONParsable = APIJSONParsable & Decodable

同时为方便业务使用,添加了一个别名,如果使用默认方式 Decodable 进行解析,最外层 Model 就可以直接实现该协议。

APIPlugin

public protocol APIPlugin {
    /// 构造URLRequest
    func prepare<T: APIRequest>(_ request: URLRequest, targetRequest: T) -> URLRequest

    /// 发送之前
    func willSend<T: APIRequest>(_ request: URLRequest, targetRequest: T)

    /// 接收结果,时机在返回给调用方之前
    func willReceive<T: APIRequest>(_ result: APIResponse<T.Response>, targetRequest: T)

    /// 接收结果,时机在返回给调用方之后
    func didReceive<T: APIRequest>(_ result: APIResponse<T.Response>, targetRequest: T)
}

在具体网络请求层次上提供的拦截器协议,这样业务使用过程中可以感知到请求请求中的重要节点,从而完成一些逻辑,如Loading的加载与消失就可以通过构造一些对应的实例去完成。

目前提供了一个默认的 Plugin,是: NetworkActivityPlugin

APIService

这是最外层的供业务发起网络请求的API

open class APIService {
    private let reachabilityManager = APINetworkReachabilityManager()

    public let client: APIClient

    public init(client: APIClient) {
        self.client = client
    }

    /// 单例
    public static let `default` = APIService(client: AlamofireAPIClient())
}

APIService提供类方法以及实例方法,其中类方法就是使用的default实例,当然也可以其他的APIClient实现实例然后调用实例方法,等后续对底层实现进行替换是,只需要替换default实例的默认实现就可以了。

public func sendRequest<T: APIRequest>(
        _ request: T,
        plugins: [APIPlugin] = [],
        encoding: APIParameterEncoding? = nil,
        progressHandler: APIProgressHandler? = nil,
        completionHandler: @escaping APICompletionHandler<T.Response>
    ) -> APIRequestTask? { }

实例方法定义如上,支持传入APIPlugin 实例数组。

业务使用实践

相关代码在 Demo 工程里面可以看到。

最外层的 Model

/// 网络请求结果最外层Model
public protocol APIModelWrapper {
    associatedtype DataType: Decodable

    var code: Int { get }

    var msg: String { get }

    var data: DataType? { get }
}

对于大多数的网络请求而言,拿到的回调结果最外层肯定是最基础的 Model,也就是所谓的BaseReponseBean,其字段主要也是codemsgdata

我们定义这样的一个协议方便后续使用,其实这个协议当时是直接放在库里面的,后来发现库内部对其没有依赖,其更多是一种更优的编程实践,就提出来放到了 Demo 工程里面去。

我们需要构造一个实体去实现该协议,

public struct CSBaseResponseModel<T>: APIModelWrapper, APIDefaultJSONParsable where T: Decodable {
    public var code: Int
    public var msg: String
    public var data: T?

    enum CodingKeys: String, CodingKey {
        case code
        case msg = "desc"
        case data
    }
}

因为最外层的 CSBaseResponseModel 已经满足了 APIDefaultJSONParsable协议,所以业务Model不需要再实现该协议了,而是直接实现Decodable就好。

有的小伙伴可能会想不能直接使用实体吗?为什么还需要一个协议,这个协议在后面会用到。

业务 APIRequest

/// 注意这个 CSBaseResponseModel
protocol CSAPIRequest: APIRequest where Response == CSBaseResponseModel<DataResponse> {
    associatedtype DataResponse: Decodable

    var isMock: Bool { get }
}

extension CSAPIRequest {
  	var isMock: Bool {
        return false
    }

    var baseURL: URL {
        if isMock {
            return NetworkConstants.baseMockURL
        }
        switch NetworkConstants.env {
        case .prod:
            return NetworkConstants.baseProdURL
        case .dev:
            return NetworkConstants.baseDevURL
        }
    }

    var method: APIRequestMethod { .get }


    var parameters: [String: Any]? {
        return nil
    }

    var headers: APIRequestHeaders? {
        return nil
    }

    var taskType: APIRequestTaskType {
        return .request
    }

    var encoding: APIParameterEncoding {
        return APIURLEncoding.default
    }
  
    public func intercept(urlRequest: URLRequest) throws -> URLRequest {
        /// 我们可以在这个位置添加统一的参数、header的信息;
        return urlRequest
    }

    public func intercept<U: APIRequest>(request: U, response: APIResponse<Response>, replaceResponseHandler: @escaping APICompletionHandler<Response>) {
        /// 我们在这里位置可以处理统一的回调判断相关逻辑
        replaceResponseHandler(response)
    }
}

APIResult 扩展

我们通过APIResult最终获得的是最外层的Model,那对于大部分业务方而言,他们拿到数据后还会有一些通用逻辑,如:

  • 根据code值判断请求是否成功;
  • 错误本地化;
  • 获取实际的data数据;
  • ...

而这些逻辑在每一个域名服务又可能是不同的,属于业务逻辑,所以不宜放入库内部。

那对于这些逻辑,我们就可以对 APIResult 进行扩展,将这些逻辑收进去,业务方可以根据自己的需求决定在拿到APIResult之后是否还调用这个扩展。

如果有多种逻辑,可以考虑增加一些特定前缀去区别,如下面的validateResult我们可以扩展为多个 -- csValidateResultcfValidateResult等等。

public enum APIValidateResult<T> {
    case success(T, String)
    case failure(String, APIError)
}

public enum CSDataError: Error {
    case invalidParseResponse
}

/// APIModelWrapper 在这个地方用到了
extension APIResult where T: APIModelWrapper {
    var validateResult: APIValidateResult<T.DataType> {
        var message = "出现错误,请稍后重试"
        switch self {
        case let .success(reponse):
            if reponse.code == 200, let data = reponse.data {
                return .success(data, reponse.msg)
            } else {
                return .failure(message, APIError.responseError(APIResponseError.invalidParseResponse(CSDataError.invalidParseResponse)))
            }
        case let .failure(apiError):
            if apiError == APIError.networkError {
                message = apiError.localizedDescription
            }

            assertionFailure(apiError.localizedDescription)
            return .failure(message, apiError)
        }
    }
}

业务使用

基础使用方式

enum HomeBannerAPI {
    struct HomeBannerRequest: CSAPIRequest {
        typealias DataResponse = HomeBanner

        var parameters: [String: Any]? {
            return nil
        }

        var path: String {
            return "/config/homeBanner"
        }
    }
}

APIService.sendRequest(HomeBannerAPI.HomeBannerRequest()) { reponse in
    switch reponse.result.validateResult {
    case let .success(info, _):
        /// 这个 Info 就是上面我们传入的 HomeBanner 类型
        print(info)
    case let .failure(_, error):
        print(error)
    }
}

未来规划

  • 重试机制

apiservice's People

Contributors

coder-star avatar gaoyuexit avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.