FlexNetworking 1.1

FlexNetworking 1.1

Dennis Lysenko 维护。



  • Dennis Lysenko, Andriy Katkov

安装

Cocoapods

pod 'FlexNetworking', '~> 1.0'

# optionally:

pod 'FlexNetworking/SwiftyJSON', '~> 1.0' 
# automatically parses JSON responses using SwiftyJSON

pod 'FlexNetworking/RxSwift', '~> 1.0'
# provides properly disposable Single operations for making requests

什么是 FlexNetworking?

FlexNetworking 是一个现代、方便、Codable 优化、Rx 功能的 networking 库,专为在避免样板代码的同时进行安全的 API 调用而构建的应用而设计。我们(开发者)从第一个版本开始就在生产中使用了它,并且不断进化以满足我们的需求。到目前为止,它已在三个社交网络应用和两个基于团队的内容创建应用中使用。

让我们先简单介绍一下一些用法。

用法

// asynchronous usage
AppNetworking.runRequestAsync(path: "/conversations/\(conversation.id)/messages", method: "POST", body: ["text": text]) { result in 
    // note that you did not have to pass your api endpoint or auth info to your networking instance 
    // because you have configured this FlexNetworking instance for your API.

    switch result {
    case .success(let response) where response.status == 200:
        // use response.asJSON, response.asString, or response.rawData
        if let id = response.asJSON?["id"].string, let text = response.asJSON?["text"].string {
            self.messages.append(Message(id: id, text: text))
        }
    case .success(let response):
        // probably internal server error, bad request error or permission/auth error

        print("bad response: \(response)")
        // ^ this logs response status, body, and request details to help diagnose
    case .failure(let error):
        switch error {
        case RequestError.noInternet:
            SVProgressHUD.showError(withStatus: "No internet. Please check your connection and try again")

        case RequestError.cancelledByCaller:
            break // do not show error if cancelled by user

        default:
            print("error making request: \(error)")
            SVProgressHUD.showError(withStatus: "Error. Please try again later")
        }
    }
}

// synchronous usage
let response = try AppNetworking.runRequest(path: "/users/\(user.id)/profile-picture", method: "GET", body: nil)
if response.status == 200, let data = response.rawData {
    profileImageView.image = UIImage(data: data) // this is a really bad example, please use SDWebImage for images...
} else {
    print("Bad response status getting user's profile picture; details in \(response)")
}

// rx + codable usage. this is where flex really shines
struct SearchFilters: Codable {
    var query: String? = nil
    var minimumScore: Int = 0
}

let userDefinedFilters = SearchFilters(query: "programming", minimumScore: 1)

FlexNetworking.rx.requestCodable(path: "/users/\(user.id)/posts/search", method: "GET", codableBody: userDefinedFilters)
    .subscribe(onSuccess: { [weak self] (posts: [Post]) in 
        // automatically called on main queue, but you can of course change this by calling observeOn(_:) with another scheduler
        guard let `self` = self else { return }
        self.posts = posts
        self.collectionView.reloadData()
    }, onError: { (error) in
        Log.e("Error getting posts with search filters:", error)
        // SEE: section labeled "Benefit: Detailed Errors" below
    }).disposed(by: viewModel.disposeBag)

您可以将以下参数传递给请求方法:

  • path:您想要请求的网页的 URL
  • method:任何 HTTP 方法(GET、POST、PUT、PATCH、DELETE...)
  • body:
    • 对于查询字符串(GET)请求:一个 Dictionary 实例
    • 对于任何其他请求:一个 DictionaryRawBody(data:contentType:)JSON(如果安装了 FlexNetworking/SwiftyJSON subpod)。
  • headers:请求头的字典

您可以选择像这样调用所有内容:FlexNetworking().runRequest,或者创建一个实例:

let APINetworking = FlexNetworking(...)

并使用类似的方式调用:APINetworking.runRequest(...)。在实践中,后一种选择要好得多,因为它允许您保持多个实例,每个实例都有自己的配置和钩子...详情稍后提及。目前,我必须告诉你为什么我还要制作另一个网络库。

为什么???

三年或四年前,最初的动机很简单。我们讨厌样板代码。我们热爱Swift。我不喜欢那些没有有效使用Swift的库。

与其他那些要么不灵活,要么不够“Swift式”(比如那个可选项参数和Objective-C风格的回调的Alamofire库,你不能确保既有数据也有错误,而且大家往往以三种不同的方式处理,即使在同一个项目中,Swift团队也做出了伟大的,强大的枚举实现,🙄) ... Flex将允许您以最Swift的方式,按需运行任何类型的网络请求:使用枚举支持的Result单子进行异步调用,以及同步调用的抛出非可选返回值。

