与我们一起于10月8日至10日在纽约市参加活动,了解有关 GraphQL 联邦和API平台工程的最新提示、趋势和新闻。加入我们,参加2024年纽约市的 GraphQL Summit 活动
文档
免费开始

请求链自定义

了解如何通过自定义拦截器来自定义 Apollo iOS 请求链


中,ApolloClient使用NetworkTransport对象从远程服务器中抓取查询。

默认的 NetworkTransportRequestChainNetworkTransport。因此,此网络传输使用一个名为 请求链 的结构来逐步处理每个

要了解如何配置客户端支持通过 WebSocket 发送 ,请参阅 启用订阅支持

请求链

一个 请求链 定义了一串 拦截器,这些拦截器处理特定 GraphQL 操作 执行的生命周期。一个拦截器可能会向请求添加自定义 HTTP 头部,而另一个则负责实际上 发送 请求到 GraphQL 服务器。第三个拦截器随后会将操作的结果写入 Apollo iOS 缓存。

执行操作时,一个名为 InterceptorProvider 的对象为该操作生成一个 RequestChain。然后,调用请求链上的 kickoff,它运行链中的第一个拦截器:

kickoff called
proceedAsync called
No result available
Result available
Client executes
operation
InterceptorProvider
creates request chain
First
interceptor
Second
interceptor
Chain complete
Return Result
Handle Error

拦截器可以在任何线程上执行任意的异步逻辑。当拦截器运行完成后,它在其 RequestChain 上调用 proceedAsync,从而转到下一个拦截器。

默认情况下,当链中的最后一个拦截器完成时,如果解析的操作结果可用,则将该结果返回给操作的原调用者。否则,调用错误处理逻辑。

每个请求都有自己的短生命周期的 RequestChain 这意味着每个操作的拦截器序列可能不同。

拦截器提供者

为了为每个 GraphQL 操作生成一个 请求链,Apollo iOS 将操作传递到一个名为 拦截器提供者 的对象。该对象遵循 InterceptorProvider 协议

默认提供者

DefaultInterceptorProvider 是拦截器提供者的默认实现。该默认拦截器提供者支持:

  • 读取/写入响应数据到标准化缓存。
  • 使用 URLSession 发送网络请求。
  • 解析 JSON 格式的 GraphQL 响应数据
  • 自动持久化查询

默认拦截器提供器DefaultInterceptorProvider使用一个URLSessionClient和一个ApolloStore来初始化,这些将被传递到它创建的拦截器中。通过配置这两个对象,DefaultInterceptorProvider可以支持大多数常见用法。

如果您需要进一步自定义请求管道,您可以创建一个自定义拦截器提供器

默认拦截器

对于每次操作DefaultInterceptorProvider都会创建以下拦截器的请求链。

以下是对内置拦截器的描述

自定义拦截器提供器

如果您的用例需要,您可以创建一个符合InterceptorProvider协议的定制struct或class。

如果您定义了自定义的InterceptorProvider,它通常应该创建一个类似于默认RequestChain结构,但需要根据特定操作添加或修改。

提示:如果您只需向默认请求链的开始或结束处添加拦截器,}可以子类化 DefaultInterceptorProvider而不是从头开始创建一个新的类。

在创建自定义拦截器提供器中的请求链时请注意以下事项

  • 拦截器设计为是短暂的。您的拦截器提供器应针对每次请求提供一组全新的拦截器,以免同时使用相同的拦截器实例。
  • 通常不建议保留个别拦截器的引用(测试验证除外)。相反,您可以创建一个持有关联较长的对象的拦截器,提供器可以将此对象传递到每个新的拦截器集合中。这样,每个拦截器都是可丢弃的,但您不需要重创建执行更多重工作的底层对象。

如果您创建了您自己的InterceptorProvider,您可以使用任何在Apollo iOS中包含的内置拦截器:

内置拦截器

