往期导航:
简介
这俩都是用来在创建请求时,对参数进行编码用的,传入的参数
相同点
- 都是在Session中创建Request时使用
- 都是用来把把参数编码进URLRequest中
- 都可以决定参数的编码位置(url query string、body表单、bodyjson)
- UploadRequest因为不带参数,所以不会使用这俩
不同点
- 初始化参数不同
- ParameterEncoding只能编码字典数据, ParameterEncoder用来编码任意实现Encodable协议的数据类型
- ParameterEncoding编码实现简单,因为都是字典数据,body表单编码时,只需要先编码成query string,然后utf8转成data丢入body就行,ParameterEncoder使用的是一个自己Alamofire自己实现的URLEncodedFormEncoder来进行表单数据编码,可以编码Date,Data等特殊数据
- ParameterEncoding只有在创建DataRequest跟DownloadRequest时使用,DataStreamRequest无法使用,而ParameterEncoder这三个Request子类都能用来初始化
ParameterEncoding
首先定义了Parameters别名为[String: Any], 只能用来编码字典参数, 协议很简单,只有一个方法用来把参数编码到URLRequest中,并返回新的URLRequest:
/// A dictionary of parameters to apply to a `URLRequest`.
public typealias Parameters = [String: Any]
/// A type used to define how a set of parameters are applied to a `URLRequest`.
public protocol ParameterEncoding {
/// 使用URLRequestConvertible创建URLRequest, 然后把字典参数编码进URLRequest中, 可以抛出异常, 抛出异常时会返回AFError.parameterEncodingFailed错误
func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
}
Alamofire提供了默认实现,分别用来编码url query string跟json
1.URLEncoding默认实现,用来编码url query string
- 根据参数编码的位置分为: querystring与form表单两种, 种类由Destination枚举控制
- 若是表单编码, 请求头的Content-Type会被设置为application/x-www-form-urlencoded; charset=utf-8
- 数组与字典通过递归来全部编码
- 因为没有统一规范规定如何编码集合参数, 因此数组参数编码有两个选择, 由ArrayEncoding枚举控制:, 默认带方括号
例子: key: [value1, value2]
1.使用key后面跟方括号然后跟等号跟值,例如: key[]=value1&key[]=value2
2.key后面不跟括号, 例如: key=value1&key=value2 - 字典参数编码使用key跟方括号subkey跟等号跟值
key[subkey1]=value1&key[subkey2]=value2
- Bool值编码可以选择使用数值0,1还是使用字符串true,false, 由BoolEncoding枚举控制, 默认为数值
代码注释:
public struct URLEncoding: ParameterEncoding {
// MARK: 辅助数据类型
/// 定义参数被编码到url query中还是body中
public enum Destination {
/// 有method决定(get, head, delete为urlquery, 其他为body)
case methodDependent
/// url query
case queryString
/// body
case httpBody
/// 返回是否要把参数编入到url query中
func encodesParametersInURL(for method: HTTPMethod) -> Bool {
switch self {
case .methodDependent: return [.get, .head, .delete].contains(method)
case .queryString: return true
case .httpBody: return false
}
}
}
/// 决定如何编码Array
public enum ArrayEncoding {
/// key后跟括号编码
case brackets
/// key后不跟括号编码
case noBrackets
/// 对key进行编码
func encode(key: String) -> String {
switch self {
case .brackets:
return "\(key)[]"
case .noBrackets:
return key
}
}
}
///决定如何编码Bool
public enum BoolEncoding {
/// 数字: 1, 0
case numeric
/// string: true, false
case literal
/// 对值进行编码
func encode(value: Bool) -> String {
switch self {
case .numeric:
return value ? "1" : "0"
case .literal:
return value ? "true" : "false"
}
}
}
// MARK: 快速初始化的三个静态计算属性
/// 默认使用method决定编码位置, 数组使用带括号, bool使用数字
public static var `default`: URLEncoding { URLEncoding() }
/// url query 编码, 数组使用带括号, bool使用数字
public static var queryString: URLEncoding { URLEncoding(destination: .queryString) }
/// form 表单编码到body, 数组使用带括号, bool使用数字
public static var httpBody: URLEncoding { URLEncoding(destination: .httpBody) }
//MARK: 属性与初始化
/// 参数编码位置
public let destination: Destination
/// 数组编码格式
public let arrayEncoding: ArrayEncoding
/// Bool编码格式
public let boolEncoding: BoolEncoding
public init(destination: Destination = .methodDependent,
arrayEncoding: ArrayEncoding = .brackets,
boolEncoding: BoolEncoding = .numeric) {
self.destination = destination
self.arrayEncoding = arrayEncoding
self.boolEncoding = boolEncoding
}
// MARK: 实现协议的编码方法
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
//先拿到URLRequest
var urlRequest = try urlRequest.asURLRequest()
//没参数的话直接返回
guard let parameters = parameters else { return urlRequest }
//先拿到method, 然后使用method判断下往哪里编码参数
//不够严谨, 如果method为空, 应该抛出异常的. ParameterEncoder中有处理
if let method = urlRequest.method, destination.encodesParametersInURL(for: method) {
//url query编码
guard let url = urlRequest.url else {
// url为空直接抛出异常
throw AFError.parameterEncodingFailed(reason: .missingURL)
}
if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty {
//先获取到已有的query string, 存在的话就加上个&, 然后拼接上新的query string
let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
urlComponents.percentEncodedQuery = percentEncodedQuery
urlRequest.url = urlComponents.url
}
} else {
//body编码
if urlRequest.headers["Content-Type"] == nil {
//设置Content-Type
urlRequest.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8"))
}
//把query string转成utf8编码丢入body中
urlRequest.httpBody = Data(query(parameters).utf8)
}
return urlRequest
}
/// 对key-value对进行编码, value主要处理字典,数组,nsnumber类型的bool,bool以及其他值
public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
var components: [(String, String)] = []
switch value {
case let dictionary as [String: Any]:
//字典处理, 遍历字典递归调用
for (nestedKey, value) in dictionary {
components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
}
case let array as [Any]:
for value in array {
//数组处理, 根据数组key编码的类型遍历递归调用
components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
}
case let number as NSNumber:
//nsnumber使用objCType类判断是否是bool
if number.isBool {
components.append((escape(key), escape(boolEncoding.encode(value: number.boolValue))))
} else {
components.append((escape(key), escape("\(number)")))
}
case let bool as Bool:
//bool处理, 根据编码类型来处理
components.append((escape(key), escape(boolEncoding.encode(value: bool))))
default:
//其他的,直接转成string
components.append((escape(key), escape("\(value)")))
}
return components
}
/// url转义, 转成百分号格式的
/// 会忽略 :#[]@!$&'()*+,;=
public func escape(_ string: String) -> String {
string.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? string
}
/// 把参数字典转成query string
private func query(_ parameters: [String: Any]) -> String {
//存放key,value元组
var components: [(String, String)] = []
for key in parameters.keys.sorted(by: <) {
let value = parameters[key]!//直接强制解包
//对每个key-value对进行编码
components += queryComponents(fromKey: key, value: value)
}
//拼接成query string返回
return components.map { "\($0)=\($1)" }.joined(separator: "&")
}
}
2.JSONEncoding默认实现,用来把参数编码成json丢入body中
使用JSONSerialization来把参数字典编码为json, 一定会被编码到body中, 并且会设置Content-Type为application/json
代码注释:
public struct JSONEncoding: ParameterEncoding {
// MARK: 用来快速初始化的静态计算变量
//默认类型, 压缩json格式
public static var `default`: JSONEncoding { JSONEncoding() }
//标准json格式
public static var prettyPrinted: JSONEncoding { JSONEncoding(options: .prettyPrinted) }
// MARK: 属性与初始化
//保存JSONSerialization.WritingOptions
public let options: JSONSerialization.WritingOptions
public init(options: JSONSerialization.WritingOptions = []) {
self.options = options
}
// MARK: 实现协议的编码方法
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
//拿到Request
var urlRequest = try urlRequest.asURLRequest()
guard let parameters = parameters else { return urlRequest }
do {
//编码成data
let data = try JSONSerialization.data(withJSONObject: parameters, options: options)
//设置Content-Type
if urlRequest.headers["Content-Type"] == nil {
urlRequest.headers.update(.contentType("application/json"))
}
//丢入body
urlRequest.httpBody = data
} catch {
//解析json出错就抛出错误
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}
return urlRequest
}
//把json对象编码进body中, 其实上面的编码方法可以直接掉这个方法, 两个方法实现一毛一样
public func encode(_ urlRequest: URLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest {
var urlRequest = try urlRequest.asURLRequest()
guard let jsonObject = jsonObject else { return urlRequest }
do {
let data = try JSONSerialization.data(withJSONObject: jsonObject, options: options)
if urlRequest.headers["Content-Type"] == nil {
urlRequest.headers.update(.contentType("application/json"))
}
urlRequest.httpBody = data
} catch {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}
return urlRequest
}
}
最后还扩展了下OC的NSNumber类,添加了检测是否为Bool类型的方法:
extension NSNumber {
fileprivate var isBool: Bool {
// Use Obj-C type encoding to check whether the underlying type is a `Bool`, as it's guaranteed as part of
// swift-corelibs-foundation, per [this discussion on the Swift forums](https://forums.swift.org/t/alamofire-on-linux-possible-but-not-release-ready/34553/22).
String(cString: objCType) == "c"
}
}
ParameterEncoder
协议很简单,也是只有一个方法,把Parameters类型的参数编码进URLRequest中,但是要求Parameters类型必须符合Encodable协议。
public protocol ParameterEncoder {
func encode<Parameters: Encodable>(_ parameters: Parameters?, into request: URLRequest) throws -> URLRequest
}
其实有很多地方类似ParameterEncoding,也是把参数编码编码进Request,编码位置也是可以控制,但是对参数要求不同:
ParameterEncoding要求参数是字典类型,字典的value是Any的,编码为url query string时会直接强制转成String,因此对于标准类型以外的数据,编码出来的值就会错误。编码为JSON时,标准类型以外的数据,会导致编码错误,抛出异常
ParameterEncoder要求参数符合Encodable协议,编码时使用的是Encoder协议对象,编码为json时,用的是JSONEncoder,编码为url query string时,用的是自己实现的URLEncodedFormEncoder编码器
因此,若编码的参数为符合Encodable类型的字典时,使用两种编码方式都ok。比如parameter = ["a": 1, "b": 2]
这样的参数。
也有两个默认实现,分别用来进行json编码与url query string编码:
1.JSONParameterEncoder编码json数据
使用系统的JSONEncoder来编码数据,可以控制json的格式,ios11以上还支持根据key来排序(json字典为无序),实现方法也是比较简单:
open class JSONParameterEncoder: ParameterEncoder {
//MARK: 用来快速创建对象的静态计算属性
/// 默认类型, 使用默认的JSONEncoder初始化, 会压缩json格式
public static var `default`: JSONParameterEncoder { JSONParameterEncoder() }
/// 使用标准json格式输出
public static var prettyPrinted: JSONParameterEncoder {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
return JSONParameterEncoder(encoder: encoder)
}
/// ios11以上支持输出的json根据key排序
@available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)
public static var sortedKeys: JSONParameterEncoder {
let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
return JSONParameterEncoder(encoder: encoder)
}
// MARK: 属性与初始化
/// 用来编码参数的JSONEncoder
public let encoder: JSONEncoder
public init(encoder: JSONEncoder = JSONEncoder()) {
self.encoder = encoder
}
/// 实现协议的编码方法:
open func encode<Parameters: Encodable>(_ parameters: Parameters?,
into request: URLRequest) throws -> URLRequest {
//获取参数
guard let parameters = parameters else { return request }
var request = request
do {
//把参数编码成json data
let data = try encoder.encode(parameters)
//丢入body
request.httpBody = data
//设置Content-Type
if request.headers["Content-Type"] == nil {
request.headers.update(.contentType("application/json"))
}
} catch {
//解析json异常就抛出错误
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}
return request
}
}
2.URLEncodedFormParameterEncoder编码url query string数据
url编码, 使用Destination来判断编码到url query还是body中, 编码数据使用的是URLEncodedFormEncoder类
open class URLEncodedFormParameterEncoder: ParameterEncoder {
/// 参数编码位置,与ParameterEncoding一样
public enum Destination {
case methodDependent
case queryString
case httpBody
func encodesParametersInURL(for method: HTTPMethod) -> Bool {
switch self {
case .methodDependent: return [.get, .head, .delete].contains(method)
case .queryString: return true
case .httpBody: return false
}
}
}
// MARK: 默认初始化对象, 使用URLEncodedFormEncoder默认参数, 编码位置由method决定
public static var `default`: URLEncodedFormParameterEncoder { URLEncodedFormParameterEncoder() }
/// 用来编码数据的URLEncodedFormEncoder对象
public let encoder: URLEncodedFormEncoder
/// 编码位置
public let destination: Destination
public init(encoder: URLEncodedFormEncoder = URLEncodedFormEncoder(), destination: Destination = .methodDependent) {
self.encoder = encoder
self.destination = destination
}
// 实现协议的编码参数方法
open func encode<Parameters: Encodable>(_ parameters: Parameters?,
into request: URLRequest) throws -> URLRequest {
//获取参数
guard let parameters = parameters else { return request }
var request = request
//首先要保证url存在
guard let url = request.url else {
throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.url))
}
//然后保证method存在
guard let method = request.method else {
let rawValue = request.method?.rawValue ?? "nil"
throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.httpMethod(rawValue: rawValue)))
}
//根据编码位置, 进行编码操作
if destination.encodesParametersInURL(for: method),
var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
//url query
//这里格式化了下写法写下详细注释(吹爆swift!)
let query: String = try Result<String, Error> {//初始化Request(参数为可以抛出异常的闭包)
try encoder.encode(parameters)//编码参数
}
.mapError {//编码出错转换为AFError
AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0))
}
.get()//get可以获取成功数据, 若为error, 会抛出异常
//这里写法也很骚, 把原querystring与新的querystring组合成一个[String?]数组, 然后compactMap去掉nil, 再用&组合起来
let newQueryString = [components.percentEncodedQuery, query].compactMap { $0 }.joinedWithAmpersands()
components.percentEncodedQuery = newQueryString.isEmpty ? nil : newQueryString
//url不能为空
guard let newURL = components.url else {
throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.url))
}
request.url = newURL
} else {
//设置Content-Type
if request.headers["Content-Type"] == nil {
request.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8"))
}
//编码, 然后丢入body 吐槽:尾随闭包+写一行读起来太难受了
request.httpBody = try Result<Data, Error> {
try encoder.encode(parameters)
}
.mapError {
AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0))
}
.get()
}
return request
}
}
URLEncodedFormEncoder用来把数据编码成url query string的核心类
- 该类被声明为final, 不允许继承, 只允许在初始化时通过参数控制行为.
- 该类定义了很多控制编码行为的数据类型,并在初始化时设置这些类型
- 编码时使用自定义的实现了Encoder协议的 _URLEncodedFormEncoder 内部类型的属性。用来编码数据
- 编码的数据储存对象为URLEncodedFormComponent枚举,可以保存string,array以及使用元组数组代表的object三种类型,使用元组数组来代表object类型可以使参数保持顺序。
- 持有一个URLEncodedFormContext上下文属性,该属性持有URLEncodedFormComponent来保存数据,作为上下文递归编码时传递使用
- 最后实现了一个URLEncodedFormSerializer序列器,用来把编码完成的URLEncodedFormComponent数据序列化成query string给上层
这个URLEncodedFormEncoder自定义编码器,有点复杂,会放在下一篇中详解
以上纯属个人理解,难免有误,如发现有错误的地方,欢迎评论指出,将第一时间修改,非常感谢~