Swift 网络

pre-commit CI/CD codecov

Swift 网络,或者 swift-networking,是一个用于在 Swift 应用程序内部构建灵活网络堆栈的库。它可以在任何 Apple 平台上使用。您可以使用它来提供丰富的网络功能,例如身份验证、去重、节流等等。

📚 文档

浏览 main, 0.4.0 的文档。

🤔 什么是 Swift 网络?

Swift 网络是一个 Swift Package,它提供了一些用于发出 HTTP 网络请求的核心工具。

它的理念围绕着这样一个想法:在高层,客户端发送请求并等待每个请求的响应。

与编程中的几乎所有事情一样,这是一个数据类型的转换,因此适合函数式编程风格。考虑到这一点,该库提供了可以组合在一起以执行此转换的组件。所有内置组件都经过了充分的测试,并带有测试助手,可以轻松测试您自己的自定义组件。

为什么不直接使用 URLSession?

该库使用了 URLSession,因为它提供了最终负责发送请求的终端组件。 Swift Networking 抽象了这个细节,同时提供了比 URLSession 更多的便利。 此外,此库提供了许多有用的构建块,而 URLSession 本身并不提供这些构建块。

🤿 深入了解

如果我们认为当客户端发出网络请求时,它本质上是一个函数:(Request) async throws -> Response,这可以通过一个名为 NetworkingComponent 的协议来表示。我们可以在 URLSession 上提供对这个协议的遵循,并使用它来发出网络请求。但是,在将请求提供给 URLSession 之前,有机会进一步转换它,也许需要修改它,或者我们希望收集指标,或者甚至从另一个系统返回 Response。

更进一步,我们可以考虑一个组件链,

                 ┌────────────────────────────────────────────┐
                 │               Network Stack                │      ┌────────┐
┌─────────┐      │ ┌─────────┐    ┌─────────┐    ┌──────────┐ │      │        │
│ Request │─ ─ ─▶│ │    A    │───▶│    B    │───▶│ Terminal │ │─ ─ ─▶│ Server │
└─────────┘      │ └─────────┘    └─────────┘    └──────────┘ │      │        │
                 └────────────────────────────────────────────┘      └────────┘

在此图中,应用程序(或客户端)有 3 个组件:ABTerminal。当一个请求被提供给整个堆栈时,它从组件 A 开始,最终将其提供给组件 B,最终将其提供给终端组件,终端组件是最后一个组件,负责获取响应。 来自服务器的响应会通过堆栈返回。

为了使这种组合变得容易,该库提供了一个 NetworkingModifier 协议,它的工作方式与 SwiftUI 中的 ViewModifier 完全相同。除了终端组件之外的所有网络组件实际上都是修饰符,因此除了 Request 值之外,它们还会收到“上游”网络组件。这就是组件 A 如何将请求传递到组件 B 等的原因。

每个内置组件都在 NetworkingComponent 上提供公共扩展,就像 SwiftUI 中的 ViewModifiers 通常通过对 View 协议的扩展来提供 API 一样。 这会产生一个声明式的网络堆栈,如下所示

let network = URLSession.shared
  .removeDuplicates()
  .logged()

更新我们上面的图表,我们可以看到网络堆栈使我们能够将组件连接在一起,输入请求并获取响应。

 ┌─────────┐
 │ Request │─ ─ ─▶┌────────────────────────────────────────────┐
 └─────────┘      │               Network Stack                │       ┌────────┐
                  │ ┌─────────┐───▶┌─────────┐───▶┌──────────┐ │       │        │
                  │ │ Logged  │    │ De-dupe │    │URLSession│ │◀─ ─ ─▶│ Server │
┌──────────┐      │ └─────────┘◀───└─────────┘◀───└──────────┘ │       │        │
│ Response │◀ ─ ─ └────────────────────────────────────────────┘       └────────┘
└──────────┘

🛠️ 如何使用这个库

我强烈建议您使用 pointfreeco/swift-dependencies 进行依赖注入和客户端管理。这意味着您应该创建一个像这样的“网络客户端”

import Dependencies
import Foundation
import Networking

struct NetworkClient {
    var network: () -> any NetworkingComponent

    init(network: @escaping () -> any NetworkingComponent) {
        self.network = network
    }
}

extension NetworkClient: DependencyKey {
    static let liveValue: Self = {
        let network = URLSession.shared
            .duplicatesRemoved()
            .automaticRetry()

        return .init(network: {
            network
        })
    }()

    static let testValue: Self = .init(
        network: unimplemented("\(Self.self).network")
    )
}

extension DependencyValues {
    var networkClient: NetworkClient {
        get { self[NetworkClient.self] }
        set { self[NetworkClient.self] = newValue }
    }
}

这是一个有些简陋的网络堆栈,可以通过访问 @Dependency(\.networkClient) var networkClient 来使用。

🍭 内置组件

该库附带以下内置组件。

📮 发出请求

该库提供了名为 HTTPRequestDataHTTPResponseData 的结构。 在内部,它们使用 AppleHTTPRequestHTTPResponse 值类型。

