用Apollo + RxSwift + RxCocoa搭建一套网络请求框架

要基于 iOS 平台的 Apollo GraphQL 客户端,结合 RxSwift/RxCocoa 封装一套高复用、易维护、贴合响应式编程思想的网络框架,核心需求是解耦 Apollo 原生回调与 Rx 的可观察序列统一异常处理封装通用请求层支持请求配置 / 取消 / 复用,同时遵循 iOS 开发的最佳实践(如模块化、单例管理、泛型约束)。

以下是一套完整的封装方案,从环境准备核心层封装业务层扩展使用示例进阶优化逐步实现,代码可直接在项目中落地,兼容最新的 Apollo iOS(1.0+)和 RxSwift 6.x+。

一、环境准备

首先在Podfile中引入核心依赖,确保版本兼容(Apollo 1.0+ 已适配 Swift 5.5+,RxSwift 6.x+ 是主流稳定版):

ruby

复制代码
pod 'Apollo', '~> 1.0'          # GraphQL核心客户端
pod 'Apollo/SQLite', '~> 1.0'   # 可选,本地缓存(如需要离线查询)
pod 'RxSwift', '~> 6.0'         # 响应式核心
pod 'RxCocoa', '~> 6.0'         # 结合UI的响应式扩展
pod 'RxRelay', '~> 6.0'         # 无误差的可观察序列(可选,用于状态管理)
pod 'Moya/ReactiveSwift', '~> 15.0' # 可选,若项目同时有RESTful请求可统一管理

执行pod install后,完成 Apollo 的基础配置(关键 :Apollo 需要生成 GraphQL 的 Model 代码,需在Build Phases添加脚本,参考Apollo 官方配置)。

二、核心设计思路

  1. 单例管理 ApolloClient:全局统一配置 GraphQL 的 BaseURL、请求头、缓存、超时等,避免多次初始化;
  2. Rx 封装 Apollo 请求 :将 Apollo 原生的completionHandler回调转换为Observable/Single(GraphQL 请求单次结果,优先用 Single);
  3. 统一异常处理:封装自定义网络错误枚举,覆盖 Apollo 原生错误、网络错误、解析错误、业务错误等;
  4. 泛型约束请求层 :支持所有 Apollo 生成的GraphQLQuery/GraphQLMutation(查询 / 修改操作),做到一次封装、全局复用;
  5. 支持请求配置:支持单个请求自定义头、超时、缓存策略,覆盖全局配置;
  6. 贴合 Rx 生命周期 :请求自动绑定到DisposeBag,支持手动取消,避免内存泄漏;
  7. 可选的请求拦截:支持请求前拦截(如添加 Token)、响应后拦截(如统一处理 Token 过期)。

三、完整框架封装

模块化 拆分文件,建议在项目中创建Network/GraphQL目录,包含以下核心文件:

plaintext

复制代码
GraphQL/
├── ApolloManager.swift       # ApolloClient单例管理、全局配置
├── ApolloRxExtension.swift   # Apollo请求的RxSwift扩展(核心)
├── GraphQLRequest.swift      # 通用请求封装、配置模型
├── GraphQLResult.swift       # 统一响应结果、自定义错误
└── GraphQLInterceptor.swift  # 可选,请求/响应拦截器
3.1 自定义错误与统一结果(GraphQLResult.swift)

封装全局通用的错误枚举,覆盖 Apollo 所有可能的错误类型,同时统一响应结果格式,让业务层无需处理原生 Apollo 结果:

swift

复制代码
import Foundation
import Apollo
import RxSwift

// MARK: - 自定义GraphQL网络错误
enum GraphQLRequestError: Error, LocalizedError {
    case invalidURL          // 无效BaseURL
    case noNetwork           // 无网络(可结合Reachability)
    case apolloError(ApolloError) // Apollo原生错误(请求、解析、服务端)
    case serverError(code: Int, message: String) // 服务端业务错误(如code≠200)
    case emptyResponse       // 响应体为空
    case tokenExpired        // Token过期(自定义业务)
    case customError(String) // 自定义其他错误
    