Apollo iOS提供一系列内建的拦截器,您可以在自定义拦截器提供者中创建。自定义拦截器提供者中定义。您还可以通过定义一个符合ApolloInterceptor接口的类来创建一个自定义拦截器。

名称描述

预网络

MaxRetryInterceptor

强制对初始失败的GraphQL操作执行最大次数的重试(默认三次重试)。

CacheReadInterceptor

在服务器上执行操作之前,从Apollo iOS缓存中读取数据,根据该操作cachePolicy

如果找到缓存数据可以完全解决该操作,则将返回该数据。然后根据操作cachePolicy继续或终止请求链。

网络

NetworkFetchInterceptor

接收一个URLSessionClient,并使用它将准备好的HTTPRequest(或其子类)发送到GraphQL服务器。

如果您通过网络发送操作,则您的RequestChain需要此拦截器(或处理网络通信的自定义拦截器)。

后网络

ResponseCodeInterceptor

对于未成功执行的 操作,检查GraphQL服务器的HTTP响应状态码并将它传递给RequestChainhandleErrorAsync回调。

请注意,大多数在GraphQL层的错误都伴随着状态码200和信息在errors数组中返回(根据GraphQL规范)。这个拦截器有助于处理服务器级别的错误(例如500)和中继件返回的错误。

有关更多信息,请参阅这篇关于GraphQL错误处理的文章

AutomaticPersistedQueryInterceptor

检查GraphQL服务器在执行后的响应,以确定提供的哈希是否成功由服务器找到。如果没有找到,拦截器将重新启动链并使用完整的字符串重新尝试操作。

MultipartResponseParsingInterceptor

使用增量交付HTTP规范解析多部分响应,并将其传递给下一个拦截器。

注意,下一个拦截器必须是JSONResponseParsingInterceptor,以便将单个消息解析为GraphQLResult

JSONResponseParsingInterceptor

将 GraphQL 服务器's JSON 响应解析为 GraphQLResult 对象,并将其附加到 HTTPResponse

CacheWriteInterceptor

在服务器上执行操作后,根据该操作的 缓存策略 将响应数据写入 Apollo iOS 缓存

additionalErrorInterceptor

InterceptorProvider 可以选择提供 additionalErrorInterceptor,在将错误返回给调用者之前调用。这主要用于日志记录和错误跟踪。此拦截器必须遵守 ApolloErrorInterceptor 协议。ApolloErrorInterceptor.

additionalErrorInterceptor 不是请求链的一部分。相反,任何其他拦截器都可以通过调用 chain.handleErrorAsync 来调用此拦截器。

注意:对于有明确解决方法的预期错误(比如更新过期的认证令牌),你应该在你的请求链中定义一个可以解决该问题并重试操作的中继器。

拦截器流程

大多数拦截器执行其逻辑然后调用 chain.proceedAsync 以继续到请求链中的下一个拦截器。然而,拦截器可以调用其他方法来覆盖此默认流程。

重试操作

任何拦截器都可以调用 chain.retry 来立即从开始重播当前请求链。如果在拦截器需要刷新访问令牌或修改操作以成功执行其他配置的情况下,这很有用。

重要:不要无限制地调用重试。如果你的服务器返回 500 或者用户没有互联网连接,反复重试可以创建请求无限循环(特别是在你没有使用 MaxRetryInterceptor 来限制重试次数时)。

无限制的重试会耗尽用户的电池电量,并可能增加他们的数据使用量。确保只有在代码可以处理原始失败的情况下才进行重试!

返回值

拦截器可以直接向操作的原调用人返回值,而不是等待请求链完成。为此,拦截器可以调用 chain.returnValueAsync

这不会阻止请求链中其余部分的执行。拦截器仍然可以在调用 chain.returnValueAsync 后按常规调用 chain.proceedAsync。但在调用 chain.returnValueAsync 后,拦截器仍然可以按常规继续调用 chain.proceedAsync。但是,如果遇到的错误会导致 操作 失败,则可以跳过调用 chain.proceedAsync 来结束请求链。

