Moya url 和 body 同时提交参数
Moya url 和 body 同时提交参数
2021.03.15 15:59:59字数 597阅读 881
在使用 Moya 的过程中,会遇到这种情况:url 当中需要动态设置参数,body 中也要设置参数。
比如请求的 URL 当中需要添加 userID ,在 body 当中需要传入一个 JSON 格式的参数 {"roomID": "123"} 。
url 和 body 同时提交
方案一(失败)
在 path 方法中拼接 url 的参数,在 task 中提交 body 的参数。
publicenumLiveShowRequest{caseanchorHeartBeat(userID:String,roomID:String)}extensionLiveShowRequest:TargetType{publicvarpath:String{switchself{case.anchorHeartBeat(letuserID,_):return"anchor_heartbeat?userID=\(userID)"}}publicvartask:Task{switchself{caselet.anchorHeartBeat(_,roomID):varparams=["roomID":roomID]as[String:Any]return.requestParameters(parameters:params,encoding:JSONEncoding.default)}}}
经过测试,这样是不行的,因为 Moya 会对 path 的返回值进行编码,导致用于拼接参数的 ? 被编码成了 %3f ,服务器无法解析参数了。
方案二(成功)
在 task 中使用 requestCompositeParameters ,可以同时设置 bodyParameters 和 urlParameters 。
extensionLiveShowRequest:TargetType{publicvarpath:String{switchself{case.anchorHeartBeat(letuserID,_):return"anchor_heartbeat"}}publicvartask:Task{switchself{caselet.anchorHeartBeat(userID,roomID):varparams=["roomID":roomID]as[String:Any]varurlParameters=["userID":userID]return.requestCompositeParameters(bodyParameters:params,bodyEncoding:JSONEncoding.default,urlParameters:urlParameters)}}}
这种方式可以处理同时传入 url 和 body 参数的情况。
方法三(成功)
在 baseURL 中拼接 url 参数,在 task 中设置 body 参数,可以避免方法一中 ? 被编码的问题。
extensionLiveShowRequest:TargetType{publicvarbaseURL:URL{switchself{caselet.anchorHeartBeat(userID,_):returnURL(string:BaseURL+"?userID=\(userID)")!}}publicvarpath:String{switchself{case.anchorHeartBeat:return"anchor_heartbeat"}}publicvartask:Task{switchself{caselet.anchorHeartBeat(_,roomID):varparams=["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 进行编码的。
publicstructURLEncoding: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.publicfuncescape(_string:String)->String{letgeneralDelimitersToEncode=":#[]@"// does not include "?" or "/" due to RFC 3986 - Section 3.4letsubDelimitersToEncode="!$&'()*+,;="varallowedCharacterSet=CharacterSet.urlQueryAllowed allowedCharacterSet.remove(charactersIn:"\(generalDelimitersToEncode)\(subDelimitersToEncode)")varescaped=""//==========================================================================================================//// 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(iOS8.3,*){escaped=string.addingPercentEncoding(withAllowedCharacters:allowedCharacterSet)??string}else{letbatchSize=50varindex=string.startIndexwhileindex!=string.endIndex{letstartIndex=indexletendIndex=string.index(index,offsetBy:batchSize,limitedBy:string.endIndex)??string.endIndexletrange=startIndex..<endIndexletsubstring=string[range]escaped+=substring.addingPercentEncoding(withAllowedCharacters:allowedCharacterSet)??String(substring)index=endIndex}}returnescaped}}
拼接进 URL 的参数的 key 和 value,都会调用 escape 方法进行编码。escape 方法根据 RFC 3986 返回百分号转移的字符串。在调用 addingPercentEncoding 之前,先移除了 CharacterSet.urlQueryAllowed 中包含在 generalDelimitersToEncode 和 subDelimitersToEncode 中的字符。
letgeneralDelimitersToEncode=":#[]@"// does not include "?" or "/" due to RFC 3986 - Section 3.4letsubDelimitersToEncode="!$&'()*+,;="varallowedCharacterSet=CharacterSet.urlQueryAllowedallowedCharacterSet.remove(charactersIn:"\(generalDelimitersToEncode)\(subDelimitersToEncode)")
可以通过下面的扩展方法查看包含在 CharacterSet.urlQueryAllowed 的字符串。(方法来自 stackoverflow)
extensionCharacterSet{funccharacters()->[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.returncodePoints().compactMap{UnicodeScalar($0)}.map{Character($0)}}funccodePoints()->[Int]{varresult:[Int]=[]varplane=0// following documentation at https://developer.apple.com/documentation/foundation/nscharacterset/1417719-bitmaprepresentationfor(i,w)inbitmapRepresentation.enumerated(){letk=i%0x2001ifk==0x2000{// plane index byteplane=Int(w)<<13continue}letbase=(plane+k)<<3forjin0..<8wherew&1<<j!=0{result.append(base+j)}}returnresult}}// 使用方法varallowedCharacterSet=CharacterSet.urlQueryAlloweddebugPrint(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。
structMultipleEncoding:ParameterEncoding{varurlParameters:[String]?init(urlParameters:[String]?){self.urlParameters=urlParameters}funcencode(_urlRequest:Alamofire.URLRequestConvertible,with parameters:Parameters?)throws->URLRequest{guardletparameters=parameterselse{returnurlRequestas!URLRequest}varurlParams:[String:Any]=[:]varjsonParams:[String:Any]=[:]parameters.forEach{(key:String,value:Any)inifurlParameters?.contains(key)??false{urlParams[key]=value}else{jsonParams[key]=value}}// Encode URL Params// 过程和 URLEncoding.queryString.encode(urlRequest, with: urlParams) 一致varurlRequest=tryurlRequest.asURLRequest()ifleturl=urlRequest.url{ifvarurlComponents=URLComponents(url:url,resolvingAgainstBaseURL:false),!urlParams.isEmpty{letpercentEncodedQuery=(urlComponents.percentEncodedQuery.map{$0+"&"}??"")+query(urlParams)urlComponents.percentEncodedQuery=percentEncodedQuery urlRequest.url=urlComponents.url}}//Encode JSONreturntryJSONEncoding.default.encode(urlRequest,with:jsonParams)}privatefuncquery(_parameters:[String:Any])->String{// 直接复制 URLEncoding 中的方法}publicfuncqueryComponents(fromKey key:String,value:Any)->[(String,String)]{// 直接复制 URLEncoding 中的方法}// escape 只需要根据需要移出不希望被编码的字符publicfuncescape(_string:String)->String{letgeneralDelimitersToEncode=":#[]@"// does not include "?" or "/" due to RFC 3986 - Section 3.4letsubDelimitersToEncode="!$&'()*+,;="varallowedCharacterSet=CharacterSet.urlQueryAllowed// allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")// 下面的代码和 URLEncoding 中的方法一致}}
使用时如下。
extensionLiveShowRequest:TargetType{publicvarpath:String{switchself{case.anchorHeartBeat(letuserID,_):return"anchor_heartbeat"}}publicvartask:Task{switchself{caselet.anchorHeartBeat(userID,roomID):varparams=["roomID":roomID,"userID":userID,]as[String:Any]leturlParameters=["userID"]return.requestParameters(parameters:params,encoding:MultipleEncoding(urlParameters:urlParameters))}}}
这样就自定了 url 和 body 编码的整个过程。