    // 本地化描述(方便调试/提示)
    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "无效的GraphQL请求地址"
        case .noNetwork:
            return "网络连接失败,请检查网络"
        case .apolloError(let error):
            return "Apollo请求错误:\(error.localizedDescription)"
        case .serverError(let code, let message):
            return "服务端错误[\(code)]:\(message)"
        case .emptyResponse:
            return "服务端返回空数据"
        case .tokenExpired:
            return "登录状态过期,请重新登录"
        case .customError(let msg):
            return msg
        }
    }
}

// MARK: - 统一GraphQL响应结果
/// 泛型封装,T为Apollo生成的GraphQL数据模型
struct GraphQLResult<T> {
    let data: T?               // 业务数据
    let response: HTTPURLResponse? // 原始响应头
    let extensions: [String: Any]? // 服务端扩展字段(如traceId、code)
}

// MARK: - 无数据响应(用于Mutation无返回值的场景)
struct EmptyGraphQLData: Codable {}
3.2 ApolloClient 单例管理(ApolloManager.swift)

全局唯一管理 ApolloClient,配置 BaseURL、请求头、缓存、超时,提供全局配置入口客户端获取方法,支持运行时动态修改请求头(如 Token 刷新后更新):

swift

复制代码
import Foundation
import Apollo
import Apollo/SQLite

// MARK: - Apollo全局管理单例
final class ApolloManager {
    // 单例(线程安全)
    static let shared = ApolloManager()
    private init() {}
    
    // 核心ApolloClient(懒加载)
    private lazy var apolloClient: ApolloClient = {
        guard let url = URL(string: baseURL) else {
            fatalError(GraphQLRequestError.invalidURL.errorDescription!)
        }
        // 1. 创建网络传输层(配置超时、请求头)
        let transport = RequestChainNetworkTransport(
            interceptorProvider: DefaultInterceptorProvider(client: self),
            endpointURL: url,
            requestBodyCreator: ApolloRequestBodyCreator(),
            timeout: globalTimeout
        )
        // 2. 可选:配置本地缓存(SQLite),支持离线查询
        let cache = SQLiteNormalizedCache(fileURL: cacheFileURL)
        let store = ApolloStore(cache: cache)
        // 3. 初始化ApolloClient
        let client = ApolloClient(networkTransport: transport, store: store)
        return client
    }()
    
    // MARK: - 全局配置项(可在AppDelegate/SceneDelegate中初始化)
    var baseURL: String = "" // GraphQL服务地址,如https://xxx.com/graphql
    var globalTimeout: TimeInterval = 30 // 全局超时时间
    var globalHeaders: [String: String] = [:] // 全局请求头,如Token、AppVersion
    /// 本地缓存文件路径(可选)
    private var cacheFileURL: URL {
        let documents = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
        return URL(fileURLWithPath: documents).appendingPathComponent("ApolloCache.sqlite")
    }
    
    // MARK: - 对外提供客户端(避免直接访问私有属性)
    func getClient() -> ApolloClient {
        return apolloClient
    }
    
    // MARK: - 动态更新全局请求头(如Token刷新后)
    func updateGlobalHeaders(_ headers: [String: String]) {
        globalHeaders.merge(headers) { _, new in new }
        // 重新初始化传输层,让新的请求头生效
        guard let url = URL(string: baseURL) else { return }
        let newTransport = RequestChainNetworkTransport(
            interceptorProvider: DefaultInterceptorProvider(client: self),
            endpointURL: url,
            requestBodyCreator: ApolloRequestBodyCreator(),
            timeout: globalTimeout
        )
        apolloClient.networkTransport = newTransport
    }
}

// MARK: - 自定义拦截器提供者(注入全局请求头)
extension ApolloManager: InterceptorProvider {
    func interceptors<Operation>(for operation: Operation) -> [ApolloInterceptor] where Operation : GraphQLOperation {
        var interceptors = DefaultInterceptorProvider().interceptors(for: operation)
        // 插入自定义请求头拦截器(在所有拦截器最前面)
        interceptors.insert(GlobalHeaderInterceptor(), at: 0)
        return interceptors
    }
}

