T21HTTPRequester 1.2.0

T21HTTPRequester 1.2.0

Salvador MartinMarcos Molero 维护。



 
依赖项
T21Mapping>= 0
T21LoggerSwift>= 0
Moya>= 0
 

  • 作者
  • Eloi Guzman Ceron

T21HTTPRequester

版本 1.0.0

T21HTTPRequester 利用 Moya 网络抽象层库提供了一种简单且通用的方式来与 REST API 进行交互。

请求者添加了一个额外功能,它使用具体的映射解析每个端点(服务),从而得到一个特定的响应类型。

配置服务端点

有几种不同的选项可用于配置 API 服务/端点。

实现 TargetType & TargetTypeMapping 协议

每个端点必须实现这两个协议

  • TargetType:这个协议是 Moya 库的一部分。它声明了执行端点请求所需的所有内容。
  • TargetTypeMapping:此协议用于声明映射和请求的响应类型。请求者将使用此协议来推断结果响应类型。

以下代码显示了一个示例端点。

import Foundation
import Moya
import T21Mapping
import T21HTTPRequester

public class ExampleService : TargetType,TargetTypeMapping  {
    
    public var baseURL: URL { return URL(string: "https://swapi.co/api")! }
    
    public var path: String {
        return "/films/"
    }
    
    public var method: Moya.Method {
        return .get
    }
    
    public var parameters: [String: Any]? {
        return nil
    }
    
    public var parameterEncoding: ParameterEncoding {
        return URLEncoding.default
    }
    
    public var sampleData: Data {
        return "Sample data".utf8Encoded
    }
    
    public var task: Task {
        return .request
    }
    
    public var mapping: Mapping<HTTPRequesterResult<Moya.Response, MoyaError>,ExampleResponseType> {
        return Mapping({ (inputResponse) in 
        	return ExampleResponseType(inputResponse) 
        })
    }
}

如您所见,为了定义一个特定的服务,需要大量的编码。然后,一个可能的解决方案是创建一个BaseService,该服务已定义了公共值,如baseURL、参数编码以及URL请求任务的类型。子类仅定义了所需的映射、路径、HTTP方法(如果与BaseService不同)以及参数

这里是完整的示例。

首先是基服务类。

public class BaseService <ResponseType> : TargetType,TargetTypeMapping  {
    
    public var baseURL: URL { return URL(string: "https://swapi.co/api")! }
    
    public var path: String {
        return ""
    }
    
    public var method: Moya.Method {
        return .get
    }
    
    public var parameters: [String: Any]? {
        return nil
    }
    
    public var parameterEncoding: ParameterEncoding {
        return URLEncoding.default
    }
    
    public var sampleData: Data {
        return "Sample data".utf8Encoded
    }
    
    public var task: Task {
        return .request
    }
    
    public var mapping: Mapping<HTTPRequesterResult<Moya.Response, MoyaError>,ResponseType> {
        //BaseService mapping is not valid, overwrite it.
        return Mapping({ _ in return ("" as! ResponseType) })
    }
}

然后是一个具体的端点子类(此子类包含正文参数)。

class LoginService : BaseService<GetLoginResponseType> {
    
    let userName: String
    let password: String
    
    init( _ userName: String, _ password: String) {
        self.userName = userName
        self.password = password
    }
    
    override var path : String {
        return "/"
    }
    
    override var parameters: [String: Any]? {
        return ["user" : userName, "password" : password]
    }
    
    override var method: Moya.Method {
        return .post
    }
    
    override var mapping: Mapping<HTTPRequesterResult<Moya.Response, MoyaError>,GetLoginResponseType> {
        return MappingAPILogin()
    }
}

从理论上讲,Moya库使用枚举方法来定义多个服务,但这会导致枚举类型变得很大,并且每当添加新的服务时都会增长。另外,当使用枚举类型时,为每个不同的枚举类型(每个不同的端点)定义一个具体的响应类型是不可行的

使用HTTPGenericService类

HTTPGenericService类允许创建任何类型的服务的通用实例。所有必需的参数都在构造特定的新实例时发送。

使用HTTPGenericService类配置服务的示例。

let mappingA: Mapping<HTTPRequesterResult<Moya.Response, MoyaError>,String> = Mapping{ (result : HTTPRequesterResult<Moya.Response, MoyaError>) -> String in
    return "this example mapping returns an String literal"
}
let getFilmsServiceA = HTTPGenericService<String>(URL(string: "https://swapi.co/api")!,"/films/",.get,nil,mappingA)

如您所见,在这里我们指定了所有类型,而不让编译器推断创建映射和服务所需的泛型类型。利用编译器所知的信息的简化版本将是

let mappingB = MoyaMapping{ (result) in
    return "this example mapping returns an String literal"
}
let getFilmsServiceB = HTTPGenericService<String>(URL(string: "https://swapi.co/api")!,"/films/",.get,nil,mappingB)

