Alamofire-从一个简单的请求深入源代码(1)

2017-10-10  本文已影响81人  yww

今天我们从一个简单的例子中, 来一步一步剖析 Alamofire 的源代码.

简单的例子

例子很简单, 就是请求一个api, 然后打印输出结果而已.

Alamofire.request("https://httpbin.org/get").responseString { (response) in
    if let string = response.result.value {
        print(string)
    }
}

Alamofire.request

你如果自己写这段代码就知道, 你在写Alamofire 的时候, 是没有代码提示的, 这并不是一个 bug, 因为 Alamofire 并不是一个类型, 而是一个模块名, 在这里只是起到一个类似命名空间的作用. 所以你大可将例子改为request(xxx)...

request 函数签名

request 函数其实并不只有一个参数, 我们这里其他的参数都是使用的默认值而已.
完成的函数签名

func request(
    _ url: URLConvertible,
    method: HTTPMethod = .get,
    parameters: Parameters? = nil,
    encoding: ParameterEncoding = URLEncoding.default,
    headers: HTTPHeaders? = nil)
    -> DataRequest

这里一下出现了很多未知的类型, 我们慢慢来.

URLConvertible

我们在示例中明明是传入的一个字符串, 为何这里是一个URLConvertible 类型? 想要回答这个问题, 我们不妨看一下定义

public protocol URLConvertible {
    func asURL() throws -> URL
}

URLConvertible 只是一个协议, 这个协议也只有一个方法. 方法的目的也很简单--想办法变成一个URL.
同时, String 类型也是实现了这个协议的(为了方便阅读, 稍微调整了一下代码缩进)

extension String: URLConvertible {
    public func asURL() throws -> URL {
        guard let url = URL(string: self) else {
            throw AFError.invalidURL(url: self) 
        }
        return url
    }
}

几行代码, 一目了然, 无需多言, 错误处理也很到位.
String 类似, URL, URLComponents 也是实现了这个协议的. 同时, 如果你愿意, 你也可以自己实现, 例如你想把一个枚举转换成 url, 这样请求 api, 就可以直接填写枚举, 岂不美哉?

HTTPMethod

这个就不难理解, 请求方法而已, 这里默认的方法是GET, 除此之外, 还有"POST", "PUT" 等等. 如果你想对 http 请求方法有更深入了解, 可以查阅 HTTP 权威指南

Parameters

这个类型只是一个别名, 定义如下
public typealias Parameters = [String: Any]
你可以在这里填入你要请求的参数.

ParameterEncoding

参数的编码方式.
常用的编码方式为URLEncoding, 这种编码方式, 会将参数编码为 param1=value1&param2=value2 类似的结构.
初次之外, 还有编码为 JSON, PLIST 方式.
ParameterEncoding 本身是一个枚举, 定义如下

public protocol ParameterEncoding {
    func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
}

可以看出来, 目的很简单, 就是想要根据参数和请求(URLRequestConvertible, 这个我们后面来讲, 这里你就认为他是一个URLRequest 吧)生成一个含有参数的 URLRequest
上面说到, Alamofire 为我们已经预定义了三种编码方式, 我们先来看 URLEncoding.

URLEncoding

编码的最终格式如上面所示, 而编码后的数据放在那里, 则有两种情况, 分别是直接放在 URL 上和放在请求体中( http body).
而具体放在那里, Alamofire 给我们两种方案, 一种是自动根据请求方法选择, 会根据 HTTP 请求方法来确定, 例如针对 GET 请求, 通常是直接放在 URL 中.第二种是手动制定
为此, Alamofire 在 URLEncoding 类型内部, 定义了一个结构体, 同时, 定了了几个便捷属性, 方便外部使用.

public struct URLEncoding: ParameterEncoding {
    public enum Destination {
        case methodDependent, queryString, httpBody
    }
   /// 依赖方法 
    public static var `default`: URLEncoding { return URLEncoding() }
    /// 与 default 相同
    public static var methodDependent: URLEncoding { return URLEncoding() }
    /// 编码时, 参数放到 url 中
    public static var queryString: URLEncoding { return URLEncoding(destination: .queryString) }
    /// 编码时, 参数放到请求体中
    public static var httpBody: URLEncoding { return URLEncoding(destination: .httpBody) }
    ///编码后的参数位置
    public let destination: Destination
    /// 构造函数
    public init(destination: Destination = .methodDependent) {
        self.destination = destination
    }
    ....
}

接下来我们来看看核心的编码功能

public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
    // 获取 request
    var urlRequest = try urlRequest.asURLRequest()
    // 获取参数, 如果没有参数, 那么直接返回
    guard let parameters = parameters else { return urlRequest }
    // 获取请求方法, 同时, 根据请求方法来判断是否需要编码参数到 url 中
    if let method = HTTPMethod(rawValue: urlRequest.httpMethod ?? "GET"), encodesParametersInURL(with: method) {
        // 直接编码到 url 中
        // 获取 url
        guard let url = urlRequest.url else {
            throw AFError.parameterEncodingFailed(reason: .missingURL)
        }
        // 构建一个URLComponents 对象, 并在其中添加参数
        if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) {
            // 此处 map 是 optional 的map, 如果 optionvalue 不会空的时候, 会调用 map 内的闭包
            // 如果 url 中本来就有一部分参数了, 那么就将新的参数附加在后面
            let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
            urlComponents.percentEncodedQuery = percentEncodedQuery
            urlRequest.url = urlComponents.url
        }
    } else {
        // 这里是要添加到请求体中
        if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
            // 在请求头中设置编码格式
            urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
        }
        // 编码到 body 中
        urlRequest.httpBody = query(parameters).data(using: .utf8, allowLossyConversion: false)
    }
    return urlRequest
}

注释我都写在代码中里面了.
其中这里面有用到几个函数, 一个是encodesParametersInURL
这个函数是来判断是否要编码参数到URL 中
实现如下

private func encodesParametersInURL(with method: HTTPMethod) -> Bool {
    /// 如果在编码位置中明确指出了, 那么就根据这个来确定是否要编码在 url 中
    switch destination {
    case .queryString:
        return true
    case .httpBody:
        return false
    default:
        break
    }
    /// 如果是依赖方法, 那么就根据请求的方法来决定
    switch method {
    case .get, .head, .delete:
        return true
    default:
        return false
    }
}

还有一个函数, query, 这个用于将参数编码. 这个很细节, 可以后面有时间单独讲讲. 这里就略过.
URLEncoding 就这些了, JSONEncoding, PropertyListEncoding 和这个类似, 只不过编码方式不同而已, 需要注意的是, 后面这两种, 都是直接编码到 HTTPBody 中的. 如果你想把 JSON 编码到 URL 中, 你就只有自己写一个, 你可以依葫芦画瓢, 也是很容易的.
接下来我们继续回到 Alamofire.request 函数中

HTTPHeaders

这个也是跟 Parameters 参数一样的, 一个别名而已
public typealias HTTPHeaders = [String: String]
你可以用这个参数, 在请求头中添加一些自定义的请求头.
讲到这里, 我们才把 Alamofire.request 的参数列表理了一遍, 接下来, 我们来看看函数体
另外, 我再 alamofire 中添加了一些中文注释, 愿意的话, 你也可看看https://github.com/ywwzwb/alamofire-chinese

上一篇下一篇

猜你喜欢

热点阅读