你甚至可以在请求链中多次调用 chain.returnValueAsync!这在最初返回本地缓存值,然后返回由 GraphQL 服务器返回的值时很有用。

返回错误

如果拦截器遇到错误,它可以调用 chain.handleErrorAsync 来返回该错误的详细信息。

这不会阻止请求链中其余部分的执行。拦截器仍然可以按常规在调用 chain.handleErrorAsync 后调用 chain.proceedAsync。然而,如果遇到的错误会导致操作失败,您可以跳过调用 chain.proceedAsync 以结束请求链。

示例

以下示例代码片段演示了如何使用带有自定义拦截的请求管道。此代码假定您有以下 假设 的类在您的代码中(这些类不是 Apollo iOS 的部分):

  • UserManager: 检查活动用户是否已登录,在错误和响应上进行相关检查,看是否有必要续订令牌,并在必要时执行续订。
  • Logger: 根据日志级别处理打印日志。支持 .debug.error.always 日志级别。

示例拦截器

UserManagementInterceptor

此示例拦截器检查活动用户是否已登录。如果是,并且令牌已过有效期,则异步续订该用户的访问令牌。最后,在继续到请求链中的下一个拦截器之前,将其添加到 Authorization 标头中。

import Foundation
import Apollo
class UserManagementInterceptor: ApolloInterceptor {
enum UserError: Error {
case noUserLoggedIn
}
public var id: String = UUID().uuidString
/// Helper function to add the token then move on to the next step
private func addTokenAndProceed<Operation: GraphQLOperation>(
_ token: Token,
to request: HTTPRequest<Operation>,
chain: RequestChain,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) {
request.addHeader(name: "Authorization", value: "Bearer \(token.value)")
chain.proceedAsync(
request: request,
response: response,
interceptor: self,
completion: completion
)
}
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) {
guard let token = UserManager.shared.token else {
// In this instance, no user is logged in, so we want to call
// the error handler, then return to prevent further work
chain.handleErrorAsync(
UserError.noUserLoggedIn,
request: request,
response: response,
completion: completion
)
return
}
// If we've gotten here, there is a token!
if token.isExpired {
// Call an async method to renew the token
UserManager.shared.renewToken { [weak self] tokenRenewResult in
guard let self = self else {
return
}
switch tokenRenewResult {
case .failure(let error):
// Pass the token renewal error up the chain, and do
// not proceed further. Note that you could also wrap this in a
// `UserError` if you want.
chain.handleErrorAsync(
error,
request: request,
response: response,
completion: completion
)
case .success(let token):
// Renewing worked! Add the token and move on
self.addTokenAndProceed(
token,
to: request,
chain: chain,
response: response,
completion: completion
)
}
}
} else {
// We don't need to wait for renewal, add token and move on
self.addTokenAndProceed(
token,
to: request,
chain: chain,
response: response,
completion: completion
)
}
}
}

RequestLoggingInterceptor

此示例拦截器使用假设的 Logger 类记录出站请求,然后继续到请求链中的下一个拦截器:

import Foundation
import Apollo
class RequestLoggingInterceptor: ApolloInterceptor {
public var id: String = UUID().uuidString
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) {
Logger.log(.debug, "Outgoing request: \(request)")
chain.proceedAsync(
request: request,
response: response,
interceptor: self,
completion: completion
)
}
}

‌ResponseLoggingInterceptor

此示例拦截器使用假设的 Logger 类记录请求的响应(如果存在),然后继续到请求链中的下一个拦截器:

这是一个可以继续执行并抛出错误的拦截器的示例。我们不一定想在拦截器被错误地放置时停止处理,但我们确实希望知道错误信息。并且想要知道该错误。

