Moya+Alamofire搭建网络框架

要基于 Moya+Alamofire 搭建一套适配 RESTful API、可直接落地生产的 iOS 网络框架,核心需求是实现请求统一管理、全局配置、自动数据解析、统一异常处理、请求解耦 ,同时兼顾易用性、可扩展性、贴合 MVVM 架构 ,让业务层无需关注底层网络细节,一行代码就能发起 RESTful 请求 ------ 这套框架会贴合 RESTful「资源驱动、HTTP 标准」的特性,基于 Swift 5.5+、Moya 15.x+(最新稳定版)实现,包含生产级必备的多环境、请求拦截、加载状态、缓存策略等能力,代码可直接复制复用。

以下方案遵循模块化、协议化、单例化 设计原则,从环境准备→框架核心设计→完整代码封装→初始化配置→业务层使用→进阶扩展逐步实现,贴合 iOS 现代开发思想(Combine/RxSwift、MVVM),同时保留 Moya「枚举管理请求、解耦 URL / 参数」的核心优势,以及 Alamofire 的底层网络能力。

一、前置知识与环境准备

1. 核心依赖关系

Moya 是Alamofire 的上层封装 ,专门解决 RESTful API 的「请求分散、URL 硬编码、参数混乱」问题,核心能力是用枚举集中管理所有 RESTful 请求 ;Alamofire 负责底层 HTTP 请求、网络会话管理,二者搭配是 iOS RESTful 开发的事实标准,无需额外引入其他网络库。

2. Podfile 配置

适配 iOS 13+、Swift 5.5+,引入核心依赖(含 Combine 扩展,贴合现代响应式开发):

ruby

复制代码
platform :ios, '13.0'
use_frameworks!

target 'YourApp' do
  # Moya核心(内置Alamofire依赖,无需单独引入)
  pod 'Moya', '~> 15.0'
  # Moya的Combine扩展(适配iOS响应式开发,替代闭包)
  pod 'Moya/Combine'
  # 可选:RxSwift/RxCocoa扩展(若项目用RxSwift而非Combine)
  # pod 'Moya/RxSwift'
  # JSON解析辅助(可选,简化Codable使用)
  pod 'HandyJSON', '~> 5.0'
end

执行 pod install 完成依赖安装,建议使用Xcode 14+ 保证语法兼容。

3. 核心前提:RESTful 规范对齐