后来,我们发现自己在使用大量的Codable和大量的Rx,并决定将它们作为一等公民构建到这个库中,所以我们基本上无法想象使用任何其他库来为我们的基于Rx和 Codable的应用程序服务。

最后,错误处理非常出色,比我们过去使用的其他库要好得多。我们特别构建了抛出的错误和Response结构体,以便仅通过记录错误或响应就可以轻松诊断出了什么问题,因为我们对那些没有提供足够信息来根据日志进行诊断的模糊错误感到愤怒,导致错误被搁置和未解决(没有重现案例)。通过在实现上花费5分钟的工作,可以避免小时的调试(实际上,将请求数据添加到响应结构体中就花了我这么多时间,这已经为我节省了几个小时和大量的挫败感)。

记录错误将给出其特定类别(包括“noInternet”(而不是 code == -1020)和“cancelledByCaller”(而不是 code == -999)),这样您就可以专门处理某些错误(例如,如果网络连接断开,排队再次执行操作)或者只记录错误,以便在问题最终出现时更容易进行故障排除。

记录响应将告知您其状态、主体以及构成实际执行的URL请求的所有原始参数。

关于详细错误的更多细节

制作请求的基本过程抛出一系列关闭的错误(除非你在请求前后的钩子中抛出自己的错误。详情稍后提及)

这些都是枚举RequestError的成员,分为六个类别

  • `.noInternet(Error)`
  • `.cancelledByCaller`
  • .miscURLSessionError(Error)(直接包装一个来自URLSession的错误,除非上面两个之一)
  • .invalidURL(message: String) (将指定无效的URL字符串)
  • .emptyResponseError(Response) (包含响应状态和请求数据)
  • .unknownError(message: String)

FlexNetworking+RxSwift 在此基础上进一步扩展,引入了DecodingError结构体,用于.rx.requestCodable。如果在请求中遇到期望的输出类型解码错误,将沿着观察者链级联DecodingError实例。 打印此实例将为您提供关于请求参数、响应(包括状态)以及解码器在尝试解码输出类型时遇到的确切错误的上下文。

配置

Flex 还允许您为每个实例指定请求前和请求后钩子。

请求前钩子

请求前钩子允许您在请求之前全局修改请求参数(URL会话、请求URL、HTTP方法、请求正文和请求头)。常见用例如下

  • 预先向所有由FlexNetworking实例发出的API调用请求中添加API端点
  • 向所有由FlexNetworking实例发出的API调用请求中传递令牌头
  • 记录请求参数

请求前钩子只是遵守PreRequestHook协议,并定义一个将RequestParameters的一个值映射到另一个值的函数。预定义的符合该协议的类是BlockPreRequestHook,它接受一个自定义块,允许您实现自己的逻辑。请求前钩子按照传递给FlexNetworking.init的顺序执行。如果您想要应用更复杂的请求前逻辑,您可以定义自己的类型,该类型符合PreRequestHook协议。您的钩子将只有一个实例,它将存在于您附加到的FlexNetworking实例的生命周期内,因此如果您需要在请求之间维护额外的状态,您可以使用此功能。

请求后钩子

请求后钩子允许您实现请求成功执行并返回可恢复错误,但这些错误您希望在请求自动恢复时的请求后逻辑。常见用例如下

  • 在请求由于令牌过期而失败时自动初始化令牌刷新,并重试引起过期问题的原始请求(半事务性操作)
  • 速率限制事件发生时的指数退避
  • 记录显著响应

请求后挂钩简单遵循 PostRequestHook 协议,并定义一个函数,将最新的 Response 和初始的 RequestParameters(来自请求前挂钩)映射到三个操作之一

  • 继续 - 继续到链中的下一个项目,将响应传入下一个挂钩。 (您可能用于应用一些副作用并附带相同响应)
  • makeNewRequest(RequestParameters) - 创建一个新请求并在该请求的响应上运行下一个挂钩。 (如果请求未能完成,则不会运行更多挂钩。)
  • completed - 跳过链中剩余的部分,将最新响应逐个传到原始调用者。

预定义的符合规范的类是 BlockPostRequestHook,它与之前一样,接受一个自定义块。请求后挂钩按照它们传递给 FlexNetworking.init 的顺序执行,但需要注意的是,如果链中遇到任何 .completed 命令,则会跳过部分链。

如果您想要执行比简单的块更复杂的请求后逻辑,您可以定义自己的符合 PostRequestHook 协议的类型。请注意,您的挂钩将只有一个实例,并且它将与它挂钩的 FlexNetworking 实例的生存期一样长,因此如果您在请求之间需要维护额外的状态,则可以使用这个特性。

请注意,请求后挂钩中发出的请求不会触发请求前挂钩。