import Foundation
import Apollo
class ResponseLoggingInterceptor: ApolloInterceptor {
enum ResponseLoggingError: Error {
case notYetReceived
}
public var id: String = UUID().uuidString
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) {
defer {
// Even if we can't log, we still want to keep going.
chain.proceedAsync(
request: request,
response: response,
interceptor: self,
completion: completion
)
}
guard let receivedResponse = response else {
chain.handleErrorAsync(
ResponseLoggingError.notYetReceived,
request: request,
response: response,
completion: completion
)
return
}
Logger.log(.debug, "HTTP Response: \(receivedResponse.httpResponse)")
if let stringData = String(bytes: receivedResponse.rawData, encoding: .utf8) {
Logger.log(.debug, "Data: \(stringData)")
} else {
Logger.log(.error, "Could not convert data to string!")
}
}
}

LoggingErrorInterceptor

此示例错误拦截器演示了在请求过程中抛出错误时如何进行自定义日志记录。

import Foundation
import Apollo
class LoggingErrorInterceptor: ApolloErrorInterceptor {
weak var errorLogger: MyErrorLogger?
init(errorLogger: MyErrorLogger) {
self.errorLogger = errorLogger
}
func handleErrorAsync<Operation: GraphQLOperation>(
error: Error,
chain: RequestChain,
request: Apollo.HTTPRequest<Operation>,
response: Apollo.HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) {
errorLogger?.requestFailed(response?.httpResponse, withError: error)
completion(.failure(error))
}
}

示例拦截器提供者

InterceptorProvider创建请求链,使用默认拦截器按照常规顺序,并将上面定义的所有示例拦截器添加到请求管道中的适当位置:

import Foundation
import Apollo
struct NetworkInterceptorProvider: InterceptorProvider {
// These properties will remain the same throughout the life of the `InterceptorProvider`, even though they
// will be handed to different interceptors.
private let store: ApolloStore
private let client: URLSessionClient
init(store: ApolloStore, client: URLSessionClient) {
self.store = store
self.client = client
}
func interceptors<Operation: GraphQLOperation>(for operation: Operation) -> [ApolloInterceptor] {
return [
MaxRetryInterceptor(),
CacheReadInterceptor(store: self.store),
UserManagementInterceptor(),
RequestLoggingInterceptor(),
NetworkFetchInterceptor(client: self.client),
ResponseLoggingInterceptor(),
ResponseCodeInterceptor(),
JSONResponseParsingInterceptor(),
AutomaticPersistedQueryInterceptor(),
CacheWriteInterceptor(store: self.store)
]
}
}

示例 ApolloClient 配置

以下是如何使用我们的示例NetworkInterceptorProvider设置ApolloClient

import Foundation
import Apollo
let client: ApolloClient = {
// The cache is necessary to set up the store, which we're going
// to hand to the provider
let cache = InMemoryNormalizedCache()
let store = ApolloStore(cache: cache)
let client = URLSessionClient()
let provider = NetworkInterceptorProvider(store: store, client: client)
let url = URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/graphql")!
let requestChainTransport = RequestChainNetworkTransport(
interceptorProvider: provider,
endpointURL: url
)
// Remember to give the store you already created to the client so it
// doesn't create one on its own
return ApolloClient(networkTransport: requestChainTransport, store: store)
}()

说明如何设置一个可以处理WebSocket和的客户端的示例包括在订阅文档中。

URLSessionClient

由于URLSession只支持通过基于代理的API在后台使用,提供了一个URLSessionClient类,帮助管理URLSessionDelegate

请注意,由于只能在URLSession的初始化器中设置代理,因此只能将URLSessionClient的初始化器传递给URLSessionConfiguration而不是现有的URLSession

默认情况下,URLSessionClient实例使用URLSessionConfiguration.default设置会话,DefaultInterceptorProvider实例使用URLSessionClient的默认初始化器。

URLSessionClient类及其大多数方法都是公开的,所以如果您需要覆盖任何URLSession代理的代理方法,或者需要处理其他代理场景,您可以对它进行子类化。

上一页
文件上传
评分文章评分在GitHub上编辑编辑论坛Discord

©2024Apollo Graph Inc.,即Apollo GraphQL。

隐私政策

公司