完全指南:MultipartFormData 与两种参数传递方案)
Moya 多部分上传Multipart Upload完全指南MultipartFormData 与两种参数传递方案【免费下载链接】MoyaNetwork abstraction layer written in Swift.项目地址: https://gitcode.com/gh_mirrors/mo/Moya导读本文基于 Moya 官方文档 docs/Examples/MultipartUpload.md 整理而成系统讲解如何用 Moya 在单次请求中同时上传文件如 GIF与附加业务参数。文章覆盖两种典型场景参数随请求体body发送以及参数拼接进 URLquery string并结合仓库源码MultipartFormData.swift、Task.swift、MoyaProviderInternal.swift与真实示例GiphyAPI.swift深入剖析底层实现让读者不仅能照抄可运行的代码还能理解 Moya 在 multipart 上传上的设计约束与进阶用法。问题场景一次请求同时上传文件与附加参数假设业务需求是在一次请求中上传一张 GIF并附带额外的描述信息description。在 Moya 的TargetType模型中我们会把枚举 case 定义为携带原始数据与参数的形式public enum MyService { case uploadGif(Data, description: String) }这里Data是 GIF 的二进制内容description是附加的String参数。方案的选择取决于该参数应该放在哪里参数属于**请求体body**的一部分例如 POST、PUT 请求 —— 使用Task.uploadMultipartFormData(_:)参数属于URL 的一部分例如 GET 请求 —— 使用Task.uploadCompositeMultipartFormData(_:urlParameters:)。这两种方案分别对应 Moya 中两个核心数据类型MultipartFormBodyPart单个表单部件与MultipartFormData表单整体。认识核心类型MultipartFormBodyPart 与 MultipartFormData在深入两种方案之前先了解支撑它们的底层数据结构。源码定义位于 Sources/Moya/MultipartFormData.swiftpublic struct MultipartFormData: Hashable { public enum FormDataProvider: Hashable { case data(Foundation.Data) // 以内存数据形式提供 case file(URL) // 以文件路径形式提供 case stream(InputStream, UInt64) // 以输入流形式提供需给定长度 } public let fileManager: FileManager // 文件操作使用的 FileManager默认 .default public let boundary: String? // 分隔表单部件的边界字符串默认 nil public let parts: [MultipartFormBodyPart] // 组成表单的部件数组 public init(fileManager: FileManager .default, boundary: String? nil, parts: [MultipartFormBodyPart]) { ... } }要点拆解MultipartFormData代表完整的multipart/form-data表单由fileManager、boundary与parts组成。boundary为nil时由底层Alamofire自动生成MultipartFormBodyPart代表表单中的单个部件其初始化参数为public init(provider: MultipartFormData.FormDataProvider, name: String, fileName: String? nil, mimeType: String? nil)参数类型说明providerFormDataProvider部件数据的来源.data内存数据、.file文件 URL、.stream输入流 长度nameString部件在表单中的字段名服务端multipart/form-data解析所用的 keyfileNameString?文件名可选对文件上传部件通常必填如gif.gifmimeTypeString?内容的 MIME 类型可选如image/gif另外MultipartFormData实现了ExpressibleByArrayLiteral见 MultipartFormData.swift因此可以直接用数组字面量构造let multipartData: MultipartFormData [gifData, descriptionData]这一语法糖与官方文档示例中的写法完全一致等价于MultipartFormData(parts: [gifData, descriptionData])。方案一参数放在请求体body中当附加参数需要进入 multipart 请求体时需要为每一个部件创建一个MultipartFormBodyPart然后让Task返回.uploadMultipartFormData(_:)extension MyService: TargetType { // ... baseURL、path、method、sampleData、headers 等其他 TargetType 要求的属性 public var task: Task { switch self { case let .uploadGif(data, description): let gifData MultipartFormBodyPart(provider: .data(data), name: file, fileName: gif.gif, mimeType: image/gif) let descriptionData MultipartFormBodyPart(provider: .data(description.data(using: .utf8)!), name: description) let multipartData: MultipartFormData [gifData, descriptionData] // 或者如果你需要显式指定 boundary 与 fileManager // let multipartData MultipartFormData(fileManager: .default, boundary: ..., parts: [gifData, descriptionData]) return .uploadMultipartFormData(multipartData) } } // ... }代码说明GIF 部件provider: .data(data)直接把内存中的 GIF 数据作为部件内容name: file是服务端约定接收文件的字段名fileName: gif.gif与mimeType: image/gif让服务端能正确识别文件名与类型描述部件description是纯文本参数不需要fileName与mimeType只需把字符串转成Data组装两个部件通过数组字面量合并为MultipartFormData显式控制可选若需自定义boundary或指定fileManager改用完整初始化器MultipartFormData(fileManager:boundary:parts:)。其中fileManager在部件以.file(URL)形式提供时用于文件读取等操作默认值为FileManager.defaultboundary默认nil交由底层自动生成。为什么这个方法支持 GET—— 底层 Method 约束从源码 MoyaProviderInternal.swift 可以看到Moya 对 HTTP 方法做了 multipart 支持校验public extension Method { var supportsMultipart: Bool { switch self { case .post, .put, .patch, .connect: return true default: return false } } }也就是说.uploadMultipartFormData(_:)只适用于 POST、PUT、PATCH、CONNECT 这类允许携带 body 的方法。在真正发起上传前MoyaProvider内部会执行守卫检查MoyaProviderInternal.swiftlet onSendUploadMultipart: (MultipartFormData) - Cancellable { multipartFormData in guard !multipartFormData.parts.isEmpty endpoint.method.supportsMultipart else { fatalError(\(target) is not a multipart upload target.) } return self.sendUploadMultipart(...) }两个硬性前提缺一不可parts 不能为空且HTTP 方法必须支持 multipart否则会在运行时直接fatalError。这也是为什么如果参数必须进 URL例如 GET 语义就不能使用.uploadMultipartFormData而要采用下面的复合方案。方案二参数放在 URL 中当附加参数需要进入 URLquery string时使用Task的复合类型.uploadCompositeMultipartFormData(_:urlParameters:)文件/数据走 multipart body参数走 URLextension MyService: TargetType { // ... baseURL、path、method、sampleData、headers 等其他 TargetType 要求的属性 public var task: Task { switch self { case let .uploadGif(data, description): let gifData MultipartFormBodyPart(provider: .data(data), name: file, fileName: gif.gif, mimeType: image/gif) let multipartData: MultipartFormData [gifData] // 或者如果你需要显式指定 boundary 与 fileManager // let multipartData MultipartFormData(fileManager: .default, boundary: ..., parts: [gifData]) let urlParameters [description: description] return .uploadCompositeMultipartFormData(multipartData, urlParameters: urlParameters) } } // ... }该方案与方案一的差异在于只把真正的“文件/数据”放进MultipartFormData附加参数放入urlParameters: [String: Any]字典Task返回复合类型.uploadCompositeMultipartFormData(_:urlParameters:)。复合任务在 Endpoint 层的处理从 Endpoint.swift 可以看到当Endpoint把Task转换为URLRequest时复合 multipart 任务的urlParameters会被编码为 query string 追加到 URL 上case let .uploadCompositeMultipart(_, urlParameters), let .uploadCompositeMultipartFormData(_, urlParameters): let parameterEncoding URLEncoding(destination: .queryString) return try request.encoded(parameters: urlParameters, parameterEncoding: parameterEncoding)即urlParameters使用URLEncoding(destination: .queryString)编码最终以?keyvalue的形式出现在请求 URL 中而 multipart body 部分则交由上传通道处理。对应地EndpointSpec.swift 中也有专门的测试用例验证uploadCompositeMultipartFormData会正确更新 URL如endpoint.url ?HarveyNemesis。仓库真实案例Giphy 上传仓库自带示例 Examples/_shared/GiphyAPI.swift 正是“文件进 body、参数进 URL”的实战范本public var task: Task { switch self { case let .upload(data): let multipartFormBodyParts [MultipartFormBodyPart(provider: .data(data), name: file, fileName: gif.gif, mimeType: image/gif)] let multipartFormData MultipartFormData(fileManager: .default, boundary: nil, parts: multipartFormBodyParts) return .uploadCompositeMultipartFormData(multipartFormData, urlParameters: [api_key: dc6zaTOxFJmzC, username: Moya]) } }这里 GIF 数据作为name: file的部件走 multipart body而api_key、username两个认证类参数走urlParameters进入 URL——这与官方文档方案二的写法完全一致同时演示了完整初始化器MultipartFormData(fileManager:boundary:parts:)的用法。配套的示例控制器 Examples/Basic/ViewController.swift 还展示了如何为该上传请求提供progress与completion回调示例中用进度条直观呈现上传进度。底层调用链MultipartFormData 如何变成真正的上传理解了两种 Task 方案后再看 Moya 内部如何处理 multipart 上传MoyaProviderInternal.swiftfunc sendUploadMultipart(_ target: Target, request: URLRequest, callbackQueue: DispatchQueue?, multipartFormData: MultipartFormData, progress: Moya.ProgressBlock? nil, completion: escaping Moya.Completion) - CancellableToken { let formData RequestMultipartFormData(fileManager: multipartFormData.fileManager, boundary: multipartFormData.boundary) formData.applyMoyaMultipartFormData(multipartFormData) let interceptor self.interceptor(target: target) let uploadRequest: UploadRequest session.requestQueue.sync { let uploadRequest session.upload(multipartFormData: formData, with: request, interceptor: interceptor) setup(interceptor: interceptor, with: target, and: uploadRequest) return uploadRequest } ... }关键点Moya 先用MultipartFormData携带的fileManager与boundary构建底层RequestMultipartFormDataapplyMoyaMultipartFormData定义于 MultipartFormData.swift遍历parts按FormDataProvider的三种形态分别处理.data→append(data:withName:fileName:mimeType:).file→append(url:withName:)有fileName/mimeType时带上否则仅按名字追加.stream→append(stream:withLength:name:fileName:mimeType:)。最终交给 Alamofire 的session.upload(multipartFormData:with:interceptor:)真正编码并发出请求。这也解释了FormDataProvider三种 case 的设计意图小数据用.data直接放内存大文件用.file避免整块读入内存需要流式读取的场景用.stream必须提供流长度。关于已废弃的旧 API在 Task.swift 中还能看到两个被标记为available(*, deprecated)的旧枚举 caseuploadMultipart([MultipartFormBodyPart])与uploadCompositeMultipart([MultipartFormBodyPart], urlParameters:)。它们只接收[MultipartFormBodyPart]数组无法携带自定义fileManager/boundary新代码应统一使用uploadMultipartFormData/uploadCompositeMultipartFormData这两个基于MultipartFormData的版本内部执行时旧 API 也会被自动包装为MultipartFormData见 MoyaProviderInternal.swift。测试与验证进度追踪、URL 编码与响应校验仓库测试提供了丰富的验证参考结构与默认值Tests/MoyaTests/MultipartFormDataSpec.swift 验证了MultipartFormData(parts:)初始化后boundary为nil、fileManager FileManager.default、parts数量与部件字段name/fileName/mimeType/provider均正确真实上传与进度Tests/MoyaTests/MoyaProviderSpec.swift 使用HTTPBin.uploadMultipartFormData发起真实 multipart 请求并断言progressValues多次回调、最后一次completed true可作为实现上传进度 UI 的行为依据URL 编码Tests/MoyaTests/EndpointSpec.swift 验证复合 multipart 的urlParameters被正确编码进 URL与 ValidationType 协同Tests/MoyaTests/MoyaProviderIntegrationTests.swift 验证 multipart 上传同样支持状态码校验如期望 287 时收到非 287 会返回错误解决的是 ValidationType not working with multipart uploadissue #1590这类边界问题。测试辅助代码 Tests/MoyaTests/TestHelpers.swift 中的createTestMultipartFormData()还展示了FormDataProvider三种形态的混用return [ MultipartFormBodyPart(provider: .file(url), name: file, fileName: testImage), MultipartFormBodyPart(provider: .data(data), name: data) ]小结如何选择正确的 Task需求使用的 Task附加参数位置文件/数据 参数都进请求体.uploadMultipartFormData(multipartData)作为MultipartFormBodyPart放进MultipartFormData.parts文件/数据进请求体参数进 URL.uploadCompositeMultipartFormData(multipartData, urlParameters: urlParameters)放进urlParameters字典编码为 query string需要自定义 boundary / fileManager两种 Task 均可配合MultipartFormData(fileManager:boundary:parts:)使用—回顾文档开头的场景上传 GIF 同时附带description若服务端约定该参数在 form-data 中解析选方案一若约定参数在 URL query 中解析例如 Giphy API 的api_key/username选方案二。无论哪种方案都需保证parts非空、HTTP 方法属于supportsMultipartPOST/PUT/PATCH/CONNECT否则MoyaProvider会在运行时以fatalError终止这是 Moya multipart 上传最需要注意的约束。延伸阅读官方示例代码Examples/_shared/GiphyAPI.swift、Examples/Basic/ViewController.swift核心源码MultipartFormData.swift、Task.swift、MoyaProviderInternal.swift、Endpoint.swift相关测试MultipartFormDataSpec.swift、EndpointSpec.swift、MoyaProviderSpec.swift、MoyaProviderIntegrationTests.swiftMoya 其他使用指南Targets、Providers、Endpoints、Multipart 上传的更多示例目录【免费下载链接】MoyaNetwork abstraction layer written in Swift.项目地址: https://gitcode.com/gh_mirrors/mo/Moya创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考