美文网首页
Moya url 和 body 同时提交参数

Moya url 和 body 同时提交参数

作者: fuyoufang | 来源:发表于2021-03-15 15:59 被阅读0次

    在使用 Moya 的过程中,会遇到这种情况:url 当中需要动态设置参数,body 中也要设置参数。

    比如请求的 URL 当中需要添加 userID ,在 body 当中需要传入一个 JSON 格式的参数 {"roomID": "123"}

    url 和 body 同时提交

    方案一(失败)

    path 方法中拼接 url 的参数,在 task 中提交 body 的参数。

    public enum LiveShowRequest {
        case anchorHeartBeat(userID: String,
                             roomID: String)
    }
    
    extension LiveShowRequest: TargetType {
        
        public var path: String {
            switch self {
            case .anchorHeartBeat(let userID, _):
                return "anchor_heartbeat?userID=\(userID)"
            }
        }
        
        public var task: Task {
            switch self {
            case let .anchorHeartBeat(_, roomID):
    
                var params = ["roomID": roomID] as [String : Any]
                return .requestParameters(parameters: params, encoding: JSONEncoding.default)
            }
        }
    }
    

    经过测试,这样是不行的,因为 Moya 会对 path 的返回值进行编码,导致用于拼接参数的 ? 被编码成了 %3f ,服务器无法解析参数了。

    方案二(成功)

    task 中使用 requestCompositeParameters ,可以同时设置 bodyParametersurlParameters

    extension LiveShowRequest: TargetType {
        
        public var path: String {
            switch self {
            case .anchorHeartBeat(let userID, _):
                return "anchor_heartbeat"
            }
        }
        
        public var task: Task {
            switch self {
            case let .anchorHeartBeat(userID, roomID):
                var params = ["roomID": roomID] as [String : Any]
                var urlParameters = ["userID": userID]
                
                return .requestCompositeParameters(bodyParameters: params,
                                                   bodyEncoding: JSONEncoding.default,
                                                   urlParameters: urlParameters)
            }
        }
        
    }
    

    这种方式可以处理同时传入 url 和 body 参数的情况。

    方法三(成功)

    在 baseURL 中拼接 url 参数,在 task 中设置 body 参数,可以避免方法一中 ? 被编码的问题。

    extension LiveShowRequest: TargetType {
        
        public var baseURL: URL {
            switch self {
            case let .anchorHeartBeat(userID, _):
                return URL(string: BaseURL + "?userID=\(userID)")!
            }
        }
    
        public var path: String {
            switch self {
            case .anchorHeartBeat:
                return "anchor_heartbeat"
            }
        }
        
        public var task: Task {
            switch self {
            case let .anchorHeartBeat(_, roomID):
                var params = ["roomID": roomID] as [String : Any]
                return .requestParameters(parameters: params, encoding: JSONEncoding.default)
            }
        }
    

    url 参数的编码问题

    假如传入的 url 参数当中有 * ,比如上面的例子当中的 userID 为 0558eba*1400489990 ,Moya 会将 * 转换为 %2A ,也就是进行了 url 编码。但是 * 本身不需要进行 url 编码,Moay 为什么要对 * 进行编码呢?

    在 Moya 当中,url 的参数是通过 URLEncoding 进行编码的。

    public struct URLEncoding: ParameterEncoding {
        
        /// Returns a percent-escaped string following RFC 3986 for a query string key or value.
        ///
        /// RFC 3986 states that the following characters are "reserved" characters.
        ///
        /// - General Delimiters: ":", "#", "[", "]", "@", "?", "/"
        /// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
        ///
        /// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
        /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
        /// should be percent-escaped in the query string.
        ///
        /// - parameter string: The string to be percent-escaped.
        ///
        /// - returns: The percent-escaped string.
        public func escape(_ string: String) -> String {
            let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
            let subDelimitersToEncode = "!$&'()*+,;="
    
            var allowedCharacterSet = CharacterSet.urlQueryAllowed
            allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
    
            var escaped = ""
    
            //==========================================================================================================
            //
            //  Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few
            //  hundred Chinese characters causes various malloc error crashes. To avoid this issue until iOS 8 is no
            //  longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more
            //  info, please refer to:
            //
            //      - https://github.com/Alamofire/Alamofire/issues/206
            //
            //==========================================================================================================
    
            if #available(iOS 8.3, *) {
                escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
            } else {
                let batchSize = 50
                var index = string.startIndex
    
                while index != string.endIndex {
                    let startIndex = index
                    let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex) ?? string.endIndex
                    let range = startIndex..<endIndex
    
                    let substring = string[range]
    
                    escaped += substring.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? String(substring)
    
                    index = endIndex
                }
            }
    
            return escaped
        }
    }
    

    拼接进 URL 的参数的 key 和 value,都会调用 escape 方法进行编码。escape 方法根据 RFC 3986 返回百分号转移的字符串。在调用 addingPercentEncoding 之前,先移除了 CharacterSet.urlQueryAllowed 中包含在 generalDelimitersToEncodesubDelimitersToEncode 中的字符。

    let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
    let subDelimitersToEncode = "!$&'()*+,;="
    
    var allowedCharacterSet = CharacterSet.urlQueryAllowed
    allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
    

    可以通过下面的扩展方法查看包含在 CharacterSet.urlQueryAllowed 的字符串。(方法来自 stackoverflow

    extension CharacterSet {
        func characters() -> [Character] {
            // A Unicode scalar is any Unicode code point in the range U+0000 to U+D7FF inclusive or U+E000 to U+10FFFF inclusive.
            return codePoints().compactMap { UnicodeScalar($0) }.map { Character($0) }
        }
    
        func codePoints() -> [Int] {
            var result: [Int] = []
            var plane = 0
            // following documentation at https://developer.apple.com/documentation/foundation/nscharacterset/1417719-bitmaprepresentation
            for (i, w) in bitmapRepresentation.enumerated() {
                let k = i % 0x2001
                if k == 0x2000 {
                    // plane index byte
                    plane = Int(w) << 13
                    continue
                }
                let base = (plane + k) << 3
                for j in 0 ..< 8 where w & 1 << j != 0 {
                    result.append(base + j)
                }
            }
            return result
        }
    }
    
    // 使用方法
    var allowedCharacterSet = CharacterSet.urlQueryAllowed
    debugPrint(allowedCharacterSet.characters())
    // 结果
    ["!", "$", "&", "\'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "=", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "_", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "~"]
    

    如果,恰好 url 参数中有这些字符,又不想被编码,要怎么处理呢?只能自定义一个符合 ParameterEncoding 的类型,实现自定义编码方式。

    下面的 MultipleEncoding 由 https://github.com/Moya/Moya/issues/1059 提供的思路实现。初始化 MultipleEncoding 时,通过 urlParameters 定义需要被 url 编码的 key,其他的 key 则为编码为 body。

    struct MultipleEncoding : ParameterEncoding {
    
        var urlParameters: [String]?
        
        init(urlParameters:[String]?) {
            self.urlParameters = urlParameters
        }
        
        func encode(_ urlRequest: Alamofire.URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
            guard let parameters = parameters else { return urlRequest as! URLRequest }
            
            
            var urlParams: [String: Any] = [:]
            var jsonParams: [String: Any] = [:]
            
            parameters.forEach { (key: String, value: Any) in
                if urlParameters?.contains(key) ?? false {
                    urlParams[key] = value
                } else {
                    jsonParams[key] = value
                }
            }
            
            // Encode URL Params
            // 过程和 URLEncoding.queryString.encode(urlRequest, with: urlParams) 一致
            var urlRequest = try urlRequest.asURLRequest()
            if let url = urlRequest.url {
                if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !urlParams.isEmpty {
                    let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(urlParams)
                    urlComponents.percentEncodedQuery = percentEncodedQuery
                    urlRequest.url = urlComponents.url
                }
            }
            
            //Encode JSON
            return try JSONEncoding.default.encode(urlRequest, with: jsonParams)
        }
        
        private func query(_ parameters: [String: Any]) -> String {
            // 直接复制 URLEncoding 中的方法
        }
        
        public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
            // 直接复制 URLEncoding 中的方法
        }
        
        // escape 只需要根据需要移出不希望被编码的字符
        public func escape(_ string: String) -> String {
            let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
            let subDelimitersToEncode = "!$&'()*+,;="
    
            var allowedCharacterSet = CharacterSet.urlQueryAllowed
    //        allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
    
            // 下面的代码和 URLEncoding 中的方法一致
        }
    }
    

    使用时如下。

    extension LiveShowRequest: TargetType {
        
        public var path: String {
            switch self {
            case .anchorHeartBeat(let userID, _):
                return "anchor_heartbeat"
            }
        }
        
        public var task: Task {
            switch self {
            case let .anchorHeartBeat(userID, roomID):
                var params = ["roomID": roomID,
                              "userID": userID,
                              ] as [String : Any]
                
                let urlParameters = ["userID"]
                return .requestParameters(parameters: params, encoding: MultipleEncoding(urlParameters: urlParameters))
            }
        }
    }
    

    这样就自定了 url 和 body 编码的整个过程。

    参考

    https://github.com/Moya/Moya/issues/1059

    相关文章

      网友评论

          本文标题:Moya url 和 body 同时提交参数

          本文链接:https://www.haomeiwen.com/subject/rkbkcltx.html