精选文章

13. 网络实战

2019-07-22 · 网络

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)

网络层做到这一套,剩下就是按业务扩展:日志上报、灰度开关、请求合并、批量接口。

JJ

作者简介

专注于内容创作、产品策略与设计实践。欢迎交流合作。

上一篇 下一篇