iOS 网络层分层与错误映射
iOS 网络层分层与错误映射
网络层不仅负责发出请求,还承担参数编码、响应解析、身份处理、错误分类和取消传播等职责。如果页面直接操作底层请求对象,协议变化会扩散到整个应用,测试也只能依赖真实环境。通过分层和稳定的错误模型,可以让业务代码只关注“请求什么”和“结果意味着什么”。
一、划分网络职责
一个可维护的网络链路通常包含:
- 请求描述:方法、参数、身份要求和响应类型。
- 传输层:执行请求,返回原始数据和响应信息。
- 解析层:把数据转换为协议模型。
- 仓库层:组合接口与本地数据,映射为业务结果。
- 页面模型:把业务结果转换为界面状态。
每层只暴露上层真正需要的信息。页面不需要知道状态码细节,传输层也不应该决定提示文案。
二、用类型描述请求
protocol Endpoint {
associatedtype Response: Decodable
var method: RequestMethod { get }
var parameters: [String: String] { get }
var requiresAuthentication: Bool { get }
}
struct OrderListEndpoint: Endpoint {
typealias Response = OrderListResponse
let page: Int
let pageSize: Int
var method: RequestMethod { .get }
var parameters: [String: String] {
[
"page": String(page),
"pageSize": String(pageSize)
]
}
var requiresAuthentication: Bool { true }
}
具体请求位置可以由项目统一配置和路由表管理,避免在业务页面散落字符串。示例只表达参数和响应类型,不重复实现已有网络基础设施。
三、保持传输接口稳定
protocol NetworkClient {
func send<E: Endpoint>(_ endpoint: E) async throws -> E.Response
}
调用方只关心解码后的响应。具体会话配置、请求构造和底层回调桥接都封装在实现中。测试时可以替换为可控客户端,而不需要真正发起通信。
四、区分不同错误层级
错误至少可以分为:
- 传输错误:连接中断、超时、任务取消。
- 协议错误:响应格式或状态不符合约定。
- 解析错误:字段类型与模型不匹配。
- 业务错误:未登录、余额不足、操作被拒绝。
enum NetworkError: Error, Equatable {
case unavailable
case timeout
case invalidResponse
case decodingFailed
case cancelled
}
enum OrderError: Error, Equatable {
case loginRequired
case permissionDenied
case rejected(reason: String)
case temporarilyUnavailable
}
底层错误不应原样穿透到用户界面。仓库层根据协议中明确的错误码映射业务错误,不必为未约定字段添加猜测性兜底。
五、正确传播取消
页面离开或新搜索到来时,上层任务取消应传递到底层请求。捕获所有错误并统一映射,会把取消误判为网络失败。
func loadOrders() async throws -> [Order] {
do {
let response = try await client.send(
OrderListEndpoint(page: 1, pageSize: 20)
)
try Task.checkCancellation()
return response.items.map(orderMapper.toDomain)
} catch is CancellationError {
throw CancellationError()
} catch let error as NetworkError {
throw mapNetworkError(error)
}
}
底层桥接旧回调接口时,也要在任务取消后调用请求自身的取消方法,避免只是不接收结果但资源仍被占用。
六、统一解码策略
日期格式、键名转换和空值规则应在统一解码器中配置。各业务自行创建解码器容易造成同一个字段在不同模块表现不一致。
模型应忠实表达接口契约。字段被协议定义为必填时,就使用非可选类型,让解析失败尽早暴露;只有确实允许缺失或为空的字段才使用可选类型,不要把所有字段都做成可选来掩盖协议错误。
七、身份刷新与请求重放
身份过期时,可以由网络基础层协调一次刷新,等待中的请求共享同一个刷新任务。刷新成功后,只重放符合安全和幂等要求的请求。
提交订单、支付确认等操作不能无条件自动重放,否则可能产生重复业务结果。是否允许重试必须成为请求描述的一部分,并遵守服务端幂等约定。
刷新失败后,应统一清理身份状态并通知上层进入登录流程,避免多个页面同时弹出重复提示。
八、设计缓存边界
网络层可以处理协议级缓存,仓库层处理业务数据缓存。用户资料、实时行情、静态配置的有效期完全不同,不能使用一套统一时间作为万能策略。
缓存命中后是否仍刷新、失败时是否展示旧数据,都应由具体业务决定。旧数据如果可能误导用户,就不应为了“页面有内容”而盲目展示。
九、保护日志中的数据
调试日志不应完整输出身份信息、请求体和响应体。统一网络组件可以对允许记录的字段做白名单或脱敏处理,并在正式构建中遵循项目日志级别。
错误日志应保留定位所需的请求标识、业务码和阶段信息,但避免记录用户敏感数据。
十、测试网络层
使用客户端替身可以稳定覆盖:
- 正常响应与空列表。
- 传输中断和超时映射。
- 必填字段缺失时的解析失败。
- 业务拒绝码的映射。
- 页面取消后是否停止处理。
- 身份刷新并发时是否只执行一次。
- 不可重放请求是否被错误重试。
总结
清晰的 iOS 网络层应把请求描述、传输、解析和业务映射分开。使用强类型契约、保留取消语义、按业务决定缓存与重放,并集中保护日志数据,可以让接口变化和异常处理停留在合理边界内,而不是扩散到每个页面。
- 点赞
- 收藏
- 关注作者
评论(0)