AbstractHTTP
最简单的使用示例
https://foo.net 打开并打印响应字符串的例子。
HTTP("https://foo.net”).asString {
print($0)
}
概要
与上述的简单示例不同,本库不是旨在简化HTTP通信。
本库的目的是 提供API客户端的设计。
本库将以下内容抽象化,并使其可以通过自由的可实现方式和可重用方式进行
- API请求和响应规范
- HTTP通信处理
- 对通信的通用应用行为
用户内容API的请求响应规范
一个API有几个典型的固有元素。
包括URL、查询、HTTP方法、头部、体等请求规范和返回什么样的响应规范。
这个库将这些API规范作为协议。
// リクエストの仕様
public protocol RequestSpec {
// リクエスト先のURL
var url: String { get }
// HTTPメソッド
var httpMethod: HTTPMethod { get }
// リクエストヘッダー
var headers: [String: String] { get }
// URLに付与するクエリパラメーター
var urlQuery: URLQuery? { get }
// リクエストボディ
func body() -> Data?
}
// レスポンスの仕様
public protocol ResponseSpec {
// レスポンスのデータ型
associatedtype ResponseModel
// レスポンスデータのバリデーション
func validate(response: Response) -> Bool
// HTTPレスポンスをassociated typeに指定した型に変換する
func parseResponse(response: Response) throws -> ResponseModel
}
此外,为了方便,还提供了一些继承自RequestSpec
和ResponseSpec
的ConnectionSpec
协议。
protocol ConnectionSpec: RequestSpec, ResponseSpec {}
创建一个实现了ConnectionSpec
的类(以下称为Spec类),为本库的基本用法。
对于REST API,建议为每个URL和HTTP方法的组合创建一个Spec类。
HTTP通信处理
HTTP通信处理也有一些典型的特点,如超时时间、缓存策略、cookie的处理和SSL证书的处理等,但本库对这些没有过多的干预。
HTTP通信处理使用了一个简单的协议HTTPConnector
,您可以自由地根据各个应用的需求进行实现。
也可以实现一个mock,用于unittest测试,不执行通信而返回固定值。
protocol HTTPConnector {
func execute(request: Request, complete: @escaping (Response?, Error?) -> Void)
func cancel()
}
但是为了方便,我们还提供了一种标准实现DefaultHTTPConnector
,如果需求不是非常严格,则无需自定义实现。
对通信的应用的通用行为
大多数应用程序在通信错误时都会有显示弹出窗口等在多个通信中通用的行为。
本库提供了3种实现此类通用行为的协议,可以使用这些协议在不同的API中实现通用的行为。
- ConnectionListener 监听通信的开始和结束
- ConnectionResponseListener 监听响应数据、解析后的模型的获取等
- ConnectionErrorListener 监听网络错误、解析错误等错误
Listener
目录中实现了各种监听器的示例。
连接监听器
连接监听器
可以钩住通信的开始和结束。
可以实施在通信开始和结束时共同执行的通用操作,例如通信指示器的显示/隐藏切换。
ConnectionResponseListener
ConnectionResponseListener
可以在任何通信数据接收后的某些时刻进行钩子操作,以检查响应内容并引起验证错误。
使用此协议可以实现对多个API进行相同的验证或对多个API的响应数据进行相同的处理。
ConnectionErrorListener
ConnectionErrorListener
用于监听网络错误、验证错误和解析错误等不同类型的错误。同时,与 ConnectionResponseListener
结合使用时,可以在任意条件下触发验证错误,并将错误处理交给 ConnectionErrorListener
来执行。
编程指南(Programming guide)
基本使用方法 (Basic usages)
ConnectionSpec
协议(或 RequestSpec
和 ResponseSpec
协议)的类创建。
// ConnectionSpecの簡単な実装例
class ExampleSpec: ConnectionSpec {
typealias ResponseModel = String
var url: String { return "https://example.com/" }
var httpMethod: HTTPMethod { return .get }
var headers: [String: String] { return [:] }
var urlQuery: URLQuery? { return [
"id": "123",
"name": "john"
]}
func body() -> Data? { return nil }
func validate(response: Response) -> Bool {
// ステータスコード200以外はエラー扱いにする
return response.statusCode == 200
}
func parseResponse(response: Response) throws -> ResponseModel {
if let string = String(bytes: response.data, encoding: .utf8) {
return string
}
throw ConnectionErrorType.parse
}
}
请求的规格定义
URL、HTTP方法
url: String
、httpMethod: HTTPMethod
属性用来确定请求的URL和HTTP方法。
ヘッダー
headers: [String: String]
属性用于设置请求头部。
请求体
body: Data?
属性用于指定请求体(POST数据)。
如果没有请求体,则返回 nil
。
查询参数
urlQuery: URLQuery?
属性用于确定附加到URL的查询参数。
URLQuery
类型可以使用DictionaryLiteral进行定义。
响应规范定义
响应的验证
使用函数 validate(response: Response) -> Bool
进行响应的验证。这里的验证是默认状态的检查,或者是在响应数据解析之前的一种简单检查。
如果不需要验证,可以固定返回 true
,没有问题。
パース
接受到的响应类型指定为 typealias ResponseModel
,并通过 parseResponse(response: Response)
函数将响应数据转换为指定的类型。
typealias ResponseModel
的类型可以是任意的,也可以将 Void
设置为不返回任何内容。
此外,如果在函数中抛出任何错误,调用者会将错误视为解析错误。
通信开始
通信执行使用 Connection
类。
实现了Spec类并在初始化器中指定了通信成功时的回调函数,然后调用 start()
方法来启动通信。
// ConnectionSpecを引数にする例
Connection(ExampleSpec()) {
print($0)
}.start()
// RequestSpecとResponseSpecを別々に引数にする例
Connection(requestSpec: BarRequestSpec(), responseSpec: BazResponseSpec()) {
print($0)
}.start()
ConnectionTask (仅适用于iOS)
Connection
是一个泛型类型,但在 Swift 中,由于泛型类型不能用于变量或作为参数,因此当需要在外部调用 Connection
函数时,通常需要使用 ConnectionTask
协议(*)
ConnectionTask
是一个协议,它将 Connection
中与泛型无关的函数或变量提取出来,而 Connection
实现了 ConnectionTask
协议
class Connection<ResponseModel>: ConnectionTask
Kotlin (Java) 没有这样的限制,可以通过使用通配符来指定泛型参数变量的类型,因此 Android 版的 AbstractHTTP 中没有这样的协议。
严格来说,如果变量或参数也是泛型,也是可以实现这一点的(*)
各种事件流程
全体の流流程
- ConnectionListener.onStart
- (HTTP通信開始)
- (ネットワークエラーの場合 -> onNetworkErrorへ)
- (HTTP通信終了)
- ConnectionResponseListener.onReceived (エラーの場合 -> onResponseErrorへ)
- ResponseSpec.validate (エラーの場合 -> onResponseErrorへ)
- ResponseSpec.parseResponse (エラーの場合 -> onParseErrorへ)
- ConnectionResponseListener.onReceivedModel (エラーの場合 -> onValidationErrorへ)
- Connection.onSuccess
- ConnectionResponseListener.afterSuccess
- ConnectionListener.onEnd
错误时的情况流程
在调用ConnectionErrorListener对应的错误处理函数(如onNetworkError等)之后,将按以下顺序执行事件。
- ConnectionErrorListener.afterError
- ConnectionListener.onEnd
通信处理的自定义
本库中默认已集成了使用URLSession的通信实现DefaultHTTPConnector
,但可以通过实现HTTPConnector
协议的类来自定义通信处理。
標準実装のカスタマイズ
标准的通信实现 DefaultHTTPConnector
可以修改以下属性。
- timeoutInterval (超时秒数)
- isRedirectEnabled (是否自动进行重定向)
- cachePolicy (缓存设置)
修改设置的一种方法如下,将Connection.httpConnector
强制转换为 DefaultHTTPConnector
并直接修改属性。
let connection = Connection(ExampleAPISpec())
// 通信タイムアウトの時間を60秒に変更する
(connection.httpConnector as? DefaultHTTPConnector)?.timeoutInterval = 60
如果您希望一次性修改所有Connection的通信设置,请按以下方式修改 ConnectionConfig.shared.httpConnector
。
ConnectionConfig.shared.httpConnector = {
let connector = DefaultHTTPConnector()
connector.timeoutInterval = 60
return connector
}
通信処理の独自実装
若想进一步定制通信处理,应自行实现HTTPConnector
协议,并通过ConnectionConfig.shared.httpConnector
设置标准的httpConnector,如下所示:
ConnectionConfig.shared.httpConnector = {
return YourOriginalHTTPConnector()
}
通信的模拟
通过自定义实现 HTTPConnector
协议,可以将API处理转换为不实际进行通信的模拟。
因此,在单元测试中,可以返回任意的响应或对请求参数进行断言。
在 Mock
目录中实现了通信模拟的示例。
通信設定の一括変更
通过修改ConnectionConfig.shared
的属性,可以一次性更改所有Connection
的以下属性。
- httpConnector HTTP通信处理
- urlEncoder URL编码处理
- isLogEnabled 是否输出日志
// 全てのConnectonのログ出力を無効化する
ConnectionConfig.shared.isLogEnabled = false
但是,ConnectionConfig.shared
决定的是属性的初始值,因此可以在创建Connection
实例后分别将这些属性更改为其他值。
最小構成的通信示例 (最简单的例子)
最小构的通信示例已实现于 Simplest
目录下。
JSON格式API的加载
在GetJSON
目录下实现了JSON格式API的加载示例。
在多个API中实现共同的请求规范
在大多数项目中,通常在API中指定共同的User-Agent等,使多个API具有共同的请求规范。
实现请求规范共通化的一种方法是通过创建一个共同的基底类来实现 ConnectionSpec
,然后通过继承这个基底类来实现各个API。
在 CommonRequestSpec
目录中,实现了请求规范共通化的示例。
通信中にインジケーターを表示する
使用 ConnectionIndicator
类可以在通信时显示任意的指示器。
该类通过 ConnectionListener
协议来展示和隐藏指示器,也可以使用 ConnectionListener
协议来自定义指示器的显示控制。
ConnectionIndicator
是为了应对一个指示器在多个通信中显示的情况而设计的。它不是在单纯的通信开始和结束时切换显示,而是通过实现引用计数的方式来进行显示控制,当正在进行的通信数为 0 时,指示器将不再显示。
在 Indicator
目录中实现了指示器显示的示例。
重試
您可以通过调用Connection.restart()
或Connection.repeatRequest()
函数来重试通信。
restart
和repeatRequest
的区别在于,是重新构建请求还是发送前一个请求的副本。
例如,当需要在请求参数中包含当前时间时,使用restart
则在重试时会包含重试时的时间参数,而使用repeatRequest
则会使用之前发送的请求的相同时间。
Retry
目录实现了重试的示例。
重试时的回调流程
ConnectionResponseListener.onReceived
等回调函数在执行通信重试时,一系列回调函数的调用顺序有以下两种模式。
在onEnd被调用后进行再通信
例如在显示警报并点击重试按钮后进行再通信的情况,如果在回调函数中不立即进行再通信而是将线程让出后再进行再通信,则回调函数首先会执行到最后一个ConnectionListener.onEnd
,然后因为重新通信,会再次从头开始调用回调函数。
在这种情况下,将从onStart到onEnd的流程执行两次。
回调过程中重传
在ConnectionResponseListener.onReceived
等回调函数内立即重传的情况下,第一次通信的回调调用将被中断,转而执行重传的回调调用,但此时将跳过调用onStart
。
在这种情况下,将进行两次通信,但onStart
和onEnd
各自只执行一次,并且对监听器来说,表现得好像只进行了一次通信。
轮询(自动更新)
使用 Polling
类可以通过轮询(定期自动更新)进行通信。
该类通过 ConnectionListener
协议执行轮询,也可以使用 ConnectionListener
协议来自定义轮询实现。
在通信成功或网络错误之后,Polling
类会在 arbitrary interval 后由初始化器调用指定的回调,因此可以在回调内部重新执行通信处理以执行轮询。
该类持续轮询的条件是通信成功或出现网络错误,其他验证错误或解析错误发生时,轮询将停止。
Polling
目录内包含通信轮询的示例实现。
连接实例的生命周期
Swift中,局部变量在离开函数作用域后会自动释放,但Connection
的实例在通信结束前会被ConnectionHolder
保留,因此即使离开作用域也不会被释放并保留下来。
func request() {
Connection(Spec()) { response in
self.response = response
}.start()
// Connection インスタンスは関数を抜けても解放されない
}
然而,通信完成后Connection
会被释放,所以在通信完成后的一段时间间隔内进行重试的情况下,就必须以某种方式保留Connection
实例。
ConnectionHolder
默认情况下使用一个共享对象处理所有的Connection
,但可以通过设置Connection.holder
属性来使用任意对象。
通信のキャンセル
使用 Connection.cancel()
函数可以取消正在进行的通信。
该函数也可以通过 ConnectionTask
协议调用。
另外,由于 Connection
被存储在 ConnectionHolder
中,您还可以通过 ConnectionHolder
同时取消多个通信。
如果使用标准 ConnectionHolder
共享对象,则可以使用以下代码在屏幕退出时取消所有通信:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// 画面離脱時に全ての通信をキャンセルする
ConnectionHolder.shared.cancelAll()
}
在 Cancel
目录中实现了取消的示例。
取消时调用回调
执行 Connection.cancel()
函数时,将跳过正常回调的调用,并执行以下回调函数。
- ConnectionErrorListener.onCanceled
- ConnectionErrorListener.afterError
- ConnectionListener.onEnd
如果在执行 ConnectionResponseListener.onReceived
等正常回调的过程中取消,也将跳过后续的正常回调调用,并调用上述回调。
404错误被当作正常情况处理
响应数据的解析处理通常包括将二进制数据转换为字符串或将JSON数据映射为对象等常规映射,但实现是自由的,也可以在特定条件下执行异常解析。
有些情况下返回数据列表的API会在响应为0条记录时返回404错误,如果希望将这些情况当作0条记录的常规响应来处理,可以通过以下方式的解析处理来实现。
func parseResponse(response: Response) throws -> ResponseModel {
// 404エラーの場合は空配列にする
if response.statusCode == 404 {
return []
}
// それ以外はJSONをパースしてString配列を取り出す
return try JSONDecoder().decode([String].self, from: response.data)
}
401错误发生时刷新访问令牌
通过组合前面说明的功能,可以实现当通信发生401错误时代码令牌刷新,成功刷新令牌后,重复同一通信的功能。
首先,HTTP状态码401的处理可以在以下两个位置之一进行。
ConnectionResponseListener.onReceived
ConnectionErrorListener.onResponseError
如果使用ConnectionErrorListener.onResponseError
来处理401错误的处理,则需要将401状态视为响应错误处理,并在HTTP状态码为401的情况下,确保ConnectionResponseListener.onReceived
或ResponseSpec.validate
返回false
。
func validate(response: Response) -> Bool {
// ステータスコード401はエラーにする
return response.statusCode != 401
}
接下来,当在onReceived
或onResponseError
中检测到401状态码时,执行令牌刷新处理,完成后使用restart
参数通过connection
重新执行通信。
此外,在重新执行通信时,由于发生401错误的第一次通信和重试通信,onEnd
会两次被调用来处理,为了解决这个问题,在重试之前,执行Connection.interrupt
。
在TokenRefresh
目录中实现了令牌刷新的示例。
URL编码处理的定制
本库内置了标准的URL编码实现,包括DefaultURLEncoder
,但如果你想定制URL编码的方法,可以通过创建实现了URLEncoder
协议的定制类来达成。
简易接口 (Convenient interface)
为实现无需实现Spec类而仅用HTTP进行简单通信的接口。
该类通过包装Connection和ConnectionSpec来提供各种方法的指定,可以使用此接口进行与Connection相同的操作。
在Convenient目录中实现了简易接口的示例。
使用方法
- 使用初始化器指定URL以创建
HTTP
对象。 - 使用
ConnectionSpec
定义的各项请求信息、响应处理以及可覆盖的Connection
属性,可以使用HTTP
方法进行设置。 - 最后,使用以下以
as
开头的HTTP
方法获取响应数据。- asString 以String类型获取。可指定字符编码。
- asData 以Data类型获取。
- asResponse 以Response类型获取。
- asDecodable 使用指定的Decodable类型,将JSON映射到该类型并获取。
- asModel 使用任意类型及其转换处理机制获取数据。
最简单的GET通信示例
HTTP("https://foo.net”).asString {
print($0)
}
各种设置后的POST通信示例
HTTP("https://foo.net”)
.httpMethod(.post)
.headers(["Content-Type": "application/json"])
.urlQuery(["key": "value"])
.body(data)
.addListener(listener)
.asDecodable(User.self) { user in
print(user.name)
}
默认HTTPConnector的设置
当使用标准DefaultHTTPConnector
作为httpConnector
时,可以通过HTTP.setupDefaultHTTPConnector
方法修改DefaultHTTPConnector
的属性。