// MARK: - 全局请求头拦截器(核心:将全局Headers注入请求)
final class GlobalHeaderInterceptor: ApolloInterceptor {
    func interceptAsync<Operation>(
        chain: RequestChain,
        request: HTTPRequest<Operation>,
        response: HTTPResponse<Operation>?,
        completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
    ) where Operation : GraphQLOperation {
        // 注入全局请求头
        let globalHeaders = ApolloManager.shared.globalHeaders
        globalHeaders.forEach { key, value in
            request.addHeader(value, for: key)
        }
        // 继续执行拦截器链
        chain.proceedAsync(request: request, response: response, completion: completion)
    }
}
3.3 Apollo 的 RxSwift 核心扩展(ApolloRxExtension.swift)

最核心的文件 :将 Apollo 原生的闭包回调 转换为 RxSwift 的Single可观察序列,实现响应式的请求封装,同时统一异常处理,让业务层直接订阅结果即可:

swift

复制代码
import Foundation
import Apollo
import RxSwift
import RxCocoa

// MARK: - ApolloClient的RxSwift扩展(支持Query/Mutation)
extension ApolloClient {
    /// 执行GraphQL查询(Query),返回Single可观察序列
    /// - Parameter query: Apollo生成的Query模型(遵守GraphQLQuery协议)
    /// - Parameter config: 单个请求的自定义配置(可选,覆盖全局)
    func rx_fetch<Query: GraphQLQuery>(
        query: Query,
        config: GraphQLRequestConfig? = nil
    ) -> Single<GraphQLResult<Query.Data>> {
        return Single.create { [weak self] single in
            guard let self = self else {
                single(.failure(GraphQLRequestError.customError("ApolloClient已释放")))
                return Disposables.create()
            }
            // 1. 构建请求选项(支持自定义缓存、队列)
            let cachePolicy = config?.cachePolicy ?? .returnCacheDataAndFetch
            let queue = DispatchQueue.global(qos: .default)
            // 2. 执行Apollo原生请求
            let cancellable = self.fetch(
                query: query,
                cachePolicy: cachePolicy,
                queue: queue,
                resultHandler: { [weak self] result in
                    self?.handleApolloResult(result, single: single)
                }
            )
            // 3. 返回取消器(Rx销毁时自动取消请求)
            return Disposables.create {
                cancellable.cancel()
            }
        }
        // 切换到主线程(方便业务层直接更新UI)
        .observe(on: MainScheduler.instance)
        // 统一超时处理(覆盖全局/自定义超时)
        .timeout(config?.timeout ?? ApolloManager.shared.globalTimeout, scheduler: MainScheduler.instance)
    }
    
    /// 执行GraphQL修改(Mutation),返回Single可观察序列
    /// - Parameter mutation: Apollo生成的Mutation模型(遵守GraphQLMutation协议)
    /// - Parameter config: 单个请求的自定义配置(可选,覆盖全局)
    func rx_perform<Mutation: GraphQLMutation>(
        mutation: Mutation,
        config: GraphQLRequestConfig? = nil
    ) -> Single<GraphQLResult<Mutation.Data>> {
        return Single.create { [weak self] single in
            guard let self = self else {
                single(.failure(GraphQLRequestError.customError("ApolloClient已释放")))
                return Disposables.create()
            }
            let queue = DispatchQueue.global(qos: .default)
            // 执行Apollo原生Mutation
            let cancellable = self.perform(
                mutation: mutation,
                queue: queue,
                resultHandler: { [weak self] result in
                    self?.handleApolloResult(result, single: single)
                }
            )
            // 返回取消器
            return Disposables.create {
                cancellable.cancel()
            }
        }
        .observe(on: MainScheduler.instance)
        .timeout(config?.timeout ?? ApolloManager.shared.globalTimeout, scheduler: MainScheduler.instance)
    }
    