框架基于标准 RESTful API 设计,需和服务端对齐以下规则(企业级项目通用):

  1. URL 规范 :基础路径 + 资源路径(如https://api.xxx.com/v1/user),版本号放 URL(v1/v2)或请求头;
  2. HTTP 方法:GET(查询)、POST(创建)、PUT(全量更新)、PATCH(局部更新)、DELETE(删除);
  3. 数据格式 :请求 / 响应均为JSON,请求头Content-Type: application/json
  4. 响应格式:服务端返回统一格式(核心!框架解析依赖此格式),示例:

json

复制代码
{
  "code": 200,        // 业务状态码(200成功,其他失败)
  "message": "success",// 提示信息(失败时为错误描述)
  "data": {}          // 业务数据(成功时返回,失败时为null/{})
  "timestamp": 1735689600 // 可选:时间戳,用于请求校验
}
  1. 错误码约定 :如401(Token 过期)、403(无权限)、500(服务端内部错误),框架会统一处理。

二、框架核心设计思路与目录结构

1. 核心设计原则

  • 请求解耦:用 Moya 枚举集中管理所有 RESTful 请求,避免 URL / 参数分散在业务层;
  • 全局统一:单例管理全局配置(基础 URL、请求头、超时、多环境),支持运行时动态更新;
  • 解析自动化 :基于 Swift Codable 封装统一数据解析,业务层直接获取强类型模型,无需手动解析 JSON;
  • 异常归一化:将 Alamofire/Moya 原生错误、服务端业务错误、网络错误统一为自定义枚举,业务层只需处理一种错误类型;
  • 层间隔离:按「配置层→基础层→核心层→工具层」拆分,业务层仅依赖工具层,底层修改不影响上层;
  • 贴合 MVVM:基于 Combine/RxSwift 封装请求,返回可观察序列,支持数据绑定和响应式开发。

2. 标准目录结构

在项目中创建Network/RESTful目录,严格按模块拆分,便于维护和扩展,建议直接复用

plaintext

复制代码
Network/
└── RESTful/
    ├── Config/                # 配置层:全局配置、多环境、单例管理
    │   ├── NetworkConfig.swift # 全局配置(基础URL、超时、请求头)
    │   └── NetworkManager.swift # 核心单例(MoyaProvider管理、环境切换)
    ├── Base/                  # 基础层:通用模型、自定义错误、协议
    │   ├── BaseResponse.swift  # 统一响应模型(适配服务端返回格式)
    │   └── NetworkError.swift  # 全局自定义错误枚举(所有错误归一)
    ├── Core/                  # 核心层:Moya请求封装、解析逻辑、拦截器
    │   ├── BaseAPI.swift       # Moya基础枚举(所有业务API的父类)
    │   └── MoyaRequest.swift   # 统一请求封装(Combine/RxSwift)
    └── Tool/                  # 工具层:业务层极简入口(一行代码发起请求)
        └── RESTfulRequestTool.swift

业务 API 目录(和 Network 同级,按业务模块拆分请求枚举):

plaintext

复制代码
API/
├── UserAPI.swift   # 用户模块(登录、获取用户信息)
├── OrderAPI.swift  # 订单模块(创建订单、查询订单)
└── GoodsAPI.swift  # 商品模块(查询商品、加入购物车)

三、框架完整代码封装

所有代码均做了容错处理、内存安全([weak self])、线程切换,贴合生产级要求,关键位置添加详细注释,可直接复制到对应文件中。

3.1 基础层:统一响应模型 + 自定义错误

3.1.1 统一响应模型(BaseResponse.swift)

核心作用 :适配服务端统一的 JSON 返回格式,封装Codable 解析逻辑,业务层只需关注泛型T(实际业务模型),屏蔽通用字段(code/message)。

swift

复制代码
import Foundation
import Moya

/// RESTful统一响应模型(必须和服务端返回格式严格一致)
/// T:业务数据模型(如User、Order,需遵守Codable)
struct BaseResponse<T: Codable>: Codable {
    let code: Int          // 业务状态码
    let message: String    // 提示/错误信息
    let data: T?           // 业务数据(成功时有值,失败时nil)
    let timestamp: TimeInterval? // 可选:服务端时间戳
    
    /// 快速判断是否请求成功(根据业务状态码,默认200为成功)
    var isSuccess: Bool {
        return code == 200
    }
}

/// 无数据响应(用于无需返回业务数据的请求,如退出登录、删除操作)
struct EmptyResponse: Codable {}
3.1.2 全局自定义错误(NetworkError.swift)

核心作用 :将「网络错误、解析错误、服务端业务错误、Token 过期、无网络」等所有异常归一为一个枚举,业务层只需处理NetworkError,屏蔽底层错误复杂性,支持本地化描述和自定义业务错误。

swift

复制代码
import Foundation
import Moya
import Alamofire

/// RESTful全局自定义错误(覆盖所有可能的错误场景)
enum NetworkError: Error, LocalizedError {
    case invalidBaseURL          // 无效的基础URL(配置错误)
    case moyaError(MoyaError)    // Moya/Alamofire原生错误(网络、超时、404/500等)
    case serverError(Int, String) // 服务端业务错误(非200状态码,code+message)
    case jsonParseError(String)  // JSON解析错误(模型和返回数据不匹配)
    case emptyResponseData       // 服务端返回data为nil(但业务要求有数据)
    case tokenExpired            // Token过期(自定义业务错误,code=401)
    case noNetwork               // 无网络(需结合网络检测扩展)
    case customError(String)     // 本地自定义错误(如参数校验失败)
    
    /// 本地化错误描述(直接给用户看/调试用,支持多语言可扩展)
    var errorDescription: String? {
        switch self {
        case .invalidBaseURL:
            return "网络服务地址配置错误"
        case .moyaError(let error):
            return "网络请求错误:\(error.localizedDescription)"
        case .serverError(_, let msg):
            return msg.isEmpty ? "服务端业务错误" : msg
        case .jsonParseError(let msg):
            return "数据解析错误:\(msg)"
        case .emptyResponseData:
            return "服务端返回空数据"
        case .tokenExpired:
            return "登录状态已过期,请重新登录"
        case .noNetwork:
            return "网络连接失败,请检查网络设置"
        case .customError(let msg):
            return msg
        }
    }
    
    /// 快速判断是否为Token过期(方便全局处理)
    var isTokenExpired: Bool {
        return self == .tokenExpired
    }
    
    /// 从Moya错误转换为自定义错误(便捷初始化)
    static func fromMoyaError(_ error: MoyaError) -> NetworkError {
        return .moyaError(error)
    }
    
    /// 从服务端响应转换为业务错误(便捷初始化)
    static func fromServerCode(_ code: Int, _ msg: String) -> NetworkError {
        if code == 401 { return .tokenExpired } // 约定401为Token过期
        return .serverError(code, msg)
    }
}

3.2 配置层:全局配置 + Moya 单例管理

3.2.1 全局网络配置(NetworkConfig.swift)

核心作用 :管理多环境、基础 URL、全局请求头、超时时间等配置,通过枚举管理多环境(开发 / 测试 / 生产),避免硬编码,支持 Build Configuration 自动切换环境。

swift

复制代码
import Foundation
import Moya

/// 网络环境枚举(开发/测试/生产,按项目需求扩展)
enum NetworkEnvironment: String {
    case dev  // 开发环境
    case test // 测试环境
    case prod // 生产环境
    
    /// 对应环境的基础URL(RESTful核心,所有请求拼接此路径)
    var baseURL: String {
        switch self {
        case .dev:
            return "https://dev-api.xxx.com/v1"
        case .test:
            return "https://test-api.xxx.com/v1"
        case .prod:
            return "https://api.xxx.com/v1"
        }
    }
}

/// RESTful全局网络配置(单例+静态配置,App启动时初始化)
final class NetworkConfig {
    // 单例(GCD一次性初始化,线程安全)
    static let shared = NetworkConfig()
    private init() {}
    
    // MARK: - 可配置项(App启动时设置,支持运行时动态更新)
    /// 当前网络环境(默认开发环境,可根据Build Configuration自动切换)
    var currentEnv: NetworkEnvironment = .dev
    /// 全局请求超时时间(默认30秒,单个请求可覆盖)
    var timeoutInterval: TimeInterval = 30
    /// 全局请求头(如Token、App版本、设备类型,支持运行时动态更新)
    var globalHeaders: [String: String] = [:]
    /// 是否开启调试模式(DEBUG下打印请求/响应日志,RELEASE关闭)
    var isDebug: Bool = true
    /// 全局参数(如appId、deviceId,所有请求自动拼接)
    var globalParameters: [String: Any] = [:]
    
    // MARK: - 便捷获取当前基础URL
    var baseURL: String {
        return currentEnv.baseURL
    }
    
    // MARK: - 动态更新全局请求头(如Token刷新、用户切换)
    func updateGlobalHeaders(_ newHeaders: [String: String]) {
        globalHeaders.merge(newHeaders) { _, new in new }
    }
    
    // MARK: - 清空全局请求头(如用户退出登录)
    func clearGlobalHeaders() {
        globalHeaders.removeAll()
    }
    
    // MARK: - 动态更新全局参数
    func updateGlobalParams(_ newParams: [String: Any]) {
        globalParameters.merge(newParams) { _, new in new }
    }
}

// MARK: - 可选:根据Build Configuration自动切换环境
extension NetworkConfig {
    /// 从Xcode Build Configuration读取环境(需先配置Xcode的Scheme)
    func configEnvFromBuildConfig() {
        #if DEBUG
        currentEnv = .dev
        #elseif TEST
        currentEnv = .test
        #else
        currentEnv = .prod
        #endif
    }
}
3.2.2 Moya 核心单例管理(NetworkManager.swift)

核心作用 :全局唯一管理MoyaProvider实例(Moya 的核心请求器),配置请求拦截器、插件(日志 / 网络活动)、会话管理器 ,支持运行时重新初始化 Provider(如更新请求头后生效),同时封装 Moya 插件(日志、网络状态)。

swift

复制代码
import Foundation
import Moya
import Alamofire

/// RESTful网络核心单例(管理MoyaProvider、插件、请求拦截)
final class NetworkManager {
    // 单例
    static let shared = NetworkManager()
    private init() {}
    
    // 全局MoyaProvider(泛型为BaseAPI,所有业务API的父类)
    private var moyaProvider: MoyaProvider<BaseAPI>!
    
    // MARK: - 初始化MoyaProvider(App启动时调用,核心!)
    func setupProvider() {
        // 1. 配置Alamofire会话管理器(超时、请求头、信任证书等)
        let sessionManager = createSessionManager()
        // 2. 配置Moya插件(日志插件、网络活动插件,按需添加)
        let plugins = createMoyaPlugins()
        // 3. 初始化MoyaProvider(指定会话管理器和插件)
        moyaProvider = MoyaProvider<BaseAPI>(
            session: sessionManager,
            plugins: plugins,
            trackInflights: true // 防止重复请求(相同URL+参数的请求合并)
        )
    }
    
    // MARK: - 对外提供MoyaProvider(业务层无需直接访问)
    func getProvider() -> MoyaProvider<BaseAPI> {
        if moyaProvider == nil { setupProvider() }
        return moyaProvider
    }
    
    // MARK: - 重新初始化Provider(动态更新配置后调用,如切换环境、更新Token)
    func reloadProvider() {
        setupProvider()
    }
}

// MARK: - 私有方法:创建Alamofire会话管理器(配置超时、请求头)
extension NetworkManager {
    private func createSessionManager() -> Session {
        let config = URLSessionConfiguration.default
        // 设置全局超时时间
        config.timeoutIntervalForRequest = NetworkConfig.shared.timeoutInterval
        config.timeoutIntervalForResource = NetworkConfig.shared.timeoutInterval
        // 开启HTTP缓存(可选,根据业务需求配置)
        config.requestCachePolicy = .useProtocolCachePolicy
        // 配置全局请求头(从NetworkConfig读取)
        config.httpAdditionalHeaders = NetworkConfig.shared.globalHeaders
        // 创建Alamofire会话管理器
        return Session(configuration: config)
    }
}

// MARK: - 私有方法:创建Moya插件(日志、网络活动,按需扩展)
extension NetworkManager {
    private func createMoyaPlugins() -> [PluginType] {
        var plugins: [PluginType] = []
        // 1. 网络活动插件(控制状态栏网络加载指示器)
        plugins.append(NetworkActivityPlugin { state, _ in
            switch state {
            case .began: UIApplication.shared.isNetworkActivityIndicatorVisible = true
            case .ended: UIApplication.shared.isNetworkActivityIndicatorVisible = false
            }
        })
        // 2. 调试日志插件(仅DEBUG模式生效,避免生产环境泄露信息)
        if NetworkConfig.shared.isDebug {
            plugins.append(CustomLogPlugin())
        }
        // 可选:添加自定义插件(如请求加密、响应解密、Token刷新)
        return plugins
    }
}

// MARK: - 自定义日志插件(打印请求/响应详情,替代Moya默认的NetworkLoggerPlugin)
final class CustomLogPlugin: PluginType {
    func willSend(_ request: RequestType, target: TargetType) {
        // 打印请求信息
        print("📡 [RESTful Request] \(target.method.rawValue) | \(target.baseURL.absoluteString + target.path)")
        print("📋 [Request Headers] \(request.request?.allHTTPHeaderFields ?? [:])")
        if let params = target.parameters {
            print("📝 [Request Params] \(params)")
        }
    }
    
    func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
        switch result {
        case .success(let response):
            // 打印成功响应
            print("✅ [RESTful Success] \(target.baseURL.absoluteString + target.path) | StatusCode: \(response.statusCode)")
            if let json = try? response.mapJSON() {
                print("📦 [Response Data] \(json)")
            }
        case .failure(let error):
            // 打印失败信息
            print("❌ [RESTful Failure] \(target.baseURL.absoluteString + target.path) | Error: \(error.localizedDescription)")
        }
    }
}

