我从来不是个天赋型的程序员,我只是站在巨人的脚下,仰望着巨人. 这篇文章,是以解读wildog的网络层封装后进行的描述 --致敬我心目中的大神, wildog.
Moya+Alamofire是现阶段大部分 Swift 项目所喜欢使用的网络层框架,其简洁明了的协议式接口设计,非常让人上头. 但是项目中,一般都会基于这个框架再进行二次封装,以适用于公司业务.本篇文章就是讲解下我司所封装的框架(大部分一致,但有部分是自己的修改)
目的
先说说网络层封装的最终目的,我们希望我们封装的请求框架,调用简单方便,封装简洁清晰易读,易拖展,本身已经具备了基础的加密,debug 打印,业务错误码处理等等功能. 以此为目的,一步步分析下如何封装.
步骤
最基础接入:
import Foundation
import Moya
enum MineAPI: TargetType {
case user
}
extension MineAPI {
var baseURL: URL {
URL(string: "http://www.baidu.com" + "/appName")!
}
var path: String {
switch self {
case .user:
return "/user/info"
}
}
var method: Moya.Method {
switch self {
case .user:
return .post
}
}
var sampleData: Data {
"".data(using: .utf8)!
}
var task: Task {
switch self {
case .user:
return .requestPlain
}
}
var headers: [String : String]? {
nil
}
}
这种调用弊端很大,我们一般会去做二次的封装,这里讲解下我司封装的网络层框架(我单独把公司框架网络层提取出来,自己做了一点修改).
流程走,先封装 TargetType
这里对 targetType 进行拖展.我们不希望对外暴露 Moya 接口,所有关于 moya 的结构,都进行了二次封装.
protocol APIService: TargetType {
/// The target's base `URL`.
var baseURL: URL { get }
/// The path to be appended to `baseURL` to form the full `URL`.
var path: String { get }
/// The HTTP method used in the request.
var method: Moya.Method { get }
/// Provides stub data for use in testing.
var sampleData: Data { get }
/// The type of HTTP task to be performed.
var task: Task { get }
/// The type of validation to perform on the request. Default is `.none`.
var validationType: ValidationType { get }
/// The headers to be used in the request.
var headers: [String: String]? { get }
var parameters: APIParameters? { get }
/// API 路由和 HTTP 方法,不包含域名、服务名和版本号,
///
/// 如一个 GET API 完整地址为 http://xx.com/message/v1/group/create
/// 这里返回
/// ```
/// .get("/group/create")
/// ```
var route: APIRoute { get }
}
extension APIService {
var url: URL {
path.isEmpty ? baseURL : baseURL.appendingPathComponent(path)
}
var baseURL: URL {
URL(string: Env.current.constants.baseUrl + servicePath)!
}
var servicePath: String {
""
}
var headers: [String: String]? {
[
"Accept": "*/*"
// "Content-Type": "application/x-www-form-urlencoded; application/json; text/plain"
]
}
var sampleData: Data {
fatalError("sampleData has not been implemented")
}
var task: Task {
guard let params = self.parameters?.values else {
return .requestPlain
}
let defaultEncoding: ParameterEncoding = self.method == .get ? URLEncoding.queryString : APIParamEncoding.default
return .requestParameters(parameters: params, encoding: self.parameters?.encoding ?? defaultEncoding)
}
var route: APIRoute {
fatalError("route has not been implemented")
}
var path: String {
NSString.path(withComponents: [self.route.path])
}
var method: Moya.Method {
route.method
}
var parameters: APIParameters? {
nil
}
var identifier: String {
route.method.rawValue + url.absoluteString
}
func makeHeaders() -> [String: String]? {
var headers = self.headers ?? ["Accept": "application/json;application/x-www-form-urlencoded"]
// if method == .get || method == .head || method == .delete {
// headers["Content-Type"] = contentType ?? "application/json"
// } else {
headers["Content-Type"] = "application/x-www-form-urlencoded"
// }
return headers
// return headers
}
}
/// `APIProviderSharing` 为所有的 `APIService` 提供了一个
/// `APIProvider` 的单例用于执行请求和管理内部状态
protocol APIProviderSharing where Self: APIService {
static var shared: APIProvider<Self> { get }
// func make(_ duringOfObject: AnyObject?, behaviors: Set<APIRequestBehavior>?, hotPlugins: [APIPlugin]) -> SignalProducer<APIResult, APIError>
}
APIRoute 是对 method 的二次封装,顺便把 path 也封装进去
public enum APIRoute {
case get(String)
case post(String)
case put(String)
case delete(String)
case options(String)
case head(String)
case patch(String)
case trace(String)
case connect(String)
public var path: String {
switch self {
case .get(let path): return path
case .post(let path): return path
case .put(let path): return path
case .delete(let path): return path
case .options(let path): return path
case .head(let path): return path
case .patch(let path): return path
case .trace(let path): return path
case .connect(let path): return path
}
}
public var method: Moya.Method {
switch self {
case .get: return .get
case .post: return .post
case .put: return .put
case .delete: return .delete
case .options: return .options
case .head: return .head
case .patch: return .patch
case .trace: return .trace
case .connect: return .connect
}
}
}
Env 是环境配置,属于公司业务范畴,这里不作展示.
Provider封装
我司使用的响应式框架为 ReactiveSwift
Moya 对此的拖展函数为
func request(_ token: Base.Target, callbackQueue: DispatchQueue? = nil) -> SignalProducer<Response, MoyaError> {
return SignalProducer { [weak base] observer, lifetime in
let cancellableToken = base?.request(token, callbackQueue: callbackQueue, progress: nil) { result in
switch result {
case let .success(response):
observer.send(value: response)
observer.sendCompleted()
case let .failure(error):
observer.send(error: error)
}
}
lifetime.observeEnded {
cancellableToken?.cancel()
}
}
}
在此基础上,我们进行封装
func make(_ target: Target, behaviors: Set<APIRequestBehavior>? = nil, hotPlugins: [APIPlugin] = [], within: AnyObject? = nil) -> SignalProducer<APIResult, APIError> {
let pluginApplied = apiPlugins + hotPlugins
// 拿到初始的moya 请求signalprducer
let originalSignalProducer = self.reactive.request(target)
// 拿到初始的moya response, 并转成 apiresult
let responseMapped = originalSignalProducer.observe(on: QueueScheduler()).map { response -> Moya.Response in
let decodeResponse = Moya.Response(statusCode: response.statusCode, data: response.data, request: response.request, response: response.response)
return decodeResponse
}.map(APIResult.self).observe(on: QueueScheduler.main)
// 错误匹配
let apiErrorMapped = responseMapped.mapError { APIError(from: $0) }
// 尝试去验证业务错误
let validateMapped = apiErrorMapped.attempt { result in
let errors = pluginApplied.compactMap { $0.validate(result, behaviors: behaviors) }
if errors.count > 0 {
return .failure(errors.first!)
} else {
return .success(())
}
}
// 插件执行生命周期
var lifetimeObsered = validateMapped.on(started: {
pluginApplied.forEach { $0.didStart(target: target, behaviors: behaviors)}
}, failed: { error in
pluginApplied.forEach { $0.didEnd(target: target, behaviors: behaviors, result: nil, error: error) }
}, interrupted: {
pluginApplied.forEach { $0.didEnd(target: target, behaviors: behaviors, result: nil, error: nil)}
}, value: { response in
pluginApplied.forEach { $0.didEnd(target: target, behaviors: behaviors, result: response, error: nil)}
})
// 生命周期监听
if let obj = within {
lifetimeObsered = lifetimeObsered.take(duringLifetimeOf: obj)
}
return lifetimeObsered
}
我做了一些业务筛减,保留存储的请求处理,说明都在code 里,这里有几个参数定义
target: Target
操作目标,用于生成节点,节点最后会转化成 request
behaviors: Set<APIRequestBehavior>? = nil
定义本次请求的行为,如不展示错误弹出,忽略警告报错,隐藏请求活动图标,自定义超时时间等等
hotPlugins: [APIPlugin]
本次请求额外的插件(预留插件)
behavior 属于功能比较小的插件,概念不用,本质和 plugin 差不多,都是对请求行为的补充
在 plugins 基础上,我们定义了一个新的概念,APIPlugin,并且生命周期由rac 控制,其实对 PluginType 做拖展也能做到(选择自己喜欢的即可)
插件
Moya 初始化函数为:
/// Initializes a provider.
public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
stubClosure: @escaping StubClosure = MoyaProvider.neverStub,
callbackQueue: DispatchQueue? = nil,
session: Session = MoyaProvider<Target>.defaultAlamofireSession(),
plugins: [PluginType] = [],
trackInflights: Bool = false) {
self.endpointClosure = endpointClosure
self.requestClosure = requestClosure
self.stubClosure = stubClosure
self.session = session
self.plugins = plugins
self.trackInflights = trackInflights
self.callbackQueue = callbackQueue
}
moya 接收一个[PluginType]
的插件数组初始化,并提供了基础的 log 插件NetworkLoggerPlugin
PluginType
生命周期函数为
public protocol PluginType {
/// 配置 request
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
/// 请求发送前
func willSend(_ request: RequestType, target: TargetType)
/// 接受到响应
func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)
}
核心调用顺序位置为, moya 实现了alamofire 的RequestInterceptor
func adapt(_ urlRequest: URLRequest, for session: Alamofire.Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
// prepare
let request = prepare?(urlRequest) ?? urlRequest
// willSend
willSend?(request)
completion(.success(request))
}
加密
数据加密请求,请求头的通用参数,我们可以通过插件形式实现
自定一个插件,实现
final class RequestTransformation: PluginType {
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
//业务加密代码,aes,rsa,md5,base64,爱咋搞咋搞
}
}
用插件实现,结构会非常的清晰
log
自定义一个 log 插件
public final class NetworkLoggerPlugin {
public var configuration: Configuration
/// Initializes a NetworkLoggerPlugin.
public init(configuration: Configuration = Configuration()) {
self.configuration = configuration
}
}
注意一点,Moya 框架自带了一个NetworkLoggerPlugin
,如果不想自定义的话,可以使用它,但是注意它接受一个 Configuration参数
public final class NetworkLoggerPlugin {
public var configuration: Configuration
/// Initializes a NetworkLoggerPlugin.
public init(configuration: Configuration = Configuration()) {
self.configuration = configuration
}
}
这个参数里面有个Formatter,记得将 data->string, 使用.prettyPrinted,这样打印出来的结果会好看点,我司使用的是自定义,为了区分打印 globalParam, 业务param等等,不过实现原理和 Moya 自带的差不多
static func ResponseLoggingDataFormatter(_ data: Data) -> Data {
do {
let dataAsJSON = try JSONSerialization.jsonObject(with: data)
let prettyData = try JSONSerialization.data(withJSONObject: dataAsJSON, options: .prettyPrinted)
return prettyData
} catch {
return data
}
}
业务错误码
从此插件开始,后续均为 APIPlugin
class APIResponseValidation: APIPlugin {
static let shared = APIResponseValidation()
private init() {}
func validate(_ result: APIResult, behaviors: Set<APIRequestBehavior>?) -> APIError? {
APIError(from: result)
}
}
核心是,在 APIProvider 中,我们在执行插件 didEnd之前
// 尝试去验证业务错误
let validateMapped = apiErrorMapped.attempt { result in
let errors = pluginApplied.compactMap { $0.validate(result, behaviors: behaviors) }
if errors.count > 0 {
return .failure(errors.first!)
} else {
return .success(())
}
}
APIResponseValidation去 validate APIResult 的业务,如果业务上有特殊需要,可以对特殊的 code,进行错误抛出,比如业务上code: 8888,尽管状态码200,但是我们仍然认为是不成功的一次请求,走的是failed,从而走插件 的 didEnd业务处理(Toast 啥的),而不会进入 success
Toast插件
final class ToastErrorHandler: APIPlugin {
static let shared = ToastErrorHandler()
private init() {}
func didEnd(target: TargetType, behaviors: Set<APIRequestBehavior>?, result: APIResult?, error: APIError?) {
guard let error = error else { return }
var shouldEmitToast = true
var shouldBalance = true
if let behaviors = behaviors {
for behavior in behaviors {
if case let .suppressMessage(codes) = behavior {
if let codes = codes {
if codes.contains(error.errorCode) {
shouldEmitToast = false
}
if codes.contains(APIError.balanceCode) {
shouldBalance = false
}
} else {
shouldEmitToast = false
}
break
}
}
}
if case let .balance(message) = error, shouldBalance {
log("\(message ?? "")")
} else if case let .notVIP(message) = error {
log("\(message ?? "")")
} else {
if error.errorCode == -6 { // AFN特有的-6网络请求失败
return
}
if shouldEmitToast {
DispatchQueue.main.async {
print("\(error.description)")
}
}
}
}
}
toast 我简单处理了下,根据自己的业务处理弹窗即可
提到 toast,这里再埋一个坑,toast 大家很常用,但是 toast 封装也很重要,如果有时间,我会抽出我司封装的 toast 组件,非常非常 nice!
稍微透露下
enum Behavior {
case replaceAll // 全部消失,清空队列(当前显示的和队列中的所有的spinner会被保留并延后)
case replaceCurrent // 只消失当前显示(当前显示的spinnner会被保留并延后),不清空队列
case queue(_ priority: Operation.QueuePriority = .normal) // 加入队列
}
ps:太久没写 blog,感觉写起来好麻烦,写着写着就不想写了,最近事情确实忙,忙了接近一年,没时间也没精力去写这种总结性的文章,虽然知道写起来对我自己有帮助,但是确实太忙了,唉~这篇文章写的也很水,本来想把框架里的每个文件都介绍一下,写着写着,就懒得写了,希望后续有所改善吧.