
文章目录一、URL 简介二、服务端三、客户端2.1、客户端构造的 URL 地址的正确方式2.1.1、直接构造2.1.2、使用NSURLComponents构造2.2、服务器下发的 URL 的正确使用方式2.2.1 直接使用2.2.2 防护之后使用2.2.3 二次加工之后使用2.3、读取 URL 中的参数处理 URL 是常见的开发需求但是在十几年的从业生涯中几乎所有公司包括以技术见长互联网大厂都发生过URL相关的问题究其根本原因其实就是 URL 使用不规范造成的。一、URL 简介 根据 RFC 3986 标准URL 由 scheme、host、path、query 等部件Component通过保留符号连接而成。[scheme]://[username]:[password][host]:[port]/[path]?[query]#[fragment]保留符号除了上面这些包括如下在部件中使用的符号在 scheme 中用到的-符号。在 host 中用到.符号。在 path 中用到的/符号。在 query 中用到的和符号。 标准 RFC 3986 还规定了 URL 每个部件可使用的字符集且每个部件可用字符集都不相同下面是 iOS 中对这些字符集的定义。NSCharacterSet.URLUserAllowedCharacterSet;// usernameNSCharacterSet.URLPasswordAllowedCharacterSet;// passwordNSCharacterSet.URLHostAllowedCharacterSet;// hostNSCharacterSet.URLPathAllowedCharacterSet;// pathNSCharacterSet.URLQueryAllowedCharacterSet;// queryNSCharacterSet.URLFragmentAllowedCharacterSet;// fragment二、服务端服务端下发给客户的的 URL 必须是符合 RFC 3986 规范的形式具体操作就是应该先将 URL 的各个部件进行编码然后再用保留符号连接起来。对于部件的编码应该是先对部件的“组成部分”进行编码然后用保留符号连接比如对 query 编码应该先将字段名和字段值分别编码用连接起来后再然后用连接起来不能先连接然后再整体编码。对部件或部件组成部分的编码应该使用encodeURIComponent方法如果平台没有该方法应将除字母和数字之外的所有字符都进行编码。三、客户端 在 iOS 客户端中使用到的 URL 要么是客户端生成的要么是服务器下发的需要根据具体情形来处理。2.1、客户端构造的 URL 地址的正确方式 由客户端直接产生的 URL 地址比如构造接口请求的地址需按情形处理。2.1.1、直接构造 如果 URL 中所有字符都是已知或可以预知的都符合 URL 规范或已进行 URL 编码那么就可以直接构造。编码时使用.alphanumericCharacterSet字符集仅保留字母和数字编码其它所有字符。不能使用.URLQueryAllowedCharacterSet字符集这个字符集包含、、?这三个符字符如果字段名或字段值包含这三个字符就无法正常使用了。// URL 中所有字符串都是已知的且符合规范[NSURL URLWithString:https://api.host.com/path?keyvalue];// URL 中有不合法的字符但是可以提前对不合法的字符进行转码[NSURL URLWithString:https://api.host.com/path?key%20%5F];// 可以预知结果的简单拼接。NSString*encodedValue[originValue stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.alphanumericCharacterSet];NSString*urlString[NSString stringWithFormat:https://api.host.com/path?key%,encodedValue];NSURL*url[NSURL URLWithString:urlString];2.1.2、使用NSURLComponents构造 如果在 URL 中用到从外部传入的动态数据时推荐使用NSURLComponents不推荐使用字符串拼接。// 创建 components 对象不包含参数NSURLComponents*components[NSURLComponents componentsWithString:https://api.host.com/path];// 固定参数。如果有的话key 和 value 必须都使用 NSString 类型NSMutableDictionary*parameters[NSMutableDictionary dictionaryWithDictionary:{key:value}];// 动态参数将外部传入参数 inputParameters 与固定参数合并[inputParameters enumerateKeysAndObjectsUsingBlock:^(id key,id obj,BOOL*stop){// 将动态参数的 key 和 value 都转换为 NSString 类型if(![key isKindOfClass:NSString.class]){key[NSString stringWithFormat:%,key];}if(![obj isKindOfClass:NSString.class]){obj[NSString stringWithFormat:%,obj];}parameters[key]obj;}];// 生成参数NSMutableArray*queryItems[NSMutableArray arrayWithCapacity:parameters.count];[parameters enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key,id _Nonnull obj,BOOL*_Nonnull stop){// 在上面 key obj 已处理为字符串类型// 在 NSURLQueryItem 中使用原始值即可不需要事先编码。NSURLQueryItem*item[NSURLQueryItem queryItemWithName:key value:obj];[queryItems addObject:item];}];components.queryItemsqueryItems;// 将参数赋值给 components 对象// 生成 URLNSURL*apiURLcomponents.URL;2.2、服务器下发的 URL 的正确使用方式2.2.1 直接使用 一般场景下服务器下发的地址比如图片地址因为可以动态调整且影响较小应由服务端进行编码客户端可以直接使用。NSURL*imageURL[NSURL URLWithString:imageURLString];NSLog(%,imageURL);2.2.2 防护之后使用 重要场景比如页面跳转即使服务器已经进行了编码客户端也应该进行必要的防护。 不合法 URL 千差万别实际很难有万全的防护逻辑大部分情况下都是先尝试能不能创建 URL 如果不能则尝试转码之后再试。NSString*pageURLString;// 必要的防护。NSString*safePageURLString[pageURLString addCustomURLEncoding];NSURL*nextPageURL[NSURL URLWithString:safePageURLString];NSLog(%);2.2.3 二次加工之后使用 在实际操作中不可避免的要对服务器下发的URL进行二次加工比如处理 URL 参数。服务器下发的URL任何形式都有可能只建议使用NSURLComponents处理。NSURLComponents*components[NSURLComponents componentsWithString:urlStringFromServer];// 如果服务器下发的 URL 不合法先尝试使用系统的方式处理if(componentsnil){components[NSURLComponents componentsWithString:urlStringFromServer encodingInvalidCharacters:YES];}// 如果系统的处理不了使用自定义的防护逻辑if(componentsnil){NSString*fixedURLString[urlStringFromServer addCustomURLEncoding];components[NSURLComponents componentsWithString:fixedURLString];}// 对 NSURLComponents 二次加工见后文// 从 NSURLComponents 获取 NSURLNSURL*urlcomponents.URL;1、删除参数// 逆向遍历删除NSMutableArray*queryItemscomponents.queryItems.mutableCopy;for(NSInteger iqueryItems.count-1;i0;i--){NSURLQueryItem*itemqueryItems[i];if([item.name isEqualToString:keyToDelete]){[queryItems removeObjectAtIndex:i];// 如果确定字段只有一个值可以直接 break}}components.queryItemsqueryItems;2、添加参数NSMutableArray*queryItemscomponents.queryItems.mutableCopy?:NSMutableArray.new;// 从字典中添加参数[parameters enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key,id _Nonnull obj,BOOL*_Nonnull stop){// key obj 都必须是字符串if(![key isKindOfClass:NSString.class]){key[NSString stringWithFormat:%,key];}if(![obj isKindOfClass:NSString.class]){obj[NSString stringWithFormat:%,obj];}// 在 NSURLQueryItem 中使用原始值不需要事先编码。NSURLQueryItem*item[NSURLQueryItem queryItemWithName:key value:obj];[queryItems addObject:item];}];components.queryItemsqueryItems;3、修改参数 由于NSURLQueryItem是只读的修改参数按照“先删除后添加”的方式处理即可即上面的“删除参数”和“添加参数”部分。4、修改其它部分// 使用原始值直接使用对应的属性即可NSURLComponents 会自动处理编码components.hostnew.host;// 修改域名components.path/new/path;// 修改路径// 若已有现成的、确定已编码的值否则不建议使用 encoded 开头的属性。components.encodedHostencoded.new.host;components.percentEncodedPathencoded/new/path;2.3、读取 URL 中的参数 URL 中的参数一般有下面几种形式。形式值说明keyvaluevalue一般情况key空字符串keynil包含字段但没有值keyvaluekeykey[value, , NSNull.null]值为数组值为上面三种任意情况使用字符分割的方式读取参数需要自行处理URL编码不建议。NSURLComponents*components[NSURLComponents componentsWithString:urlFromServer];NSMutableArray*results[NSMutableArray array];// 以下通用处理逻辑一般情况下找到第一个符合的就可以结束循环。NSArray*queryItemscomponents.queryItems;for(NSInteger iqueryItems.count-1;i0;i--){NSURLQueryItem*itemqueryItems[i];if([item.name isEqualToString:keyToRead]){// keyToRead 为待读取的字段名if(item.value){[results addObject:item.value];}else{[results addObject:NSNull.null];}}}switch(results.count){case0:valuenil;break;case1:valueresults[0];break;default:valueresults;break;}if(value){NSLog(URL 中包含 % 字段,keyToRead);if(valueNSNull.null){NSLog(URL 中字段 % 的值为 nil,keyToRead);}elseif([value isKindOfClass:NSArray.class]){NSLog(URL 中字段 % 的包含多个值%,keyToRead,value);}else{NSLog(URL 中字段 % 的值为%,keyToRead,value);}}else{NSLog(URL 中不包含 % 字段,keyToRead);} 值为数组的字段处理方式。// 假设字段 key 的值为 value1, value2, value3 组成的数组。NSArray*values[value1,value2,value3];// 一般服务器默认都支持 keyvalue1keyvalue2keyvalue3 这种形式iOS默认也是这种形式。for(NSString*valueinvalues){[queryItems addObject:[NSURLQueryItem queryItemWithName:keyvalue:value]];}// 部分服务器默认也支持 key[]value1key[]value2key[]value3 这种形式需要自行处理。for(NSString*valueinvalues){[queryItems addObject:[NSURLQueryItem queryItemWithName:key[]value:value]];}// 上述两种方式的区别就是第一种方式无法表示只有一个元素的数组。// 在实践中不论使用何种方式都要与后端约定好比如还可以使用 JSON 字符串。NSString*json[values toJSONString];[queryItems addObject:[NSURLQueryItem queryItemWithName:keyvalue:json]];