3.3 核心层:Moya 基础枚举 + 统一请求封装

3.3.1 Moya 基础枚举(BaseAPI.swift)

核心作用 :作为所有业务 API 枚举的父类 ,实现 Moya 的TargetType协议的通用方法(基础 URL、请求头、参数编码等),业务 API 只需实现个性化部分(path、method、parameters),减少重复代码,是 Moya 解耦的核心。

swift

复制代码
import Foundation
import Moya

/// Moya基础API枚举(所有业务API的父类,实现通用TargetType方法)
/// 业务API只需遵循此协议,实现个性化属性即可
protocol BaseAPI: TargetType {
    /// 单个请求的超时时间(nil则使用全局配置)
    var requestTimeout: TimeInterval? { get }
    /// 是否忽略全局请求头(如登录请求无需Token)
    var ignoreGlobalHeaders: Bool { get }
    /// 是否忽略全局参数(如部分请求无需拼接appId)
    var ignoreGlobalParams: Bool { get }
}

// MARK: - 实现BaseAPI的默认TargetType方法(通用配置,业务层无需重写)
extension BaseAPI {
    // 基础URL(从全局配置读取)
    var baseURL: URL {
        guard let url = URL(string: NetworkConfig.shared.baseURL) else {
            fatalError(NetworkError.invalidBaseURL.errorDescription!)
        }
        return url
    }
    
    // 请求方法(默认GET,业务层可重写)
    var method: Moya.Method { .get }
    
    // 请求参数(默认空,业务层重写)
    var parameters: [String: Any]? { nil }
    
    // 参数编码方式(默认JSON,表单提交可重写为URLEncoding)
    var parameterEncoding: ParameterEncoding {
        return JSONEncoding.default
    }
    
    // 任务类型(默认请求数据,上传/下载可重写)
    var task: Task {
        // 合并全局参数和单个请求参数
        var allParams = NetworkConfig.shared.globalParameters
        if let params = parameters, !ignoreGlobalParams {
            allParams.merge(params) { _, new in new }
        }
        return .requestParameters(parameters: allParams, encoding: parameterEncoding)
    }
    
    // 请求头(合并全局请求头,单个请求可重写)
    var headers: [String: String]? {
        ignoreGlobalHeaders ? nil : NetworkConfig.shared.globalHeaders
    }
    
    // 单元测试用(默认空,无需实现)
    var sampleData: Data { Data() }
    
    // 单个请求超时(默认nil,使用全局)
    var requestTimeout: TimeInterval? { nil }
    
    // 是否忽略全局请求头(默认false)
    var ignoreGlobalHeaders: Bool { false }
    
    // 是否忽略全局参数(默认false)
    var ignoreGlobalParams: Bool { false }
}

// MARK: - 扩展:支持上传/下载任务(按需使用)
extension BaseAPI {
    /// 构建文件上传任务(Multipart/form-data,如图片/文件上传)
    func uploadTask(with formData: [MultipartFormData]) -> Task {
        return .uploadMultipart(formData)
    }
    
    /// 构建文件下载任务
    func downloadTask(with destination: DownloadDestination) -> Task {
        return .downloadDestination(destination)
    }
}
3.3.2 统一请求封装(MoyaRequest.swift)

核心作用 :封装 Moya 的原生请求,基于Combine 实现响应式请求,完成统一错误转换、JSON 自动解析、线程切换 (结果切到主线程,方便业务层更新 UI),屏蔽 Moya 的Publisher细节,返回业务层易使用的AnyPublisher

