这是实际工作中的一个例子,也算是一个坑吧,把过程记录一下也是好的。
问题描述
输入url:
https://host/path/index.html
输入参数,key值qresult:
http://qd.xxx.com/?paymentCode=ZA20632016080917074300000000268043
期望输出(Android):
https://host/path/index.html?qresult=http%3A%2F%2Fqd.xxx.com%2F%3FpaymentCode%3DZA20632016080917074300000000268043
实际输出:
https://host/path/index.html?qresult=http://qd.xxx.com/?paymentCode%3DZA20632016080917074300000000268043
问题:
http://qd.xxx.com/? ---- 问号前面部分没有编码
实现代码
let rawUrlString = "https://host/path/index.html"
guard let components = NSURLComponents(string: rawUrlString) else {
return
}
let key = "qresult"
let value = "http://qd.xxx.com/?paymentCode=ZA20632016080917074300000000268043"
let item = NSURLQueryItem(name:key, value: value)
if let _ = components.queryItems {
components.queryItems?.append(item)
} else {
components.queryItems = [item]
}
let newUrlString = components.URL?.absoluteString
print(newUrlString)
这段代码的输出:
Optional("https://host/path/index.html?qresult=http://qd.xxx.com/?paymentCode%3DZA20632016080917074300000000268043")
真有问题吗?
基本思路是利用NSURLComponents进行参数添加参数,方案很优雅
整个过程不牵涉到url的解码编码等问题,全部交给系统API去完成
从打印出来的结果看是有问题的,query参数中出现“://?”之类的是不符合URL标准的,从现象上看跟Android的有差异
单从功能上来讲,经过验证是可行的。整体的url字符串可以形成一个合法的NSURL,用WebView能够正确加载页面。qresult后面的参数也能够正确取出,这也是个url字符串,也可以形成一个合法的NSURL,也能用WebView进行正确加载。 === 整个过程全能通,开发者不需要关心url编码和解码的事,系统API在底层能很好地工作。
调试print,或者断点po出来的信息,是对象的描述信息。可以推测NSURLComponents在实现描述信息,或者实现描述协议的时候,进行了一层编解码,方便查看,但是底层实际工作还是按照标准的URL进行的。
如果在NSURLQueryItem的构建过程中,对值进行编码,经过测试,最终形成的结果中,会对百分号%本身也进行编码,结果反而不对,打印信息和实际功能(形成合法的NSURL)都不对。
let item = NSURLQueryItem(name:key, value: value.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLHostAllowedCharacterSet()))
如何一致?
优雅的NSURLComponents是用不上了,那就用最原始的字符串拼接。系统函数是stringByAddingPercentEncodingWithAllowedCharacters,确定参数是一个难点。
下面这段代码,在目前这个实例下面是有效的,功能和输出都能和Android保持一致。
let rawUrlString = "https://host/path/index.html"
let key = "qresult"
let value = "http://qd.xxx.com/?paymentCode=ZA20632016080917074300000000268043"
// 这里如果用参数NSCharacterSet.URLHostAllowedCharacterSet(),会出现"="没有编码的情况
// valueEncode = "http%3A%2F%2Fqd.xxx.com%2F%3FpaymentCode=ZA20632016080917074300000000268043" --- 这里有个等号
if let valueEncode = value.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet(charactersInString:":=\"#%/<>?@\\^`{|}").invertedSet) {
let newUrlString = rawUrlString + "?" + key + "=" + valueEncode
print(newUrlString)
}
这段代码的输出:
https://host/path/index.html?qresult=http%3A%2F%2Fqd.xxx.com%2F%3FpaymentCode%3DZA20632016080917074300000000268043
功能正常,由这个字符串能够创建NSURL对象
打印输出和Android保持一致,符合标准url格式
NSCharacterSet(charactersInString:":=\"#%/<>?@\\^`{|}").invertedSet
比较难理解。这是反逻辑,它的意思是除了":="#%/<>?@\^`{|}"以外的其他字符的集合这是从stackoverflow上抄的代码,其中的“:”还是我加上去的。这里是OK了,其他地方呢?
参考Android
既然要做到和Android的输出结果一致,那么参考Android的做法应该是可取的。
- Android代码截图
这段代码的意思是,url中允许的字符包括:数字,大小写字母,以及“_-!.~'()*”
将上面代码的反逻辑改成和Android一样的正逻辑,应该可行,也容易理解
let rawUrlString = "https://host/path/index.html"
let key = "qresult"
let value = "http://qd.xxx.com/?paymentCode=ZA20632016080917074300000000268043"
let unreservedChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-!.~'()*"
if let valueEncode = value.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet(charactersInString: unreservedChars)) {
let newUrlString = rawUrlString + "?" + key + "=" + valueEncode
print(newUrlString)
}
这段代码的输出:
https://host/path/index.html?qresult=http%3A%2F%2Fqd.xxx.com%2F%3FpaymentCode%3DZA20632016080917074300000000268043
功能和打印输出都是正确的
和Android在标准和行为上都达到了一致
几个工具
extension String {
func getScheme() -> String? {
return NSURLComponents(string: self)?.scheme
}
func getHost() -> String? {
return NSURLComponents(string: self)?.host
}
func getQueryParameter(key: String) -> String? {
let urlItem = NSURLComponents(string: self)?.queryItems?.filter({ (item) -> Bool in
return item.name == key
}).first
return urlItem?.value
}
func appendQueryParameter(key: String, value: String) -> String {
guard let components = NSURLComponents(string: self) else {
return self
}
let item = NSURLQueryItem(name:key, value: value)
if let _ = components.queryItems {
components.queryItems?.append(item)
} else {
components.queryItems = [item]
}
if let url = components.URL?.absoluteString {
return url
} else {
return self
}
}
func urlEncode() -> String? {
let unreservedChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-!.~'()*"
let unreservedCharset = NSCharacterSet(charactersInString: unreservedChars)
let encodedString = stringByAddingPercentEncodingWithAllowedCharacters(unreservedCharset)
return encodedString
}
func appendQueryParameterUseEncode(key: String, value: String) -> String {
guard let components = NSURLComponents(string: self), url = components.URL, encodedValue = value.urlEncode() else {
return self
}
if let queryItems = components.queryItems { // 带参数
if (queryItems.count > 0) && !url.absoluteString.hasSuffix("&") {
return url.absoluteString + "&" + key + "=" + encodedValue
} else { // 带问号,但没有参数
return url.absoluteString + key + "=" + encodedValue
}
} else { // 不带参数
return url.absoluteString + "?" + key + "=" + encodedValue
}
}
}
NSURL和NSURLComponents都是类,结构比较大
NSURL还有很大一部分是文件系统的内容,这又是一个大坑
在模块间传递,可能涉及到多中平台,字符串还是用得更多
客户端和网络之间,大多数是在字符串的基础上组装字典,JSON编码传输。这几个工具可以方便使用。
计算属性本质上也是函数,不过函数可以指定参数,感觉功能更强大一点,也更灵活一点。当然,用计算属性会显得更简洁一些。这两种都是可以考虑的
遗留问题
RFC1738里面的相关部分如下:
通过比较可以知道,有两个字符“,$”漏掉了。是有原因,故意不放上去,还是某种原因遗漏,无从知晓。
例子代码
UrlParseUtility
参考文章
在线编码转换
Swift - encode URL
How to encode a URL in Swift
关于URL编码