    // MARK: - 统一处理Apollo原生结果,转换为自定义Result/Error
    private func handleApolloResult<Operation: GraphQLOperation>(
        _ result: Result<GraphQLResult<Operation.Data>, Error>,
        single: @escaping (SingleEvent<GraphQLResult<Operation.Data>>) -> Void
    ) {
        switch result {
        case .success(let apolloResult):
            // 1. 检查服务端返回的错误(Apollo的errors字段)
            if let graphQLErrors = apolloResult.errors, !graphQLErrors.isEmpty {
                let firstError = graphQLErrors.first!
                let code = firstError.extensions?["code"] as? Int ?? -1
                let message = firstError.message ?? "服务端GraphQL错误"
                // 自定义业务错误:Token过期(示例,根据后端约定调整)
                if code == 401 {
                    single(.failure(GraphQLRequestError.tokenExpired))
                } else {
                    single(.failure(GraphQLRequestError.serverError(code: code, message: message)))
                }
            } else if apolloResult.data == nil {
                // 2. 数据为空
                single(.failure(GraphQLRequestError.emptyResponse))
            } else {
                // 3. 请求成功,封装为自定义统一结果
                let customResult = GraphQLResult(
                    data: apolloResult.data,
                    response: apolloResult.response,
                    extensions: apolloResult.extensions
                )
                single(.success(customResult))
            }
        case .failure(let error):
            // 4. Apollo原生错误(网络、解析、超时等)
            if let apolloError = error as? ApolloError {
                single(.failure(GraphQLRequestError.apolloError(apolloError)))
            } else {
                single(.failure(GraphQLRequestError.customError(error.localizedDescription)))
            }
        }
    }
}
3.4 通用请求配置模型(GraphQLRequest.swift)

封装单个请求的自定义配置,支持覆盖全局的超时、缓存策略、请求头,让框架更灵活(比如某些请求需要单独的 Token / 超时):

swift

复制代码
import Foundation
import Apollo

// MARK: - GraphQL单个请求的自定义配置
struct GraphQLRequestConfig {
    var timeout: TimeInterval? // 单个请求超时(nil则使用全局)
    var cachePolicy: CachePolicy = .returnCacheDataAndFetch // 缓存策略(Apollo原生)
    var customHeaders: [String: String]? // 单个请求自定义头(覆盖全局同Key)
    var isIgnoreToken: Bool = false // 是否忽略全局Token(如登录/注册请求)
}

// MARK: - 通用GraphQL请求工具(可选,进一步简化业务层调用)
struct GraphQLRequest {
    /// 通用查询方法
    static func fetch<Query: GraphQLQuery>(
        _ query: Query,
        config: GraphQLRequestConfig? = nil
    ) -> Single<GraphQLResult<Query.Data>> {
        return ApolloManager.shared.getClient().rx_fetch(query: query, config: config)
    }
    
    /// 通用修改方法
    static func perform<Mutation: GraphQLMutation>(
        _ mutation: Mutation,
        config: GraphQLRequestConfig? = nil
    ) -> Single<GraphQLResult<Mutation.Data>> {
        return ApolloManager.shared.getClient().rx_perform(mutation: mutation, config: config)
    }
}
3.5 可选:请求 / 响应拦截器(GraphQLInterceptor.swift)

封装全局拦截逻辑,比如统一处理 Token 过期、网络状态检查、请求日志打印,避免在业务层重复写逻辑,这里实现两个常用拦截器:

swift

复制代码
import Foundation
import Apollo
import RxSwift
import SystemConfiguration

// MARK: - 网络状态检查拦截器(全局)
final class NetworkReachabilityInterceptor: ApolloInterceptor {
    func interceptAsync<Operation>(
        chain: RequestChain,
        request: HTTPRequest<Operation>,
        response: HTTPResponse<Operation>?,
        completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
    ) where Operation : GraphQLOperation {
        if !isNetworkReachable() {
            completion(.failure(GraphQLRequestError.noNetwork))
        } else {
            chain.proceedAsync(request: request, response: response, completion: completion)
        }
    }
    