若项目使用RxSwift ,只需将 Combine 的Future/AnyPublisher替换为 RxSwift 的Single/Observable,逻辑完全一致。

swift

复制代码
import Foundation
import Moya
import Combine

/// RESTful请求配置(单个请求自定义配置,覆盖全局)
struct RESTfulRequestConfig {
    var cachePolicy: URLRequest.CachePolicy? // 缓存策略(可选)
    var retryCount: Int = 0                  // 失败重试次数(默认0,不重试)
}

/// Moya请求核心封装(统一处理请求、解析、错误转换)
final class MoyaRequest {
    // 单例
    static let shared = MoyaRequest()
    private init() {}
    
    // 全局MoyaProvider
    private var moyaProvider: MoyaProvider<BaseAPI> {
        return NetworkManager.shared.getProvider()
    }
}

// MARK: - 核心:发起RESTful请求并自动解析为强类型模型
extension MoyaRequest {
    /// 执行RESTful请求,自动解析为泛型模型
    /// - Parameters:
    ///   - api: 业务API枚举(遵循BaseAPI)
    ///   - config: 单个请求配置(可选)
    ///   - T: 业务数据模型(需遵守Codable)
    /// - Returns: AnyPublisher<T, NetworkError>(成功返回业务模型,失败返回自定义错误)
    func request<T: Codable>(
        _ api: BaseAPI,
        config: RESTfulRequestConfig = .init()
    ) -> AnyPublisher<T, NetworkError> {
        return Future<T, NetworkError> { [weak self] promise in
            guard let self = self else {
                promise(.failure(.customError("MoyaRequest已释放")))
                return
            }
            // 1. 获取Moya请求的Publisher
            let requestPublisher = self.moyaProvider.requestPublisher(api)
                // 超时处理(覆盖全局/单个请求的超时)
                .timeout(api.requestTimeout ?? NetworkConfig.shared.timeoutInterval, scheduler: DispatchQueue.main)
                // 重试机制
                .retry(config.retryCount)
            
            // 2. 订阅请求结果
            _ = requestPublisher.sink(
                receiveCompletion: { completion in
                    if case .failure(let moyaError) = completion {
                        // Moya原生错误转换为自定义错误
                        promise(.failure(NetworkError.fromMoyaError(moyaError)))
                    }
                },
                receiveValue: { [weak self] response in
                    // 3. 处理响应数据,统一解析
                    self?.handleResponse(response, promise: promise)
                }
            )
        }
        // 切换到主线程(业务层可直接更新UI,无需手动切换)
        .receive(on: DispatchQueue.main)
        // 封装为AnyPublisher,屏蔽底层实现
        .eraseToAnyPublisher()
    }
    
    /// 执行无数据返回的请求(如退出登录、删除),仅返回是否成功
    func requestWithoutData(
        _ api: BaseAPI,
        config: RESTfulRequestConfig = .init()
    ) -> AnyPublisher<Bool, NetworkError> {
        return request<EmptyResponse>(api, config: config)
            .map { _ in true } // 解析成功即返回true
            .eraseToAnyPublisher()
    }
}

// MARK: - 私有方法:统一处理Moya响应,完成JSON解析和错误判断
extension MoyaRequest {
    private func handleResponse<T: Codable>(
        _ response: Response,
        promise: @escaping (Result<T, NetworkError>) -> Void
    ) {
        do {
            // 1. 验证HTTP状态码(200~299为成功,否则抛错)
            let validResponse = try response.filterSuccessfulStatusCodes()
            // 2. 解析为统一响应模型BaseResponse<T>
            let baseResponse = try validResponse.decode(BaseResponse<T>.self)
            // 3. 判断业务状态码是否成功
            if baseResponse.isSuccess {
                // 4. 检查业务数据是否为空
                guard let data = baseResponse.data else {
                    promise(.failure(.emptyResponseData))
                    return
                }
                // 5. 解析成功,返回业务模型
                promise(.success(data))
            } else {
                // 6. 业务错误,转换为自定义错误
                promise(.failure(NetworkError.fromServerCode(baseResponse.code, baseResponse.message)))
            }
        } catch let moyaError as MoyaError {
            // Moya过滤状态码失败(如404/500)
            promise(.failure(NetworkError.fromMoyaError(moyaError)))
        } catch let decodeError as DecodingError {
            // JSON解析错误(模型和数据不匹配)
            promise(.failure(.jsonParseError(decodeError.localizedDescription)))
        } catch {
            // 其他未知错误
            promise(.failure(.customError(error.localizedDescription)))
        }
    }
}

// MARK: - 扩展:Moya Response快速解码为Codable模型
extension Response {
    /// 快速将Moya Response解码为遵循Codable的模型
    func decode<T: Codable>(_ type: T.Type) throws -> T {
        return try JSONDecoder().decode(type, from: self.data)
    }
}

3.4 工具层:业务层极简入口(RESTfulRequestTool.swift)

核心作用 :对核心层的MoyaRequest做一层极简封装 ,是业务层唯一需要依赖的文件 ,屏蔽所有底层细节,业务层只需一行代码发起请求,无需创建实例、处理泛型细节,最大限度降低使用成本。

swift

复制代码
import Foundation
import Moya
import Combine

/// RESTful请求工具类(业务层唯一入口,极简API)
/// 所有业务层请求均通过此类发起,无需接触底层Moya/Alamofire
struct RESTfulRequestTool {
    // 私有化构造器,避免实例化
    private init() {}
    
    // MARK: - 发起带数据返回的请求(自动解析为强类型模型)
    static func request<T: Codable>(
        _ api: BaseAPI,
        config: RESTfulRequestConfig = .init()
    ) -> AnyPublisher<T, NetworkError> {
        return MoyaRequest.shared.request(api, config: config)
    }
    
    // MARK: - 发起无数据返回的请求(仅返回是否成功)
    static func requestWithoutData(
        _ api: BaseAPI,
        config: RESTfulRequestConfig = .init()
    ) -> AnyPublisher<Bool, NetworkError> {
        return MoyaRequest.shared.requestWithoutData(api, config: config)
    }
}

// MARK: - Combine扩展:简化订阅,自动存储Cancellable(业务层必备)
/// 给AnyPublisher添加扩展,方便业务层快速订阅,无需手动管理Cancellable
extension AnyPublisher {
    /// 快速订阅RESTful请求结果
    /// - Parameters:
    ///   - cancellables: 存储订阅的Cancellable(业务层只需创建Set<AnyCancellable>()即可)
    ///   - onSuccess: 成功回调(返回强类型模型)
    ///   - onFailure: 失败回调(返回自定义NetworkError)
    func sinkRESTful(
        to cancellables: inout Set<AnyCancellable>,
        onSuccess: @escaping (Output) -> Void,
        onFailure: @escaping (Failure) -> Void
    ) where Failure == NetworkError {
        self.sink(
            receiveCompletion: { completion in
                if case .failure(let error) = completion {
                    onFailure(error)
                }
            },
            receiveValue: { value in
                onSuccess(value)
            }
        )
        .store(in: &cancellables)
    }
}

