摘要HTTP 0.7.05

AbstractHTTP 0.7.05

altonotes Inc.维护。



  • altonotes

CocoaPods Compatible

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
}

此外,为了方便,还提供了一些继承自RequestSpecResponseSpecConnectionSpec协议。

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 协议(或 RequestSpecResponseSpec 协议)的类创建。

// 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: StringhttpMethod: 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 中没有这样的协议。

严格来说,如果变量或参数也是泛型,也是可以实现这一点的(*)

各种事件流程

全体の流流程

  1. ConnectionListener.onStart
  2. (HTTP通信開始)
  3. (ネットワークエラーの場合 -> onNetworkErrorへ)
  4. (HTTP通信終了)
  5. ConnectionResponseListener.onReceived (エラーの場合 -> onResponseErrorへ)
  6. ResponseSpec.validate (エラーの場合 -> onResponseErrorへ)
  7. ResponseSpec.parseResponse (エラーの場合 -> onParseErrorへ)
  8. ConnectionResponseListener.onReceivedModel (エラーの場合 -> onValidationErrorへ)
  9. Connection.onSuccess
  10. ConnectionResponseListener.afterSuccess
  11. ConnectionListener.onEnd

错误时的情况流程

在调用ConnectionErrorListener对应的错误处理函数(如onNetworkError等)之后,将按以下顺序执行事件。

  1. ConnectionErrorListener.afterError
  2. 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()函数来重试通信。

restartrepeatRequest的区别在于,是重新构建请求还是发送前一个请求的副本。
例如,当需要在请求参数中包含当前时间时,使用restart则在重试时会包含重试时的时间参数,而使用repeatRequest则会使用之前发送的请求的相同时间。

Retry目录实现了重试的示例。

重试时的回调流程

ConnectionResponseListener.onReceived 等回调函数在执行通信重试时,一系列回调函数的调用顺序有以下两种模式。

在onEnd被调用后进行再通信

例如在显示警报并点击重试按钮后进行再通信的情况,如果在回调函数中不立即进行再通信而是将线程让出后再进行再通信,则回调函数首先会执行到最后一个ConnectionListener.onEnd,然后因为重新通信,会再次从头开始调用回调函数。

在这种情况下,将从onStart到onEnd的流程执行两次。

回调过程中重传

ConnectionResponseListener.onReceived等回调函数内立即重传的情况下,第一次通信的回调调用将被中断,转而执行重传的回调调用,但此时将跳过调用onStart

在这种情况下,将进行两次通信,但onStartonEnd各自只执行一次,并且对监听器来说,表现得好像只进行了一次通信。

轮询(自动更新)

使用 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() 函数时,将跳过正常回调的调用,并执行以下回调函数。

  1. ConnectionErrorListener.onCanceled
  2. ConnectionErrorListener.afterError
  3. 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.onReceivedResponseSpec.validate返回false

func validate(response: Response) -> Bool {
    // ステータスコード401はエラーにする
    return response.statusCode != 401
}

接下来,当在onReceivedonResponseError中检测到401状态码时,执行令牌刷新处理,完成后使用restart参数通过connection重新执行通信。

此外,在重新执行通信时,由于发生401错误的第一次通信和重试通信,onEnd会两次被调用来处理,为了解决这个问题,在重试之前,执行Connection.interrupt

TokenRefresh目录中实现了令牌刷新的示例。

URL编码处理的定制

本库内置了标准的URL编码实现,包括DefaultURLEncoder,但如果你想定制URL编码的方法,可以通过创建实现了URLEncoder协议的定制类来达成。

简易接口 (Convenient interface)

为实现无需实现Spec类而仅用HTTP进行简单通信的接口。

该类通过包装Connection和ConnectionSpec来提供各种方法的指定,可以使用此接口进行与Connection相同的操作。

在Convenient目录中实现了简易接口的示例。

使用方法

  1. 使用初始化器指定URL以创建HTTP对象。
  2. 使用ConnectionSpec定义的各项请求信息、响应处理以及可覆盖的Connection属性,可以使用HTTP方法进行设置。
  3. 最后,使用以下以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的属性。