    // 简单的网络状态检查(可替换为ReachabilitySwift库,更精准)
    private func isNetworkReachable() -> Bool {
        var zeroAddress = sockaddr_in()
        zeroAddress.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
        zeroAddress.sin_family = sa_family_t(AF_INET)
        guard let defaultRouteReachability = withUnsafePointer(to: &zeroAddress, {
            $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
                SCNetworkReachabilityCreateWithAddress(nil, $0)
            }
        }) else { return false }
        var flags: SCNetworkReachabilityFlags = []
        if !SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags) { return false }
        let isReachable = flags.contains(.reachable)
        let needsConnection = flags.contains(.connectionRequired)
        return isReachable && !needsConnection
    }
}

// MARK: - Token过期全局拦截器(响应后)
final class TokenExpiredInterceptor: ApolloInterceptor {
    func interceptAsync<Operation>(
        chain: RequestChain,
        request: HTTPRequest<Operation>,
        response: HTTPResponse<Operation>?,
        completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
    ) where Operation : GraphQLOperation {
        // 先执行后续拦截器,再处理响应
        chain.proceedAsync(request: request, response: response) { [weak self] result in
            self?.handleTokenExpired(result, completion: completion)
        }
    }
    
    private func handleTokenExpired<Operation>(
        _ result: Result<GraphQLResult<Operation.Data>, Error>,
        completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
    ) where Operation : GraphQLOperation {
        if case .failure(let error) = result, error is GraphQLRequestError,
           (error as! GraphQLRequestError) == .tokenExpired {
            // 全局处理Token过期:如发送通知、跳转到登录页
            NotificationCenter.default.post(name: NSNotification.Name("kTokenExpiredNotification"), object: nil)
            completion(result)
        } else {
            completion(result)
        }
    }
}

// MARK: - 日志打印拦截器(调试用)
final class LogInterceptor: ApolloInterceptor {
    func interceptAsync<Operation>(
        chain: RequestChain,
        request: HTTPRequest<Operation>,
        response: HTTPResponse<Operation>?,
        completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
    ) where Operation : GraphQLOperation {
        // 打印请求信息
        #if DEBUG
        print("📡 GraphQL Request: \(request.operation.operationName) | \(request.url?.absoluteString ?? "")")
        print("📋 Request Headers: \(request.allHTTPHeaderFields ?? [:])")
        if let body = request.httpBody, let bodyStr = String(data: body, encoding: .utf8) {
            print("📝 Request Body: \(bodyStr)")
        }
        #endif
        // 执行后续拦截器
        chain.proceedAsync(request: request, response: response) { result in
            // 打印响应信息
            #if DEBUG
            switch result {
            case .success(let res):
                print("✅ GraphQL Success: \(request.operation.operationName) | Data: \(res.data ?? "nil")")
            case .failure(let error):
                print("❌ GraphQL Failure: \(request.operation.operationName) | Error: \(error.localizedDescription)")
            }
            #endif
            completion(result)
        }
    }
}

// MARK: - 给ApolloManager添加拦截器(在InterceptorProvider中注入)
extension ApolloManager {
    /// 初始化所有全局拦截器(可在App启动时调用)
    func setupGlobalInterceptors() {
        // 注意:拦截器顺序很重要!请求前拦截→日志→头信息→网络检查→响应后拦截
        // 可根据需求调整顺序
    }
}

使用拦截器 :在ApolloManagerinterceptors(for:)方法中,将上述拦截器插入到拦截器链中即可。

四、框架初始化与全局配置

在 App 启动时(AppDelegate.application(_:didFinishLaunchingWithOptions:)SceneDelegate.scene(_:willConnectTo:options:))初始化 Apollo 全局配置,这是框架使用的前提

swift

