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结果。