注意: Rx 和标准绑定都使用并发调度队列来安排挂钩,因此如果在挂钩中休眠,您将只阻塞正在执行的(非主)线程。如果您开发了更复杂的挂钩,请确保其 execute 方法是线程安全的,并请遵循良好的并发实践:在必要时同步访问,但尽量减少同步操作以避免瓶颈。

提示:您可以在请求前和后挂钩的任何位置抛出,这将立即停止请求并将错误完全传回调用者。

为什么有请求挂钩?为什么我们不只是在请求体内部制作调用 FlexNetworking 的自定义方法呢?

这些请求挂钩,尤其是请求后挂钩,旨在通过一系列对清晰结构化输入数据的操作产生清晰结构化的输出数据,从而使它们对所使用的实际实现可能不足够关心。

用更简单的话说,无论您使用常规的同步请求方法、异步请求方法还是 Rx 绑定(实际上,Rx 是关键部分),您的请求前和请求后挂钩都将自动运行,而无需更改任何逻辑。这些只是返回不可变数据的更改版本的东西。它们可以保持自己的状态,这与您用于执行 HTTP 请求的内联方法完全无关。

你仍然可以在正文中使用自定义方法来调用FlexNetworking,我不介意-但是使用请求前和请求后的钩子意味着,你不仅可以不需要方法覆盖混用Rx和非Rx,我们还可以在未来不断添加新的绑定(例如Rx绑定)和新方法签名,你将能够立即使用它们,前提是我们不改变“请求参数”的定义。(即使我们改变了,你每个钩子也只需更改一次。)

钩子做有用的事的例子

以下是实现上述部分用例(提前添加端点、传递令牌头和自动启动令牌刷新)的请求前和请求后钩子的示例。

// with flex, you can specify encoders and decoders for Codable requests.
// if we are using a JSON Spring backend, for example, we may want to pass Date objects as millisecond timestamps.
let defaultEncoder: JSONEncoder = {
    let encoder = JSONEncoder()
    encoder.dateEncodingStrategy = .millisecondsSince1970
    return encoder
}()

let defaultDecoder: JSONDecoder = {
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .millisecondsSince1970
    return decoder
}()

let AppNetworking = FlexNetworking(
    preRequestHooks: [
        BlockPreRequestHook { (requestParameters) in
            // mutate path to allow us to use relative API paths
            // and send authorization header with every request when present
            let (session, path, method, body, headers) = requestParameters

            var additionalHeaders = headers

            if ActiveUser.isLoggedIn {
                additionalHeaders["Authorization"] = "Bearer \(ActiveUser.token)"
            }

            return (session, Constants.apiEndpoint.appending(path), method, body, additionalHeaders)
        }
    ],
    postRequestHooks: [
        BlockPostRequestHook { (response, originalRequestParameters) -> PostRequestHookResult in
            let (session, path, _, _, _) = originalRequestParameters

            if response.status == 401, let refreshToken = ActiveUser.refreshToken {
                // do token refresh if we got a 401
                let loginRequestParameters: RequestParameters = (
                    session: session,
                    path: Constants.apiEndpoint.appending("/token-refresh"),
                    method: "POST",
                    body: ["refreshToken": refreshToken],
                    headers: [:]
                )

                return .makeNewRequest(loginRequestParameters)
            } else {
                return .completed
            }
        },
        BlockPostRequestHook { (tokenRefreshResponse, originalRequestParameters) -> PostRequestHookResult in
            guard let rawData = loginResponse.rawData else {
                throw SimpleError(message: "no data in token refresh login response")
            }

            do {
                let tokenRefreshDTO = try defaultDecoder.decode(TokenRefreshDTO.self, from: rawData)

                let token = tokenRefreshDTO.token
                ActiveUser.token = token

                var headers = originalRequestParameters.headers
                headers["Authorization"] = "Bearer \(token)"

                // copy the original request from before the token refresh, 
                // but add the new token to the headers
                var retryRequestParameters = originalRequestParameters
                retryRequestParameters.headers = headers

                return .makeNewRequest(retryRequestParameters)
            } catch let error {
                Log.e(error)

                // TODO: kick the user back to a login screen

                throw error // rethrow to caller
            }
        }
    ],
    defaultEncoder: defaultEncoder,
    defaultDecoder: defaultDecoder
)

待办事项

  • Rx集成在处理请求取消方面做得很好,但非Rx版本则不行。我们应该探讨解决这个问题的方法。
  • 最终以更全面的方式解决身份验证问题,并可能捆绑对常用认证方案有用的常用钩子。
  • 非Rx的请求/响应集成
  • 多部分!
  • 无论你认为什么重要:请提交一个issue!

作者