复制代码
import UIKit
import RxSwift

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    let disposeBag = DisposeBag()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // 1. 初始化Apollo全局配置
        let apolloManager = ApolloManager.shared
        apolloManager.baseURL = "https://your-server.com/graphql" // 替换为你的GraphQL服务地址
        apolloManager.globalTimeout = 20
        apolloManager.globalHeaders = [
            "App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0",
            "Device-Type": "iOS",
            "Token": UserDefaults.standard.string(forKey: "kUserToken") ?? "" // 从本地获取Token
        ]
        
        // 2. 监听Token过期通知,全局处理(如跳转到登录页)
        NotificationCenter.default.rx.notification(NSNotification.Name("kTokenExpiredNotification"))
            .takeUntil(rx.deallocated) // 绑定到AppDelegate生命周期
            .subscribe(onNext: { [weak self] _ in
                self?.gotoLoginPage()
            })
            .disposed(by: disposeBag)
        
        return true
    }
    
    // Token过期跳转到登录页
    private func gotoLoginPage() {
        let loginVC = LoginViewController()
        let nav = UINavigationController(rootViewController: loginVC)
        window?.rootViewController = nav
        window?.makeKeyAndVisible()
        // 清除本地Token
        UserDefaults.standard.removeObject(forKey: "kUserToken")
    }
}

五、业务层使用示例

假设你已经通过 Apollo 生成了 GraphQL 的 Model 代码(如UserQuery:查询用户信息,LoginMutation:用户登录),业务层(如 VM 层)可以一行代码发起请求,完全贴合 RxSwift 的响应式编程,无需处理任何原生 Apollo 逻辑。

5.1 基础使用:查询用户信息(Query)

swift

复制代码
import UIKit
import RxSwift
import RxCocoa

class UserViewModel {
    let disposeBag = DisposeBag()
    // 用于绑定UI的用户信息(RxCocoa,方便更新UI)
    let userInfo = PublishRelay<UserQuery.Data.User?>()
    // 用于绑定UI的错误提示
    let errorMsg = PublishRelay<String>()
    
    /// 查询用户信息
    func fetchUserInfo(userId: String) {
        // 1. 构建Apollo生成的Query(传入参数)
        let query = UserQuery(id: userId)
        // 2. 自定义请求配置(可选,nil则使用全局)
        let config = GraphQLRequestConfig(
            timeout: 15,
            cachePolicy: .fetchIgnoringCacheData
        )
        // 3. 发起请求,订阅结果
        GraphQLRequest.fetch(query, config: config)
            .subscribe(onSuccess: { [weak self] result in
                // 请求成功,将数据发送到Relay,UI层绑定即可
                self?.userInfo.accept(result.data?.user)
            }, onFailure: { [weak self] error in
                // 请求失败,发送错误信息
                self?.errorMsg.accept(error.localizedDescription)
            })
            .disposed(by: disposeBag)
    }
}

// UI层绑定(ViewController)
class UserViewController: UIViewController {
    let vm = UserViewModel()
    let disposeBag = DisposeBag()
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var tipLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bindVM()
        // 发起请求
        vm.fetchUserInfo(userId: "123456")
    }
    
    private func bindVM() {
        // 绑定用户名称到Label
        vm.userInfo
            .compactMap { $0?.name }
            .bind(to: nameLabel.rx.text)
            .disposed(by: disposeBag)
        
        // 绑定错误信息到TipLabel
        vm.errorMsg
            .bind(to: tipLabel.rx.text)
            .disposed(by: disposeBag)
    }
}
5.2 基础使用:用户登录(Mutation)

swift

复制代码
class LoginViewModel {
    let disposeBag = DisposeBag()
    let loginSuccess = PublishRelay<LoginMutation.Data.Login?>()
    let errorMsg = PublishRelay<String>()
    
    /// 用户登录
    func login(account: String, pwd: String) {
        let mutation = LoginMutation(account: account, password: pwd)
        // 登录请求忽略全局Token(自定义配置)
        let config = GraphQLRequestConfig(isIgnoreToken: true)
        GraphQLRequest.perform(mutation, config: config)
            .subscribe(onSuccess: { [weak self] result in
                guard let loginData = result.data?.login else { return }
                // 登录成功,保存Token到本地
                UserDefaults.standard.set(loginData.token, forKey: "kUserToken")
                // 更新Apollo全局Token
                ApolloManager.shared.updateGlobalHeaders(["Token": loginData.token])
                // 发送登录成功事件
                self?.loginSuccess.accept(loginData)
            }, onFailure: { [weak self] error in
                self?.errorMsg.accept(error.localizedDescription)
            })
            .disposed(by: disposeBag)
    }
}
5.3 高级使用:请求取消 / 复用

