Swift客户端向Vapor服务器同时发送图片与JSON的实现咨询
问题解答
一、multipart/form-data是否适合你的场景?
是的,这是最优方案。因为你需要同时传输结构化的用户注册数据(文本、布尔值等)和二进制的头像图片,multipart/form-data就是为这种混合内容传输设计的标准格式,能避免数据编码冲突,且被绝大多数后端框架支持。
二、Swift端扩展MRNetwork库支持multipart请求
MRNetwork基于URLSession封装,我们可以扩展它的请求构建能力,添加multipart表单的支持。以下是具体实现:
1. 扩展MRNetwork添加multipart发送方法
import Foundation extension MRNetwork { /// 发送包含图片和表单数据的multipart请求 func sendMultipartRequest<T: Codable>( url: String, method: HTTPMethod, formParameters: [String: Any], imageData: Data, imageFieldName: String = "profileImage", fileName: String = "avatar.jpg", mimeType: String = "image/jpeg", responseType: T.Type, completion: @escaping (Result<T, Error>) -> Void ) { guard let url = URL(string: url) else { completion(.failure(NSError(domain: "MRNetwork", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))) return } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = method.rawValue // 生成multipart边界标识 let boundary = UUID().uuidString urlRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") // 构建请求体 var body = Data() // 添加表单参数 for (key, value) in formParameters { body.append("--\(boundary)\r\n".data(using: .utf8)!) body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!) body.append("\(value)\r\n".data(using: .utf8)!) } // 添加图片文件 body.append("--\(boundary)\r\n".data(using: .utf8)!) body.append("Content-Disposition: form-data; name=\"\(imageFieldName)\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!) body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!) body.append(imageData) body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) urlRequest.httpBody = body // 利用MRNetwork现有逻辑发送请求 sendRequest(request: urlRequest, responseType: responseType, completion: completion) } }
2. SwiftUI中调用示例
// 假设你已获取用户选择的头像UIImage guard let avatarData = avatarImage.jpegData(compressionQuality: 0.8) else { print("头像转Data失败") return } // 构建注册表单参数 let formParams: [String: Any] = [ "name": "张三", "username": "zhangsan", "email": "zhangsan@example.com", "password": "123456", "repeatPassword": "123456", "APIKey": "你的APIKeyBase64字符串", "termsConditions": true, "rating": 0.0 ] // 发送注册请求 MRNetwork.shared.sendMultipartRequest( url: "http://你的服务器地址/api/users", method: .post, formParameters: formParams, imageData: avatarData, responseType: HTTPStatus.self, completion: { result in switch result { case .success(let status): print("注册成功:\(status)") case .failure(let error): print("注册失败:\(error.localizedDescription)") } } )
三、Vapor服务器端接收并处理multipart数据
你需要修改原有的create函数,从multipart表单中解析字段和图片,再完成用户创建与图片存储。以下是修改后的代码:
1. 修改后的create函数
func create(req: Request) async throws -> HTTPStatus { // 解析multipart表单 let form = try await req.content.decode(MultipartForm.self) // 提取必填字段并做类型校验 guard let name = form["name"]?.string, let username = form["username"]?.string, let email = form["email"]?.string, let password = form["password"]?.string, let repeatPassword = form["repeatPassword"]?.string, let apiKeyStr = form["APIKey"]?.string, let termsAccepted = form["termsConditions"]?.bool, let rating = form["rating"]?.double else { throw Abort(.badRequest, reason: "缺少必填字段或字段格式错误") } // APIKey验证逻辑(保持原有逻辑) guard let apiKeyData = Data(base64Encoded: apiKeyStr), apiKeyData == Data(AK) else { throw Abort(.badRequest, reason: "无效的请求权限") } // 密码匹配验证(保持原有逻辑) guard password == repeatPassword else { throw Abort(.badRequest, reason: "两次输入的密码不一致") } // 检查用户名是否已存在(保持原有逻辑) let existingUser = try await Users.query(on: req.db) .filter(\.$username == username) .first() guard existingUser == nil else { throw Abort(.badRequest, reason: "用户名已被占用") } // 创建并保存用户 let newUser = Users( name: name, username: username, email: email, password: try Bcrypt.hash("\(username):\(password)"), termsConditions: termsAccepted ) try await newUser.save(on: req.db) // 处理头像图片 let profileImagePath: String if let imagePart = form["profileImage"]?.file { // 生成唯一文件名避免冲突 let fileName = "\(UUID().uuidString).jpg" // 图片存储路径:服务器Public/avatars目录 let saveDir = req.application.directory.publicDirectory.appendingPathComponent("avatars") let filePath = saveDir.appendingPathComponent(fileName) // 创建目录(如果不存在) try FileManager.default.createDirectory(at: saveDir, withIntermediateDirectories: true) // 写入图片数据 try await imagePart.data.write(to: filePath) // 存储相对路径(供前端访问) profileImagePath = "/avatars/\(fileName)" } else { // 无头像时使用默认图路径 profileImagePath = "/avatars/default.jpg" } // 创建用户Profile let newProfile = Profiles( profileImage: profileImagePath, averageRating: rating, user: try newUser.requireID() ) try await newUser.$profile.create(newProfile, on: req.db) return .created }
2. 服务器配置补充
在configure.swift中添加静态文件中间件,允许前端访问存储的头像:
// 允许访问Public目录下的静态文件 app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
内容的提问来源于stack exchange,提问作者Miguelon
相关产品推荐
相关产品推荐

