# RRNetwork **Repository Path**: sheen/rrnetwork ## Basic Information - **Project Name**: RRNetwork - **Description**: No description available - **Primary Language**: Swift - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-08-14 - **Last Updated**: 2025-09-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 目录 - [主要特性](#主要特性) - [快速开始](#快速开始) - [核心功能](#核心功能) - [智能请求管理](#1-智能请求管理) - [详细状态监控](#2-详细状态监控) - [智能缓存策略](#3-智能缓存策略) - [文件操作支持](#4-文件操作支持) - [表单文件上传](#表单文件上传multipartform-data) - [纯文件上传](#纯文件上传不依赖表单) - [文件下载](#文件下载) - [拦截器支持](#5-拦截器支持) - [环境配置管理](#6-环境配置管理) - [错误处理系统](#错误处理系统) - [代码架构](#代码架构) - [高级特性](#高级特性) - [最佳实践](#最佳实践) - [系统要求](#系统要求) - [安装](#安装) # RRNetwork 一个基于 Alamofire 的现代化 iOS 网络库,采用纯 async/await 设计,支持智能缓存、拦截器、详细状态管理和灵活的请求取消机制。 ## 主要特性 - 🚀 **现代化设计**: 纯 async/await 实现,告别 Combine 复杂性 - 🎯 **智能请求管理**: 支持标签、分组的批量取消和状态监控 - 📊 **详细状态跟踪**: 实时监控请求状态(空闲、请求中、解析中、完成等) - 📦 **智能缓存**: 清晰的缓存策略,支持纯缓存、纯网络、网络+缓存模式 - 📁 **完整文件支持**: 内存友好的文件上传/下载,支持进度监控和断点续传 - 🔍 **详细日志**: 可配置的日志记录,支持文件输出 - 🛡️ **拦截器支持**: 请求和响应拦截,支持认证、日志等 - ⚙️ **灵活配置**: 支持运行时重新配置 - 🧵 **线程安全**: 完整的线程安全保证,状态回调自动切换到主线程 - 🧩 **易于扩展**: 基于协议的设计,支持自定义组件 ## 快速开始 ### 1. 配置网络库 ```swift // 创建配置 let config = RRNetworkConfiguration() config.baseURL = "https://api.example.com" config.timeoutInterval = 30 // 配置日志 config.logConfiguration.enabled = true config.logConfiguration.logLevel = .debug config.logConfiguration.writeToFile = true // 配置缓存 config.cacheConfiguration.enabled = true config.cacheConfiguration.defaultCacheExpiration = 3600 config.cacheConfiguration.maxCacheSize = 50 * 1024 * 1024 // 应用配置 let network = RRNetwork.shared // let network = RRNetwork() //如果需要自己实现持有,不需要默认单例 network.setup(config) ``` ### 2. 创建请求类 ```swift class UserRequest: RRBaseRequest { override init() { super.init() self.path = "/users/1" self.method = .get } } class LoginRequest: RRBaseRequest { private let username: String private let password: String init(username: String, password: String) { self.username = username self.password = password super.init() self.path = "/login" self.method = .post self.parameters = ["username": username, "password": password] } } ``` ### 3. 发起请求 #### Async/Await 方式(推荐) ```swift func loadUserData() async { let request = UserRequest() do { let result = try await RRNetwork.shared.request(request) print("请求成功: \(result)") } catch { print("请求失败: \(error)") } } ``` #### Closure 方式 ```swift func loadUserData() { let request = UserRequest() RRNetwork.shared.request(request) { result, error in if let error = error { print("请求失败: \(error)") } else { print("请求成功: \(result)") } } } ``` #### 并发请求 ```swift func loadMultipleData() async { async let userData = RRNetwork.shared.request(UserRequest()) async let postsData = RRNetwork.shared.request(PostsRequest()) async let commentsData = RRNetwork.shared.request(CommentsRequest()) do { let (user, posts, comments) = try await (userData, postsData, commentsData) // 处理所有数据 } catch { print("请求失败: \(error)") } } ``` ## 核心功能 ### 1. 智能请求管理 #### 基本取消 ```swift let request = MyRequest() RRNetwork.shared.request(request) { result, error in } // 直接取消请求 request.cancel() // 检查请求状态 if request.isCancelled { print("请求已被取消") } ``` #### 标签批量取消 ```swift // 设置请求标签 let request1 = UserRequest() request1.tag = "user_data" let request2 = ProfileRequest() request2.tag = "user_data" // 批量取消相同标签的请求 RRNetwork.shared.cancelRequests(tag: "user_data") ``` #### 分组批量取消 ```swift // 设置请求分组(通常用于页面级管理) let request = DataRequest() request.groupId = "ProfileViewController" // 页面销毁时取消该页面的所有请求 RRNetwork.shared.cancelRequests(groupId: "ProfileViewController") ``` #### 页面级请求管理 ```swift class MyViewController: UIViewController { private let groupId = "MyViewController" func loadData() { let request = DataRequest() request.groupId = groupId RRNetwork.shared.request(request) { result, error in // 处理结果 } } deinit { // 页面销毁时取消该页面的所有请求 RRNetwork.shared.cancelRequests(groupId: groupId) } } ``` ### 2. 详细状态监控 ```swift let request = MyRequest() // 监听状态变更 request.onStateChange { oldState, newState in print("状态变更: \(oldState.description) -> \(newState.description)") switch newState { case .requesting: showLoading() case .parsing: showProcessing() case .completed: hideLoading() showSuccess() case .failed: hideLoading() showError(request.error) case .cancelled: hideLoading() default: break } } // 查询状态信息 let currentState = request.state let duration = request.duration let isActive = request.isActive ``` #### 全局状态回调(onRequestStateChange) ```swift // 监听所有请求的状态变更(已在主线程回调) RRNetwork.shared.onRequestStateChange = { request, oldState, newState in print("[Global] \(type(of: request)) 状态: \(oldState.description) -> \(newState.description)") // 按需做全局处理,例如:统一埋点/统计/日志/Toast switch newState { case .requesting: GlobalHUD.showLoading() case .completed: GlobalHUD.hideLoading() case .failed: GlobalHUD.hideLoading() GlobalHUD.showError(request.error) case .cancelled: GlobalHUD.hideLoading() default: break } // 也可以根据标签或分组做筛选 if request.tag == "critical" { // 关键请求的特殊处理 } if request.groupId == "ProfileViewController" { // 针对某页面的请求统计 } } ``` ### 3. 智能缓存策略 RRNetwork 提供三种清晰的缓存策略: ```swift // 1. 只请求网络,不缓存(默认) class NetworkOnlyRequest: RRBaseRequest { override init() { super.init() self.cachePolicy = .networkOnly } } // 2. 请求网络并缓存结果 class NetworkThenCacheRequest: RRBaseRequest { override init() { super.init() self.cachePolicy = .networkThenCache self.cacheExpiration = 3600 // 1小时缓存 } } // 3. 只读取缓存,不发起网络请求 class CacheOnlyRequest: RRBaseRequest { override init() { super.init() self.cachePolicy = .cacheOnly self.cacheExpiration = 3600 // 1小时缓存 } } ``` #### 实现"先缓存后网络"模式 如果需要先返回缓存数据,然后再获取最新数据,可以分别执行两次请求: ```swift func loadDataWithCacheThenNetwork() async { // 第一步:尝试获取缓存数据 let cacheRequest = CacheOnlyRequest() do { let cachedResult = try await RRNetwork.shared.request(cacheRequest) // 立即显示缓存数据 updateUI(with: cachedResult) } catch { // 没有缓存数据,继续网络请求 print("No cached data available") } // 第二步:获取最新数据并缓存 let networkRequest = NetworkThenCacheRequest() do { let latestResult = try await RRNetwork.shared.request(networkRequest) // 更新为最新数据 updateUI(with: latestResult) } catch { print("Network request failed: \(error)") } } ``` ### 4. 文件操作支持 #### 表单文件上传(multipart/form-data) ```swift class UploadAvatarRequest: RRBaseRequest { private let imageURL: URL private let userId: String init(imageURL: URL, userId: String) { self.imageURL = imageURL self.userId = userId super.init() self.path = "/user/avatar" self.method = .post self.parameterEncoding = .multipart // parameters 中的内容会自动作为表单字段添加 self.parameters = [ "userId": userId, "token": "abcdef" ] // multipartFormDataBuilder 用于添加文件和额外字段 self.multipartFormDataBuilder = { [weak self] builder in guard let self = self else { return } // 添加图片文件 builder.appendImageFile(at: self.imageURL, name: "avatar") } } } ``` #### 纯文件上传(不依赖表单) RRNetwork 支持两种文件上传方式,推荐使用文件路径方式以节省内存: ##### 方式1:文件路径上传(推荐,内存友好) ```swift // 大文件上传 - 使用文件路径(推荐) let filePath = "/path/to/large/video.mp4" let request = RRFileUploadRequest(filePath: filePath) // 自动检测MIME类型 // 或手动指定MIME类型 let request = RRFileUploadRequest(filePath: filePath, mimeType: "video/mp4") request.path = "/api/upload/video" request.tag = "video_upload" request.groupId = "VideoViewController" // 设置上传进度回调 request.onProgress { progress in let percentage = progress.fractionCompleted * 100 let uploaded = progress.completedUnitCount let total = progress.totalUnitCount print("上传进度: \(Int(percentage))% (\(uploaded)/\(total) bytes)") } do { let result = try await RRNetwork.shared.request(request) print("上传成功: \(result)") } catch { print("上传失败: \(error)") } ``` ##### 方式2:文件数据上传(适用于小文件) ```swift // 小文件上传 - 使用文件数据 let imageData = // ... 获取图片数据 let request = RRFileUploadRequest(fileData: imageData, mimeType: "image/jpeg") request.path = "/api/upload/avatar" request.onProgress { progress in print("头像上传进度: \(Int(progress.fractionCompleted * 100))%") } do { let result = try await RRNetwork.shared.request(request) print("头像上传成功: \(result)") } catch { print("头像上传失败: \(error)") } ``` ##### 文件下载特性 - **内存优化**:直接写入磁盘,内存占用极低 - **自动MIME检测**:支持50+种常见文件格式自动识别 - **进度监控**:实时下载进度回调 - **文件信息**:自动获取文件大小、文件名等信息 - **取消支持**:支持随时取消下载 - **断点续传**:暂未实现稳定版本 - **MD5校验**:支持下载文件的MD5完整性校验 ```swift // 检查文件信息 let request = RRFileUploadRequest(filePath: "/path/to/document.pdf") print("文件大小: \(request.fileSize) bytes") print("文件名: \(request.fileName ?? "未知")") print("MIME类型: \(request.getMimeType())") print("是否文件路径上传: \(request.isFilePathUpload)") // 支持的文件格式(自动检测MIME类型) // 图片: jpg, png, gif, bmp, webp, svg // 视频: mp4, avi, mov, wmv, flv, webm // 音频: mp3, wav, flac, aac, ogg // 文档: pdf, doc, docx, xls, xlsx, ppt, pptx // 压缩: zip, rar, 7z, tar, gz ``` ##### 自定义文件上传请求 ```swift class VideoUploadRequest: RRFileUploadRequest { init(filePath: String) { super.init(filePath: filePath) self.path = "/api/upload/video" self.method = .post self.headers = [ "X-Upload-Type": "video", "X-Client-Version": "1.0.0" ] } override var timeoutInterval: TimeInterval? { return 300 // 5分钟超时 } } // 使用自定义请求 let videoPath = "/path/to/video.mp4" let request = VideoUploadRequest(filePath: videoPath) request.onProgress { progress in print("视频上传: \(Int(progress.fractionCompleted * 100))%") } ``` ##### 批量文件上传 ```swift func uploadMultipleFiles(_ filePaths: [String]) async { await withTaskGroup(of: Void.self) { group in for (index, filePath) in filePaths.enumerated() { group.addTask { let request = RRFileUploadRequest(filePath: filePath) request.path = "/api/upload/batch" request.tag = "batch_upload" request.onProgress { progress in print("文件\(index + 1)上传进度: \(Int(progress.fractionCompleted * 100))%") } do { _ = try await RRNetwork.shared.request(request) print("文件\(index + 1)上传成功") } catch { print("文件\(index + 1)上传失败: \(error)") } } } } } ``` ##### 内存使用对比 | 上传方式 | 1GB文件内存占用 | 推荐场景 | | ------------ | --------------- | -------------- | | **文件路径** | 几MB | 大文件(推荐) | | 文件数据 | 1GB | 小文件 | #### 文件下载 ```swift // 使用方式 - 直接创建实例无需继承 let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let destinationURL = documentsPath.appendingPathComponent("document.pdf") // 直接创建RRFileDownloadRequest实例 // 可选择性地提供temporaryURL参数来自定义临时文件路径 let downloadRequest = RRFileDownloadRequest(destinationURL: destinationURL) // 或者自定义临时文件路径 // let customTempURL = documentsPath.appendingPathComponent("temp_download.tmp") // let downloadRequest = RRFileDownloadRequest(destinationURL: destinationURL, temporaryURL: customTempURL) // 直接设置下载路径 downloadRequest.path = "/files/document.pdf" // 设置进度回调 downloadRequest.onProgress { progress in print("下载进度: \(Int(progress.fractionCompleted * 100))%") } // 设置期望的MD5值,用于完整性校验 // 一旦设置了MD5值,系统会自动进行校验,无需手动处理 downloadRequest.fileMD5 = "d41d8cd98f00b204e9800998ecf8427e" do { let localURL = try await RRNetwork.shared.request(downloadRequest) as! URL print("下载完成: \(localURL)") // 如果执行到这里,说明文件下载成功且MD5校验通过 } catch let error as RRNetworkError { if case .md5ValidationFailed(let expected, let actual) = error { print("MD5校验失败: 期望 \(expected),实际得到 \(actual)") } else { print("下载失败: \(error)") } } catch { print("下载失败: \(error)") } #### 断点续传功能 支持基于文件大小的自动Range头设置,通过以下方式使用: ```swift let request = RRFileDownloadRequest(destinationURL: destinationURL) request.path = "/your/file/path" // 启用断点续传功能 request.supportsResume = true // 取消下载保留临时文件 request.cancel() // 重新创建请求自动续传 do { let localURL = try await RRNetwork.shared.request(request) as! URL print("下载完成路径:\(localURL)") } catch { print("下载失败: \(error)") } ``` 特性说明: - 设置supportsResume = true启用续传功能,有以下注意点 - 默认临时目录为'tmp目录'+'destinationURL文件名'+'.tmp' - 可能会造成同一文件名不同下载目录重复的情况 - 不支持多个RRFileDownloadRequest同时下载文件到同一个相同文件名路径下,如果重复,需要自定义temporaryURL - 调用cancel()自动保留临时文件 - 自动检测未完成文件并设置Range请求头 - 支持HTTP 416 Range Not Satisfiable错误自动处理 - 通过返回的URL获取最终文件路径 - 支持MD5校验确保文件完整性 ##### MD5完整性校验 RRFileDownloadRequest支持下载文件的MD5完整性校验,确保下载的文件完整无损。 - **非阻塞主线程**:MD5计算在后台线程执行,不会阻塞UI - **配置灵活**:可以选择是否启用完整性校验 - **结果回调**:通过回调获取校验结果 ##### 本地文件路径设置 - 本地文件保存路径通过初始化时的`destinationURL`参数设置 - 可选择性地提供`temporaryURL`参数来自定义临时文件路径 - 如果未提供临时路径,系统会自动在临时目录生成`.tmp`后缀的临时文件 ### 5. 拦截器支持 RRNetwork支持请求和响应拦截器,内置拦截器会根据配置自动添加: ```swift let config = RRNetworkConfiguration() config.logConfiguration.enabled = true // 自动添加日志拦截器 config.cacheConfiguration.enabled = true // 自动添加缓存拦截器 RRNetwork.shared.setup(with: config) // 添加自定义认证拦截器 let authProvider = CustomAuthProvider() let authInterceptor = RRAuthInterceptor(authProvider: authProvider) RRNetwork.shared.addInterceptor(authInterceptor) ``` #### 内置拦截器 - **日志拦截器**:当`logConfiguration.enabled = true`时自动添加 - **缓存拦截器**:当`cacheConfiguration.enabled = true`时自动添加 #### 自定义拦截器 你可以创建自定义拦截器来处理认证、请求修改等: ```swift class CustomInterceptor: NSObject, RRRequestInterceptor { func adapt(request: URLRequest, originalRequest: RRBaseRequest) -> URLRequest { var modifiedRequest = request // 修改请求 modifiedRequest.setValue("custom-value", forHTTPHeaderField: "Custom-Header") return modifiedRequest } func handle(response: HTTPURLResponse, data: Data?, originalRequest: RRBaseRequest) throws { // 处理响应 if response.statusCode >= 400 { // 处理错误响应 } } } // 添加自定义拦截器 let customInterceptor = CustomInterceptor() RRNetwork.shared.addInterceptor(customInterceptor) ``` #### 认证系统 RRNetwork 提供了灵活的自定义认证系统: ```swift class CustomAuthProvider: NSObject, RRAuthProviderProtocol { func getAuthHeaders(for request: RRBaseRequest) -> [String: String] { return [ "X-Timestamp": String(Int(Date().timeIntervalSince1970)), "X-Device-ID": UIDevice.current.identifierForVendor?.uuidString ?? "unknown", "X-App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" ] } // 异步回调版本的 refreshAuth(必须实现) func refreshAuth(completion: @escaping (Bool) -> Void) { // 模拟异步认证刷新 DispatchQueue.global().async { // 执行认证刷新逻辑 let success = self.performAuthRefresh() DispatchQueue.main.async { completion(success) } } } // 同步版本(可选实现,不推荐用于网络请求) func refreshAuthSync() -> Bool { // 自定义刷新逻辑 return performAuthRefresh() } func shouldRefreshAuth(for response: HTTPURLResponse) -> Bool { // 自定义刷新条件 return response.statusCode == 401 || response.statusCode == 403 } private func performAuthRefresh() -> Bool { // 实际的认证刷新逻辑 return true } } ``` ### 6. 环境配置管理 ```swift // 开发环境配置 let devConfig = RRNetworkConfiguration() devConfig.baseURL = "https://api-dev.example.com" devConfig.logConfiguration.enabled = true devConfig.logConfiguration.logLevel = .debug devConfig.logConfiguration.writeToFile = true devConfig.cacheConfiguration.enabled = false // 生产环境配置 let prodConfig = RRNetworkConfiguration() prodConfig.baseURL = "https://api.example.com" prodConfig.logConfiguration.enabled = false prodConfig.cacheConfiguration.enabled = true prodConfig.cacheConfiguration.defaultCacheExpiration = 3600 // 运行时切换环境 RRNetwork.shared.setup(with: prodConfig) ``` ## 代码架构 ### 类结构与职责 当前实现以清晰的单类结构为主,并辅以必要的私有方法。以下为核心类与职责概览: - RRNetwork - 单例入口:`shared` - 可配置组件:`configuration`、`cacheManager`、`logger` - 全局回调:`onRequestStateChange`(任何请求状态变化都会回调,并在最终态自动移除活跃集合) - 请求发起:`request(_:)`(async/await)与闭包包装方法 - 取消能力:按对象、URL、tag、groupId、全部取消 - 统计能力:活跃请求数量与筛选统计 - 私有实现:线程安全管理 `activeRequests` - RRBaseRequest - 基本属性:`path`(**必填**,默认为空字符串) - 注:若未设置,在请求执行时将抛出 `RRNetworkError.invalidRequest` 错误 - 可配置选项:`method`、`parameters`、`headers`、`parameterEncoding`、`timeoutInterval`、`cachePolicy`、`cacheExpiration`、`requiresAuthentication` - 标识能力:`tag`、`groupId` - 状态查询:`state`、`isActive`、`isFinalState`、`duration`、`error`、`response` - 生命周期:`onStateChange(_:)`、`cancel()`、`reset()` - URL/缓存:`fullURL(with:)`、`cacheKey(with:)` - 解码扩展:`decodeResponse(from:)` 可被子类重写 - 请求修改:`willSendRequest(_:)` 可被子类重写,在请求发起前最后修改URLRequest #### RRNetwork 结构速览 ```swift @objc open class RRNetwork: NSObject { public static let shared = RRNetwork() // 配置与组件 @objc public var configuration: RRNetworkConfiguration @objc public var cacheManager: RRCacheManagerProtocol! @objc public var logger: RRLoggerProtocol! // 全局请求状态回调(oldState -> newState) @objc public var onRequestStateChange: ((RRBaseRequest, RRRequestState, RRRequestState) -> Void)? // 发起请求(async/await 与闭包) public func request(_ request: RRBaseRequest) async throws -> Any { /* ... */ } @objc public func request(_ request: RRBaseRequest, completion: @escaping (Any?, NSError?) -> Void) { /* ... */ } // 取消能力 @objc public func cancelAllRequests() { /* ... */ } @objc public func cancelRequests(url: String) { /* ... */ } @objc public func cancelRequest(request: RRBaseRequest) { /* ... */ } @objc public func cancelRequests(tag: String) { /* ... */ } @objc public func cancelRequests(groupId: String) { /* ... */ } @objc public func cancelRequests(groupId: String, tag: String) { /* ... */ } // 统计 @objc public var activeRequestCount: Int { /* ... */ } @objc public func activeRequestCount(withTag tag: String) -> Int { /* ... */ } @objc public func activeRequestCount(withGroupId groupId: String) -> Int { /* ... */ } @objc public func activeRequestCount(withGroupId groupId: String, tag: String) -> Int { /* ... */ } } ``` #### RRBaseRequest 结构速览 ```swift @objcMembers open class RRBaseRequest: NSObject, RRResponseDecodable { // 基本属性(存储属性) open var path: String = "" // 可配置选项(存储属性) open var method: RRHTTPMethod = .get open var parameters: Any? = nil open var headers: [String: String]? = nil open var parameterEncoding: RRParameterEncoding = .json open var timeoutInterval: TimeInterval? { nil } open var cachePolicy: RRCachePolicy { .networkOnly } open var cacheExpiration: TimeInterval { 0 } open var requiresAuthentication: Bool { true } // 标识 @objc public var tag: String? @objc public var groupId: String? // 状态查询(线程安全读取) @objc public var state: RRRequestState { /* ... */ } @objc public var isActive: Bool { /* ... */ } @objc public var isFinalState: Bool { /* ... */ } @objc public var duration: TimeInterval { /* ... */ } @objc public var error: NSError? { /* ... */ } @objc public var response: Any? { /* ... */ } // 生命周期控制 public func onStateChange(_ callback: @escaping (RRRequestState, RRRequestState) -> Void) { /* ... */ } @objc public func cancel() { /* ... */ } @objc public func reset() { /* ... */ } // URL/缓存 public func fullURL(with baseURL: String) -> String { /* ... */ } public func cacheKey(with baseURL: String) -> String { /* ... */ } // 解码(默认 JSON,可自定义) open func decodeResponse(from data: Data) throws -> Any { /* ... */ } } ``` #### 参数类型支持 RRNetwork 的 `parameters` 属性支持多种数据类型,提供了灵活的参数传递方式: ```swift // 字典类型(最常用) request.parameters = [ "username": "john", "age": 25, "active": true ] // 数组类型 request.parameters = ["item1", "item2", 123] // 其他 JSON 兼容类型 request.parameters = "simple string" request.parameters = 42 request.parameters = true // 无参数 request.parameters = nil ``` **编码方式支持:** | 参数类型 | JSON 编码 | URL 编码 | Multipart | Custom | | --------------- | --------- | -------- | --------- | ------ | | `[String: Any]` | ✅ | ✅ | ✅ | ✅ | | `[Any]` | ✅ | ❌ | ❌ | ✅ | | 其他类型 | ✅ | ❌ | ❌ | ✅ | | `nil` | ✅ | ✅ | ✅ | ✅ | **注意事项:** - URL 编码(`.urlEncoded`)只支持字典类型参数 - Multipart 编码(`.multipart`)只支持字典类型参数 - JSON 编码(`.json`)支持所有 JSON 兼容类型 - 自定义编码(`.custom`)由子类实现决定支持的类型 ### 设计优势 1. **清晰的职责分离**:每个 extension 专注于特定功能 2. **易于维护**:相关方法聚集在一起,便于查找和修改 3. **良好的扩展性**:新功能可以通过新的 extension 添加 4. **访问权限明确**:public、internal、private 方法分组清晰 5. **文件整合**:相关功能集中在主文件中,减少文件碎片化 ### 扩展指南 如果你需要为 RRNetwork 添加新功能,建议遵循以下模式: ```swift // MARK: - Your Feature Name extension RRNetwork { // 你的公共方法 } // MARK: - Your Feature Private Methods private extension RRNetwork { // 你的私有实现 } ``` ## 高级特性 ### 错误处理系统 RRNetwork 提供了完整的错误处理体系,包含三种主要错误类型和丰富的错误信息查询功能。 #### 错误类型说明 RRNetwork 定义了三种主要的网络错误类型,每种都有明确的使用场景: 1. **HTTP错误 (`httpError`)** - **用途**:处理 HTTP 协议层面的错误 - **场景**:服务器返回了 HTTP 响应,但状态码表示错误(如 404、500、401 等) - **特点**:携带具体的 HTTP 状态码信息 2. **URL错误 (`urlError`)** - **用途**:处理 URL 相关的系统级错误 - **场景**:网络请求在 URL 层面出现问题(如无效 URL、DNS 解析失败、SSL 证书问题等) - **特点**:包装了系统的 `URLError`,保留原始错误信息 3. **网络错误 (`networkError`)** - **用途**:处理通用的网络连接错误 - **场景**:网络连接层面的问题(如网络不可达、连接超时、连接被拒绝等) - **特点**:包装任意的网络相关错误 #### 错误处理示例 ```swift func handleNetworkError(_ error: NSError) { // 检查是否是 RRNetwork 错误 if error.isRRNetworkError { print("RRNetwork 错误码: \(error.code)") print("错误描述: \(error.rrNetworkErrorDescription)") // 获取原始错误信息 if let originalCode = error.rrNetworkOriginalErrorCode { print("原始错误码: \(originalCode)") } if let underlyingError = error.rrNetworkUnderlyingError { print("底层错误: \(underlyingError.localizedDescription)") } // 根据具体错误类型处理 switch RRNetworkErrorCode(rawValue: error.code) { case .httpError: if let statusCode = error.rrNetworkHTTPStatusCode?.intValue { handleHTTPError(statusCode: statusCode) } case .urlError: if let urlError = error.rrNetworkURLError { handleURLError(urlError) } case .networkError: handleNetworkConnectionError(error.rrNetworkUnderlyingError) case .authenticationFailed: // 处理认证失败 redirectToLogin() default: // 处理其他错误 showGenericError(error.rrNetworkErrorDescription) } } } private func handleHTTPError(statusCode: Int) { switch statusCode { case 400: showAlert("请求参数错误") case 401: showAlert("未授权,请重新登录") redirectToLogin() case 403: showAlert("权限不足") case 404: showAlert("请求的资源不存在") case 500...599: showAlert("服务器内部错误,请稍后重试") default: showAlert("HTTP错误:\(statusCode)") } } ``` #### 错误信息查询 RRNetwork 提供了多种方式查询原始错误信息: ```swift // 使用 RRNetworkErrorHandler 类方法 let originalCode = RRNetworkErrorHandler.getOriginalErrorCode(from: error) let underlyingError = RRNetworkErrorHandler.getUnderlyingError(from: error) let httpStatusCode = RRNetworkErrorHandler.getHTTPStatusCode(from: error) let urlError = RRNetworkErrorHandler.getURLError(from: error) // 使用 NSError 扩展属性 let originalCode = error.rrNetworkOriginalErrorCode let underlyingError = error.rrNetworkUnderlyingError let httpStatusCode = error.rrNetworkHTTPStatusCode?.intValue let urlError = error.rrNetworkURLError // 错误类型判断 if error.isRRNetworkHTTPError { // 处理 HTTP 错误 } else if error.isRRNetworkURLError { // 处理 URL 错误 } else if error.isRRNetworkConnectionError { // 处理网络连接错误 } ``` #### Objective-C 兼容性 RRNetwork 完全支持 Objective-C,提供了相同的错误处理功能: ```objc - (void)handleNetworkError:(NSError *)error { if (error.isRRNetworkError) { NSLog(@"RRNetwork 错误码: %ld", (long)error.code); NSLog(@"错误描述: %@", error.rrNetworkErrorDescription); // 获取原始错误信息 NSNumber *originalCode = [RRNetworkErrorHandler getOriginalErrorCodeFrom:error]; NSError *underlyingError = [RRNetworkErrorHandler getUnderlyingErrorFrom:error]; // 根据错误类型处理 switch (error.code) { case RRNetworkErrorCodeHttpError: { NSNumber *statusCode = [RRNetworkErrorHandler getHTTPStatusCodeFrom:error]; [self handleHTTPErrorWithStatusCode:statusCode.integerValue]; break; } case RRNetworkErrorCodeNetworkError: [self handleNetworkConnectionError:underlyingError]; break; // ... 其他错误处理 } } } ``` ### 错误处理和重试 ```swift func loadDataWithRetry() async { let maxRetries = 3 var retryCount = 0 while retryCount < maxRetries { do { let result = try await RRNetwork.shared.request(DataRequest()) return result } catch { retryCount += 1 if retryCount < maxRetries { try? await Task.sleep(nanoseconds: 2_000_000_000) } else { throw error } } } } ``` ### 自定义解码 ```swift class JSONRequest: RRBaseRequest { override func decodeResponse(from data: Data) throws -> Any { // 自定义JSON解码逻辑 let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return try decoder.decode(MyModel.self, from: data) } } ``` #### 自定义请求修改 使用 `willSendRequest(_:)` 方法在请求发起前最后修改URLRequest: ```swift class AuthenticatedRequest: RRBaseRequest { private let authToken: String init(authToken: String) { self.authToken = authToken super.init() self.path = "/api/user/profile" } override func willSendRequest(_ urlRequest: URLRequest) -> URLRequest { var modifiedRequest = urlRequest // 添加认证头 modifiedRequest.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization") // 添加时间戳 let timestamp = Int(Date().timeIntervalSince1970) modifiedRequest.setValue("\(timestamp)", forHTTPHeaderField: "X-Timestamp") // 修改超时时间 modifiedRequest.timeoutInterval = 60 return modifiedRequest } } ``` ## 最佳实践 ### 1. 代码组织建议 #### 请求类组织 ```swift // 推荐:按业务模块组织请求类 // UserRequests.swift class UserProfileRequest: RRBaseRequest { override init() { super.init() self.path = "/user/profile" } } class UserSettingsRequest: RRBaseRequest { override init() { super.init() self.path = "/user/settings" } } // PostRequests.swift class PostListRequest: RRBaseRequest { override init() { super.init() self.path = "/posts" } } ``` #### 页面级请求管理 ```swift class ProfileViewController: UIViewController { private let groupId = "ProfileViewController" override func viewDidLoad() { super.viewDidLoad() loadData() } private func loadData() { let request = UserProfileRequest() request.groupId = groupId RRNetwork.shared.request(request) { result, error in // 处理结果 } } deinit { // 页面销毁时取消所有请求 RRNetwork.shared.cancelRequests(groupId: groupId) } } ``` ### 2. 页面级请求管理(手动设置 groupId) ```swift class ProfileViewController: UIViewController { private let groupId = "ProfileViewController" override func viewDidLoad() { super.viewDidLoad() loadData() } private func loadData() { let req1 = UserProfileRequest() req1.groupId = groupId RRNetwork.shared.request(req1) { _, _ in } let req2 = UserPostsRequest() req2.groupId = groupId RRNetwork.shared.request(req2) { _, _ in } } deinit { RRNetwork.shared.cancelRequests(groupId: groupId) } } ``` ### 2. 网络状态处理 ```swift func handleNetworkRequest() async { let request = MyRequest() request.onStateChange { oldState, newState in DispatchQueue.main.async { // 状态回调已经在主线程,可以直接更新UI self.updateUI(for: newState) } } do { let result = try await RRNetwork.shared.request(request) // 处理成功结果 } catch { // 处理错误 } } ``` ### 3. 文件上传最佳实践 #### 选择合适的上传方式 ```swift func uploadFile(at path: String) async { let fileURL = URL(fileURLWithPath: path) // 检查文件大小决定上传方式 if let fileSize = try? fileURL.resourceValues(forKeys: [.fileSizeKey]).fileSize { if fileSize > 1024 * 1024 { // 大于1MB // 使用文件路径上传(内存友好) let request = RRFileUploadRequest(filePath: path) request.path = "/api/upload" request.tag = "file_upload" request.onProgress { progress in print("上传进度: \(Int(progress.fractionCompleted * 100))%") } do { _ = try await RRNetwork.shared.request(request) print("大文件上传成功") } catch { print("上传失败: \(error)") } } else { // 小文件可以使用数据上传 if let data = try? Data(contentsOf: fileURL) { // 自动检测MIME类型或使用默认值 let request = RRFileUploadRequest(fileData: data, mimeType: "application/octet-stream") request.path = "/api/upload" do { _ = try await RRNetwork.shared.request(request) print("小文件上传成功") } catch { print("上传失败: \(error)") } } } } } ``` #### 文件上传生命周期管理 ```swift class FileUploadViewController: UIViewController { private let groupId = "FileUploadViewController" func uploadVideo(at path: String) { let request = RRFileUploadRequest(filePath: path) request.path = "/api/upload/video" request.groupId = groupId // 设置组ID request.tag = "video_upload" // 设置进度回调 request.onProgress { [weak self] progress in DispatchQueue.main.async { self?.updateProgressUI(progress.fractionCompleted) } } // 发起上传请求 RRNetwork.shared.request(request) { [weak self] result, error in DispatchQueue.main.async { if let error = error { self?.showError("上传失败: \(error.localizedDescription)") } else { self?.showSuccess("视频上传成功") } } } } } Task { do { let result = try await RRNetwork.shared.request(request) await MainActor.run { self.showUploadSuccess(result) } } catch { await MainActor.run { self.showUploadError(error) } } } } deinit { // 页面销毁时取消所有上传请求 RRNetwork.shared.cancelRequests(groupId: groupId) } } ``` #### 批量上传控制 ```swift class BatchUploadManager { private let maxConcurrentUploads = 3 // 控制并发数量 func uploadFiles(_ filePaths: [String]) async { // 使用TaskGroup控制并发数量 await withTaskGroup(of: Void.self) { group in var index = 0 for filePath in filePaths { // 控制并发数量 if group.isEmpty || index < maxConcurrentUploads { group.addTask { await self.uploadSingleFile(filePath, index: index) } index += 1 } else { // 等待一个任务完成再添加新任务 await group.next() group.addTask { await self.uploadSingleFile(filePath, index: index) } index += 1 } } } } private func uploadSingleFile(_ filePath: String, index: Int) async { let request = RRFileUploadRequest(filePath: filePath) request.path = "/api/upload/batch" request.tag = "batch_upload" request.onProgress { progress in print("文件\(index)上传进度: \(Int(progress.fractionCompleted * 100))%") } do { _ = try await RRNetwork.shared.request(request) print("文件\(index)上传成功") } catch { print("文件\(index)上传失败: \(error)") } } } ``` ### 4. 配置管理 #### 环境配置 ```swift enum Environment { case development, staging, production var config: RRNetworkConfiguration { let config = RRNetworkConfiguration() switch self { case .development: config.baseURL = "https://api-dev.example.com" config.logConfiguration.enabled = true config.logConfiguration.logLevel = .debug config.cacheConfiguration.enabled = false case .staging: config.baseURL = "https://api-staging.example.com" config.logConfiguration.enabled = true config.logConfiguration.logLevel = .info config.cacheConfiguration.enabled = true config.cacheConfiguration.defaultCacheExpiration = 300 case .production: config.baseURL = "https://api.example.com" config.logConfiguration.enabled = false config.cacheConfiguration.enabled = true config.cacheConfiguration.defaultCacheExpiration = 3600 } return config } } // 应用启动时设置环境 RRNetwork.shared.setup(with: Environment.production.config) ``` #### 模块化配置扩展 ```swift // 为配置添加便利扩展 extension RRNetworkConfiguration { // MARK: - Preset Configurations static var development: RRNetworkConfiguration { let config = RRNetworkConfiguration() config.baseURL = "https://api-dev.example.com" config.setupDevelopmentDefaults() return config } static var production: RRNetworkConfiguration { let config = RRNetworkConfiguration() config.baseURL = "https://api.example.com" config.setupProductionDefaults() return config } } // 私有配置方法 private extension RRNetworkConfiguration { func setupDevelopmentDefaults() { logConfiguration.enabled = true logConfiguration.logLevel = .debug logConfiguration.writeToFile = true cacheConfiguration.enabled = false } func setupProductionDefaults() { logConfiguration.enabled = false cacheConfiguration.enabled = true cacheConfiguration.defaultCacheExpiration = 3600 } } // 使用预设配置 RRNetwork.shared.setup(with: .production) ``` ## 系统要求 - iOS 13.0+ - Swift 5.5+ - Xcode 13.0+ ## 安装 ### CocoaPods ```ruby target 'YourTarget' do # pod 'RRNetwork' //暂未push到pod仓库 pod 'RRNetwork', :git => 'https://gitee.com/sheen/rrnetwork.git' # pod 'RRNetwork', :git => 'https://gitee.com/sheen/rrnetwork.git', :commit => '92b827c896b36ae47be6c025e5d054147666299b' //依赖某个commit end ``` ## 示例项目 运行示例项目: ```bash git clone https://github.com/wushengtao/RRNetwork.git cd RRNetwork/Example pod install open RRNetwork.xcworkspace ``` ## 相关文档 - [错误处理详细指南](./RRNetwork_Error_Handling_Guide.md) - 完整的错误处理系统说明 - [错误码使用说明](./RRNetwork_ErrorCode_Usage.md) - 错误码的详细使用方法 ## 单元测试 项目包含完整的单元测试,位于 `Example/Tests/` 目录: - **RRNetworkErrorHandlingTests.swift**: 错误处理功能测试(Swift) - **RRNetworkErrorHandlingObjCTests.m**: 错误处理功能测试(Objective-C) - **CancelErrorTests.swift**: 测试取消错误的正确处理 - **CancelStateTests.swift**: 测试取消状态管理机制 - **FileUploadTests.swift**: 文件上传功能测试 - **CacheTests.swift**: 缓存功能测试 运行测试: ```bash # 在 Example 目录下 xcodebuild test -workspace RRNetwork.xcworkspace -scheme RRNetwork -destination 'platform=iOS Simulator,name=iPhone 14's.swift**: 测试文件上传功能 - **CancelRequestTests.swift**: 测试请求取消功能 - **EquatableTests.swift**: 测试Equatable协议实现 - **CacheTests.swift**: 测试缓存管理功能 - **TestHelpers.swift**: 共用测试工具类和请求类 - **CompilationTest.swift**: 编译验证测试 - **ServiceTests.swift**: 测试网络服务功能 - **LoggerTests.swift**: 测试日志系统功能 - **InterceptorTests.swift**: 测试拦截器功能 - **ConfigurationTests.swift**: 测试配置功能 - **MockHelpers.swift**: 模拟测试辅助类 - **CompilationTests.swift**: 综合性编译测试 ### 运行测试 - 打开 `Example/RRNetwork.xcworkspace` - 按 `Cmd+U` 运行所有测试 ### 测试覆盖 - ✅ 请求取消功能 - ✅ 取消错误处理 - ✅ 状态管理机制 - ✅ 文件上传功能 - ✅ MIME类型检测 - ✅ 批量请求管理 - ✅ 标签和分组管理 - ✅ Equatable协议实现 - ✅ 缓存管理功能 - ✅ 编译验证测试 ## 文件上传快速参考 ### 常用上传方式 ```swift // 1. 大文件上传(推荐)- 内存友好 let request = RRFileUploadRequest(filePath: "/path/to/large/file.mp4") // 2. 小文件上传 - 使用数据 let request = RRFileUploadRequest(fileData: imageData, mimeType: "image/jpeg") // 3. 表单上传 - multipart/form-data(推荐使用回调方式) class FormUploadRequest: RRBaseRequest { private let imageURL: URL private let userId: String init(imageURL: URL, userId: String) { self.imageURL = imageURL self.userId = userId super.init() self.parameterEncoding = .multipart // 推荐:使用回调方式(内存友好,支持 OC) self.multipartFormDataBuilder = { [weak self] builder in guard let self = self else { return } builder.appendField(name: "userId", value: self.userId) builder.appendImageFile(at: self.imageURL, name: "avatar") } } } ``` ### Multipart 表单上传详细指南 #### 1. 推荐方式:使用回调构建器(内存友好 + OC 兼容) ```swift class UserProfileUploadRequest: RRBaseRequest { private let avatarURL: URL private let userId: String private let bio: String init(avatarURL: URL, userId: String, bio: String) { self.avatarURL = avatarURL self.userId = userId self.bio = bio super.init() self.path = "/api/user/profile" self.method = .post self.parameterEncoding = .multipart // parameters 中的内容会自动作为表单字段添加 self.parameters = [ "userId": self.userId, "bio": self.bio ] // multipartFormDataBuilder 用于添加文件和额外字段 self.multipartFormDataBuilder = { [weak self] builder in guard let self = self else { return } // 添加文件(内存友好) builder.appendImageFile(at: self.avatarURL, name: "avatar") } } } ``` #### 2. Objective-C 使用示例 ```objc // 在 .h 文件中 @interface OCUserProfileUploadRequest : RRBaseRequest + (instancetype)createWithAvatarData:(NSData *)avatarData userId:(NSString *)userId bio:(NSString *)bio; @end // 在 .m 文件中 @implementation OCUserProfileUploadRequest + (instancetype)createWithAvatarData:(NSData *)avatarData userId:(NSString *)userId bio:(NSString *)bio { OCUserProfileUploadRequest *request = [[OCUserProfileUploadRequest alloc] init]; request.path = @"/api/user/profile"; request.method = RRHTTPMethodPost; request.parameterEncoding = RRParameterEncodingMultipart; // parameters 中的内容会自动作为表单字段添加 request.parameters = @{ @"userId": userId, @"bio": bio }; // multipartFormDataBuilder 用于添加文件 request.multipartFormDataBuilder = ^(RRMultipartFormDataBuilder *builder) { [builder appendImage:avatarData name:@"avatar" filename:@"avatar.jpg"]; }; return request; } @end ``` #### 3. 数据构建逻辑 **自动处理流程:** 1. **parameters 字段**:`parameters` 中的所有键值对会自动作为表单字段添加 2. **额外内容**:`multipartFormDataBuilder` 回调用于添加文件和额外字段 3. **合并数据**:两部分数据会合并成完整的 multipart 请求 **示例数据流:** ```swift // 设置基本参数 self.parameters = ["userId": "12345", "type": "avatar"] // 添加文件 self.multipartFormDataBuilder = { builder in builder.appendImageFile(at: imageURL, name: "file") } // 最终 multipart 数据包含: // - userId: "12345" (来自 parameters) // - type: "avatar" (来自 parameters) // - file: image.jpg (来自 builder) ``` #### 4. 构建器 API 参考 ```swift // 文本字段 builder.appendField(name: "key", value: "value") // 小文件数据(注意内存占用) builder.appendData(data, name: "file", filename: "image.jpg", mimeType: "image/jpeg") // 文件路径(推荐,内存友好) builder.appendFile(at: fileURL, name: "file", filename: "custom.jpg", mimeType: "image/jpeg") // 便利方法 builder.appendImage(imageData, name: "avatar", filename: "avatar.jpg", imageType: "jpg") builder.appendImageFile(at: imageURL, name: "avatar", filename: "avatar.jpg") ``` #### 5. 设计优势 - **内存友好**:文件数据在请求时才读取,不会长期占用内存 - **OC 兼容**:完全支持 Objective-C 调用 - **自动合并**:parameters 和 builder 数据自动合并 - **职责分离**:基本字段用 parameters,文件用 builder - **灵活构建**:支持动态构建 multipart 数据 - **自动推断**:自动推断文件的 MIME 类型 - **简洁统一**:只有一套 API,避免混淆 ### 支持的文件格式(自动MIME检测) | 类型 | 扩展名 | MIME类型示例 | | ---- | ------------------------ | -------------------------------------------- | | 图片 | jpg, png, gif, bmp, webp | image/jpeg, image/png | | 视频 | mp4, avi, mov, wmv | video/mp4, video/quicktime | | 音频 | mp3, wav, flac, aac | audio/mpeg, audio/wav | | 文档 | pdf, doc, docx, xls | application/pdf, application/msword | | 压缩 | zip, rar, 7z, tar | application/zip, application/x-7z-compressed | ### 内存使用指南 | 文件大小 | 推荐方式 | 内存占用 | | ----------- | ------------------ | -------- | | < 1MB | 文件数据或文件路径 | 低 | | 1MB - 100MB | 文件路径 | 极低 | | > 100MB | 文件路径 | 极低 | ## 待完成 - 增加适配器以适配不同网络框架 - 完善单元测试 - 完善测试UI Demo ## 许可证 RRNetwork 基于 MIT 许可证开源。详见 LICENSE 文件。