HTTPTransport
描述
这个库是一个 Alamofire 包装器,允许同步 HTTP 请求。
基本上,您可以使用如下这种常规的流程控制
Alamofire.request(someRequest).response { reponse in
if response ...
}
HTTPTransport 允许您使用这样的常规流程控制
let result = transport.send(someRequest)
if result ...
注意:库作者假设您知道如何构建移动应用程序,并将有关多线程和同步网络弊端的讨论留在了括号之后。
使用方法
安装:CocoaPods
pod 'HTTPTransport'
主要参与者
库的基本概念非常直接:您需要发送一个 请求 经过一个 传输 来接收一些 结果 — 这三个主要角色您将需要处理。
HTTP传输
除了执行实际的HTTP调用外,一个HTTP传输实例还包含对连接的非功能要求,如保持已建立的HTTP会话、实施的安全措施,以及默认的请求和响应处理堆栈,包括错误处理。
class HTTPTransport {
let session: Session
let requestInterceptors: [HTTPRequestInterceptor]
let responseInterceptors: [HTTPResponseInterceptor]
}
HTTPRequest
一个瑞士军刀般的多功能工具,能满足您构建HTTP请求的所有需求。
class HTTPRequest {
let httpMethod: HTTPMethod
let endpoint: String
var headers: [String: String]
var parameters: [HTTPRequestParameters]
var requestInterceptors: [HTTPRequestInterceptor]
var responseInterceptors: [HTTPResponseInterceptor]
let session: Session?
let timeout: TimeInterval
}
基本上按您期望的方式工作。首先,它是一个HTTP请求信封字段的容器对象,包括一个 HTTPMethod
、一个 endpoint
(URL或其部分)、请求头和请求主体。
其次,每个HTTPRequest
实例指定其自己的超时间隔、一个自定义的Session
(如果需要),以及应用于此特定请求及其响应的两个拦截器集合。其中大多数选项都有默认值,因此不会太困扰您。
HTTPRequest
类提供多种修改其内容的方式,包括一个智能构造函数,允许基于其他HTTPRequest
实例来创建HTTPRequest
实例,详见食谱中的基础依赖请求。
class HTTPRequest {
func with(header: String, value: String) -> Self
func with(cookieName name: String, value: String) -> Self
func with(cookie: HTTPCookie) -> Self
func with(parameter: String, value: Any, encoding: HTTPRequestParameters.Encoding) -> Self
func with(parameters: [String: Any], encoding: HTTPRequestParameters.Encoding) -> Self
func with(parameters: HTTPRequestParameters) -> Self
func with(parameters: [HTTPRequestParameters]) -> Self
func with(interceptors: [HTTPRequestInterceptor]) -> Self
func with(interceptors: [HTTPResponseInterceptor]) -> Self
}
let userSearchRequest =
HTTPRequest(endpoint: "/user")
.with(cookieName: "SESSION_ID", value: sessionId)
.with(parameters: ["first_name": "John", "last_name": "Appleseed"], encoding: .url)
请求参数通过一个独立的容器类 HTTPRequestParameters
表示,允许每个HTTPRequest
包含几组不同编码的参数。
有两组扩展自HTTPRequest
的子类:DataUploadHTTPRequest
和FileUploadHTTPRequest
。两者基本上都是不言自明的;它们用于上传Data
和文件。
HTTP传输的结果
第三个主要角色,表示HTTP调用的结果。要么是.success
(成功)要么是.failure
(失败)。
enum Result {
case success(response: HTTPResponse)
case failure(error: NSError)
}
您需要知道的主要观点是,对于成功的HTTP调用来讲或失败的HTTP调用,定义依赖于您所应用验证技术的不同。
默认情况下,Alamofire的validate()
方法会被调用(见HTTPTransport.useDefaultValidation
属性),这意味着只有状态码为2xx
的响应被认定为成功的,否则会被转换为错误。禁用useDefaultValidation
会将所有服务器响应都视为成功,无论响应内容如何,而像网络断开等情况则会导致失败。
在低级别上,响应会受到设置的一组响应拦截器的影响,这些拦截器在Alamofire验证之前生效。这就是为什么你可能可以考虑将ClarifyErrorInterceptor
加入到你的传输响应拦截器栈中,因为它会丰富结果中的NSError
对象。
HTTPRequestParameters
本质上,这是一个字典,额外包含了这个字典将被如何编码的信息。
class HTTPRequestParameters {
var parameters: [String: Any]
let encoding: Encoding
subscript(parameterName: String) -> Any? { get set }
enum Encoding {
case json
case url
case propertyList
case custom(encode: EncodeFunction)
}
}
json
和propertyList
将被编码到体中,url
参数进入查询字符串。
你的HTTPRequest
可能包含多个HTTPRequestParameters
集合
class HTTPRequest {
var parameters: [HTTPRequestParameters]
}
let request =
HTTPRequest(
parameters: [
HTTPRequestParameters(parameters: ["name": "John"], encoding: .json),
HTTPRequestParameters(parameters: ["dob": "12/12/12"], encoding: .json),
HTTPRequestParameters(parameters: ["age": 5], encoding: .url),
]
)
这里的规则如下:
- 具有相同编码的参数合并为一个字典;
- 具有相同编码和相同键的参数会覆盖合并字典中的先前值;
- 参数将在具有相同编码的
base
请求参数之后追加; - 参数会覆盖具有相同编码和相同键的
base
参数; propertyList
参数和json
参数不会在同一个体中混合,它们会相互覆盖;最后一个是胜利者;FileUploadHTTPRequest
请求忽略json
参数;propertyList
参数在文件多部分之后附加;DataUploadHTTPRequest
请求同时忽略propertyList
和json
参数。
Session
会话对象包含Alamofire的SessionManager
并提供了一种方便的配置连接安全的方法,这与Security
对象相关。
class Session {
let manager: SessionManager
convenience init()
init(security: Security)
}
Security
对象允许通过证书指纹检查主机
class Security {
class var noEvaluation: Security
init(certificates: [Certificate])
}
struct Certificate {
let host: String
let fingerprint: Fingerprint
enum Fingerprint {
case sha1(fingerprint: String)
case sha256(fingerprint: String)
case publicKey(fingerprint: String)
case debug
case disable
}
}
通过字符串交集检查主机名。这意味着Certificate(host: "host.com", fingerprint:...)
应用于诸如https://www.host.com/query
、https://host.com
、https://api.host.com/v1
等URL。
拦截器
HTTPRequestInterceptor
和 HTTPResponseInterceptor
是受启发于 okhttp拦截器、Django中间件 等的抽象中间件类。拦截器可以改变输入和输出,每个 HTTPTransport
实例都包含两个列表,分别对应用于每个请求的请求拦截器和随后应用于每个响应的响应拦截器。
换句话说,当您的应用通过 传输 发送一个 请求 时,后者会在实际发送之前将这个 请求 通过其请求拦截器列表。收到 响应 后,传输会将它通过响应拦截器列表传递,然后将其传输到您的 应用 中。
class HTTPRequestInterceptor {
func intercept(request: URLRequest) -> URLRequest
}
class HTTPResponseInterceptor {
func intercept(response: RawResponse) -> RawResponse
struct RawResponse {
let request: URLRequest?
let response: HTTPURLResponse?
let data: Data?
let error: Error?
}
}
拦截器可能会也可能不会改变它们处理的数据。例如,您的一个请求拦截器可能会为每个请求添加一个 Authentication
标头。另一个请求拦截器可能会只将请求数据打印到控制台日志中。
您可以通过扩展上述提到的类来实现自己的拦截器。《HTTPTransport》库已经包含了一些基本的实用拦截器,例如:
LogRequestInterceptor
和LogResponseInterceptor
— 允许您记录请求和响应;AddCookieInterceptor
— 将cookieProvider
中的 cookies 添加到每个请求;ReceivedCookieInterceptor
— 将接收到的 cookies 存储到cookieStorage
;ClarifyErrorInterceptor
— 将 API 错误 JSON 载荷(例如{"code": 500, "message": "Database error"}
)转换为NSError
实例,见下文。
NSError
HTTPTransport 提供了对现有 NSError
类的扩展,增加了实时 HTTP 响应的一些额外部分(如果有)。
大多数这些功能只有在启用 ClarifyErrorInterceptor
时才会工作。
extension NSError {
var url: String? // contains URL when HTTPRequest have failed to serialize into URLRequest
var httpStatusCode: HTTPStatusCode? // HTTP status
var responseBodyData: Data? // received bytes
var responseBodyString: String? // received bytes as UTF8 string
var responseBodyJSON: Any? // received bytes as a JSON object
var responseBodyJSONDictionary: [String: Any]? // received JSON object casted to dictionary
var responseBodyErrorCode: String? // parsed error code from received JSON
var responseBodyErrorMessage: String? // parsed error message from received JSON
}
食谱
基本 GET 请求
// assuming all following code runs in a background thread
let request = HTTPRequest(endpoint: "https://api.service.com")
let transport = HTTPTransport()
let result: HTTPTransport.Result = transport.send(request: request)
switch result {
case .success(let httpResponse):
print(httpResponse.httpStatus)
do {
if let json: [String: Any] = try httpResponse.getJSONDictionary() {
print(json)
}
} catch {
print("JSONSerialization error")
}
case .failure(let nsError):
if let httpStatus: HTTPStatusCode = nsError.httpStatusCode {
print(httpStatus)
} else {
print(nsError.localizedDescription)
}
}
基本依赖请求
// assuming all following code runs in a background thread
let transport = HTTPTransport()
let baseRequest =
HTTPRequest(endpoint: "https://api.service.com")
.with(header: "User-Agent", value: "Application/iOS")
let authRequest = HTTPRequest(endpoint: "/session", base: baseRequest)
let authResult = transport.send(request: authRequest)
if let sessionId: String = getSessionId(authResult) {
let userSearchRequest =
HTTPRequest(endpoint: "/user", base: baseRequest)
.with(cookieName: "SESSION_ID", value: sessionId)
.with(parameters: ["first_name": "John", "last_name": "Appleseed"], encoding: .url)
let searchResult = transport.send(request: userSearchRequest)
if let users: [User] = getUsers(searchResult) {
showUsers(users)
} else {
showEmptyScreen()
}
} else {
showError()
}
日志记录
let transport = HTTPTransport(
requestInterceptors: [
LogRequestInterceptor(logLevel: LogRequestInterceptor.LogLevel.url),
],
responseInterceptors: [
LogResponseInterceptor(logLevel: LogResponseInterceptor.LogLevel.everything),
]
)
let result = transport.send(...)
发送和接收 cookie
let cookieStorage: CookieStoring & CookieProviding = getCookieStorage()
let transport = HTTPTransport(
requestInterceptors: [
AddCookieInterceptor(cookieProvider: cookieStorage),
],
responseInterceptors: [
ReceivedCookieInterceptor(cookieStorage: cookieStorage),
]
)
let result = transport.send(...)
带有 body 和 URL 参数的 POST 请求
let urlParameters = HTTPRequestParameters(
parameters: ["first_name" : "John"],
encoding: .url
)
let bodyParameters = HTTPRequestParameters(
parameters: ["salary" : 100000],
encoding: .json
)
let updateSalaryRequest = HTTPRequest(
httpMethod: HTTPRequest.HTTPMethod.post,
endpoint: "https://api.company.com/employees",
parameters: [urlParameters, bodyParameters]
)
let result = transport.send(request: updateSalaryRequest)
使用 SHA1 数字指纹进行 SSL 硬件
let fingerprint =
"ED D6 27 B8 8B 51 B0 24 B9 BF 90 4C D4 AB 9A AB E2 4B 93 00"
.replacingOccurrences(of: " ", with: "")
let security = Security(
certificates: [
TrustPolicyManager.Certificate(host: "google.com", fingerprint: .sha1(fingerprint: fingerprint))
]
)
let transport = HTTPTransport(security: security)
let result = transport.send(request: HTTPRequest(endpoint: "https://google.com/ncr"))
演进
你可能已经注意到我们的库试图不暴露 Alamofire 接口。有一个简单的方法来消除这种递归依赖,并在 URLSession 框架之上建立独立逻辑。
这些深远计划需要我们目前无法承担的大量努力。尽管如此,这是我们最终渴望达成的重大目标。
因此,欢迎提交拉取请求,但在实际编码之前,请考虑先创建一个预问题。