MoyaX 是一款网络抽象层的封装库,基于 Moya 6.1.3 进行了大规模的重构,它目前主要使用在我的 iOS 学习项目 中,目前测试基本覆盖,可以证明项目是可用的。
MoyaX 的目标是提供一个的 Moya 改进版本,并且功能上覆盖 Moya 的使用场景。
MoyaX 虽然已经和 Moya 有极大的不同,但我仍在跟随原始项目的变动,学习、吸取经验和好的部分。
非常、特别、强烈希望能够对设计、实现以及功能上提供反馈。
实现Target
协议即可,其中包含了描述一个 API端点的必要信息,详情请见 定义
其中baseURL
和path
为必须实现,其余为可选。
可以沿用 Moya 的方式,例:
enum GitHub {
case Zen
case UserProfile(String)
case UserRepositories(String)
}
extension GitHub: Target {
// Required
var baseURL: NSURL {
return NSURL(string: "https://api.github.com")!
}
var path: String {
switch self {
case .Zen:
return "/zen"
case .UserProfile(let name):
return "/users/\(name.URLEscapedString)"
case .UserRepositories(let name):
return "/users/\(name.URLEscapedString)/repos"
}
}
// Optional
// Default is .GET
var method: HTTPMethod {
return .GET
}
// Default is empty dictionary
var parameters: [String: AnyObject] {
switch self {
case .UserRepositories(_):
return ["sort": "pushed"]
default:
return [:]
}
}
// Default is .Form
var parameterEncoding: ParameterEncoding {
return .Form
}
// Default is empty dictionary
var headerFields: [String: String] {
return [:]
}
struct ListingTopics: Target {
enum TypeFieldValue: String {
case LastActived = "last_actived"
case Recent = "recent"
case NoReply = "no_reply"
case Popular = "popular"
case Excellent = "excellent"
}
var type: TypeFieldValue?
var nodeId: String?
var offset: Int
var limit: Int
init(type: TypeFieldValue? = nil, nodeId: String? = nil, offset: Int = 0, limit: Int = 20) {
self.type = type
self.nodeId = nodeId
self.offset = offset
self.limit = limit
}
var baseURL: NSURL {
return NSURL(string: "https://ruby-china.org/api/v3/")!
}
var path: String {
return "topics"
}
var parameters: [String: AnyObject] {
var parameters = [String: AnyObject]()
if let type = self.type {
parameters["type"] = type.rawValue
}
if let nodeId = self.nodeId {
parameters["nodeId"] = nodeId
}
parameters["limit"] = self.limit
parameters["offset"] = self.offset
return parameters
}
}
最基本的使用方式,默认使用AlamofireBackend
后端
// Common version
let provider = MoyaXProvider()
// Generic version
let provider = MoyaXGenericProvider<GitHub>()
以泛型Provider
为例,和 Moya 的使用方式完全相同
provider.request(.Zen) { result in
// `result` is either .Response(response) or .Incomplete(error)
}
你可以通过覆写Target
的endpoint
计算属性在运行时自定义Endpoint
的构造过程,比如根据应用的状态附加一些内容,这在使用枚举方式声明的时候非常有用
var endpoint: Endpoint {
var endpoint = Endpoint(URL: self.fullURL, method: self.method, parameters: self.parameters, parameterEncoding: self.parameterEncoding, headerFields: self.headerFields)
endpoint.headerFields["X-Xapp-Token"] = XAppToken().token ?? ""
return endpoint
}
ReactiveCocoa 和 RxSwift 的支持将被移除 MoyaX,以独立库的方式存在。
Provider
的构造函数可以接受如下几个可选参数:
backend: Backend
:指定后端middlewares: [Middleware]
:插件,自带网络状态插件,参见源码prepareForEndpoint: Endpoint -> ()
钩子,用于公共的对Endpoint
修饰,例如附加 Token
可以传入withCustomBackend: BackendType
来临时性指定一个后端来执行请求。
manager: Manager
指定 Alamofire 的 Manager 实例willPerformRequest: (Endpoint, Alamofire.Request) -> ()
在请求发送前的钩子,完全暴露出了 Alamofire 的Request
对象didReceiveResponse: (Endpoint, Alamofire.Response) -> ()
响应后执行的钩子,完全暴露出了 Alamofire 的Response<NSData, NSError>
对象
如果需要 Moya 风格的 Mock,让 API 的声明实现 TargetWithSample
替代 Target
,并且实现 var sampleResponse: StubResponse { get }
属性,当后端为StubBackend
时,就可以使用 API 声明里的默认响应了,StubResponse
见 定义。
可以运行时动态的设置Stub,规则是:
动态设置的Stub > API 定义中的响应 > StubBackend
的默认响应
具体方法见 定义
使用GenericStubBackend<T: Target>
,这在为使用枚举方式声明的 Targets 在Stub的时候提供了一些便利,具体方法见 定义
baseURL
和path
外所有属性均为可选,返回值均不为Optional
- 可以设置参数的编码方式(即
parameterEncoding
),默认值为.Form
,支持 Multipart 上传(使用 `.FormWithMultipart) - 可以设置请求头(即
headerFields
字典) - 不再包含
sampleData
,如果需要使用TargetWithSample
来声明Targets
,并且sampleData
被sampleResponse
取代,其直接接受StubResponse
Endpoint
的泛型并无意义,故取消。
提供非泛型的MoyaXProvider
来匹配使用类、结构体的Target
,同时提供了泛型版本MoyaXGenericProvider
可以按照Moya
的风格使用。
Provider#request
现在只负责串联数据流
真正处理请求由后端(即Backend
)完成,目前实现了AlamofireBackend
和StubBackend
,这样做还有好处:
- 实现自己的后端很容易,实现
BackendType
协议即可 - 可以增强后端的功能,没有抽象泄漏或者单一职责的负担
Provider
可以全局复用
可以通过覆写Target#endpoint
计算属性来实现 Moya 的Provider#endpointClosure
的功能。
Target
提供API端点的原始定义,转换成结构化的Endpoint
,用于进一步修饰(如附加 Token):
Target
- Target#endpoint
计算属性 -> Endpoint
对Endpoint
进行修饰,经过中间件后交给Backend
,同步返回用于取消请求的令牌Cancellable
:
Enpoint
- Provider#prepareForEndpoint
- middlewares
- Backend
-> Cancellable
更改为 Middleware
基于泛型的Provider
搭配枚举时确实方便,因为编辑器和编译器可以进行类型推断,但是在声明Targets
的时候,就未必方便了,考察 官方示例 ,由于属性需要使用对自身进行枚举(switch self {}
语法)三个端点的声明中包含了大量的冗余,尤其当端点数量增多时,代码维护的难度会增大。
此外,枚举的case
的签名要求数据类型和顺序强一致性,并且不允许默认值,这对于复杂端点(如字段可选值、参数存在互斥的情况或者复杂的数据类型)而言,并不是最佳的表达方式。从理论上讲,Provider
接受的是实现Target
协议的值、对象,但是Provider
由于泛型的缘故会与该类型绑定,导致非枚举的情况下Provider
无法被复用,即使使用枚举类型,由于代码组织的需要,拆分成多个枚举后,Provider
也是无法复用的,这不合理,再需要更上层封装时(例如需要将Provider
和其他组件组合),也会增加复杂度。
最后,经过试验可知,拆除Provider
的泛型约束并不会破坏其功能。
请看 Provider#request 实现代码,在生产环境中,是否真实发送请求是通过StubClosure
来确定的,更多的代码意味着更多的潜在错误。
上一条“用于测试的逻辑和用于生产的杂糅在一起”已经暗示了Provider
包含了测试和生产两方面职责,然而这还不完全。
当发送真实请求时的数据流(同步)为:
TargetType
- endpointClosure
-> Endpoint<T>
- requestClosure
-> NSURLRequest
- plugins
-> Alamofire's Request
-> Cancellable
测试用途的省略。
两套流程揉在了统一个方法中,单看某一条流程虽然合理,但在代码在表达上非常不直观。
由于Provider
负责一切工作,并且不可变,导致,需要针对特殊场景定制的时候(包括测试需要设置特定的返回),无法操作,只能生成新的Provider
实例,并且继承Provider
来做扩展也是难度极大的。
MIT license.