背景
在年初的时候看到了喵神关于一篇基于Protocol的网路请求的文章.
当初的理解有限, 项目感觉也没有那么强烈的解耦需求.所以我最终写了一个基于泛型与关联枚举实现高度复用代码的网络层.. 很像是比较火的一个网络封装库-Moya.
但是Moya封装后的每一个请求回调仍然是一个不知具体数据类型的Result(Data or JSON). 这不是我想要的, 我主张的是最终调用的网络请求的回调是包含我们目标类型的. 一般是一个具体模型Model, 或者是成功失败的Bool值, 这个由接口暴露出来自己定. 如果是返回的data or JSON, 那么重复的转换代码(JSON->Model)就会散步到逻辑代码当中.
/// In ViewController or ViewModel.
provider.request(.zen) { result in
switch result {
case let .success(moyaResponse):
let data = moyaResponse.data
let statusCode = moyaResponse.statusCode
// Then, JSON -> Model
case let .failure(error):
}
}
并且, 当业务开发人员发起这个请求并准备对回调数据进行处理的时候, 他每次都会去考虑我应当转换为什么类型, 好像其它某个地方也调用过这个请求, 我翻过去看看那边怎么处理的吧... Copy & Paste
所以我认为, 在调用请求之后最终的Result应该是这样的.
enum NetResult {
case value(V)
case error(RequestError)
}
value关联的类型V就是我们最终想要得到的具体类型. 而不是Data/JSON
最终调用起来应该是这样的.
request.LoadList(page: 1, pageSize: 1).load { (NetResult) in
}
这个请求回调我们直接可以看出来,回调的数据是一个Value为List
类型的NetResult
, 不需要再去转换.
基层协议
protocol Request: class {
// The type we expect to end up
associatedtype ExpectedType
var path: RequestConfig.Path { get }
var action: RequestConfig.Action { get }
var method: HTTPMethod { get }
var parameter: [String: Any] { get set }
var headers: HTTPHeaders { get }
var encoding: ParameterEncoding { get }
// How to convert from response to expected type
var parse: (Dict) -> ExpectedType? { get }
}
重点1: 因为每一个请求我们想要的返回类型都不同, 这里利用associatedtype
来让conform协议的具体请求指定类型.
中间的参数根据各个项目可能略有不同.我这里的path与action都是字符串枚举.
重点2: 因为要返回具体的目标类型, 那么这个请求就要负责JSON->ExpectedType的转换了.. 并且大部分时候, 我们都会通过一些JSON转换库来复用这些代码.
为此协议提供load方法
发起一个请求所需的条件协议中都已经规定了. 接下来就可以通过这些参数利用网络库发起请求了.
extension Request {
/// Common load function
func load(handler: @escaping (NetResult) -> Void) {
// Some general processing, every project is different
let path = RequestConfig.host + self.path.rawValue
parameter.toCommonAction(action: self.action.rawValue)
let manager = Alamofire.SessionManager(
configuration: {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 15
return config
}())
manager.request(path, method: method, parameters: parameter, encoding: encoding, headers: headers).responseJSON { (response) in
let result = response.result
if let _ = result.error {
handler(NetResult(value: nil, error: .serviceLost))
return
}
guard let resultDict = result.value as? Dict else {
handler(NetResult(value: nil, error: .rootDataNotDict))
return
}
guard let targetData = self.parse(resultDict) else {
handler(NetResult(value: nil, error: .jsonToModelFailure))
return
}
/// You can add more error handling based on response
handler(NetResult(value: targetData, error: nil))
}
}
}
我这里用的是Alamofire. Response是JSON.
就像我前面所描述的, 这个请求最终的回调参数是一个关联了指定类型ExpectedType
的Result枚举.
所以, 在请求回调的结果中, 对错误也进行了包装, 根据不同情况返回不同的RequestError..这里只写了两种情况. 根据项目可以扩展更多.
这里通过调用协议中规定的parse
方法, 可以直接得到我们想要的ExpectedType类型的数据, 并放到result中进行返回.
NetResult包装结果
enum NetResult {
case value(V)
case error(RequestError)
init(value: V?, error: RequestError?) {
self = error == nil ? NetResult.value(value!) : .error(error!)
}
var value: V {
switch self {
case .value(let value):
return value
case .error:
fatalError("You can't get value when error occurred")
}
}
var error: RequestError? {
switch self {
case .value:
return nil
case .error(let error):
return error
}
}
}
How to use
first: 创建一个struct
或class
遵循Request协议.
struct LoadList: PostRequest {
}
我这里直接遵循的Request的子协议,PostRequest, 它对Header和Method进行了默认配置.
// MARK: - POST Request Configuration
protocol PostRequest: Request {}
extension PostRequest {
var method: HTTPMethod {
return .post
}
var headers: HTTPHeaders {
return [:]
}
var encoding: ParameterEncoding {
return JSONEncoding.default
}
}
你也可以创建其它Request子协议进行一些默认配置.
接着, 编译器会提示你协议中有一个ExpectedType需要指定类型
struct List {
var name: String
var age: Int
}
struct LoadList: PostRequest {
typealias ExpectedType = List
}
这里我们指定期望回调的类型是一个List结构体.
接着, 编译器会继续提示有协议中规定的东西要实现.
struct LoadList: PostRequest {
typealias ExpectedType = List
typealias ExpectedType = List
var path: RequestConfig.Path = .home
var action: RequestConfig.Action = .getList
var parameter: [String : Any] = [:]
var parse: (Dict) -> List? = { root in
/// JSON Analysis
return List(name: "a", age: 3)
}
}
parse方法需要我们指定拿到后台数据从json如何转换到list结构体的. 具体转换方法随你而定.我推荐让模型再遵循一个IBaseModel协议, 此协议中有一些从dict转到model的默认方法.. 这样在这里直接调用IBaseModel的转换方法就好, 因为大部分情况下他们都是大同小异的.
到这里parameter还没有进行赋值. 因为有些值是必需从界面中获取的. 通过构造方法暴露出去就好.
/// An Example
struct LoadList: PostRequest {
typealias ExpectedType = List
var path: RequestConfig.Path = .home
var action: RequestConfig.Action = .getList
var parameter: [String : Any] = [:]
var parse: (Dict) -> List? = { root in
/// JSON Analysis
return List(name: "a", age: 3)
}
init(page: Int, pageSize: Int) {
parameter["page"] = page
parameter["pageSize"] = pageSize
}
}
比如这个请求需要一个page与pageSize参数, 在创建的时候从外部传进来, 即可满足了所有条件.
调用的地方:
request.LoadList(page: 1, pageSize: 1).load { (result) in
if let error = result.error {
return
}
let list = result.value // List
}
直接拿到List去使用吧.
看源码请到Github