13. 网络实战
稳定的网络层必须具备:请求构建统一、错误可追踪、解析可复用、重试可控。下面按组件拆开实现。
一、定义请求模型
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
struct Endpoint {
let path: String
let method: HTTPMethod
let query: [String: String]
let headers: [String: String]
let body: Data?
init(path: String,
method: HTTPMethod = .get,
query: [String: String] = [:],
headers: [String: String] = [:],
body: Data? = nil) {
self.path = path
self.method = method
self.query = query
self.headers = headers
self.body = body
}
}
二、构建 URLRequest
struct RequestBuilder {
let baseURL: URL
func build(_ endpoint: Endpoint) throws -> URLRequest {
var components = URLComponents(url: baseURL.appendingPathComponent(endpoint.path), resolvingAgainstBaseURL: false)
if !endpoint.query.isEmpty {
components?.queryItems = endpoint.query.map { URLQueryItem(name: $0.key, value: $0.value) }
}
guard let url = components?.url else { throw URLError(.badURL) }
var request = URLRequest(url: url)
request.httpMethod = endpoint.method.rawValue
request.httpBody = endpoint.body
endpoint.headers.forEach { request.addValue($0.value, forHTTPHeaderField: $0.key) }
request.timeoutInterval = 15
request.cachePolicy = .useProtocolCachePolicy
return request
}
}
三、统一响应与错误
enum APIError: Error {
case invalidResponse
case httpStatus(Int)
case decoding(Error)
case network(Error)
}
struct APIResponse<T: Decodable>: Decodable {
let code: Int
let message: String
let data: T
}
四、APIClient(async/await 版本)
final class APIClient {
private let session: URLSession
private let builder: RequestBuilder
init(baseURL: URL, session: URLSession = .shared) {
self.session = session
self.builder = RequestBuilder(baseURL: baseURL)
}
func request<T: Decodable>(_ endpoint: Endpoint, type: T.Type) async throws -> T {
let request = try builder.build(endpoint)
do {
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse else { throw APIError.invalidResponse }
guard (200..<300).contains(http.statusCode) else { throw APIError.httpStatus(http.statusCode) }
do {
let decoded = try JSONDecoder().decode(T.self, from: data)
return decoded
} catch {
throw APIError.decoding(error)
}
} catch {
throw APIError.network(error)
}
}
}
五、APIClient(回调版本)
extension APIClient {
func request<T: Decodable>(_ endpoint: Endpoint, type: T.Type, completion: @escaping (Result<T, APIError>) -> Void) {
let request: URLRequest
do { request = try builder.build(endpoint) }
catch { completion(.failure(.network(error))); return }
session.dataTask(with: request) { data, response, error in
if let error {
completion(.failure(.network(error)))
return
}
guard let http = response as? HTTPURLResponse, let data else {
completion(.failure(.invalidResponse))
return
}
guard (200..<300).contains(http.statusCode) else {
completion(.failure(.httpStatus(http.statusCode)))
return
}
do {
let decoded = try JSONDecoder().decode(T.self, from: data)
completion(.success(decoded))
} catch {
completion(.failure(.decoding(error)))
}
}.resume()
}
}
六、统一拦截器(鉴权 / 日志)
protocol RequestInterceptor {
func adapt(_ request: URLRequest) -> URLRequest
}
struct AuthInterceptor: RequestInterceptor {
let tokenProvider: () -> String?
func adapt(_ request: URLRequest) -> URLRequest {
var req = request
if let token = tokenProvider() {
req.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
return req
}
}
final class InterceptedClient {
private let client: APIClient
private let interceptors: [RequestInterceptor]
init(client: APIClient, interceptors: [RequestInterceptor]) {
self.client = client
self.interceptors = interceptors
}
func request<T: Decodable>(_ endpoint: Endpoint, type: T.Type) async throws -> T {
let request = try client.builder.build(endpoint)
let adapted = interceptors.reduce(request) { $1.adapt($0) }
let (data, response) = try await client.session.data(for: adapted)
guard let http = response as? HTTPURLResponse else { throw APIError.invalidResponse }
guard (200..<300).contains(http.statusCode) else { throw APIError.httpStatus(http.statusCode) }
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw APIError.decoding(error)
}
}
}
七、重试策略
struct RetryPolicy {
let maxRetry: Int
let delay: TimeInterval
func shouldRetry(_ error: APIError) -> Bool {
switch error {
case .network, .httpStatus(500), .httpStatus(502), .httpStatus(503):
return true
default:
return false
}
}
}
func requestWithRetry<T: Decodable>(
client: APIClient,
endpoint: Endpoint,
type: T.Type,
policy: RetryPolicy
) async throws -> T {
var attempt = 0
while true {
do {
return try await client.request(endpoint, type: T.self)
} catch let error as APIError {
attempt += 1
if attempt > policy.maxRetry || !policy.shouldRetry(error) { throw error }
try await Task.sleep(nanoseconds: UInt64(policy.delay * 1_000_000_000))
}
}
}
八、分页与去重
struct Page<T: Decodable>: Decodable {
let list: [T]
let hasMore: Bool
let nextCursor: String?
}
final class PagingStore<T: Hashable> {
private var set = Set<T>()
private(set) var items: [T] = []
func append(_ newItems: [T]) {
for item in newItems where !set.contains(item) {
set.insert(item)
items.append(item)
}
}
}
九、缓存策略
let cache = URLCache(memoryCapacity: 20 * 1024 * 1024,
diskCapacity: 100 * 1024 * 1024,
diskPath: "api-cache")
let configuration = URLSessionConfiguration.default
configuration.urlCache = cache
configuration.requestCachePolicy = .returnCacheDataElseLoad
let session = URLSession(configuration: configuration)
网络层做到这一套,剩下就是按业务扩展:日志上报、灰度开关、请求合并、批量接口。