由于客户端不需要为每个端点创建一个类,这使得HTTPGenericService类成为创建服务工厂类(如ServiceFactory或ServiceStore)的良好候选者。

创建HTTPRequester

为了启动服务调用,客户端应用程序需要实例化HTTPRequester。HTTPRequester需要一个MoyaProvider才能工作(这是因为请求者可以在幕后使用Moya)。这样我们就可以利用Moya中提供的所有可能配置(例如,插件注入等)。

创建示例请求者的代码。

//configure a custom MoyaProvider
// https://github.com/Moya/Moya/blob/master/docs/Endpoints.md
let endpointClosure = { (target: MultiTarget) -> Endpoint<MultiTarget> in
    let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target)
    return defaultEndpoint.adding(newHTTPHeaderFields: ["APP_NAME": "TODO: APP NAME"])
}
    
//add the logger plugin
let loggingPlugin = HTTPRequesterLoggerPlugin(verbose: false)
    
//create the innerRequester using a MoyaProvider
let moyaProvider = MoyaProvider<MultiTarget>(endpointClosure: endpointClosure, plugins: [loggingPlugin])
self.innerRequester = HTTPRequester(moyaProvider)

前面的代码创建了一个示例提供者,它在所有请求中添加了某些示例HTTP头部字段。它还添加了一个日志插件,用于输出请求信息到控制台。

根据您的应用程序架构,您可能希望将HTTPRequester实例的可视性提供给其他类。您可以使用依赖注入或使用单例方法。

以下是一个单例方法的示例。

import Foundation
import Moya
import T21HTTPRequester

public class HTTPProvider {
    
    private static let sharedInstance = HTTPProvider()
    private let innerRequester: HTTPRequester
    
    private init() {
        
        //configure a custom MoyaProvider
        // https://github.com/Moya/Moya/blob/master/docs/Endpoints.md
        let endpointClosure = { (target: MultiTarget) -> Endpoint<MultiTarget> in
            let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target)
            return defaultEndpoint.adding(newHTTPHeaderFields: ["APP_NAME": "TODO: APP NAME"])
        }
        
        //add the logger plugin
        let loggingPlugin = HTTPRequesterLoggerPlugin(verbose: false)
        
        //create the innerRequester using a MoyaProvider
        let moyaProvider = MoyaProvider<MultiTarget>(endpointClosure: endpointClosure, plugins: [loggingPlugin])
        self.innerRequester = HTTPRequester(moyaProvider)
    }
    
    public static var requester: HTTPRequester {
        return HTTPProvider.sharedInstance.innerRequester
    }
}

请记住,单例模式可能不是您项目的最佳方法,这里只是一个示例。

启动服务请求

获得服务实例后,现在是时候使用它来启动请求了。HTTPRequester提供了两种主要方法来启动请求

public func request<RequestType>( _ service : RequestType, _ completion: @escaping (_ response: RequestType.T) -> (Void) ) where RequestType : TargetType, RequestType : TargetTypeMapping 

public func requestSimple( _ service : TargetType, _ completion: @escaping (_ response: HTTPRequesterResult<Moya.Response, MoyaError>) -> Void)

第一个方法

service参数期望一个实现了TargetType和TargetTypeMapping协议的实例(请参阅文档部分配置服务端点)。

let mapping = MoyaMapping{ (result) in
    return "this example mapping returns an String literal"
}
let getFilmsService = HTTPGenericService<String>(URL(string: "https://swapi.co/api")!,"/films/",.get,nil,mapping)

//using the previous example singleton approach

HTTPProvider.requester.request(getFilmsService, { (response: String) in
	print(response)
})

正如您所看到的,这个请求方法从服务类中推断响应类型。这意味着,客户端将始终接收到一个具体类型。映射的主要目的是避免返回一个不受控制的响应类型,例如数据或JSON对象。如果架构强制使用映射,客户端就会被“强制”解析从端点派生的所有可能的响应类型:所有的happy paths,连接错误,映射错误,未授权错误等等...

以前的例子使用的是字符串响应类型。但在实际场景中,客户端可以有一个这样的响应类表示

public enum GetFilmsResponseType {
    case success(films: [FilmType]) // HTTP Status code: 200
    case mappingFailed // when the mapping was not possible (missing compulsory values)
    case error(error: Swift.Error) // connection related errors
    case invalidToken // specific errors from the API
}

正如您所看到的,使用这种表示方法,客户端没有留下任何可能的未管理路径。所有可能的响应都分组在这个枚举响应类型中。这将帮助我们在应用中避免不受控制的输入。

第二个方法

service参数期望一个仅实现TargetType协议的实例(不实现TargetTypeMapping)。这意味着,不会使用映射。

使用此方法,请求者不会将接收到的数据映射到期望的类型,客户端将接收到一个HTTPRequesterResult<Moya.Response, MoyaError>类型。这是没有额外层的预期Moya结果。