四、业务 API 枚举编写(按模块拆分)

基于上述框架,业务层只需按模块编写 API 枚举 (遵循BaseAPI协议),实现path、method、parameters等个性化属性即可,无需关注任何底层网络逻辑 ------ 这是 Moya 解耦的核心优势,所有请求集中管理,便于维护和修改。

以下以用户模块(UserAPI.swift) 为例,包含登录、获取用户信息、更新用户信息、退出登录等典型 RESTful 请求:

swift

复制代码
import Foundation
import Moya

/// 用户模块API枚举(遵循BaseAPI,实现个性化属性)
/// 所有用户相关请求集中管理,路径清晰,修改方便
enum UserAPI: BaseAPI {
    // 登录(POST,忽略全局Token)
    case login(account: String, password: String)
    // 根据ID获取用户信息(GET,需要用户ID参数)
    case fetchUser(id: String)
    // 更新用户信息(PUT,传入用户信息参数)
    case updateUser(name: String, avatar: String, phone: String)
    // 退出登录(POST,无参数,无返回数据)
    case logout
    
    // MARK: - 实现BaseAPI的个性化属性
    /// 请求路径(拼接在baseURL后,如/baseURL/user/login)
    var path: String {
        switch self {
        case .login: return "/user/login"
        case .fetchUser: return "/user/info"
        case .updateUser: return "/user/update"
        case .logout: return "/user/logout"
        }
    }
    
    /// HTTP请求方法(重写默认的GET)
    var method: Moya.Method {
        switch self {
        case .login, .updateUser, .logout: return .post
        case .fetchUser: return .get
        }
    }
    
    /// 请求参数(根据不同case拼接参数)
    var parameters: [String: Any]? {
        switch self {
        case .login(let account, let pwd):
            return ["account": account, "password": pwd]
        case .fetchUser(let id):
            return ["userId": id]
        case .updateUser(let name, let avatar, let phone):
            return ["userName": name, "avatarUrl": avatar, "phone": phone]
        case .logout:
            return nil
        }
    }
    
    /// 登录请求忽略全局Token(重写默认的false)
    var ignoreGlobalHeaders: Bool {
        return self == .login
    }
    
    /// 单个请求超时(登录请求超时15秒,重写全局30秒)
    var requestTimeout: TimeInterval? {
        return self == .login ? 15 : nil
    }
}

其他模块(如订单、商品) 按相同方式编写枚举即可,示例:

swift

复制代码
// OrderAPI.swift
enum OrderAPI: BaseAPI {
    case fetchOrderList(page: Int, size: Int) // 获取订单列表(GET,分页参数)
    case createOrder(goodsId: [String], totalPrice: Double) // 创建订单(POST)
    
    var path: String {
        switch self {
        case .fetchOrderList: return "/order/list"
        case .createOrder: return "/order/create"
        }
    }
    
    var method: Moya.Method {
        return self == .fetchOrderList ? .get : .post
    }
    
    var parameters: [String: Any]? {
        switch self {
        case .fetchOrderList(let page, let size):
            return ["pageNum": page, "pageSize": size]
        case .createOrder(let ids, let price):
            return ["goodsIds": ids, "totalPrice": price]
        }
    }
}

五、框架初始化配置(App 启动时执行)

在 App 启动入口(AppDelegate/SceneDelegate,SwiftUI 为App结构体)中完成全局网络配置MoyaProvider 初始化 ,这是框架使用的前提,只需配置一次,全局生效。

5.1 UIKit 项目(AppDelegate)

swift

复制代码
import UIKit
import Combine

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    // 全局Cancellable(存储全局订阅,如Token过期监听)
    private var cancellables = Set<AnyCancellable>()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // 1. 初始化网络框架(核心!必须先执行)
        setupNetworkFramework()
        // 2. 全局处理Token过期(如跳转到登录页)
        setupGlobalTokenExpiredHandler()
        return true
    }
    
    // MARK: - 初始化网络框架:配置环境、请求头、初始化MoyaProvider
    private func setupNetworkFramework() {
        let netConfig = NetworkConfig.shared
        // 1. 自动从Build Configuration切换环境(推荐)
        netConfig.configEnvFromBuildConfig()
        // 手动设置环境(测试用):netConfig.currentEnv = .dev
        // 2. 配置全局超时
        netConfig.timeoutInterval = 20
        // 3. 配置全局请求头(App版本、设备类型、OS版本等)
        netConfig.globalHeaders = [
            "Content-Type": "application/json",
            "App-Version": Bundle.main.appVersion ?? "1.0.0",
            "Device-Type": "iOS",
            "OS-Version": UIDevice.current.systemVersion,
            "Token": UserDefaults.standard.string(forKey: "kUserToken") ?? "" // 本地缓存的Token
        ]
        // 4. 配置全局参数(所有请求自动拼接)
        netConfig.globalParameters = [
            "appId": "ios_123456",
            "deviceId": UIDevice.current.identifierForVendor?.uuidString ?? ""
        ]
        // 5. 开启/关闭调试模式
        netConfig.isDebug = true
        // 6. 初始化MoyaProvider(核心!)
        NetworkManager.shared.setupProvider()
    }
    
    // MARK: - 全局处理Token过期(示例:发送通知,跳转到登录页)
    private func setupGlobalTokenExpiredHandler() {
        NotificationCenter.default.publisher(for: NSNotification.Name("kRESTfulTokenExpired"))
            .sink { [weak self] _ in
                guard let self = self else { return }
                // 1. 清除本地Token
                UserDefaults.standard.removeObject(forKey: "kUserToken")
                // 2. 清空全局请求头的Token并重新初始化Provider
                NetworkConfig.shared.clearGlobalHeaders()
                NetworkManager.shared.reloadProvider()
                // 3. 跳转到登录页(按项目实际UI架构修改)
                self.gotoLoginPage()
            }
            .store(in: &cancellables)
    }
    
    // 跳转到登录页(UIKit示例)
    private func gotoLoginPage() {
        let loginVC = LoginViewController()
        let nav = UINavigationController(rootViewController: loginVC)
        window?.rootViewController = nav
        window?.makeKeyAndVisible()
    }
}