RxSwift 的Disposable天然支持请求取消,只需将订阅的Disposable保存,需要时调用dispose()即可:

swift

复制代码
class DemoViewModel {
    let disposeBag = DisposeBag()
    var fetchDisposable: Disposable? // 保存请求的Disposable
    
    func fetchData() {
        // 先取消之前的请求,避免重复请求
        fetchDisposable?.dispose()
        let query = DemoQuery()
        fetchDisposable = GraphQLRequest.fetch(query)
            .subscribe(onSuccess: { data in
                // 处理数据
            }, onFailure: { error in
                // 处理错误
            })
        fetchDisposable?.disposed(by: disposeBag)
    }
    
    // 手动取消请求(如页面返回时)
    func cancelFetch() {
        fetchDisposable?.dispose()
    }
}

六、进阶优化与扩展

6.1 结合 MVVM+Coordinator

将网络请求完全放在VM 层 ,VM 层通过Relay/Subject将数据发送到 VC 层,VC 层只负责绑定 UI,不处理任何业务逻辑,符合 MVVM 的设计思想。

6.2 添加请求加载状态

封装全局的加载状态管理,比如通过BehaviorRelay<Bool>管理请求的加载状态,UI 层绑定到 MBProgressHUD 等加载控件:

swift

复制代码
class BaseViewModel {
    let disposeBag = DisposeBag()
    let isLoading = BehaviorRelay<Bool>(value: false) // 全局加载状态
}

class UserViewModel: BaseViewModel {
    func fetchUserInfo(userId: String) {
        isLoading.accept(true) // 开始加载
        let query = UserQuery(id: userId)
        GraphQLRequest.fetch(query)
            .subscribe(onSuccess: { [weak self] result in
                self?.userInfo.accept(result.data?.user)
                self?.isLoading.accept(false) // 结束加载
            }, onFailure: { [weak self] error in
                self?.errorMsg.accept(error.localizedDescription)
                self?.isLoading.accept(false) // 结束加载
            })
            .disposed(by: disposeBag)
    }
}
6.3 批量请求处理

结合 RxSwift 的zip/combineLatest实现批量请求,等待所有请求完成后再处理结果:

swift

复制代码
// 同时查询用户信息和商品列表,都完成后再更新UI
Observable.zip(
    GraphQLRequest.fetch(UserQuery(id: "123")),
    GraphQLRequest.fetch(GoodsListQuery(page: 1))
)
.subscribe(onNext: { userResult, goodsResult in
    // 处理用户信息和商品列表
    let user = userResult.data?.user
    let goods = goodsResult.data?.goodsList
})
.disposed(by: disposeBag)
6.4 离线缓存优化

Apollo 自带强大的本地缓存能力,可通过cachePolicy配置不同的缓存策略,比如:

  • .returnCacheDataAndFetch:先返回缓存数据,再请求网络更新(适合列表页)
  • .fetchIgnoringCacheData:忽略缓存,强制请求网络(适合登录 / 支付等敏感操作)
  • .returnCacheDataDontFetch:只从缓存获取,不请求网络(适合离线场景)
6.5 错误重试机制

结合 RxSwift 的retry/retryWhen实现错误重试,比如网络波动时自动重试 3 次:

swift

复制代码
GraphQLRequest.fetch(UserQuery(id: "123"))
    .retry(3) // 失败后自动重试3次
    .subscribe(...)
    .disposed(by: disposeBag)

// 高级:带延迟的重试(失败后延迟1秒重试,共3次)
GraphQLRequest.fetch(UserQuery(id: "123"))
    .retryWhen { errorObservable in
        errorObservable.flatMapWithIndex { error, index -> Observable<Int> in
            guard index < 3 else { return Observable.error(error) }
            return Observable.timer(.seconds(1), scheduler: MainScheduler.instance)
        }
    }
    .subscribe(...)
    .disposed(by: disposeBag)

