Moya 是 iOS 社区中备受推崇的网络抽象库。它之所以出色,并非仅仅因为简化了网络请求的发送,更在于其背后蕴含的一整套优雅、可扩展的架构设计哲学。本文将和你一起深入剖析 Moya 的核心架构,并结合当下的主流技术(如 Combine),展示如何在真实项目中构建一个健壮、可维护、面向未来的网络层。
第一部分:Moya 的宏观架构设计------职责分离的艺术
一个网络请求的完整生命周期,从意图的产生到数据的解析,涉及多个环节。Moya 的高明之处在于,它通过一系列精心设计的组件,将这个复杂的流程解耦为清晰、独立的步骤。
TargetType
(意图描述) → Endpoint
(具体规划) → URLRequest
(执行蓝图) → Plugin
(流程增强) → Response
(最终结果)
-
TargetType
:API 的抽象契约 这是开发者与 Moya 交互的起点。TargetType
协议要求我们以一种结构化的方式描述一个 API 端点,而非立即去实现它。这种"描述优于执行"的理念是解耦的第一步,它让 API 的定义与其后续的发送、认证、日志等环节彻底分离。 -
Endpoint
:从"意图"到"规划"的转换MoyaProvider
在接收到一个TargetType
后,会将其映射(Map)成一个Endpoint
对象。Endpoint
是一个包含了发起请求所需全部具体信息 的中间产物。这个映射过程可以通过endpointClosure
进行全局定制,是植入全局参数或修改请求行为的第一个重要钩子。 -
URLRequest
:可执行的蓝图Endpoint
随后被转换成底层的URLRequest
对象,这是网络会话能够理解和执行的最终指令。 -
PluginType
:非侵入式的流程增强器 这是 Moya 架构的灵魂。在请求的生命周期的关键节点(如准备发送前、收到响应后),MoyaProvider
会调用已注册的插件。这使得我们能以一种非侵入、可组合 的方式,为网络请求流程添加认证、日志、缓存、UI指示器等横切关注点 (Cross-Cutting Concerns)。我们将在第三部分详细探讨其强大威力。 -
Response
:标准化的结果 无论请求成功或失败,Moya 都会返回一个标准的Response
或MoyaError
对象,为上层提供了统一的处理接口。
第二部分:现代 API 层设计------模块化与 Combine + Codable
在复杂的 App 中,API 数量庞大。如何组织它们,并优雅地处理数据解析,是构建高质量网络层的核心挑战。
1. 使用模块化枚举划分业务 (DataService
)
当 App 包含多个功能模块时(如认证 Auth
、个人资料 Profile
、新闻 News
等),将所有 API 端点混在一个巨大的枚举中会变得难以维护。更佳的实践是利用 Swift 的枚举嵌套和协议扩展,实现模块化的 API 定义。
步骤 1:定义统一的 API 服务入口 DataService
这个顶层枚举本身不定义任何 API,只作为各个业务模块的命名空间。
swift
import Moya
// 顶层 API 服务枚举,作为所有模块的入口
enum DataService {
case profile(Profile)
case news(News)
}
步骤 2:为每个模块创建独立的协议,并让 DataService
的嵌套类型实现 这种方式利用了 Swift 的类型系统,使得 API 调用既有组织性,又富于表达。
swift
// --- Profile 模块的 API 定义 ---
extension DataService.profile {
enum Profile {
case fetchUserProfile(userId: String)
case updateUserAvatar(image: UIImage)
}
}
extension DataService.Profile {
var serviceName: String { "profile" }
var apiVersion: String { "1" }
//....
}
extension DataService.Profile {
// 实现 TargetType 的要求
// ...
}
调用方式: 这种结构使得 API 调用非常清晰,能够一眼看出所属模块。
swift
let provider = MoyaProvider<DataService.Profile>()
provider.request(.fetchUserProfile(userId: "123")) { /* ... */ }
let newsProvider = MoyaProvider<DataService.News>()
newsProvider.request(.fetchHeadlines(page: 1)) { /* ... */ }
2. 结合 Combine 和 Codable 实现模型解析
Combine 框架是 Apple 官方的响应式编程解决方案,与 Moya 的结合能极大地简化异步数据流的处理。
步骤 1:定义泛型响应包装器 假设后端响应结构统一:{ "code": 200, "message": "Success", "data": { ... } }
swift
struct APIResponse<T: Decodable>: Decodable {
let code: Int
let message: String
let data: T?
}
struct EmptyData: Decodable {} // 用于处理 data 为 null 的情况
步骤 2:扩展 MoyaProvider 以支持 Combine 和泛型解析 我们为 MoyaProvider
创建一个返回 AnyPublisher
的便利方法。
swift
import Combine
// 自定义错误类型,用于封装业务错误
enum NetworkError: Error, LocalizedError {
case serverError(code: Int, message: String)
case noData
case decodingError(Error)
case underlying(Error)
var errorDescription: String? {
switch self {
case .serverError(_, let message): return message
case .noData: return "No data received from server."
// ... 其他描述
}
}
}
extension MoyaProvider {
func request<T: Decodable>(_ target: Target, modelType: T.Type) -> AnyPublisher<T, NetworkError> {
return self.requestPublisher(target)
.mapError { NetworkError.underlying($0) } // 转换 MoyaError
.flatMap { response -> AnyPublisher<T, NetworkError> in
do {
// 第一步:尝试将整个响应解码为 APIResponse<T>
let apiResponse = try JSONDecoder().decode(APIResponse<T>.self, from: response.data)
// 第二步:检查业务码
guard apiResponse.code == 200 else {
return Fail(error: .serverError(code: apiResponse.code, message: apiResponse.message))
.eraseToAnyPublisher()
}
// 第三步:提取 data
if let data = apiResponse.data {
return Just(data)
.setFailureType(to: NetworkError.self)
.eraseToAnyPublisher()
} else if T.self == EmptyData.self {
// 如果期望的是 EmptyData,则返回一个实例
return Just(EmptyData() as! T)
.setFailureType(to: NetworkError.self)
.eraseToAnyPublisher()
} else {
// 业务成功但 data 为空,而我们期望具体模型
return Fail(error: .noData).eraseToAnyPublisher()
}
} catch {
// 解码失败
return Fail(error: .decodingError(error)).eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
}
业务代码调用: ViewModel 或 Service 层中的调用变得极其线性化和声明式。
swift
var cancellables = Set<AnyCancellable>()
let profileProvider = MoyaProvider<DataService.Profile>()
func fetchUser() {
profileProvider.request(.fetchUserProfile(userId: "123"), modelType: UserProfile.self)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Error: \(error.localizedDescription)")
}
}, receiveValue: { (user: UserProfile) in
self.userName = user.name
})
.store(in: &cancellables)
}
第三部分:插件机制实战------无感知处理 401 与 Token 刷新
一个常见的业务场景:当 Access Token 过期导致 API 返回 401 时,网络层能自动、透明地刷新 Token 并重试原始请求,业务调用方对此毫无察觉。
这需要 Moya 的 PluginType
和底层 Alamofire
的 RequestInterceptor
协同工作。
步骤 1:创建 AccessTokenPlugin
此插件负责为需要认证的请求附加 Authorization
头。
swift
// 定义一个协议,让 Target 自行声明是否需要认证
protocol AccessTokenAuthorizable {
var authorizationType: AuthorizationType { get }
}
enum AuthorizationType {
case bearer
case none
}
// 让我们的 API 定义遵循此协议
extension DataService.Profile: AccessTokenAuthorizable {
var authorizationType: AuthorizationType {
switch self {
case .fetchUserProfile, .updateUserAvatar: return .bearer // 需要认证
}
}
}
// ... 为其他模块也实现
class AccessTokenPlugin: PluginType {
private let tokenProvider: () -> String?
init(tokenProvider: @escaping () -> String?) {
self.tokenProvider = tokenProvider
}
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
guard let authorizable = target as? AccessTokenAuthorizable,
authorizable.authorizationType == .bearer else {
return request
}
var request = request
if let token = tokenProvider() {
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
return request
}
}
步骤 2:创建 TokenAuthenticator
作为 RequestInterceptor
这是处理 401 的核心。它会拦截失败的请求,如果是 401 错误,则触发 Token 刷新流程。
swift
import Alamofire
class TokenAuthenticator: RequestInterceptor {
private let lock = NSLock()
private var isRefreshing = false
private var requestsToRetry: [(RetryResult) -> Void] = []
// 依赖注入一个负责刷新 Token 的服务
private let tokenRefreshService: TokenRefreshService
init(tokenRefreshService: TokenRefreshService) {
self.tokenRefreshService = tokenRefreshService
}
// 拦截请求失败的情况
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
lock.lock()
defer { lock.unlock() }
// 只处理 401 错误,且确保不是刷新 Token 请求本身失败
guard let response = request.response, response.statusCode == 401,
!request.request?.url?.absoluteString.contains("/auth/refresh") ?? false else {
completion(.doNotRetryWithError(error))
return
}
requestsToRetry.append(completion)
if !isRefreshing {
isRefreshing = true
Task { // 使用 Swift Concurrency
let success = await tokenRefreshService.refreshToken()
lock.lock()
defer { lock.unlock() }
isRefreshing = false
// 根据刷新结果,决定是重试还是彻底失败
let result: RetryResult = success ? .retry : .doNotRetryWithError(error)
requestsToRetry.forEach { $0(result) }
requestsToRetry.removeAll()
}
}
}
}
// 刷新 Token 的服务示例
@globalActor actor TokenStorage {
static let shared = TokenStorage()
var accessToken: String?
var refreshToken: String?
}
class TokenRefreshService {
// 刷新 Token 的请求本身也用 Moya 发出,但它不能触发重试逻辑
private let refreshProvider = MoyaProvider<DataService.Auth>()
func refreshToken() async -> Bool {
guard let refreshToken = await TokenStorage.shared.refreshToken else { return false }
// 调用刷新接口
// let result = await refreshProvider.request(.refreshToken(token: refreshToken))
// ...
// 伪代码:
// if success {
// await TokenStorage.shared.update(accessToken: newAccessToken, refreshToken: newRefreshToken)
// return true
// } else {
// // 刷新失败,可能需要清除本地 Token 并引导用户重新登录
// return false
// }
return true // 假设成功
}
}
关键点 :TokenAuthenticator
中的 lock
和 isRefreshing
状态完美地解决了刷新风暴 问题。当多个 API 请求在短时间内同时因 Token 过期而失败时,此机制能确保只发起一次 Token 刷新,所有失败的请求都会排队等待,刷新成功后一起重试。
步骤 3:组装最终的 Provider 在 App 的依赖注入容器或服务定位器中,将所有部件组装起来。
swift
// 1. 创建 Token 刷新服务
let tokenRefreshService = TokenRefreshService()
// 2. 创建配置了拦截器的 Alamofire Session
let session = Session(interceptor: TokenAuthenticator(tokenRefreshService: tokenRefreshService))
// 3. 创建 AccessTokenPlugin
let tokenPlugin = AccessTokenPlugin {
// 异步获取 token
var token: String?
let semaphore = DispatchSemaphore(value: 0)
Task {
token = await TokenStorage.shared.accessToken
semaphore.signal()
}
semaphore.wait()
return token
}
// 4. 创建最终的 MoyaProvider 实例
// 例如,为 Profile 模块创建一个 Provider
let profileProvider = MoyaProvider<DataService.Profile>(session: session, plugins: [tokenPlugin])
结论
通过本文的层层递进,我们完成了一套现代、健壮的 iOS 网络层架构:
- 宏观上 ,我们理解并遵循了 Moya 职责分离的核心设计思想。
- 在 API 定义上 ,我们采用模块化枚举的方式,使项目结构清晰、易于扩展。
- 在异步处理上 ,我们结合 Combine 和 Codable,以声明式、类型安全的方式处理数据流和解析。
- 在核心痛点上 ,我们利用 Moya 插件架构 的精髓,结合 Alamofire 的拦截器,实现了一个对业务层完全透明的自动化 Token 刷新机制。
这套架构不仅解决了眼前的技术难题,更重要的是,它为 App 未来的功能迭代和维护工作打下了坚实的基础。这正是优秀架构设计的价值所在------驾驭复杂性,拥抱变化。下一篇中,我们将充分学习使用 Moya 中的插件式架构,解决实际的业务场景。