// MARK: - 可选:Bundle扩展(获取App版本号和Build号)
extension Bundle {
    var appVersion: String? {
        return infoDictionary?["CFBundleShortVersionString"] as? String
    }
    
    var appBuild: String? {
        return infoDictionary?["CFBundleVersion"] as? String
    }
}

5.2 SwiftUI 项目(App 结构体)

swift

复制代码
import SwiftUI
import Combine

@main
struct YourApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    private var cancellables = Set<AnyCancellable>()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear {
                    // 初始化网络框架
                    setupNetworkFramework()
                    // 全局Token过期处理
                    setupGlobalTokenExpiredHandler()
                }
        }
    }
    
    private func setupNetworkFramework() {
        let netConfig = NetworkConfig.shared
        netConfig.configEnvFromBuildConfig()
        netConfig.timeoutInterval = 20
        netConfig.globalHeaders = [
            "Content-Type": "application/json",
            "App-Version": Bundle.main.appVersion ?? "1.0.0",
            "Token": UserDefaults.standard.string(forKey: "kUserToken") ?? ""
        ]
        NetworkManager.shared.setupProvider()
    }
    
    private func setupGlobalTokenExpiredHandler() {
        NotificationCenter.default.publisher(for: NSNotification.Name("kRESTfulTokenExpired"))
            .sink { _ in
                UserDefaults.standard.removeObject(forKey: "kUserToken")
                NetworkConfig.shared.clearGlobalHeaders()
                NetworkManager.shared.reloadProvider()
                // SwiftUI跳转到登录页(通过NavigationStack/Environment实现)
            }
            .store(in: &cancellables)
    }
}

extension Bundle {
    var appVersion: String? {
        infoDictionary?["CFBundleShortVersionString"] as? String
    }
}

六、业务层使用示例(MVVM 架构,核心:一行代码发起请求)

框架贴合MVVM+Combine 的现代 iOS 开发思想,View 层 仅负责 UI 展示和事件绑定,ViewModel 层 发起网络请求、处理业务逻辑,通过@Published将数据 / 状态暴露给 View 层,实现视图与业务解耦

以下以用户模块为例,完成「登录、获取用户信息」的完整业务层使用流程。

6.1 ViewModel 层(核心业务逻辑)

封装网络请求、业务逻辑,通过@Published发布数据、加载状态、错误信息,View 层只需绑定即可,无需处理任何网络细节。

swift

复制代码
import Foundation
import Combine

// 基础ViewModel(封装Cancellable,避免重复代码)
class BaseViewModel {
    /// 存储Combine订阅,防止销毁
    var cancellables = Set<AnyCancellable>()
    /// 加载状态(View层绑定,控制加载动画)
    @Published var isLoading = false
    /// 错误信息(View层绑定,展示错误提示)
    @Published var errorMsg = ""
}

// 用户模块ViewModel
class UserViewModel: BaseViewModel {
    /// 发布用户信息(View层绑定,更新UI)
    @Published var userInfo: UserModel?
    /// 发布登录成功的Token(View层绑定,处理后续逻辑)
    @Published var loginToken: String?
    
    // MARK: - 1. 用户登录(调用UserAPI.login)
    func login(account: String, password: String) {
        // 校验参数(本地业务逻辑,非网络层职责)
        guard !account.isEmpty, !password.isEmpty else {
            errorMsg = "账号或密码不能为空"
            return
        }
        // 开始加载
        isLoading = true
        // 一行代码发起请求,订阅结果(核心!)
        RESTfulRequestTool.request(UserAPI.login(account: account, password: password))
            .sinkRESTful(to: &cancellables) { [weak self] loginResult in
                // 登录成功回调(loginResult为强类型LoginModel)
                self?.isLoading = false
                self?.loginToken = loginResult.token
                // 1. 保存Token到本地(UserDefaults/Keychain)
                UserDefaults.standard.set(loginResult.token, forKey: "kUserToken")
                // 2. 动态更新全局请求头的Token,后续请求自动带上
                NetworkConfig.shared.updateGlobalHeaders(["Token": loginResult.token])
                // 3. 重新初始化MoyaProvider,让新Token生效
                NetworkManager.shared.reloadProvider()
                // 4. 登录成功后,获取用户信息
                self?.fetchUser(id: loginResult.userId)
            } onFailure: { [weak self] error in
                // 失败回调(error为统一的NetworkError)
                self?.isLoading = false
                self?.errorMsg = error.errorDescription ?? "登录失败"
                // 处理Token过期(发送全局通知,跳转到登录页)
                if error.isTokenExpired {
                    NotificationCenter.default.post(name: NSNotification.Name("kRESTfulTokenExpired"), object: nil)
                }
            }
    }
    
    // MARK: - 2. 获取用户信息(调用UserAPI.fetchUser)
    func fetchUser(id: String) {
        isLoading = true
        // 一行代码发起请求,自定义配置(如重试1次)
        let config = RESTfulRequestConfig(retryCount: 1)
        RESTfulRequestTool.request(UserAPI.fetchUser(id: id), config: config)
            .sinkRESTful(to: &cancellables) { [weak self] user in
                self?.isLoading = false
                self?.userInfo = user // 赋值给发布属性,View层自动更新UI
            } onFailure: { [weak self] error in
                self?.isLoading = false
                self?.errorMsg = error.errorDescription ?? "获取用户信息失败"
                if error.isTokenExpired {
                    NotificationCenter.default.post(name: NSNotification.Name("kRESTfulTokenExpired"), object: nil)
                }
            }
    }
}

// MARK: - 业务数据模型(需遵守Codable,和服务端data字段格式一致)
/// 登录返回模型
struct LoginModel: Codable {
    let token: String
    let userId: String
    let userName: String
}

/// 用户信息模型
struct UserModel: Codable {
    let id: String
    let name: String
    let avatar: String
    let phone: String
    let email: String
    let createTime: String
}

6.2 View 层(UIKit ViewController)

无任何业务逻辑 ,只需绑定 ViewModel 的 @Published 属性,响应数据变化更新 UI,处理用户交互(如按钮点击),完全符合 MVVM 的设计思想。

swift

复制代码
import UIKit
import Combine

class UserViewController: UIViewController {
    // 绑定ViewModel
    private let vm = UserViewModel()
    // 存储Combine订阅
    private var cancellables = Set<AnyCancellable>()
    