这些是该库的构建块,用于通过堆栈发出请求,如下所示

// Access the network client
@Dependency(\.networkClient) var networkClient

// Create a http request data value, more on this later...
let request = HTTPRequestData(path: "hello")

do {

  // Await the data response
  let response = try await networkClient.data(request)

  // Access basic properties
  let originalRequest = response.request
  let payloadData: Data = response.data // might be empty.

} catch as NetworkingError {

  // Access basic properties
  let originalRequest = error.request
  if let response = error.response {
    // in some cases we might have an `HTTPResponseData` value,
    // which allows access to underlying response info
  }
}

虽然这很好,但它非常底层,不建议用于大多数用例。 相反,应用程序通常希望将有效负载 Data 值解码为特定的 Coadable 类型。 这是可以使用 Request 类型的地方。

Request 是一个泛型值,它组合了 HTTPRequestData 值,以及将 Data 解码为某些 Body 类型的能力。 如果所需的 Body 类型符合 Decodable,这是自动的,但支持完全自定义。 甚至可以在将数据转换为应用程序域类型所需的 Body 之前,将数据解码为可解码的中间“数据传输对象”。

// Access the network client
@Dependency(\.networkClient) var networkClient

// Create a http request data value, more on this later...
let http = HTTPRequestData(path: "hello")

// Create a request, assuming that MyExpectedBody is Decodable
let request = Request<MyExpectedBody>(http: http) // This convenience uses default JSON decoder

// Await the value response
let (value, response) = try await networkClient.value(request)

虽然这可以正常工作,但不得不创建看似两个请求值有点麻烦。 相反,建议使用 Request 类型的受约束扩展。

extension Request where Body == MyExpectedBody {
  static func myBodyValue() -> Self {
    Request(http: .init(
      path: "hello"
    ))
  }
}

有了这个,我们的代码就变成了,

// Access the network client
@Dependency(\.networkClient) var networkClient

// Await the value response
let (value, response) = try await networkClient.value(.myBodyValue())

🧩 NetworkClient vs APIClient

上面的示例显示了 NetworkClient 的用法。 这些示例都没有指定超出 "hello" 路径的任何内容,这缺少一些关键信息,例如 authority。 它默认为 "GET" HTTP 方法和 "https" 方案。 所有这些属性都可以在请求中获取/设置,

var request = HTTPRequestData(method: .post, authority: "my-server.com")
request.headerFields[.accepts] = "application/json"
request.path = "message"
request.greeting = "Hello World"
print(request.debugDescription) // POST https://my-server.com/message?greeting=Hello%20World

但当然,为每个请求执行此操作是不可取的,服务器的属性应该只配置一次,这就是建议创建 API Client 而不是或除了 Network Client 之外的原因。

让我们假设我们想通过使用 http://ipinfo.io/ 来找出客户端的地理位置。 这是一项为 IP 地址执行地理信息服务的服务。 如果您的应用程序需要连接到多个服务器,例如除了您自己的第一方服务器之外的第三方服务器,那么拥有一个带有多个 API Client 的单个 Network Client 是一个很好的理由。 在此示例中,我们可以创建一个 IpInfo 客户端,

import Dependencies
import Foundation

struct IpInfoClient {
    typealias FetchIpInfoData = @Sendable (String?) async throws -> IpInfoData
    var fetch: FetchIpInfoData

    init(fetch: @escaping FetchIpInfoData) {
        self.fetch = fetch
    }
}

extension IpInfoClient: TestDependencyKey {
    static var testValue: Self = .init(
        fetch: unimplemented("\(Self.self).fetch")
    )
}

extension DependencyValues {
    var ipInfoClient: IpInfoClient {
        get { self[IpInfoClient.self] }
        set { self[IpInfoClient.self] = newValue }
    }
}

// And the Live Network Client (could be a separate module)

import LiveNetwork // Lets assume we have a "live" network client as described above in this module
import Networking // Import this library, swift-networking

extension IpInfoClient: DependencyKey {
  static let liveValue: Self = {
    // Get the network client - to access the standard network stack
    @Dependency(\.networkClient) var client

    let network = client
      .network()
      .logged(using: .app(category: "Network"))
      .server(headerField: .authorization, "Bearer \(<Secret API Key>)") // Don't store secrets in SCM though!
      .server(headerField: .accept, "application/json")
      .server(authority: "ipinfo.io")

    return IpInfoClient(
      fetch: { address in
        try await network.value(.ipInfo(of: address)).body
      }
    )
  }()
}

extension Request where Body == IpInfoData {
  static func ipInfo(of address: String?) -> Self {
    Request(
      http: HTTPRequestData(path: address ?? "/")
    )
  }
}

要在应用程序中使用它,我们只需要导入一些模块并访问 api 客户端。

import IpInfoClient
import Dependencies

@Dependency(\.ipInfoClient) var ipInfoClient

// get the geographic info for the client's IP address
let ipInfo = try await ipInfoClient.fetch(nil)