七、框架核心优势

  1. 高度封装:业务层无需接触 Apollo 原生 API,一行代码发起请求,降低学习成本;
  2. 响应式编程:完全贴合 RxSwift/RxCocoa,天然支持请求取消、批量请求、状态绑定;
  3. 统一异常处理 :所有错误转换为自定义枚举,业务层只需处理localizedDescription,无需区分原生错误类型;
  4. 灵活配置:支持全局配置和单个请求自定义配置,覆盖所有业务场景;
  5. 易维护扩展:模块化拆分,新增拦截器 / 配置项只需修改核心层,不影响业务层;
  6. 生命周期安全 :请求自动绑定到DisposeBag,页面销毁时自动取消请求,避免内存泄漏;
  7. 兼容 Apollo 所有特性:保留 Apollo 的缓存、离线查询、批量操作等原生能力,不做阉割。

八、注意事项

  1. Apollo 代码生成 :必须确保 GraphQL 的.graphql文件正确配置,Build Phases 的脚本能正常生成 Model 代码,否则框架无法使用;
  2. Token 管理:建议将 Token 保存在 Keychain 中(而非 UserDefaults),提高安全性;
  3. 网络状态检查 :示例中的网络检查是简易版,建议使用ReachabilitySwift库,支持蜂窝网络 / WiFi 的精准判断;
  4. 线程切换 :框架已将结果切换到主线程,业务层可直接更新 UI,无需手动切换;
  5. 内存管理 :所有闭包中必须使用[weak self],避免循环引用;
  6. 调试日志LogInterceptor仅在DEBUG模式下打印日志,避免生产环境泄露敏感信息;
  7. Apollo 版本 :若使用 Apollo 0.x 版本,需微调NetworkTransportInterceptor的 API(1.0 + 有部分 API 变更)。

总结

这套框架基于Apollo + RxSwift/RxCocoa 实现了响应式、高复用、易维护的 GraphQL 网络层封装,核心亮点在于:

  1. 把 Apollo 原生回调无缝转换为 Rx 的Single序列,贴合 iOS 响应式开发的主流思想;
  2. 做了全局配置 + 单请求自定义的双层设计,兼顾通用性和灵活性;
  3. 统一了异常处理和响应结果,让业务层只需关注数据和 UI 绑定,大幅减少重复代码;
  4. 支持请求拦截、Token 全局管理、离线缓存等企业级开发的核心需求,可直接落地到生产项目。

后续可根据项目需求,扩展请求防抖全局错误弹窗请求埋点等功能,只需在核心拦截器或通用请求层添加逻辑即可,完全不影响现有业务代码。

相关推荐
相思难忘成疾6 小时前
通向HCIP之路:第四步:边界网关路由协议—BGP(概念、配置、特点、常见问题及其解决方案)
网络·华为·hcip
君陌社区·网络安全防护中心6 小时前
基于Mininet模拟SDN环境
网络
Porco.w6 小时前
C#与三菱PLC FX5U通信
网络·c#
枷锁—sha6 小时前
Burp Suite 抓包全流程与 Xray 联动自动挖洞指南
网络·安全·网络安全
云飞云共享云桌面6 小时前
高性能图形工作站的资源如何共享给10个SolidWorks研发设计用
linux·运维·服务器·前端·网络·数据库·人工智能
爱学习的程序媛7 小时前
PSTN(公共交换电话网)的起源与发展
网络·信息与通信
roman_日积跬步-终至千里7 小时前
【Java并发】Java 线程池实战:警惕使用CompletableFuture.supplyAsync
java·开发语言·网络
2的n次方_8 小时前
Runtime 内存管理深化:推理批处理下的内存复用与生命周期精细控制
c语言·网络·架构
郝学胜-神的一滴9 小时前
深入浅出:使用Linux系统函数构建高性能TCP服务器
linux·服务器·开发语言·网络·c++·tcp/ip·程序人生