    // UI控件(示例)
    @IBOutlet weak var accountTF: UITextField!
    @IBOutlet weak var pwdTF: UITextField!
    @IBOutlet weak var loginBtn: UIButton!
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var avatarIV: UIImageView!
    @IBOutlet weak var loadingView: UIActivityIndicatorView!
    @IBOutlet weak var errorLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 绑定ViewModel的属性到UI(核心!数据驱动UI)
        bindViewModel()
        // 配置UI控件
        setupUI()
    }
    
    // MARK: - 数据绑定(Combine自动响应,无需手动刷新UI)
    private func bindViewModel() {
        // 1. 绑定加载状态:控制加载动画
        vm.$isLoading
            .sink { [weak self] isLoading in
                isLoading ? self?.loadingView.startAnimating() : self?.loadingView.stopAnimating()
                self?.loginBtn.isEnabled = !isLoading // 加载时禁用按钮
            }
            .store(in: &cancellables)
        
        // 2. 绑定错误信息:展示错误提示
        vm.$errorMsg
            .sink { [weak self] msg in
                self?.errorLabel.text = msg
                self?.errorLabel.isHidden = msg.isEmpty
            }
            .store(in: &cancellables)
        
        // 3. 绑定用户信息:更新UI
        vm.$userInfo
            .compactMap { $0 } // 过滤nil
            .sink { [weak self] user in
                self?.nameLabel.text = user.name
                // 加载头像(使用Kingfisher/SDWebImage,示例)
                if let avatarURL = URL(string: user.avatar) {
                    self?.avatarIV.kf.setImage(with: avatarURL, placeholder: UIImage(named: "avatar_default"))
                }
            }
            .store(in: &cancellables)
        
        // 4. 绑定登录成功:可做后续逻辑(如返回上一页、跳转到首页)
        vm.$loginToken
            .compactMap { $0 }
            .sink { [weak self] _ in
                self?.showToast(message: "登录成功")
                // 跳转到首页
                self?.gotoHomePage()
            }
            .store(in: &cancellables)
    }
    
    // MARK: - UI配置
    private func setupUI() {
        loadingView.hidesWhenStopped = true
        errorLabel.textColor = .red
        loginBtn.layer.cornerRadius = 8
    }
    
    // MARK: - 事件处理:登录按钮点击
    @IBAction func loginBtnClick(_ sender: UIButton) {
        view.endEditing(true)
        // 调用ViewModel的登录方法,无需接触网络层
        vm.login(
            account: accountTF.text ?? "",
            password: pwdTF.text ?? ""
        )
    }
    
    // 跳转到首页(示例)
    private func gotoHomePage() {
        let homeVC = HomeViewController()
        navigationController?.pushViewController(homeVC, animated: true)
    }
    
    // 显示吐司(示例,可使用第三方库如MBProgressHUD)
    private func showToast(message: String) {
        let toast = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 40))
        toast.text = message
        toast.textAlignment = .center
        toast.backgroundColor = .black.withAlphaComponent(0.7)
        toast.textColor = .white
        toast.layer.cornerRadius = 20
        toast.clipsToBounds = true
        toast.center = view.center
        view.addSubview(toast)
        UIView.animate(withDuration: 2, animations: {
            toast.alpha = 0
        }) { _ in
            toast.removeFromSuperview()
        }
    }
}

七、框架进阶扩展(生产级必备)

以上封装是基础可用版 ,可根据企业级项目需求,在不修改业务层代码 的前提下,快速扩展以下生产级能力 ------ 框架的模块化设计保证了开闭原则(对扩展开放,对修改关闭)。

7.1 网络状态检测

添加无网络拦截 ,在请求前检查网络状态,无网络时直接返回.noNetwork错误,避免无用的网络请求:

  1. 引入网络状态库:pod 'ReachabilitySwift', '~> 5.0'
  2. NetworkManager中添加Reachability单例,监听网络状态;
  3. 创建网络状态插件 (遵循PluginType),在willSend中检查网络,无网络时取消请求并抛错;
  4. 将网络状态插件添加到createMoyaPlugins的插件数组中。

7.2 文件上传 / 下载封装

基于BaseAPIuploadTask/downloadTask方法,快速封装文件上传(如图片、视频)和下载功能,示例(头像上传):

swift

复制代码
// 1. 在UserAPI中添加上传枚举
enum UserAPI: BaseAPI {
    case uploadAvatar(data: Data, fileName: String) // 上传头像
    
    var path: String { "/user/avatar/upload" }
    var method: Moya.Method { .post }
    var parameters: [String: Any]? { nil }
    
    // 重写task,使用上传任务
    var task: Task {
        switch self {
        case .uploadAvatar(let data, let name):
            let formData = MultipartFormData(provider: .data(data), name: "avatar", fileName: name, mimeType: "image/jpeg")
            return uploadTask(with: [formData])
        default:
            return .requestParameters(parameters: parameters ?? [:], encoding: parameterEncoding)
        }
    }
}

// 2. 业务层一行代码发起上传请求
RESTfulRequestTool.request(UserAPI.uploadAvatar(data: imageData, fileName: "avatar.jpg"))
    .sinkRESTful(...)

7.3 全局请求加密 / 响应解密

添加加密 / 解密插件,实现全局请求参数加密、响应数据解密,无需业务层处理:

  1. 封装加密工具类(如 AES/RSA):CryptoTool.encrypt(params:)/CryptoTool.decrypt(data:)
  2. 创建加密插件 ,在willSend中对请求参数进行加密;
  3. 创建解密插件 ,在didReceive中对响应数据进行解密;
  4. 将插件添加到NetworkManager的插件数组中,注意插件执行顺序(加密在前,解密在后)。

7.4 Token 自动刷新

实现Token 过期自动刷新,无需用户重新登录,提升体验:

  1. NetworkErrortokenExpired回调中,发起刷新 Token 请求(使用刷新 Token);
  2. 刷新成功后,更新全局请求头的 Token 并重新初始化 MoyaProvider;
  3. 重新发起之前失败的请求 (可通过 Moya 的RequestRetrier实现);
  4. 刷新失败时,再发送全局通知跳转到登录页。

7.5 自定义缓存策略

基于 Alamofire 的URLRequest.CachePolicy和 iOS 的URLCache,封装业务级缓存(如列表页缓存、个人信息缓存):

  1. RESTfulRequestConfig中添加cacheKey(缓存键)和cacheExpire(缓存过期时间);
  2. 创建缓存工具类 ,基于UserDefaults/CoreData实现缓存的增删改查;
  3. 创建缓存插件,在请求前检查缓存(未过期则直接返回缓存数据),请求成功后更新缓存。

7.6 统一错误弹窗 / 吐司

封装全局错误处理工具 ,在NetworkError的失败回调中,根据错误类型自动展示全局弹窗 / 吐司,业务层无需重复写弹窗逻辑:

swift

复制代码
// 封装全局错误工具
struct NetworkErrorTool {
    static func handleError(_ error: NetworkError) {
        switch error {
        case .noNetwork, .tokenExpired, .serverError:
            // 展示全局吐司
            HUDManager.showToast(error.errorDescription!)
        case .jsonParseError, .invalidBaseURL:
            // 开发环境打印日志,生产环境隐藏
            #if DEBUG
            HUDManager.showAlert(title: "错误", message: error.errorDescription!)
            #endif
        default:
            break
        }
    }
}

// 业务层使用(一行代码处理错误)
.sinkRESTful(to: &cancellables) { data in
    // 成功处理
} onFailure: { error in
    NetworkErrorTool.handleError(error)
}

7.7 多 BaseURL 支持

若项目需要访问多个 RESTful 服务 (如主服务、支付服务、统计服务),只需修改BaseAPI,添加customBaseURL属性,让不同模块的 API 使用不同的基础 URL:

swift

复制代码
// 1. 在BaseAPI中添加自定义基础URL属性
protocol BaseAPI: TargetType {
    var customBaseURL: String? { get } // 自定义基础URL(nil则使用全局)
    // ... 其他属性
}

// 2. 重写baseURL方法
extension BaseAPI {
    var baseURL: URL {
        if let customURL = customBaseURL, let url = URL(string: customURL) {
            return url
        }
        guard let url = URL(string: NetworkConfig.shared.baseURL) else {
            fatalError(NetworkError.invalidBaseURL.errorDescription!)
        }
        return url
    }
}

// 3. 支付模块API使用自定义基础URL
enum PayAPI: BaseAPI {
    case payOrder(orderId: String)
    
    var customBaseURL: String? { "https://pay-api.xxx.com/v1" } // 支付服务基础URL
    var path: String { "/pay/order" }
    var method: Moya.Method { .post }
    // ... 其他属性
}

7.8 防止重复请求

Moya 的trackInflights: true可防止相同 URL + 参数 + 方法的重复请求,若需要更精细的控制(如按业务标识防重),可:

  1. BaseAPI中添加uniqueKey属性(业务唯一标识,如"user_login");
  2. NetworkManager中维护一个请求标识集合,记录正在进行的请求;
  3. 创建防重插件 ,在willSend中检查标识,已存在则取消请求。

八、框架核心优势

这套基于 Moya+Alamofire 的 RESTful 网络框架,贴合 iOS 开发最佳实践,相比直接使用 Alamofire,具备以下核心优势,尤其适合中大型企业级项目:

  1. 请求高度解耦:用枚举集中管理所有 RESTful 请求,URL / 参数 / 方法不再分散,修改、维护、排查问题更高效;
  2. 开发效率极高:业务层只需编写 API 枚举和数据模型,一行代码发起请求,自动解析 JSON,无需手动处理解析错误;
  3. 错误统一处理 :所有异常(网络、解析、业务、无网络)归一为NetworkError,业务层只需处理一种错误类型,避免冗余代码;
  4. 全局配置灵活:单例管理多环境、请求头、超时,支持运行时动态更新(如 Token 刷新、环境切换),无需重启 App;
  5. 贴合 MVVM/Combine:基于 Combine 实现响应式请求,支持数据绑定,完美适配现代 iOS 开发架构,实现视图与业务解耦;
  6. 可扩展性极强:模块化设计,支持插件化扩展(日志、加密、缓存、网络状态),扩展不影响业务层代码;
  7. 调试友好:DEBUG 模式下打印详细的请求 / 响应日志,RELEASE 模式自动关闭,避免生产环境信息泄露;
  8. 生态成熟:基于 Moya+Alamofire 的事实标准,社区解决方案丰富,团队学习成本低,问题易排查。

九、使用注意事项

  1. 数据模型对齐 :业务模型(如UserModel)必须和服务端data字段的 JSON 格式严格一致,否则会触发jsonParseError
  2. 多环境配置 :需在 Xcode 中配置Build Configuration (Debug/Test/Release),配合configEnvFromBuildConfig实现自动环境切换;
  3. Token 安全性 :建议将 Token 保存在Keychain 中(而非UserDefaults),UserDefaults为明文存储,存在安全风险;
  4. 避免循环引用 :所有 Combine/RxSwift 闭包中必须使用[weak self],框架和业务层均需遵循,防止内存泄漏;
  5. 重复请求控制 :开启trackInflights: true防止相同请求重复发起,高频点击按钮(如登录)需在 View 层添加点击防抖
  6. 参数编码 :表单提交(如传统网页请求)需将parameterEncoding改为URLEncoding.default,JSON 提交使用默认的JSONEncoding.default
  7. Moya 版本 :本文基于 Moya 15.x+,若使用旧版本(如 14.x),需微调TargetType的部分 API(如task的类型)。

总结

这套框架是生产级可直接落地 的 RESTful 网络封装方案,核心是在保留 Moya+Alamofire 核心优势的基础上,做高度的模块化、易用性封装,解决了 RESTful 开发中「请求分散、解析繁琐、错误处理混乱」的痛点。

框架的核心设计思路 是:配置层统一管理全局参数、基础层封装通用模型和错误、核心层处理 Moya 请求和解析、工具层暴露极简 API ,贴合 iOS 现代开发思想(MVVM/Combine/RxSwift),是传统业务 App、工具类 App、接口简单且固定的项目的最优选择 ------ 完全适配 RESTful「资源驱动、HTTP 标准」的特性,同时兼顾开发效率、可维护性和可扩展性。

相关推荐
安科士andxe6 小时前
深入解析|安科士1.25G CWDM SFP光模块核心技术,破解中长距离传输痛点
服务器·网络·5g
YJlio9 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
CTRA王大大9 小时前
【网络】FRP实战之frpc全套配置 - fnos飞牛os内网穿透(全网最通俗易懂)
网络
testpassportcn10 小时前
AWS DOP-C02 認證完整解析|AWS DevOps Engineer Professional 考試
网络·学习·改行学it
通信大师11 小时前
深度解析PCC策略计费控制:核心网产品与应用价值
运维·服务器·网络·5g
Tony Bai12 小时前
告别 Flaky Tests:Go 官方拟引入 testing/nettest,重塑内存网络测试标准
开发语言·网络·后端·golang·php
消失的旧时光-194312 小时前
从 0 开始理解 RPC —— 后端工程师扫盲版
网络·网络协议·rpc
叫我龙翔13 小时前
【计网】从零开始掌握序列化 --- JSON实现协议 + 设计 传输\会话\应用 三层结构
服务器·网络·c++·json
“αβ”13 小时前
网络层协议 -- ICMP协议
linux·服务器·网络·网络协议·icmp·traceroute·ping
袁小皮皮不皮15 小时前
数据通信18-网络管理与运维
运维·服务器·网络·网络协议·智能路由器